tomsseisums
tomsseisums

Reputation: 13367

Extending DOMNode, afterwards extending DOMDocument, that inherits extended DOMNode

Oh yes, the title is awesome, I know, I know. Sorry, not native English, and, it's possible that the title doesn't reflect the problem, but I'll do my best.

I'm working on a relatively large extension to DOM library. Recently, I was thinking of rewriting it to extend upon standard library. Though, I have stumbled into problems due to inheritance.

One of the core elements of DOM is DOMNode, so I started by extending that one:

<?php namespace DOMWorks;

use \DOMNode;

class Node extends DOMNode
{
    // methods...
}

Then, I went forward to try to work on DOMDocument, that, by default, extends DOMNode.

<?php namespace DOMWorks;

use \DOMDocument;

class Document extends DOMDocument
{
    // methods...
}

But, this loses track of previously extended Node.

How would I extend DOMNode, from that extend DOMDocument and from that create my own Document extension?

Upvotes: 1

Views: 1567

Answers (3)

llange
llange

Reputation: 767

What about using "proxy pattern" with traits ? The idea being declaring the common methods inside a "trait" in order for extended and registered Node Classes to have access even if not derived / child of the extended DOMNode…

Here's a small snippet posted on PHP.net :

    namespace my;

    trait tNode
    {    // We need the magic method __get in order to add properties such as DOMNode->parentElement
        public function __get($name)
        {    if(property_exists($this, $name)){return $this->$name;}
            if(method_exists($this, $name)){return $this->$name();}
            throw new \ErrorException('my\\Node property \''.(string) $name.'\' not found…', 42, E_USER_WARNING);
        }

        // parentElement property definition
        private function parentElement()
        {    if($this->parentNode === null){return null;}
            if($this->parentNode->nodeType === XML_ELEMENT_NODE){return $this->parentNode;}
            return $this->parentNode->parentElement();
        }

        // JavaScript equivalent
        public function isEqualNode(\DOMNode $node){return $this->isSameNode($node);}
        public function compareDocumentPosition(\DOMNode $otherNode)
        {    if($this->ownerDocument !== $otherNode->ownerDocument){return DOCUMENT_POSITION_DISCONNECTED;}
            $c = strcmp($this->getNodePath(), $otherNode->getNodePath());
            if($c === 0){return 0;}
            else if($c < 0){return DOCUMENT_POSITION_FOLLOWING | ($c < -1 ? DOCUMENT_POSITION_CONTAINED_BY : 0);}
            return DOCUMENT_POSITION_PRECEDING | ($c > 1 ? DOCUMENT_POSITION_CONTAINS : 0);
        }
        public function contains(\DOMNode $otherNode){return ($this->compareDocumentPosition($otherNode) >= DOCUMENT_POSITION_CONTAINED_BY);}
    }

    class Document extends \DomDocument
    {    public function __construct($version=null, $encoding=null)
        {    parent::__construct($version, $encoding);
            $this->registerNodeClass('DOMNode', 'my\Node');
            $this->registerNodeClass('DOMElement', 'my\Element');
            $this->registerNodeClass('DOMDocument', 'my\Document');
            /* [...] */
        }
    }

    class Element extends \DOMElement
    {    use tNode;
        /* [...] */
    }

    class Node extends \DOMNode
    {    use tNode;
        /* [...] */
    }

Upvotes: -1

hakre
hakre

Reputation: 198119

How would I extend DOMNode, from that extend DOMDocument and from that create my own Document extension?

In PHP we do not have multiple inheritance. That means, what you're trying to do is not possible in PHP. The reason is that DOMDocument extends from DOMNode (as does DOMElement, DOMAttr, DOMText etc.) so this inheritance path is already finished (DaveRandom explained this more lengthy and probably better worded in his answer, if you wonder, I mean the same).

PHP 5.4 made the situation a little bit better as you're able to put code that is shared between all your subtypes (e.g. the code you would have put into your DOMNode) into traits.

Each subtype you create can use these traits then (you see an example shortly).

If you additionally want to make them even all a your DOMNode type, you can also define an interface that is empty that you then implement with all your subtypes.

The following is an example of that technique from an exemplary scraper library:

class ScraperDoc extends DOMDocument implements ScraperNodeType
{
    use ScraperNodeTrait;

    ...

As it shows, it implements an interface (ScraperNodeType) and also a trait (ScraperNodeTrait).

So there is the interface:

/**
 * Empty Interface for typing reasons (instanceof operator and traits
 * work not well, this interface cures that limitation a bit)
 */
interface ScraperNodeType
{
}

And there is the trait; If you're new to traits, here is some example code, a one-method trait that provides string context for all nodes implementing the trait (just to give an idea, it's shortened from the original library):

Trait ScraperNodeTrait
{
    public function __toString()
    {
        /* @var $this DOMNode  */
        return trim($this->textContent);
    }
}

This is not as fluid as with traits/mixins in Ruby but as close as you can get (so far with non-dynamic code) in PHP.

This still yet does not solve all problems in creating your own hierarchy, but I think you should know about this technique (traits + empty interface).

Here is an inheritance diagram that shows the DOMNode on top, then the extended types from the PHP DOM extension, then the user-land extensions of these and their relationships to the trait (left below) and interface (right below).

The cluser on the right part is related to iterators and simplexml which is not part of this answer so not of direct interest. Albeit it shows for example that you can not overload DOMNodeList in PHP. There are some insane moves possible with SimpleXML which is why that library has it as part of the whole picture as well.

Then at the bottom-left you find a reference to Net_URL2 which is IMHO the best PHP URL class so far. The library extends from it so to have it's own URL type and the external lib is at least layered into the codebase.

enter image description here

Example Scraping Library based on DOMDocument Inheritance Diagram (full-size)

I hope this helps and also gives some inspiration. Last time I answered a question about extending DOMDocument was about the phenomena that DOMDocument is a Model, but not your Model:

Upvotes: 2

DaveRandom
DaveRandom

Reputation: 88697

Fun multiple inheritance is fun.

After a bit more playing around, I have concluded it simply isn't possible to do this in a meaningful way using inheritance alone. The only practical way to do this is by decoration (i.e. wrapping the native classes in your own classes without extending, and using magic methods to access the properties of the inner object). Obviously this is not ideal, it very quickly starts to get messy and you lose obvious benefits that true inheritance provides, things like playing nice with instanceof, is_a() et al.

The root of the problem is that because registerNodeClass() (see original answer below) is an instance method, it can only be called on an instance of a DOMDocument - and by the time the instance has been created, the inheritance has already been determined - you can't alter the base class of an object after it has been created.

There is (as far as I can tell) no way to statically set PHP to always create instances based on your extended base class at the point of new Document, which is when it would need to be done in order for your custom document to inherit your custom node. You probably wouldn't want this anyway, as it's hidden global state that could affect the consuming application beyond the reaches of your library.

I'm still playing with a slightly ugly idea involving traits to facilitate the decorator pattern, but I'm not sure yet if what I'm thinking is actually possible or whether I should simply go but to sitting in the corner, rocking back and forth and quietly muttering to myself.

I may expand on this at some point if I come up with anything more useful/concrete or generally less like a brain-fart. The first sentence below makes me want to cry :-(


Original answer:

Luckily, and in a pleasant break from the norm, the PHP devs foresaw this possibility and accommodated you in a (reasonably) sensible way:

DOMDocument::registerNodeClass()

You can build this into your lib (so the consumer doesn't have to do it) in the constructor for your extended document class:

<?php

namespace DOMWorks;

use \DOMDocument;

class Document extends DOMDocument
{
    public function __construct($version = null, $encoding = null)
    {
        $this->registerNodeClass('DOMNode', __NAMESPACE__ . '\Node');
    }

    // methods...
}

Upvotes: 4

Related Questions