views:

76

answers:

2

registerNodeClass is great for extending the various DOMNode-based DOM classes in PHP, but I need to go one level deeper.

I've created an extDOMElement that extends DOMElement. This works great with registerNodeClass, but I would like to have something that works more like this: registerNodeClass("DOMElement->nodeName='XYZ'", 'extDOMXYZElement')

Consider the following XML document, animals.xml:

<animals>
    <dog name="fido" />
    <dog name="lucky" />
    <cat name="scratchy" />
    <horse name="flicka" />
</animals>

Consider the following code:

extDomDocument extends DOMDocument {
    public function processAnimals() {
        $animals = $this->documentElement->childNodes;
        foreach($animals as $animal) {
            $animal->process();
        }
    }
}


extDOMElement extends DOMElement {
    public function process() {
        if ($this->nodeName=='dog'){
            $this->bark();
        } elseif ($this->nodeName=='cat'){
            $this->meow();
        } elseif  ($this->nodeName=='horse'){
            $this->whinny();
        }
        this->setAttribute('processed','true');
    }
    private function bark () {
        echo "$this->getAttribute('name') the $this->nodeName barks!";
    }
    private function meow() {
        echo "$this->getAttribute('name') the $this->nodeName meows!";
    }
    private function whinny() {
        echo "$this->getAttribute('name') the $this->nodeName whinnies!";
    }
}

$doc = new extDOMDocument();
$doc->registerNodeClass('DOMElement', 'extDOMElement');
$doc->loadXMLFile('animals.xml');
$doc->processAnimals();
$doc->saveXMLFile('animals_processed_' . Now() . '.xml');

Output: fido the dog barks! lucky the dog barks! scratchy the cat meows! flicka the horse whinnies!

I don't want to have to put bark(), meow() and whinny() into extDOMElement - I want to put them into extDOMDogElement, extDOMCatElement and extDOMHorseElement, respectively.

I've looked at the Decorator and Strategy patterns here, but I'm not exactly sure how to proceed. The current setup works OK, but I'd prefer to have shared properties and methods in extDOMElement with separate classes for each ElementName, so that I can separate methods and properties specific to each Element out of the main classes.

A: 

EDIT for the code you show, wouldn't it be easier not to extend DOMElement at all? Just pass in the regular DOMElements to your processing Strategies, e.g.

class AnimalProcessor
{
    public function processAnimals(DOMDocument $dom) {
        foreach($dom->documentElement->childNodes as $animal) {
            $strategy = $animal->tagName . 'Strategy';
            $strategy = new $strategy($animal);
            $strategy->process();
        }
    }
}

$dom = new DOMDocument;
$dom->load('animals.xml');
$processor = new AnimalProcessor;
$processor->processAnimals($dom);

Original answer before question update

Not sure if this is what you are looking for, but if you want specialized DOMElements, you can simply create them and use them directly, e.g. bypassing createElement, so you dont have to registerNodeClass at all.

class DogElement extends DOMElement
{
    public function __construct($value) 
    {
        parent::__construct('dog', $value);
    }
}
class CatElement extends DOMElement
{
    public function __construct($value) 
    {
        parent::__construct('cat', $value);
    }
}
$dom = new DOMDocument;
$dom->loadXML('<animals/>');
$dom->documentElement->appendChild(new DogElement('Sparky'));
$dom->documentElement->appendChild(new CatElement('Tinky'));
echo $dom->saveXml();

I don't think you can easily use registerNodeClass to instantiate elements based on the tagname or influence parsing that much. But you can override DOMDocument's createElement class, e.g.

class MyDOMDocument extends DOMDocument
{
    public function createElement($nodeType, $value = NULL)
    {
        $nodeType = $nodeType . 'Element';
        return new $nodeType($value);
    }
}
$dom = new MyDOMDocument;
$dom->loadXML('<animals/>');
var_dump( $dom->createElement('Dog', 'Sparky') ); // DogElement
var_dump( $dom->createElement('Cat', 'Tinky') ); // CatElement
echo $dom->saveXml();
Gordon
That's pretty awesome. I sorta lied above - I'm actually doing XML-authoring. As you can see from my (edited) question, I need for the correct types of Element objects to be created when I load the XML into the document. I'm going to give the factory example a shot and see if it's used by DOMDocument when creating elements based on loaded XML. Thanks for your help!
Tex
@Tex you're welcome but I really think you are overcomplicating things with extending DOMElement. You could easily have your Strategies decoupled as show in my edit. Your choice though.
Gordon
I'll just add a note here - in process(), I might also want to add an attribute to each Element - call it `processed="true"`, and I might also want to add a new child to each Element - say `<action name="barked | meowed | whinnied" />`, then save the changed XML document back to animals_procced_on_(date).xml at the end of processing. Also, I'm currently writing the starting XML docs by hand, but I'd eventually like to create them via code as well - so, create, then process, in which case I think the Factory may make more sense. If you think otherwise, feel free to let me know.
Tex
Sorry - it's sort of a complicated question. I apologize for changing the parameters on you as we go. I really didn't do a good job of explaining what I need.
Tex
@Tex well, you can do it with subclassed DOMElement nodes, but I think a dedicated processor would be better suited and easier. Basically, your approach says, a DOMElement is a Strategy, while a dedicated processor would say a Strategy uses a DOMElement. I find the latter conceptually cleaner and likely better to maintain in the long run, because it doesnt tie the implementation of the DOMElement to be processing logic.
Gordon
@Tex I had to remove the ElementFactory. It wasnt working as expected. Sorry.
Gordon
Sounds to me like you're looking for an jaxb equivalent for php.
VolkerK
@VolkerK - Yes, that looks very similar to what I've built. My tool is certainly not as complex or full-featured as jaxb - I use a simpler, lighter set of instructions in my XML documents. I'm not too happy having all of my methods in the single extDOMElement and having to use if or switch statements to decide which methods to call. Still looking for a solution to that - I haven't gotten a factory to work, and I need for loadXML to create the correct Element types, which is trickier than creating the types and appending them to an existing DOMDocument object.
Tex
@Gordon - No worries, and thanks for the help. In my case, I'm pretty sure that the node is the strategy, so I'm still looking for a solution.
Tex
@VolkerK also, I don't need to create a class for every potential node name. I have a limited vocabulary of private node names, each of which I'd like to tie to its own class... I like the DOM classes because they're standards-based and seem to be really solid. As of yet, I haven't needed to override any of their methods, I'm just adding new methods that I need.
Tex
@Gordon Thanks for working on this with me. Your answer's a good one, as well, but I decided to go with a solution similar to Volker's answer, instead.
Tex
@Tex no problem. You're welcome.
Gordon
A: 

I can't really pin point it but what you're trying has a certain smell, like Gordon already pointed out.
Anyway... you could use __call() to expose different methods on your extDOMElement object depending on the actual node (type/contents/...). For that purpose your extDOMElement object could store an helper object which is instantiated according to the "type" of the element and then delegate method calls to this helper object. Personally I don't like that too much as it doesn't exactly make documentation, testing and debugging any easier. If that sounds feasible to you I can write down a self-contained example.


This certainly needs comments/documentation ...work in progress since I don't have the time right now...

<?php
$doc = new MyDOMDocument('1.0', 'iso-8859-1');
$doc->loadxml('<animals>
  <Foo name="fido" />
  <Bar name="lucky" />
  <Foo name="scratchy" />
  <Ham name="flicka" />
  <Egg name="donald" />
</animals>');
$xpath = new DOMXPath($doc);
foreach( $xpath->query('//Foo') as $e ) {
  echo $e->name(), ': ', $e->foo(), "\n";
}
echo "----\n";
foreach( $xpath->query('//Bar') as $e ) {
  echo $e->name(), ': ', $e->bar(), "\n";
}
echo "====\n";
echo $doc->savexml();


class MyDOMElement extends DOMElement {
  protected $helper;

  public function getHelper() {
    // lazy loading and caching the helper object
    // since lookup/instantiation can be costly
    if ( is_null($this->helper) ) {
      $this->helper = $this->resolveHelper();
    }
    return $this->helper;
  }

  public function isType($t) {
    return $this->getHelper() instanceof $t;
  }

  public function __call($name, $args) {
    $helper = $this->getHelper();
    if ( !method_exists($helper, $name) ) {
      var_dump($name, $args, $helper);
      throw new Exception('yaddayadda');
    }
    return call_user_func_array( array($this->helper, $name), $args);
  }

  public function releaseHelper() {
    // you might want to consider something like this
    // to help php with the circular references
    // ...or maybe not, haven't tested the impact circual references have on php's gc
    $this->helper = null;
  }

  protected function resolveHelper() {
    // this is hardcored only for brevity's sake
    // add any kind of lookup/factory/... you like
    $rv = null;
    switch( $this->tagName ) {
      case 'Foo':
      case 'Bar':
        $cn = "DOMHelper".$this->tagName;
        return new $cn($this);
      default:
        return new DOMHelper($this);
        break;
    }
  }
}

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

class DOMHelper {
  protected $node;
  public function __construct(DOMNode $node) {
    $this->node = $node;
  }
  public function name() { return $this->node->getAttribute("name"); }
}

class DOMHelperFoo extends DOMHelper {
  public function foo() {
    echo 'foo';
    $this->node->appendChild(  $this->node->ownerDocument->createElement('action', 'something'));
  }
}

class DOMHelperBar extends DOMHelper {
  public function bar() {
    echo 'bar';
    $this->node->setAttribute('done', '1');
  }
}

prints

fido: foo
scratchy: foo
----
lucky: bar
====
<?xml version="1.0"?>
<animals>
  <Foo name="fido"><action>something</action></Foo>
  <Bar name="lucky" done="1"/>
  <Foo name="scratchy"><action>something</action></Foo>
  <Ham name="flicka"/>
  <Egg name="donald"/>
</animals>
VolkerK
Tex
I still think you shouldn't mix that "into" the DOMDocument. But anyway, give me a few minutes...
VolkerK
@VolkerK - I don't have to mix it into DOMDocument, but I do want to be able to update an element based on the results of the associated object's activities. If I say dog->bark() and the dog can't bark at the moment, I can store that information in the XML document and try to re-run it later, from where I left off. I can flag each element as successfully processed, so I have a sort of log of activities, and again, so I could try to re-run the entire thing from where I left off, if the need arises (i.e. an external resource is not available at the moment, but may become available later).
Tex
@VolkerK I also need to have full access to the document and to the DOM objects and methods from within my objects, as I may need for one object to ask another object what its value is (which could be done via XPath query, for instance). So I will have a living document that changes as it's processed and that spawns a tree of objects, having each do its job. If you can think of a better way to do this, hopefully without having to pass my $doc and $nodes into every object, I'm all ears.
Tex
@VolkerK Thanks for the ideas. I went with a solution similar to yours - instantiating a helper object inside each extDOMElement.
Tex