|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2005-11-04 11:49 UTC] php at tjworld dot net
Description:
------------
When extending a class that has a visible (public or protected) read-only dynamic property by virtue of __set(), the sub-class is prevented from modifying (or "overloading") the property as if it were merely a public consumer of an instance-object of super, rather than an extension of the class definition itself.
This shows up in particular in the DOM classes DOMNode and DOMDocument, where it causes:
Fatal error: extDOMElement::__construct() [function.--construct]: Cannot write property in...
If you extend both classes, and try to create a new derived extDOMNode object using a custom extDOMDocument->createElement(), it is impossible to set any of the new extDOMNode's dynamic properties from within extDOMNode's constructor (especially ownerDocument) because DOMNodes without an owner are forced to be read-only.
The extDOMNode class definition should be able to modify the publically visible ownerDocument property.
Since real properties accessible from a sub-class can't be private (if they're to be accessible from other objects), it follows that the same rule should apply to dynamic properties. If this were so the dynamic properties made visible by __set() would be inherited as protected or public and this issue wouldn't arise.
Reproduce code:
---------------
class ReadOnly {
protected $realProperty;
private $dynamicProperty = array();
function __construct() {
$realProperty = 12;
$this->members['test'] = 'read-only';
}
public function __set($name, $value) {
if($name=='test') {
if(isset($this->dynamicProperty[$name]))
throw new Exception("Can't overwrite $name");
$props[$name] = $value;
}
}
}
class Writeable extends ReadOnly {
function __construct($value) {
parent::__construct();
$this->realProperty = 25; // ok
$this->test = $value; // causes Fatal Error
}
}
$test = new Writeable('write to me');
Expected result:
----------------
The extended class should be able to inherit and modify protected or public properties of the super class.
Actual result:
--------------
For built-in classes causes a Fatal Error. For user defined classes causes a user-defined read-only result. In the example causes an Exception.
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Tue Oct 28 20:00:01 2025 UTC |
C:\PHP\5.1.0RC5-dev>php.exe test.php testing... Fatal error: Uncaught exception 'Exception' with message 'Can't overwrite test' in C:\test.php:12 Stack trace: #0 C:\test.php(23): ReadOnly::__set('test', 'write to me') #1 C:\test.php(30): Writeable->__construct('write to me') #2 {main} thrown in C:\test.php on line 12 ---------------test.php-------------- <?php class ReadOnly { protected $realProperty; private $dynamicProperty = array(); function __construct() { $realProperty = 12; $this->dynamicProperty['test'] = 'read-only'; } public function __set($name, $value) { if($name=='test') { if(isset($this->dynamicProperty[$name])) throw new Exception("Can't overwrite $name"); $dynamicProperty[$name] = $value; } } public function __get($name) { return $this->dynamicProperty[$name]; } } class Writeable extends ReadOnly { function __construct($value) { parent::__construct(); $this->realProperty = 25; // ok $this->test = $value; // causes Fatal Error } public function getReal() {return $this->realProperty; } public function getDynamic() {return $this->test; } } echo "testing...\r\n"; $test = new Writeable('write to me'); echo 'real: '.$test->getReal()."\r\n"; echo 'dynamic: '.$test->getDynamic()."\r\n"; ?>Further test using DOMDocument/DOMElement... C:\PHP\5.1.0RC5-dev>php.exe dom.php Fatal error: extDOMElement::__construct(): Cannot write property in C:\dom.php on line 14 ----------dom.php------------- <?php class extDOMDocument extends DOMDocument { public function createElement($name, $value=null) { $ret = new extDOMElement($name, $value, $this); // create the new element with this Document as owner return $ret; } } class extDOMElement extends DOMElement { function __construct($name, $value='', $owner=null, $namespaceURI=null) { if(!$owner instanceof extDOMDocument) throw new DOMException(DOM_NOT_FOUND_ERR); // illegal owner parent::__construct($name, $value, $namespaceURI); $this->ownerDocument = $owner; //** this line causes a Fatal Error } // ... more class definition here } $doc = new extDOMDocument('test'); $el = $doc->createElement('tagname'); ?>"but DOM properties CANNOT be overriden." Does this occur anywhere else in the PHP classes or is it unique to DOM? It's the first time I've met this situation in OO since the 80's. It pretty much makes having the DOM object-oriented pointless, when the base class (DOMNode) of the other significant DOM classes prevents useful extension. A simple solution would be to provide a courtesy: DOMNode->__construct($ownerDocument = null); But that'd be available to the public of course. Alternatively, protected DOMNode function _setOwnerDocument(DOMDocument ownerDocument); But thats a bit arbitary. Alternatively, solve the practical loss of functionality by fixing the bug in importNode() so it returns an object of the class passed in: DOMNode DOMDocument->importNode(DOMNode $node, bool deep); Currently it *casts* the passed $node to one of the DOM base classes it inherited from *and* discards all their extended properties and methods, which is surely not OO behaviour because in the following scenario, the cases listed at the end are inconsistent: class inChild extends DOMNode {} class inGrandChild extends DOMElement() class inGreatGrandChild extends inGrandChild() $node = new DOMNode(); $element = new DOMElement(); $child = new inChild(); $grandChild = new inGrandChild(); $greatGrandChild = new inGreatGrandChild(); 1. DOMDocument->importNode($node, true) instanceof DOMNode 2. DOMDocument->importNode($element, true) instanceof DOMElement 3. DOMDocument->importNode($child, true) instance of DOMNode 4. DOMDocument->importNode($grandChild, true) instanceof DOMElement (not inGrandChild) 5. DOMDocument->importNode($greatGrandChild, true) instanceof DOMElement (not inGreatGrandChild) So importNode() doesn't even cast to a consistent DOMNode, but to the 'highest* level in the built-in classes. Usually in OO although the cast is to a super-class (to guarantee portability) the extended methods and properties aren't discarded. If importNode() were fixed to return the same class as passed in the following code would solve the ownerDocument issue: <?php class extDOMDocument extends DOMDocument { public function createElement($name, $value=null) { $orphan = new extDOMElement($name, $value); $adopt = $this->importNode($ret, true); // adopt it // now $adopt satisfies $this->isSameNode($adopt->ownerDocument) && $adopt instanceof extDOMelement return $adopt; } } class extDOMElement extends DOMElement { function __construct($name, $value=null, $namespace=null) { parent::__construct($name, $value, $namespaceURI); } // ... more class definition here } (excuse any typo's - working weird hours!)Following on from my suggestion to provide a strong design template for read-only properties and inheritence, I've put together the following example. It provides for inheritence of a read-only property so the property can be modified from sub-classes. <?php class ReadOnly { const CLASS_READ_ONLY_PROPERTY_ERR = 1; protected $realProperty; private $dynamicProperty = array(); function __construct() { $this->realProperty = 12; $this->test = 'read-only'; } public function __set($name, $value) { if($name=='test') { if(get_class($this)==__CLASS__ && isset($this->dynamicProperty[$name])) throw new Exception(self::CLASS_READ_ONLY_PROPERTY_ERR); else $this->dynamicProperty[$name] = $value; } } public function __get($name) { return $this->dynamicProperty[$name]; } public function getReal() {return $this->realProperty; } public function getDynamic() {return $this->test; } } class Writeable extends ReadOnly { function __construct($value) { parent::__construct(); $this->realProperty = 25; // ok $this->test = $value; // causes Fatal Error } } echo "Testing Writeable...\r\n"; $test = new Writeable('write to me'); echo 'real: '.$test->getReal()."\r\n"; echo 'dynamic: '.$test->getDynamic()."\r\n"; echo "Testing ReadOnly...\r\n"; $test = new ReadOnly(); echo 'real: '.$test->getReal()."\r\n"; echo 'dynamic: '.$test->getDynamic()."\r\n"; try { $test->test = "can't change me"; } catch(Exception $e) { if ($e->getMessage() == ReadOnly::CLASS_READ_ONLY_PROPERTY_ERR) echo "Read-only Property Exception"; } ?> Thanks for your prompt and considered attention to this issue. Hopefully this provides a solution that elegantly solves the issue for all concerned. TJ. Nottingham, UKAnd finally... for completeness here's a worked example that solves the DOM case of setting the ownerDocument property. <?php class extDOMDocument extends DOMDocument { public function createElement($name, $value=null) { $orphan = new extDOMElement($name, $value); // new sub-class object $docFragment = $this->createDocumentFragment(); // lightweight container maintains "ownerDocument" $docFragment->appendChild($orphan); // attach return $docFragment; } // .. more class definition } class extDOMElement extends DOMElement { function __construct($name, $value='', $namespaceURI=null) { parent::__construct($name, $value, $namespaceURI); } // ... more class definition here } $doc = new extDOMDocument('test'); $el = $doc->createElement('tagname'); // append discards the DOMDocumentFragment and just adds its child nodes, but ownerDocument is maintained. $doc->appendChild($el); echo $doc->saveXML(); ?> TJ.Based on real-world experience the previous example is not sufficent - the new Element is destroyed along with the DOMDocumentFragment when the method exits. The fix is to remove the new element from the DOMDocumentFragment before returning it to the caller. Here's an updated, tested example. <?php class extDOMDocument extends DOMDocument { public function createElement($name, $value=null) { $orphan = new extDOMElement($name, $value); // new sub-class object $docFragment = $this->createDocumentFragment(); // lightweight container maintains "ownerDocument" $docFragment->appendChild($orphan); // attach $ret = $docFragment->removeChild($orphan); // remove return $ret; // ownerDocument set; won't be destroyed on method exit } // .. more class definition } class extDOMElement extends DOMElement { function __construct($name, $value='', $namespaceURI=null) { parent::__construct($name, $value, $namespaceURI); } // ... more class definition here } $doc = new extDOMDocument('test'); $el = $doc->createElement('tagname'); // append discards the DOMDocumentFragment and just adds its child nodes, but ownerDocument is maintained. $doc->appendChild($el); echo $doc->saveXML(); ?>