wikimedia/idle-dom
IDLeDOM provides PHP interfaces and helper traits for the DOM as defined by Web IDL, enabling type-safe documents, nodes, and elements with interchangeable implementations. It helps libraries offer standards-compliant DOM behavior without binding to a single backend.
wikimedia/idle-dom is not a DOM implementation — it is a set of PHP interfaces generated from the DOM WebIDL spec (plus a Helper trait that gives implementations the IDL-style magic property accessors). You almost never install it alone.
Minimum viable setup in a Laravel app:
composer require wikimedia/idle-dom wikimedia/dodo wikimedia/remex-html
wikimedia/idle-dom — the interfaces (IDLeDOM\Node, IDLeDOM\Element, IDLeDOM\Document, …)wikimedia/dodo — the concrete implementation that actually does the workwikimedia/remex-html — HTML5-spec-compliant tokenizer/tree builder, which is what makes the parse behave like a browser instead of like DOMDocument::loadHTML()There is no service provider, no config, no facade. It's a library, not a Laravel package; nothing to register.
First use case — typed, spec-correct HTML parsing inside a service class:
use Dodo\Document;
$doc = Document::loadHTML('<p class="lead">Hello <b>world</b></p>');
$p = $doc->getElementsByTagName('p')->item(0);
echo $p->textContent; // "Hello world"
echo $p->getAttribute('class'); // "lead"
Where to look first: read the IDLeDOM interface files themselves (vendor/wikimedia/idle-dom/src/) — they are auto-generated straight from the spec and are the best documentation you'll get. Then read Dodo's README for the concrete factory/parsing helpers, and Parsoid (MediaWiki) for real-world usage patterns. The GitHub repo is only a mirror; the upstream code and issue tracker live on Gerrit/Phabricator, so don't expect PRs or issues on GitHub to be triaged.
Type-hint the interfaces, not the implementation. This is the entire point of the package — decouple your application code from Dodo so a faster or stricter implementation can be swapped in later (or mocked in tests):
namespace App\Services;
use IDLeDOM\Element;
final class ClassCollector
{
/** @return list<string> */
public function collect(Element $root): array
{
$classes = [];
foreach ($root->getElementsByTagName('*') as $el) {
foreach (preg_split('/\s+/', $el->getAttribute('class') ?? '') as $c) {
if ($c !== '') {
$classes[$c] = true;
}
}
}
return array_keys($classes);
}
}
Wrap parsing in one place. A single factory/service keeps the Dodo dependency contained:
final class HtmlDocument
{
public function parse(string $html): \IDLeDOM\Document
{
return \Dodo\Document::loadHTML($html);
}
}
If your Dodo version doesn't expose loadHTML(), wire RemexHtml manually (Tokenizer → Dispatcher → TreeBuilder → DOMBuilder) once, in that same class.
IDL-style attributes via magic accessors. Implementations using IDLeDOM\Helper expose spec-named properties alongside the PHP method names:
$el->textContent; // same as $el->getTextContent() in effect
$el->textContent = 'x'; // setter
$el->attributes; // IDLeDOM\NamedNodeMap
$el->childNodes; // IDLeDOM\NodeList (iterable, countable)
$el->parentElement;
Prefer the explicit get*/set* methods in application code — they're static-analysable, and Psalm/PHPStan understands them without a plugin.
Sanitisation / rewriting pass. A common Laravel use: accept rich-text from a WYSIWYG, strip dangerous attributes, re-serialise.
$doc = $this->html->parse($userHtml);
foreach (iterator_to_array($doc->getElementsByTagName('*')) as $el) {
foreach (['onclick', 'onerror', 'style'] as $attr) {
if ($el->hasAttribute($attr)) {
$el->removeAttribute($attr);
}
}
}
$clean = $doc->saveHTML(); // or getInnerHTML() on the body element
Iterate over a snapshot (iterator_to_array or collecting into an array first) — getElementsByTagName returns a live list, and mutating while iterating will bite you.
Structured extraction into DTOs. Parse once, walk once, map to your own value objects — don't leak IDLeDOM\* types out of the service layer:
$items = [];
foreach ($doc->getElementsByTagName('article') as $article) {
$items[] = new ArticleDto(
title: trim($article->getElementsByTagName('h2')->item(0)?->textContent ?? ''),
body: $article->textContent,
);
}
Queue jobs for heavy parsing. Parsing large documents is CPU-bound; dispatch a job and cache the extracted result rather than parsing on every web request.
new IDLeDOM\Document() does not exist. Every "instantiate" path goes through Dodo (or your own implementation). Mixing native \DOMDocument objects into methods typed against IDLeDOM\Node will fatal — they are unrelated hierarchies.childNodes / getElementsByTagName are live. Snapshot before mutating, and never rely on indices surviving an insertion.textContent vs nodeValue vs innerHTML. textContent returns raw text (no entity decoding surprises, no tags); innerHTML serialises children. For HTML5 serialisation semantics, use Dodo's serialiser rather than hand-rolling string concatenation — attribute quoting and void elements are easy to get wrong.DOMDocument::loadHTML() is HTML4-era and mangles modern markup (e.g. <template>, <figure> nesting, unquoted attributes in edge cases). If your output must match a browser, use RemexHtml/Dodo, not loadHTML().getElementsByTagName won't match namespaced nodes; use getElementsByTagNameNS. Most Laravel HTML inputs are text/html so this only bites with SVG/MathML embedded content.IDLeDOM\DOMException (or RemexHtml errors, depending on configuration), not as false returns like the native API. Wrap in try/catch and decide whether to reject or repair malformed input.gerrit.wikimedia.org; bugs go to Phabricator. Forking the GitHub mirror and opening a PR is a dead end. Pin versions in composer.json and be prepared to vendor a patch if you hit an upstream bug.ext-dom. Fine for parsing a few documents per request or in a job; not fine for a loop over 10k documents. Cache aggressively (Cache::remember keyed on a content hash) and consider native DOMDocument/Symfony DomCrawler for trivial, well-formed extraction where spec fidelity doesn't matter.IDLeDOM\* interfaces yourself and use IDLeDOM\Helper; — the trait supplies the magic property glue, so you only implement the real methods. That's the supported way to add a second backend.saveHTML() / getInnerHTML()), not on object graphs — object identity isn't stable between Dodo versions.How can I help you explore Laravel packages today?