views:

62

answers:

5

Hello,

I'm trying to extend two native PHP5 classes (DOMDocument and DOMNode) to implement 2 methods (selectNodes and selectSingleNode) in order to make XPath queries easier. I thought it would be rather straighforward, but I'm stuck in a problem which I think is an OOP beginner's issue.

class nDOMDocument extends DOMDocument {
    public function selectNodes($xpath){
    $oxpath = new DOMXPath($this);
    return $oxpath->query($xpath);
  }
  public function selectSingleNode($xpath){
    return $this->selectNodes($xpath)->item(0);
  }
}

Then I tried to do extend DOMNode to implement the same methods so I can perform an XPath query directly on a node:

class nDOMNode extends DOMNode {
    public function selectNodes($xpath){
    $oxpath = new DOMXPath($this->ownerDocument,$this);
    return $oxpath->query($xpath);
  }
  public function selectSingleNode($xpath){
    return $this->selectNodes($xpath)->item(0);
  }
}

Now if I execute the following code (on an arbitrary XMLDocument):

$xmlDoc = new nDOMDocument;
$xmlDoc->loadXML(...some XML...);
$node1 = $xmlDoc->selectSingleNode("//item[@id=2]");
$node2 = $node1->selectSingleNode("firstname");

The third line works and returns a DOMNode object $node1. However, the fourth line doesn't work because the selectSingleNode method belongs to the nDOMNode class, not DOMNode. So my question: is there a way at some point to "transform" the returned DOMNode object into a nDOMNode object? I feel I'm missing some essential point here and I'd greatly appreciate your help.

(Sorry, this is a restatement of my question http://stackoverflow.com/questions/2573820/)

+1  A: 

You'd need to code nDOMDocument::selectSingleNode() to return a nDOMNode object. There's no magical transform that can happen.

I say you should continue with your experiment as you will learn some good OOP lessons along the way (albeit, the hard way). Nothing wrong with that.

webbiedave
A: 

I think you may have to take the long way here and install pointing properties that point down, up, left and right, somewhat like the DOM does for JavaScript. These will necessarily be assigned when creating the structure.

amphetamachine
+1  A: 

One of the consequences of the object-oriented approach is that inheritance is a one-way street. That is, there is no way to make the parent aware of the methods of the children. Thus while you may be able to iterate through a collection of objects, you can only reliably call the methods of the parent class. PHP does have ways to test if a method exists at runtime, and this can be useful but it can also get ugly quickly.

That means that when you extend a built-in class, you need to completely extend it so that your application is never working with instances of the built-in class--only your version. In your case, you need to expand your extended classes to override all of the parent's methods that return the parent's class type:

public myClass extends foo {
// override all methods that would normally return foo objects so that they
// return myClass objects.
}
Nathan Strong
A: 

I think you'll be better off here decorating the existing library with wrapper objects, rather than directly subclassing them. As you can see already, you can't easily append helper methods to DOMDocument and have it return your subclassed objects.

Specifically, the problem here is DOMXPath::item() does not know to return a nDOMNode.

If you tried to do something like the following, it would fail (many properties appear to be read only):

class nDOMDocument extends DOMDocument {

    ...

    public function selectSingleNode($xPath) {
        $node = $this->selectNodes($xPath)->item(0);
        $myNode = new nDOMNode;
        $myNode->set_name($node->get_name()); // fail?
        $myNode->set_content($node->get_content());
        // set namespace
        // can you set the owner document? etc

        return $myNode;
    }

}

If you wrap the object instead you could quickly expose the existing DOMNode interface using the __get() and __call() magic methods, and include your additional functionality and elect to return your own wrapped/custom classes to achieve your original goal.

Example:

class nDOMNode {

    protected $_node;

    public function __construct(DOMNode $node) {
        $this->_node = $node;
    }

    // your methods

}

class nDOMDocument {

    protected $_doc;

    public function __construct(DOMDocument $doc) {
        $this->_doc = $doc;
    }

    ...

    public function selectNodes($xPath){
        $oxPath = new DOMXPath($this->_doc->ownerDocument, $this->_doc);
        return $oxPath->query($xPath);
    }

    public function selectSingleNode($xPath) {
        return new nDOMNode($this->selectNodes($xPath)->item(0));
    }

}

$doc = new nDOMDocument(new DOMDocument);
$doc->loadXML('<xml>');
$node = $doc->selectSingleNode("//item[@id=2]"); // returns nDOMNode

Hope this helps.

Greg K
Thanks for your answer, this is what I expected, I'm learning a lot.
Glauber Rocha
A: 

You can "tell" a DOMDocument via DOMDocument::registerNodeClass() which class it shall use instead of the default classes when instantiating an object for a certain node type. E.g.

$doc->registerNodeClass('DOMElement', 'Foo');

each time the dom would "normally" instantiate a DOMElement it will now create an instance of Foo.

$doc = new nDOMDocument;
$doc->loadxml('<a><b><c>foo</c></b></a>');

$a = $doc->selectSingleNode('//a');
$c = $a->selectSingleNode('//c');

echo 'content: ', $c->textContent;

class nDOMElement extends DOMElement {
  public function selectNodes($xpath){
    $oxpath = new DOMXPath($this->ownerDocument);
    return $oxpath->query($xpath);
  }
  public function selectSingleNode($xpath){
    return $this->selectNodes($xpath)->item(0);
  }
}


class nDOMDocument extends DOMDocument {
  public function __construct($version=NULL, $encoding=NULL) {
    parent::__construct($version, $encoding);
    $this->registerNodeClass('DOMElement', 'nDOMElement');
  }

  public function selectNodes($xpath){
    $oxpath = new DOMXPath($this);
    return $oxpath->query($xpath);
  }
  public function selectSingleNode($xpath){
    return $this->selectNodes($xpath)->item(0);
  }
}

prints content: foo.

But you can't simply set registerNodeClass('DOMNode', 'MyDOMNode') and expect a DOMElement from now on to inherit from MyDOMNode instead of DOMNode. I.e. you have to register all node types you want to overwrite.

VolkerK
Thanks a lot for your answer. It's interesting and very useful to see that DOMDocument has a method to specifically achieve this.
Glauber Rocha