1

Let's say I want to append a div to a DOMDocument. I can do so with:

<?php
$dom = new DOMDocument();

$dom->appendChild(
    $dom->createElement("div")
);

Now, say I want to add some text to that div, so I try:

<?php
$dom = new DOMDocument();

$dom->appendChild(
    $dom->createElement("div")
        ->appendChild( $dom->createTextNode("foobar") )
);

But wait! Now there is a problem!

In the first case, $dom->createElement("div") returned an empty "div" DOMNode, which appendChild() had no problem accepting.

But in the second case, $dom->createElement("div")->appendChild($dom->createTextNode("foobar")) returns the already appended "foobar" DOMText. So the "div" DOMNode does not get appended, and php throws a warning.

Warning: DOMNode::appendChild(): Couldn't fetch DOMText

My question is, is there a way to get the method chain to return the original modified (with the DOMText appended) DOMNode that is returned by createElement()?

I know I could just save the DOMNode to a variable and then pass it to appendChild() but I would really love to see a one liner solution.

Thanks.

Fiddle: http://codepad.org/PFB3Ns7E

Nikita240
  • 1,377
  • 2
  • 14
  • 29
  • You could use `$dom->createElement('div', 'foobar');` – Phylogenesis Jan 17 '15 at 00:56
  • It's not exactly the same, but `createElement('div', 'foobar')` will create an element with the given text. – Barmar Jan 17 '15 at 00:56
  • I know. That was actually my original implementation, but it fails if there is a "&" character in the value. You're "supposed to" add the content by appending a text node. http://stackoverflow.com/a/22957785/2449639 – Nikita240 Jan 17 '15 at 00:57
  • 1
    Or use `html_entities()` – Phylogenesis Jan 17 '15 at 00:57
  • @Phylogenesis That would only work assuming createElement would decode the entities. – Nikita240 Jan 17 '15 at 01:01
  • 1
    @Nikita240 What's wrong with that? The output looks correct. – Phylogenesis Jan 17 '15 at 01:03
  • @Phylogenesis I tested it thoroughly, and you're right. The output is always the same as appending a text node every time. But I still want to know if it's possible to do what I am trying. – Nikita240 Jan 17 '15 at 01:10
  • 1
    @Nikita240 Not as far as I can tell. `DOMNode::appendChild()` returns the added node, so you either use an extra variable and use `DomDocument::createTextNode()` or you use a oneliner with `htmlentities()`. – Phylogenesis Jan 17 '15 at 01:12
  • @Nikita240 Installed the new version of PHP 5.6 so I could test my answer and fixed a couple bugs. (Also had the spread operator backwards.) [Works now.](http://stackoverflow.com/a/27995532/2407870) – Chris Middleton Jan 18 '15 at 06:36

2 Answers2

1

If you have PHP 5.6+, you can use the spread/splat operator (...) combined with the magic method __call to monkey patch the class to fit your needs.

Basically, you create two proxy classes that mimic the behavior of DOMDocument and DOMNode, called DOMDocumentPlus and DOMNodePlus. Each class houses a private link to the real class, and by using the magic method __call, we can delegate almost all calls to the original classes' methods. Then when appendChild is called on your DOMNodePlus element, you return $this rather than the child.

DOMDocumentPlus class

class DOMDocumentPlus {

    private $dom;

    public function __construct(...$args) {
        $this->dom = new DOMDocument(...$args);
    }

    public function __call($name, $args) {
        if($name !== 'createElement') {
            return $this->dom->$name(...$args);
        } else {
            return new DOMNodePlus($this->dom->createElement(...$args));
        }
    }   

}

DOMNodePlus class

class DOMNodePlus {

    private $node;

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

    public function __call($name, $args) {
        if($name !== 'appendChild') {
            return $this->node->$name(...$args);
        } else {
            $this->node->appendChild(...$args);
            return $this->node;
        }
    }

}

main program

$dom = new DOMDocumentPlus();

$dom->appendChild(
    $dom->createElement("div")->appendChild($dom->createTextNode("foobar"))
);

$dom->formatOutput = true;
echo $dom->saveHTML();
Chris Middleton
  • 5,654
  • 5
  • 31
  • 68
  • 1
    A ready to use version of a library doing something like that is FluentDOM - http://fluentdom.github.io/ – johannes Jan 17 '15 at 02:25
  • Here is no need for proxy classes, you can extend the existing DOMDocument (and register extended node classes). But you should not implement behavior conflicting with DOM standard. FluentDOM both extends the DOM and has special classes for XML creation. – ThW Jan 30 '15 at 11:06
1

Yes, append the div first:

$dom = new DOMDocument();

$dom
  ->appendChild(
    $dom->createElement("div")
  )
  ->appendChild( 
     $dom->createTextNode("foobar") 
  );

echo $dom->saveXml();

Output:

<?xml version="1.0"?>
<div>foobar</div>

You can use the $parentNode property to move up again.

$dom = new DOMDocument();

$dom
  ->appendChild(
    $dom->createElement("div")
  )
  ->appendChild( 
     $dom->createTextNode("foobar") 
  )
  ->parentNode
  ->appendChild(
     $dom->createElement("span")
  )
  ->appendChild( 
     $dom->createTextNode("FOOBAR") 
  );

echo $dom->saveXml();

Output:

<?xml version="1.0"?>
<div>foobar<span>FOOBAR</span></div>

But do not overuse it. Long chaining like that are not easily maintained.

ThW
  • 19,120
  • 3
  • 22
  • 44