popphp / pop-dom
Pop DOM Component for Pop PHP Framework
Requires
- php: >=8.4.0
- ext-dom: *
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^12.5.0
README
Overview
pop-dom is a component for generating, rendering and parsing DOM documents and elements. With it,
you can easily create or parse document nodes and their children and have control over node content and
attributes.
pop-dom is a component of the Pop PHP Framework.
Install
Install pop-dom using Composer.
composer require popphp/pop-dom
Or, require it in your composer.json file
"require": {
"popphp/pop-dom" : "^5.0.0"
}
Quickstart
A simple DOM node fragment
use Pop\Dom\Child; $div = new Child('div'); $h1 = new Child('h1', 'This is a header'); $p = new Child('p'); $p->setNodeValue('This is a paragraph.'); $p->setAttribute('class', 'paragraph'); $div->addChildren([$h1, $p]); echo $div;
<div> <h1>This is a header</h1> <p class="paragraph">This is a paragraph.</p> </div>
Building a full DOM document
use Pop\Dom\Document; use Pop\Dom\Child; // Title element $title = new Child('title', 'This is the title'); // Meta tag $meta = new Child('meta'); $meta->setAttributes([ 'http-equiv' => 'Content-Type', 'content' => 'text/html; charset=utf-8' ]); // Head element $head = new Child('head'); $head->addChildren([$title, $meta]); // Some body elements $h1 = new Child('h1', 'This is a header'); $p = new Child('p', 'This is a paragraph.'); $div = new Child('div'); $div->setAttribute('id', 'content'); $div->addChildren([$h1, $p]); // Body element $body = new Child('body'); $body->addChild($div); // Html element $html = new Child('html'); $html->addChildren([$head, $body]); // Create and render the DOM document $doc = new Document(Document::HTML, $html); echo $doc;
<!DOCTYPE html> <html> <head> <title>This is the title</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <div id="content"> <h1>This is a header</h1> <p>This is a paragraph.</p> </div> </body> </html>
Working with Nodes
Child::create() is a static-factory alternative to new Child(...), useful for fluent chaining:
use Pop\Dom\Child; $p = Child::create('p', 'Some text', ['attributes' => ['class' => 'lead']]); echo $p;
<p class="lead">Some text</p>
The constructor's $options array is shorthand for calling several setters at once. Supported keys:
attributes (array, same as setAttributes()), cData (bool, same as setAsCData()), childrenFirst
(bool, same as setChildrenFirst()), indent (string, same as setIndent()), and whitespace (bool, same
as preserveWhiteSpace()):
$note = new Child('note', 'Hello & welcome', [ 'cData' => true, 'attributes' => ['id' => 'greeting'] ]); echo $note;
<note id="greeting"><![CDATA[Hello & welcome]]></note>
Node name and value
$h = new Child('h2', 'Original'); $h->setNodeName('h3'); $h->setNodeValue('Replaced'); echo $h->getNodeName(); // h3 echo $h->getNodeValue(); // Replaced $note = new Child('note', 'Hello'); $note->addNodeValue(' World'); // appends to the existing value echo $note->getNodeValue(); // Hello World
Attributes
$el = new Child('div'); $el->setAttribute('id', 'main'); $el->setAttributes(['data-role' => 'content', 'class' => 'box']); $el->hasAttribute('id'); // true $el->getAttribute('class'); // 'box' $el->getAttributes(); // ['id' => 'main', 'data-role' => 'content', 'class' => 'box'] $el->hasAttributes(); // true $el->removeAttribute('class'); $el->hasAttribute('class'); // false
Attribute values are HTML-escaped automatically when rendered, so it's safe to pass values containing quotes or other markup characters.
Tree navigation and mutation
$ul = new Child('ul'); $ul->addChildren([new Child('li', 'One'), new Child('li', 'Two'), new Child('li', 'Three')]); $ul->hasChildren(); // true (hasChildNodes() is an alias) count($ul->getChildren()); // 3 (getChildNodes() is an alias) $ul->getChild(1)->getNodeValue(); // 'Two' $ul->removeChild(0); // removes 'One'; remaining children are reindexed from 0 $ul->getChild(0)->getNodeValue(); // 'Two' $ul->getChild(0)->getParent(); // the $ul Child instance $ul->removeChildren(); // clears all children $ul->hasChildren(); // false
Rendering Options
Value-first vs. children-first
By default, a node's own value renders before its children. setChildrenFirst() reverses that:
$p = new Child('p', 'Value last:'); $p->setChildrenFirst(true); $p->addChild(new Child('strong', 'child')); echo $p;
<p> <strong>child</strong> Value last: </p>
Whitespace and indentation
Output is pretty-printed (indented, one node per line) by default. preserveWhiteSpace(false) collapses a
node's own markup onto a single line instead:
$div = new Child('div'); $div->preserveWhiteSpace(false); $div->addChild(new Child('span', 'inline')); echo $div;
<div> <span>inline</span> </div>
setIndent() overrides the indentation string used for that specific node instead of the default
4-space-per-depth-level convention:
$div = new Child('div'); $div->setIndent(' '); $div->addChild(new Child('span', 'two-space indent')); echo $div;
<div> <span>two-space indent</span> </div>
CDATA
setAsCData() wraps a node's value in <![CDATA[ ... ]]>, useful for XML/RSS/Atom content that may contain
markup characters that shouldn't be escaped:
$note = new Child('note', 'Value with <special> & characters'); $note->setAsCData(); echo $note;
<note><![CDATA[Value with <special> & characters]]></note>
isCData() reports whether the flag is set.
Extracting content
getNodeContent() returns a node's inner markup (its children, rendered, without the node's own opening/
closing tag); getTextContent() does the same but strips all tags, leaving plain text. Both accept an
$ignoreWhiteSpace flag that collapses runs of whitespace to single spaces and normalizes spacing around
sentence punctuation (. ? ! , : ;) — handy for pulling readable text out of parsed or hand-built markup:
$note = new Child('note', ' Extra whitespace here. '); echo $note->getNodeContent(); // ' Extra whitespace here. ' echo $note->getNodeContent(true); // 'Extra whitespace here. ' $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])); echo $p->getNodeContent(true); // 'Some <strong>bold</strong> text' echo $p->getTextContent(true); // 'Some bold text'
Document Types
Document supports four doctype constants. XML, RSS and ATOM all emit the same XML declaration and
differ only in the Content-Type they report; HTML emits an HTML5 doctype:
use Pop\Dom\Document; $doc = new Document(); // Document::XML is the default $doc->getDoctype(); // <?xml version="1.0" encoding="utf-8"?> $doc->getContentType(); // application/xml $rss = new Document(Document::RSS); $rss->getContentType(); // application/rss+xml $atom = new Document(Document::ATOM); $atom->getContentType(); // application/atom+xml
setCharset()/getCharset() control the encoding declared in the XML doctype; setContentType() overrides
the reported content type directly. Note that Document only builds the markup string — sending it with the
right Content-Type header (e.g. via $response->addHeaders(['Content-Type' => $doc->getContentType()]) in
a full Pop application) is up to the caller.
A minimal RSS feed, built the same way as any other document:
$item = new Child('item'); $item->addChildren([ new Child('title', 'First Post'), new Child('link', 'https://example.com/first-post'), ]); $channel = new Child('channel'); $channel->addChildren([ new Child('title', 'My Feed'), new Child('link', 'https://example.com'), $item ]); $rssRoot = new Child('rss'); $rssRoot->setAttribute('version', '2.0'); $rssRoot->addChild($channel); $doc = new Document(Document::RSS, $rssRoot); echo $doc;
<?xml version="1.0" encoding="utf-8"?> <rss version="2.0"> <channel> <title>My Feed</title> <link>https://example.com</link> <item> <title>First Post</title> <link>https://example.com/first-post</link> </item> </channel> </rss>
Parsing
You can parse from a string of XML or HTML and it will return an object of Child elements that you can further manipulate or edit to then output:
$html = <<<HTML <html> <head> <title>Hello World Title</title> </head> <body> <h1 class="top-header" id="header">Hello World Header</h1> <p>How are <em>YOU</em> doing <strong><em>today</em></strong>???</p> <p class="special-p">Some <strong class="bold">more</strong> text.</p> </body> </html> HTML; $doc = new Document(Document::HTML); $doc->addChild(Child::parseString($html)); echo $doc;
And you can parse from a file as well:
$children = Child::parseFile('index.html');
Return shape
Both parseString() and parseFile() share the same Child|array|null return type, which depends on the
shape of the input:
- A full document (containing
<html>and<body>) returns a singleChild— the<html>root — as shown above. - A fragment with multiple top-level nodes (e.g.
"<div>one</div><div>two</div>") returns anarrayof top-levelChildinstances instead, since there's no single root to return. - Input with no element nodes at all (e.g. an empty or whitespace-only string) returns
null.
Because of this, prefer addChildren() over addChild() when the input shape isn't guaranteed to be a
single root — addChildren() accepts either a Child or an array of Child:
$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>
Mixed text and element content
The parser represents interleaved text and elements (e.g. <p>How are <em>YOU</em> doing?</p>) as a mix of
regular Child nodes and Child nodes named #text — a #text node renders just its value, with no
surrounding tag. This is a public pattern you can also use when building mixed content by hand instead of
parsing it:
$p = new Child('p'); $p->addChild(new Child('#text', 'How are ', ['whitespace' => true])); $p->addChild(new Child('em', 'YOU', ['whitespace' => true])); $p->addChild(new Child('#text', ' doing?', ['whitespace' => true])); echo $p;
<p> How are <em>YOU</em> doing? </p>
Exceptions
Pop\Dom\Exception is the one exception type this component throws:
use Pop\Dom\Document; use Pop\Dom\Child; use Pop\Dom\Exception; try { (new Document())->setDoctype('BOGUS'); // invalid doctype } catch (Exception $e) { /* ... */ } try { (new Child('div'))->addChildren('not-a-child'); // addChildren() given something other than a Child/array } catch (Exception $e) { /* ... */ } try { Child::parseFile('/no/such/file.html'); // parseFile() given a nonexistent path } catch (Exception $e) { /* ... */ }