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.
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:
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:
<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:
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:
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:
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:
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 "hi" & <b>".
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:
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:
use Pop\Dom\Child;
$p = new Child('p', 'Value last:');
$p->setChildrenFirst(true);
$p->addChild(new Child('strong', 'child'));
echo $p;
<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:
use Pop\Dom\Child;
$div = new Child('div');
$div->setIndent(' ');
$div->addChild(new Child('span', 'two-space indent'));
echo $div;
<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:
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:
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:
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:
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;
<!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
ChildandDocument