Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Html5 Laravel Package

masterminds/html5

HTML5-PHP is a standards-compliant HTML5 parser and serializer written entirely in PHP. It includes both a DOM tree builder and an event-based SAX-like parser, supports PHP namespaces and Composer, interoperates with QueryPath, and requires PHP 7.4+. Stable and widely used in production.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require masterminds/html5

There is no service provider, facade, or config file — it is a plain library. In Laravel you either new HTML5() inline, inject it into a constructor, or bind it once in a provider:

// app/Providers/AppServiceProvider.php
$this->app->singleton(\Masterminds\HTML5::class, fn () => new \Masterminds\HTML5([
    'disable_html_ns' => true,
]));

The two calls you will use 95% of the time:

use Masterminds\HTML5;

$html5 = app(HTML5::class);

$dom = $html5->loadHTML($rawHtml);   // returns a real \DOMDocument
$out = $html5->saveHTML($dom);       // spec-compliant HTML5 string
$html5->save($dom, storage_path('app/out.html'));

Why reach for this instead of DOMDocument::loadHTML(): PHP's native loader is libxml's HTML4 parser. It rejects HTML5 elements (<section>, <main>, <template>, <picture>), chokes on unclosed void elements, emits warnings you have to suppress, and mangles UTF-8 unless you prepend a <meta charset> hack. This package is a real HTML5 tokenizer/tree-builder written in PHP, it is lenient the way a browser is, and it round-trips through a companion serializer so the output is deterministic and spec-shaped (<br> not <br/>, bare boolean attributes, etc.).

Where to look first: src/HTML5.php (the facade you actually use — loadHTML, loadHTMLFile, save, saveHTML, and the $options array), then src/Parser/DOMTreeBuilder.php and src/Parser/EventHandler.php when you need to hook the token stream. The README's "Options" and "Known Issues" lists are the two sections that will save you the most debugging time; the issue queue is the de-facto changelog for parser edge cases.

First realistic use case: you accept user- or CMS-authored HTML (rich text from a WYSIWYG, pasted email bodies, rendered Blade output) and need to normalize it — strip disallowed tags, rewrite links, add rel="noopener" to externals, or diff it — before storing or re-rendering. Parse to DOMDocument, walk it with DOMXPath, mutate, serialize back.

$dom = $html5->loadHTML($request->input('body'));

$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//a[starts-with(@href, "http")]') as $a) {
    $a->setAttribute('rel', 'noopener noreferrer');
    $a->setAttribute('target', '_blank');
}

return $html5->saveHTML($dom);

Implementation Patterns

Wrap it in a single application service, not scattered new HTML5() calls. You want the options array in exactly one place, because serializer behaviour (encode_entities) and namespace behaviour (disable_html_ns) must stay consistent across every code path — otherwise content stored by one job serializes differently in another.

namespace App\Support;

use Masterminds\HTML5;

class HtmlNormalizer
{
    private HTML5 $html5;

    public function __construct()
    {
        $this->html5 = new HTML5([
            'disable_html_ns' => true,   // keeps DOMXPath simple, see Gotchas
            'encode_entities' => false,  // only escape the bare minimum
        ]);
    }

    public function clean(string $html, array $allowedTags = ['p','a','ul','ol','li','strong','em','br']): string
    {
        $dom = $this->html5->loadHTML($html);
        $xpath = new \DOMXPath($dom);

        // walk deepest-first so removing a parent doesn't invalidate the node list
        $nodes = iterator_to_array($xpath->query('//*'));
        foreach (array_reverse($nodes) as $node) {
            if (! in_array($node->nodeName, $allowedTags, true)) {
                while ($node->firstChild) {
                    $node->parentNode->insertBefore($node->firstChild, $node);
                }
                $node->parentNode->removeChild($node);
            }
        }

        foreach (iterator_to_array($xpath->query('//@*')) as $attr) {
            if (str_starts_with($attr->nodeName, 'on')) {
                $attr->ownerElement->removeAttribute($attr->nodeName);
            }
        }

        return $this->html5->saveHTML($dom);
    }
}

Note this is a normalizer, not a security boundary. Keep mews/purifier or HTML Purifier in front of anything rendered unescaped; this library deliberately has no allow-list policy engine.

Fragment handling — wrap, parse, extract. There is no separate fragment API you can rely on across versions, and the tree builder will otherwise move your snippet into a synthesized <html><body> anyway. The reliable pattern is to parse a full document and lift the body's children out:

$dom = $html5->loadHTML('<!DOCTYPE html><html><body>' . $fragment . '</body></html>');
$body = $dom->getElementsByTagName('body')->item(0);

$out = '';
foreach ($body->childNodes as $child) {
    $out .= $html5->saveHTML($child);   // saveHTML() accepts any \DOMNode
}

That last line is worth remembering: saveHTML() takes any node, not just a document, so serializing a single <td> or a <ul> subtree is trivial.

target_document for accumulating output. When you are merging many rendered blocks into one tree (e.g. a page builder assembling sections), pass a shared DOMDocument:

$dom = new \DOMDocument();
$html5 = new HTML5(['target_document' => $dom, 'disable_html_ns' => true]);

foreach ($blocks as $block) {
    $blockDom = $html5->loadHTML('<div>' . $block . '</div>'); // nodes land in $dom
}

Be aware this mutates the shared document on each loadHTML() call — it is a "parse into this tree" flag, not "append a document."

Bulk / queued processing. Parsing is pure PHP and CPU-bound, ~1–2 orders of magnitude slower than libxml. For a few hundred emails or imports, push each document onto a queue job rather than looping in a request; for a few thousand, chunk them (->chunkById(100)) and unset() the DOMDocument at the end of each iteration so PHP's cycle collector can actually reclaim it.

SAX-style parsing when you only need to extract, not rebuild. Implementing Masterminds\HTML5\Parser\EventHandler and driving the Scanner/Tokenizer yourself lets you react to startTag, text, endTag, and comment events without ever materializing a DOM. This is the right tool for "pull all <h2> text out of 50 MB across 5,000 files" — you still load each document fully into memory, but you skip the DOM construction cost. Wire it as:

$scanner   = new \Masterminds\HTML5\Parser\Scanner(
    new \Masterminds\HTML5\Parser\StringInputStream($html)
);
$tokenizer = new \Masterminds\HTML5\Parser\Tokenizer($scanner, $myEventHandler);
$tokenizer->parse();

Rendered-output assertions in tests. When asserting on Blade/PHPUnit output, assert on the reserialized form of both sides. Raw string comparison breaks on attribute ordering, self-closing slashes, and entity choices that are semantically identical:

$expected = $html5->saveHTML($html5->loadHTML($expectedBlade));
$actual   = $html5->saveHTML($html5->loadHTML($response->getContent()));
$this->assertSame($expected, $actual);

Scraping / integration with symfony/dom-crawler. If masterminds/html5 is installed, Symfony's Crawler picks it up as its HTML5 parser — so Goutte, symfony/panther-adjacent flows, and anything using Crawler::html() get better HTML5 parsing with zero code changes. If you also want XPath/CSS queries directly, DOMXPath + Crawler::filterXPath() on the parsed DOMDocument covers almost everything without pulling in a heavy scraping stack.

Serializer customization. For anything the default output rules get wrong for your use case (e.g. you want trailing slashes on void elements for XHTML email), implement Masterminds\HTML5\Serializer\RulesInterface and inject it — the Traverser + OutputRules split exists precisely so you can swap the rules without touching the tree walker.

Gotchas and Tips

The default HTML namespace will break your XPath. By default every element lands in http://www.w3.org/1999/xhtml, so //body, //a, getElementsByTagName behave unexpectedly and plain DOMXPath queries silently return empty. Two fixes: set 'disable_html_ns' => true at construction (simplest, and fine for content processing), or register the prefix:

$xpath = new \DOMXPath($dom);
$xpath->registerNamespace('h', 'http://www.w3.org/1999/xhtml');
$xpath->query('//h:body//h:a');

Silent empty result sets from XPath is the number-one time sink with this library. If a query returns nothing, check the namespace first, not the markup.

Framework-generated attribute names get silently dropped. PHP's DOM refuses attribute names that aren't valid XML 1.0 names, and the parser skips them rather than erroring. That means Alpine's @click, Vue's :class / @submit, and any [bracket]-style attribute disappear on a parse→serialize round trip. wire:click and x-data survive (colons and hyphens are legal). If you are normalizing Livewire/Inertia markup, do not run it through this parser unless you have audited the attributes — or you will ship templates with the interactivity quietly deleted. Verify with a serialization diff in a test before trusting it in a build step.

Option names are inconsistent between the README and the code. The document options listed in the README are snake_case (disable_html_ns, encode_entities, target_document, implicit_namespaces), while the XML-namespace section uses camelCase (xmlNamespaces, implicitNamespaces). Both styles appear in the wild and in the source; before depending on one, dump your installed HTML5 and DOMTreeBuilder default-options arrays and confirm the key you're passing is actually being read. A typo'd option key fails silently — no exception, just default behaviour.

It is not a validator and has no Adoption Agency Algorithm. Misnested inline markup (<b><i>text</b></i>), tables with stray content, and heavily broken documents will produce a different tree than Chrome/Firefox. For well-formed CMS output this never bites you; for scraping hostile third-party pages it will, and you will spend an afternoon on a DOMXPath query that "should" work. If browser-identical parsing matters, that's a headless-browser job, not this.

Do not use it to sanitize untrusted HTML for output. The library is explicitly non-validating and preserves whatever it doesn't understand, including onclick, javascript: URLs, and <script> bodies. It will happily hand you back an XSS payload in cleaner-looking markup. Combine with a real sanitizer, and escape on output as usual.

<script> and <style> are raw text. Their contents are not escaped by the serializer, which is correct per spec but means a user-supplied </script> inside a string you thought was safely embedded can terminate the block. Never build JSON-in-script via string concatenation through this serializer; use @json() in Blade.

Memory in long-running workers and Octane. DOMDocument graphs participate in reference cycles and are not freed promptly. In a queue worker processing thousands of documents, memory climbs. Explicitly unset($dom, $xpath); per iteration and run gc_collect_cycles() every N items. The HTML5 instance itself is safe to keep as a singleton — it stores only options and builds a fresh tokenizer and tree builder per loadHTML() call — but the documents are not.

loadHTMLFile() is a thin convenience, not a streaming reader. It reads the whole file into a string. For large files, read and process in chunks yourself, or reconsider whether you need a DOM at all and drop to the event-based parser.

Version notes worth pinning. 2.8.1 is the last release with a clean changelog; 2.9.0 added optional end tags for table elements — if you have tests asserting exact serialization of <table> markup, re-run them after upgrading. The package requires ext-dom (2.8.0 dropped ext-libxml but not ext-dom), and runs on PHP 7.4+; on 8.2+ you want ≥ 2.8 for the deprecation fixes. Since it is a transitive dependency of symfony/dom-crawler, a composer update in a Laravel app can bump it under you — constrain it in composer.json if you depend on exact serializer output.

Errors are quiet by default. DOMTreeBuilder is lenient and won't throw on malformed input. If you need to know about bad markup (e.g. flagging broken CMS content), implement your own EventHandler that logs parseError() calls with line/column, or extend DOMTreeBuilder and collect them — the hook is there, it just isn't wired to anything visible.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log