wikimedia/zest-css
Zest is a fast PHP CSS selector parser and matcher for DOM documents, letting you query HTML/XML with familiar CSS selectors. Lightweight and standards-focused, it’s useful for scraping, testing, and DOM traversal.
Architecture fit. Zest is a standalone, stateless library that answers one question well: given a CSS selector and a DOM subtree, return the matching nodes (Zest\Zest::getElementsBySelector($selector, $rootNode)). It has no framework coupling, no service provider, no global state, and no I/O. In a Laravel codebase it belongs at the domain/utility layer alongside ext-dom and symfony/dom-crawler, not as an infrastructure package. The fit is good if the job is server-side HTML interrogation (content scraping, rewriting stored user HTML, extracting structured data from email/imported documents). It is a poor fit if the team expects a framework feature or a Blade/DOM helper.
Integration feasibility. High. A single composer require wikimedia/zest-css, PSR-4 autoload of Zest\, and a thin wrapper service is the entire integration. There is no config to publish, no migrations, no queue contract, no middleware. Realistic effort for a competent Laravel engineer is under a day to a first working spike.
Technical risk. Mostly ecosystem risk, not functional risk:
ext-dom.Dom\HTMLDocument::querySelectorAll() covers common selectors natively. Zest's advantage narrows to deeper CSS3 selector support and consistent behavior, so the "why not just use native?" question must be answered before adoption.Key questions. What is the concrete job-to-be-done — scraping, stored-content transformation, or CSS-based content policy? Is the PHP baseline ≥ 8.4, in which case has the native querySelectorAll path been benchmarked against Zest? Who builds the DOMDocument/Dom\HTMLDocument, and is entity-expansion/XXE handling owned by us or inherited? Which CSS3 selectors actually matter to the feature (attribute, :nth-child, :not, combinators)? Is there a maintained alternative (symfony/dom-crawler's CssSelector) that already ships a more conventional API and would avoid a second selector engine in the stack?
Stack fit. Compatible with Laravel 10–12 on PHP 8.1+ with ext-dom enabled; no Laravel version constraint exists, so nothing in the framework will pin or break it. The library is stateless and therefore safe under FPM, Octane, and queue workers without any lifecycle handling.
Migration path. If existing code uses regex-on-HTML or hand-rolled DOMXPath with translated selectors, introduce Zest behind a HtmlSelector contract with two implementations: Zest for full CSS3 fidelity, and a native Dom\HTMLDocument implementation for PHP 8.4. That keeps the library swappable at a single binding and lets you A/B the two engines on real payloads. If the codebase already uses symfony/dom-crawler, do not add Zest alongside it without a reason — running two CssSelector implementations is a maintenance liability.
Compatibility. No namespace collisions (Zest\ is unique), no global functions, no Laravel service provider auto-discovery to fight. The only real environmental requirement is ext-dom; verify the CI and production images (Alpine base images sometimes ship without it).
Sequencing. (1) Spike: parse a representative corpus of real HTML, run the 10–15 selectors the feature actually needs, confirm results match expectations. (2) Wrap: ship the HtmlSelector interface plus Zest adapter, unit-tested against fixture documents. (3) Pilot: apply to one low-blast-radius feature — e.g., rewriting external links in stored article HTML, or extracting fields from inbound email bodies. (4) Measure: latency on the largest realistic DOM and behavior on malformed input. (5) Broaden or retire. Do not start by putting it under a user-facing critical path.
Maintenance. This is the dominant concern. The repo is a Gerrit mirror, so issues and PRs land in the wrong place; the Wikimedia Phabricator is the actual channel, and release cadence is driven by MediaWiki's needs, not ours. Treat the package as "vendored with intent"—pin an exact version, and accept that if it stalls we own a fork. The code is small enough to fork comfortably, which reduces the risk but does not eliminate the ongoing ownership cost.
Support. No commercial support, no vendor, no SLA. Documentation is the MediaWiki developer wiki and source. Internal ownership must be explicitly assigned; without a named owner this dependency will be picked up in an ad-hoc way by whoever needs a selector and then quietly orphaned.
Scaling. CPU-bound and in-process — no network, no external service, so it scales linearly with worker count like any other PHP work. The real cost driver is DOM size, not worker count: parsing a large document into DOMDocument and then walking it is O(n) and memory-hungry, so the ceiling is per-request memory rather than throughput. Cache results where the same document is queried repeatedly (e.g., a rendered-content pipeline) rather than re-parsing per selector. Watch for pathological selectors and very deep trees.
Failure modes.
DOMDocument::loadHTML is fed untrusted input without disabling external entity loading — this is our responsibility, not Zest's.composer audit and security review flag it, and the team must decide fork vs replace under time pressure.Ramp-up. Very low. The public surface is essentially one method plus the selector dialect; a mid-level engineer is productive in an afternoon. The hidden ramp-up cost is not Zest — it is HTML parsing discipline (DOMDocument flags, encoding/charset handling for non-UTF-8 fragments, and the difference between loadHTML and loadXML). Budget time for that, and for writing a fixture corpus, because selector-engine bugs are indistinguishable from input-encoding bugs without one.
How can I help you explore Laravel packages today?