Pop PHP
The Toolkit

DOM

pop-dom builds markup as a tree of objects rather than a string of concatenated tags. Construct nodes, give them values, attributes and children, and render at the end. It works both directions — the parser turns existing markup back into the same objects.

BASH
composer require popphp/pop-dom

Building a Document#

Every node is a Pop\Dom\Child. The constructor takes a node name and, optionally, its value:

PHP
use Pop\Dom\Child;

$div = new Child('div');
$h1  = new Child('h1', 'This is a header');
$p   = new Child('p', 'This is a paragraph.');

$p->setAttribute('class', 'paragraph');
$div->addChildren([$h1, $p]);

echo $div;

That renders pretty-printed, four spaces per level of depth:

HTML
<div>
    <h1>This is a header</h1>
    <p class="paragraph">This is a paragraph.</p>
</div>

A fragment is enough on its own — Child renders itself when you echo it. Pop\Dom\Document wraps a tree in a doctype and reports the content type that belongs with it:

PHP
use Pop\Dom\Child;
use Pop\Dom\Document;

$html = new Child('html');
$html->addChild(new Child('body', 'Hello'));

$doc = new Document(Document::HTML, $html);

echo $doc;                  // <!DOCTYPE html> followed by the tree
$doc->getContentType();     // 'text/html'

Four doctypes exist. Document::XML is the default and emits an XML declaration; Document::RSS and Document::ATOM emit the same declaration and differ in the content type they report; Document::HTML emits <!DOCTYPE html>.

Document builds the markup string; put $doc->getContentType() on the response header yourself.

Child Nodes and Attributes#

addChild() takes one node, addChildren() takes an array of them or a single node. Children are held in an ordered list, addressed by index:

PHP
use Pop\Dom\Child;

$ul = new Child('ul');
$ul->addChildren([new Child('li', 'One'), new Child('li', 'Two'), new Child('li', 'Three')]);

$ul->hasChildren();                // true
count($ul->getChildren());         // 3
$ul->getChild(1)->getNodeValue();  // 'Two'
$ul->getChild(0)->getParent();     // the $ul instance

$ul->removeChild(0);               // 'One' goes; the rest reindex from 0
$ul->getChild(0)->getNodeValue();  // 'Two'

removeChild() reindexes, so an index you captured before a removal points at a different node afterward. getChild() on an index that does not exist returns null rather than throwing. hasChildNodes() and getChildNodes() are aliases of hasChildren() and getChildren(), and removeChildren() clears the list.

The name and value are both mutable after construction, and addNodeValue() appends rather than replaces:

PHP
use Pop\Dom\Child;

$note = new Child('note', 'Hello');

$note->addNodeValue(' World');
$note->getNodeValue();  // 'Hello World'

$note->setNodeName('memo');
$note->getNodeName();   // 'memo'

Attributes are a flat map, set one at a time or in bulk:

PHP
use Pop\Dom\Child;

$div = new Child('div');

$div->setAttribute('id', 'main');
$div->setAttributes(['data-role' => 'content', 'class' => 'box']);

$div->hasAttribute('id');    // true
$div->getAttribute('class'); // 'box'

$div->removeAttribute('class');
$div->getAttribute('class'); // null

Values are HTML-escaped on render, so an attribute holding quotes or angle brackets comes out safe without any work on your part: setAttribute('data-q', 'He said "hi" & <b>') renders as data-q="He said &quot;hi&quot; &amp; &lt;b&gt;".

Child::create() is a static factory that takes the same name and value plus an options array, which is shorthand for a run of setters — attributes, cData, childrenFirst, indent and whitespace:

PHP
use Pop\Dom\Child;

$p = Child::create('p', 'Some text', ['attributes' => ['class' => 'lead']]);

echo $p;  // <p class="lead">Some text</p>

addChildren() given anything that is not a Child or an array of them throws Pop\Dom\Exception ("Error: The parameter passed must be an instance of Pop\Dom\Child or an array of Pop\Dom\Child instances.").

Rendering#

A node renders itself when you cast it to a string, and pretty-prints by default: one node per line, four spaces per level of depth. Three settings change that shape.

A node's own value renders before its children. setChildrenFirst(true) flips the order:

PHP
use Pop\Dom\Child;

$p = new Child('p', 'Value last:');
$p->setChildrenFirst(true);
$p->addChild(new Child('strong', 'child'));

echo $p;
HTML
<p>
    <strong>child</strong>
    Value last:
</p>

preserveWhiteSpace(false) drops the newlines and indentation from a node's markup, and setIndent() replaces the indentation string used for that node and everything beneath it:

PHP
use Pop\Dom\Child;

$div = new Child('div');
$div->setIndent('  ');
$div->addChild(new Child('span', 'two-space indent'));

echo $div;
HTML
  <div>
      <span>two-space indent</span>
  </div>

The custom indent applies to the node itself as well as its children, which is why the <div> is pushed in two spaces even though it's the outermost node.

preserveWhiteSpace(false) applies per node, so set it on each node whose whitespace you want collapsed.

setAsCData() wraps a node's value in a CDATA section, which is what you want for feed content that may contain markup characters you do not want escaped:

PHP
use Pop\Dom\Child;

$note = new Child('note', 'Value with <special> & characters');
$note->setAsCData();

echo $note;  // <note><![CDATA[Value with <special> & characters]]></note>

Two methods pull content back out without the node's own tags. getNodeContent() returns the inner markup; getTextContent() returns the same thing with all tags stripped. Both take a flag that collapses runs of whitespace:

PHP
use Pop\Dom\Child;

$p = new Child('p');
$p->addChild(new Child('#text', 'Some ', ['whitespace' => true]));
$p->addChild(new Child('strong', 'bold', ['whitespace' => true]));
$p->addChild(new Child('#text', ' text', ['whitespace' => true]));

$p->getNodeContent(true);  // 'Some <strong>bold</strong> text'
$p->getTextContent(true);  // 'Some bold text'

That flag also normalizes spacing around . ? ! , : ; one character at a time, so leave it on for prose and off for text carrying repeated punctuation.

Parsing Existing Markup#

Child::parseString() and Child::parseFile() turn XML or HTML into the same Child objects you build by hand, so anything on this page applies to markup you did not write:

PHP
use Pop\Dom\Child;

$root = Child::parseString('<html><body><h1 id="title">Hello</h1></body></html>');

$root->getNodeName();                     // 'html'
$root->getChild(0)->getNodeName();        // 'body'
$root->getChild(0)->getChild(0)
     ->getAttribute('id');                // 'title'

The return type is Child|array|null. A full document returns one Child for the <html> root, a fragment with several top-level nodes returns an array, and unparseable input returns null.

Because the shape is not known ahead of time, reach for addChildren() rather than addChild() when feeding parsed output into a document — it accepts either form:

PHP
use Pop\Dom\Child;
use Pop\Dom\Document;

$doc = new Document(Document::HTML);
$doc->addChildren(Child::parseString('<div>one</div><div>two</div>'));

echo $doc;
HTML
<!DOCTYPE html>
<div>one</div>
<div>two</div>

parseFile() reads the same content from a path and throws Pop\Dom\Exception ("Error: That file does not exist.") when the path is not there. It does not fetch over HTTP; hand it a local file, or fetch the markup yourself and pass the string to parseString().

Text interleaved with elements is represented as Child nodes named #text, which render their value with no surrounding tag. That's the same construct used in the previous section to build mixed content by hand — parsing and building produce identical trees.

A parse followed by a render is not byte-identical: each #text node becomes its own indented line.

See Also#

  • Views & Templates — rendering markup from templates, the usual alternative to building it as a tree
  • CSS & Color — the same generate-and-parse shape, applied to stylesheets
  • MIME — content types for the documents you produce
  • pop-dom README — every method on Child and Document