vardumper/html5-twig-component-bundle
A Symfony bundle for building reusable HTML5-style Twig components. Define components as Twig templates and render them as custom elements with attributes and slots, making it easier to organize and reuse UI markup across your application.
vardumper/extended-htmldocument is a thin DOMDocument subclass that swaps libxml's HTML4 parser for masterminds/html5, giving you an HTML5-correct parse tree plus CSS-selector querying (via symfony/css-selector). In a Laravel app it is a parsing tool, not a browser: think "extract structured data from an HTML string I already have."
composer require vardumper/extended-htmldocument
Requirements are PHP 8.x, ext-dom, masterminds/html5, and symfony/css-selector; all are pulled in transitively, so there is no service provider or config to publish.
Where to look first, in order:
vendor/vardumper/extended-htmldocument/src/ — the surface is one class (Vardumper\ExtendedHtmlDocument\HtmlDocument). Read it end to end; it is a few hundred lines and is the real documentation.composer.json of the package — confirm the masterminds/html5 constraint so you know which HTML5 spec revision you are parsing against.loadHTML() / createFromString()-style entry points against src/ rather than trusting a blog post.First use case: a queued job that pulls a supplier's product page or an inbound HTML email and extracts prices/SKUs, without dragging in headless Chrome.
use Vardumper\ExtendedHtmlDocument\HtmlDocument;
$doc = new HtmlDocument();
$doc->loadHTML($response->body()); // HTML5-correct parse tree
$price = $doc->querySelector('.product-price')?->nodeValue;
$skus = $doc->querySelectorAll('ul.variants li[data-sku]');
Verify the class name and method signatures in src/ before writing more than a scratch script — that is the single highest-value five minutes you will spend on this package.
Wrap it, don't sprinkle it. Bind a small extractor per source, so the parser never leaks into controllers or jobs:
final class SupplierPageExtractor
{
public function extract(string $html): SupplierProductData
{
$doc = new HtmlDocument();
$doc->loadHTML($this->normalise($html));
return new SupplierProductData(
title: trim($doc->querySelector('h1')?->nodeValue ?? ''),
price: $this->toCents($doc->querySelector('.price')?->nodeValue),
skus: $this->collectSkus($doc->querySelectorAll('li[data-sku]')),
);
}
private function normalise(string $html): string
{
// Fragment without <html>/<head>? Give the parser a charset hint.
return str_starts_with(ltrim($html), '<!DOCTYPE')
? $html
: '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>'.$html.'</body></html>';
}
}
Inject it into a job, wrap the call in Http::retry(...)->get(...), and persist a DTO. Keep HTTP fetching outside the extractor so you can unit-test parsing against fixtures with zero network.
Iterate NodeLists deliberately. querySelectorAll() returns a DOMNodeList; convert once and map, rather than re-querying inside the loop:
$skus = collect(iterator_to_array($doc->querySelectorAll('li[data-sku]')))
->map(fn (DOMElement $li) => $li->getAttribute('data-sku'))
->filter()
->unique()
->values();
Assert HTML structure in feature tests. This is the underrated day-to-day win. Instead of brittle assertSee string matching on markup that shifts whitespace, parse the response body and assert on selectors:
$doc = new HtmlDocument();
$doc->loadHTML($this->get('/products')->getContent());
$this->assertCount(3, $doc->querySelectorAll('.product-card'));
$this->assertSame('Out of stock', trim($doc->querySelector('.stock-badge')?->nodeValue));
Pair this with a tests/Fixtures/ directory of real-world HTML snapshots (one per supplier/template) so parser regressions surface as failing tests rather than bad rows in the database.
Mutate, then re-serialise. The document is writable, which makes it useful for normalising third-party HTML before storing or re-rendering it — strip tracking pixels, rewrite relative src attributes, drop scripts:
foreach (iterator_to_array($doc->querySelectorAll('script, iframe.tracker')) as $node) {
$node->parentNode->removeChild($node);
}
$clean = $doc->saveHTML();
Cheap structural validation. In a FormRequest or an import pipeline, use the parser to reject "HTML" that is actually plain text or truncated markup before it reaches your renderer.
composer require vardumper/extended-htmldocument:0.3.* and commit the lock file. There is no BC promise below 1.0; minor bumps have moved method names before. 0.3.4 is a patch release cut from an automated sync, so it should be a drop-in inside 0.3.x, but read the diff in src/ if anything selector-related misbehaves after composer update.masterminds/html5 inserts implied elements the way a browser does — <tbody> inside tables, <head>/<body> scaffolding, auto-closed <p> tags. XPath written against libxml's output will silently miss nodes. Always query with CSS selectors, or dump $doc->saveHTML() when a selector returns null.<meta charset="utf-8"> in any fragment you wrap. Mojibake in extracted text is almost always a missing charset hint, not a parser bug.querySelector() returns null, querySelectorAll() returns an empty list. No exceptions. Guard every scalar extraction with ?->nodeValue and an explicit fallback, otherwise a template change upstream becomes a TypeError in a queue worker at 3am. Validate extracted DTOs before persisting.saveHTML() as safe for {!! !!}. Removing <script> tags via selectors is not an XSS defence — use a dedicated sanitiser (e.g. mews/purifier) for anything user-supplied.str_contains before parsing when you only need one field.loadHTML() in a try/catch and log the source URL plus a truncated body — the HTML5 parser is lenient but still rejects genuinely broken byte sequences, and the message alone won't tell you which supplier broke.getElementById() actually works here. With native DOMDocument it requires a DTD to be loaded first; the HTML5 parser preserves id semantics, so this is one of the quieter reasons to prefer the package.masterminds/html5 + symfony/dom-crawler directly, or wrap this class behind your own HtmlExtractor interface now — then swapping implementations is a one-file change.DOMDocument, so you can subclass it to add project helpers (e.g. text(string $selector): ?string) without forking. Keep such a subclass in app/Support/, not in vendor/.How can I help you explore Laravel packages today?