Page 1 of 1

Extending DOMElement *and* DOMDocument in PHP5?

Posted: Thu Nov 03, 2005 10:52 pm
by TJ
I'm developing a package that models KML, the Google Earth geometry modelling language. As a long-time OO programmer I expected it to be trivial to extend DOMDocument and DOMElement to support the required extensions.

However I'm getting an annoying error:

Fatal error: KMLElement::__construct() [function.--construct]: Cannot write property in...

This is a problem I can't seem to resolve - there is no way to set or overload the DOMNode->ownerDocument property, which means my KMLDocument cannot tell a new KMLElement its ownerDocument because PHP5 reports the property as read-only - even when inherited (in real OO it'd be unavailable if private, and available if protected or public.

DOMNodes created without an ownerDocument are read-only, which means the new element is next to useless, and there appears to be no way to set this property when the new element is being created by a method in a class extended from DOMDocument.
If the element was created using DOMDocument->createElement() method then ownerDocument is set correctly, but there is no way to get it to create a KMLElement as I need.

Code: Select all

class KMLDocument extends DOMDocument {
 public function createElement($name, $value=null) {
  if(KMLSchema::isValidTagName($name)) {
    $ret = new KMLElement($name, $value, $this); // create the new element with this Document as owner
  }
  else
   throw new DOMException(DOM_NOT_SUPPORTED_ERR);
  return $ret;
 }
}
// ... more class definition here
}

class KMLElement extends DOMElement {
 function __construct($name, $value='', $owner=null, $namespaceURI=null) {
   if(!$owner instanceof KMLDocument)
    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
}

Posted: Fri Nov 04, 2005 3:08 am
by TJ
I appear to have found a work-around, and I hope it will be of use to others.

The trick is to create the inherited element as an orphan before attempting to add child nodes or attributes, and use DOMDocument->importNode() to adopt it into KMLDocument:

Code: Select all

class KMLDocument extends DOMDocument {
  public function createElement($name, $value=null) {
    if(KMLSchema::isValidTagName($name)) {
      $orphan = new KMLElement($name, $value); // create the new element
      $ret = $this->importNode($orphan, true); // adopt it (set's its ownerDocument property somehow)
    }
    else
      throw new DOMException(DOM_NOT_SUPPORTED_ERR);
  
    return $ret;
  }
}

Posted: Fri Nov 04, 2005 8:57 am
by TJ
Unfortunately the work-around has severe drawbacks.

Primarily because importNode() returns an object of the super class (DOMElement) rather than of the class passed to it - KMLElement. This is worse in that the new object doesn't have the functionality given it when it extended DOMElement.

This appears to be a 2nd bug in PHP Object-handling. The other is when using the magical method __set() to make dynamic properties read-only its not possible to modify the property in the sub-class.

The DOM classes are implemented directly in C. I've inspected the source code but can't see a way to get around this issue.

Posted: Fri Nov 04, 2005 8:59 am
by feyd
the __set() bug is easy to work around... use instanceof against $this to check if the calling class is a child ...

Posted: Fri Nov 04, 2005 6:13 pm
by TJ
Yes, thats the way to do it. Let's just hope developers are disciplined enough to implement that design template in modular classes.

I've had a discussion with the PHP developers today about these issues (Bug 35104), and hopefully the documentation will have strong recommendations with a design template to save a lot of grief later.

The solution to the read-only DOMNode->ownerDocument property is to use an intermediate DOMDocumentFragment:

Code: Select all

class KMLDocument extends DOMDocument {
public function createElement($name, $value=null) {
  if(KMLSchema::isValidTagName($name)) {
    $orphan = new KMLElement($name, $value); //new sub-classed element 
    $docFragment = $this->createDocumentFragment(); // lightweight container, has ownerDocument property of parent

    /* must attach and then remove othwerwise $orphan is destructed along with $docFragment when
         this method destroys its local variables
    */
    $docFragment->appendChild($orphan); // orphan adopts docFragment's ownerDocument property
    $ret = $docFragment->removeChild($orphan); //  with this KMLDocument as owner
  }
  else
   throw new DOMException(DOM_NOT_SUPPORTED_ERR);

  return $ret;
}
}
// ... more class definition here
}