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

Css Selector Laravel Package

symfony/css-selector

Converts CSS selectors to XPath expressions, letting you query HTML and XML documents with familiar CSS syntax. A port of Python's cssselect library, part of the Symfony framework.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (or discover you already have it).

composer require symfony/css-selector

laravel/dusk and anything depending on symfony/dom-crawler already pull this in transitively — check composer.lock before adding. Promoting it to a direct constraint is about pinning a minimum version and owning upgrades, not about new capability.

Where to look first. The stable public surface is tiny: Symfony\Component\CssSelector\CssSelectorConverter::toXPath(string $css, string $prefix = 'descendant-or-self::'), its constructor flag new CssSelectorConverter(bool $html = true), and the exception classes under Symfony\Component\CssSelector\Exception. Everything else (XPath\Translator, XPath\Extension\*, Node\*) is implementation detail — read it, don't build against it. The Symfony component docs page is the grammar reference; the package's own test fixtures are the fastest way to learn exactly which selectors compile.

First use case — assert the shape of rendered HTML in a test. This is the lowest-risk, highest-payoff entry point and it replaces fragile assertSee/regex assertions:

use Symfony\Component\DomCrawler\Crawler;

$html = view('mail.welcome', ['user' => $user])->render();
$crawler = new Crawler($html);

$this->assertCount(1, $crawler->filter('a.button[href*="/activate"]'));
$this->assertSame('Activate your account', $crawler->filter('a.button')->text());

The second use case — scraping — is the same code with Http::get(...)->body() as the input. Start there before designing anything larger.

Implementation Patterns

1. Go through DomCrawler, not the converter, unless you have a reason. DomCrawler wraps DOMDocument/DOMXPath, tracks HTML vs XML, handles encoding, and gives you filter(), each(), text(), attr(). The converter alone gives you a string; you still have to build and maintain the DOM plumbing.

$crawler = new Crawler(Http::get($url)->body(), $url);

$products = $crawler->filter('ul.products > li')->each(function (Crawler $li) {
    return [
        'name'  => $li->filter('h3')->text(),
        'price' => $li->filter('.price')->text(),
        'url'   => $li->filter('a')->attr('href'),
    ];
});

2. Use the converter directly when the DOM already exists — bulk extraction, repeated queries against one document, or when you're handed a DOMXPath by other code. Compile once, query many times:

$converter = new CssSelectorConverter();
$xpath = new DOMXPath($document);

$nodes = $xpath->query($converter->toXPath('table.report tbody tr'));
foreach ($nodes as $node) { /* ... */ }

3. Keep selectors out of your classes. The most common maintenance failure in scraping code is hardcoded selector strings scattered across jobs. Put them in config/scrapers.php (or a dedicated SelectorSet value object) keyed by semantic name, so a markup change is a config change:

'products' => [
    'row'   => 'ul.products > li',
    'name'  => 'h3',
    'price' => '.price',
],

4. Compile-and-cache for hot paths. toXPath() re-parses on every call. In an Octane or queue worker, memoise per instance with a bounded array (or Cache::remember with a TTL for user-authored selectors):

final class SelectorCompiler
{
    /** @var array<string, string> */
    private array $cache = [];
    private CssSelectorConverter $converter;

    public function __construct(bool $html = true)
    {
        $this->converter = new CssSelectorConverter($html);
    }

    public function toXPath(string $css): string
    {
        return $this->cache[$css] ??= $this->converter->toXPath($css);
    }
}

5. Validate before you store. If users author selectors, add a form rule that compiles the selector in a dry run and surfaces a readable message:

use Symfony\Component\CssSelector\Exception\ParseException;

try {
    (new CssSelectorConverter())->toXPath($request->string('selector'));
} catch (ParseException $e) {
    throw ValidationException::withMessages([
        'selector' => 'Unsupported or malformed selector: '.$e->getMessage(),
    ]);
}

6. Run scraping off the request thread. Http::get() + parse in a queued job with $timeout, and never let a user-supplied selector run inside a web request. This is also where you cap selector length and nesting depth before compilation.

7. In Dusk, remember you're in a different world. Dusk hands selectors to a real browser, where :hover, :focus, ::before, and :has() all work. The same string evaluated by CssSelectorConverter will throw. Don't share selector constants between server-side parsing and browser tests without checking both.

Gotchas and Tips

  • HTML vs XML is a constructor flag, and it matters. new CssSelectorConverter() assumes HTML and lowercases element names in the generated XPath (XPath 1.0 is case-sensitive; HTML isn't). For XML documents pass false, or use Crawler::addXmlContent() which tracks the mode for you. Getting this wrong produces silent zero-result queries on CamelCase XML tags.

  • Encoding bites raw DOMDocument. loadHTML() mangles UTF-8 unless an XML declaration or meta charset is present. DomCrawler handles it; if you build the DOM yourself, prefix with <?xml encoding="UTF-8"> or mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'). Also wrap parsing in libxml_use_internal_errors(true) and clear the buffer, or malformed markup floods your logs.

  • descendant-or-self:: means the root can match itself. $crawler->filter('div') where the crawler is rooted at a <div> returns that div. Useful, but surprising when you expected children only.

  • Relative/leading combinators aren't portable. ->filter('> li') is not reliable. Use ->filterXPath('./li') or ->children().

  • Know the grammar and publish it. Type/class/ID/attribute selectors (including ^=, $=, *=, ~=, |=), all four combinators, grouping, :first-child, :nth-child(), :not(), :contains() are supported. :is()/:where() arrive in 7.1, :has() in 8.1. Rendering and state pseudo-classes (:hover, :focus, ::before) are not supported and cannot be — there's no browser. :checked/:disabled/:enabled/:required compile to static attribute checks, so JS-toggled state will not be detected.

  • :has() may be unavailable to you. Symfony 8.x requires PHP ≥ 8.4; most Laravel apps on 8.2/8.3 resolve to 7.4.x. Emulate it in XPath instead: div.card[.//span[contains(@class,"badge")]].

  • :contains() is substring, case-sensitive, and not CSS. Don't use it for equality assertions; prefer text() and compare in PHP.

  • Pin a floor, not a ceiling. Releases have fixed denial-of-service-class defects that only appear with untrusted selectors: an LRU cache added to CssSelectorConverter to stop memory exhaustion (#63400, landed in 6.4.34 / 7.4.6 / 8.0.6), a nesting-depth cap on :is()/:where() (#65596, 8.1.6), and quadratic token probing in Reader::findPattern() (#65599, 8.1.6). If customers author selectors, a minimum-version constraint plus a patch SLA is a product-safety requirement, not housekeeping.

  • Catch ParseException, not something narrower. SyntaxErrorException and ExpressionErrorException both extend it. It extends \Exception — don't assume Laravel will render it nicely.

  • Guard empty result sets. Crawler::text() and attr() throw \InvalidArgumentException on an empty node list unless you pass a default: ->text(''), ->attr('href', '#'). Check ->count() first when a missing element is meaningful.

  • text() normalises whitespace by default. Pass the second argument false if exact spacing matters (e.g. diffing generated markup).

  • Namespaced XML is out of scope. The converter has no notion of namespace prefixes; drop to filterXPath() with registerNamespace() for Atom/RSS/SOAP payloads.

  • Debug by reading the XPath, not the HTML. When a selector returns nothing, dump (new CssSelectorConverter())->toXPath($css) and run it manually against the document — it almost always reveals an unexpected class-matching form (concat(' ', normalize-space(@class), ' ')) or a wrong HTML/XML mode.

  • Extend by composition. CssSelectorConverter is a stateless pure function; wrap it in your own compiler/validator service rather than subclassing or forking. Upgrades then become a composer update instead of a merge conflict, and symfony/symfony remains the single place to track issues and PRs across the 6.4/7.4/8.x branches.

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.
vento/laravel-airbrake
wfeller/laravel-batch
maco-studios/mage-os-installer
busarm/armie
siberfx/laravel-logger
kfteam1991/dcat-admin
dashed/dashed-laposta
dashed/dashed-ecommerce-montaportal
dashed/dashed-ecommerce-etsy
neohq/string-blade-compiler
log1x/acf-composer
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager