symfony/css-selector
Symfony CssSelector converts CSS selectors into XPath expressions for querying HTML/XML documents. Useful with DOM tools and crawlers, it provides a fast, reliable bridge between familiar CSS syntax and XPath in PHP applications.
Installation:
composer require symfony/css-selector
Add to composer.json under require or require-dev depending on your use case (e.g., testing vs. scraping).
First Use Case: Convert a simple CSS selector to XPath:
use Symfony\Component\CssSelector\CssSelectorConverter;
use Symfony\Component\CssSelector\XPathNodeFinder;
$converter = new CssSelectorConverter();
$xpath = $converter->toXPath('div.content > p');
echo $xpath; // Outputs: "//div[contains(@class, 'content')]/p"
Where to Look First:
src/CssSelectorConverter.php for core logic.src/XPathNodeFinder.php for querying DOM nodes with the converted XPath.use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\CssSelector\XPathNodeFinder;
$html = '<div class="container"><p>Hello</p></div>';
$crawler = new Crawler($html);
$finder = new XPathNodeFinder($crawler->getNode(0));
// Convert and query
$converter = new CssSelectorConverter();
$xpath = $converter->toXPath('div.container > p');
$nodes = $finder->find($xpath);
foreach ($nodes as $node) {
echo $node->nodeValue; // Outputs: "Hello"
}
Scraping Example (e.g., spatie/laravel-web-scraper):
use Spatie\WebScraper\Facades\WebScraper;
use Symfony\Component\CssSelector\CssSelectorConverter;
$converter = new CssSelectorConverter();
$xpath = $converter->toXPath('article.post:is(.featured, .promoted) h2');
$scraper = WebScraper::scrape('https://example.com/blog');
$titles = $scraper->xpath($xpath)->text();
Blade Template Example:
// In a Blade view or service
$converter = resolve(CssSelectorConverter::class);
$xpath = $converter->toXPath('nav ul li.active');
$activeItems = $finder->find($xpath);
use Laravel\Dusk\Browser;
use Symfony\Component\CssSelector\CssSelectorConverter;
public function testDynamicSelector(Browser $browser)
{
$browser->visit('/dashboard');
$converter = new CssSelectorConverter();
$xpath = $converter->toXPath(':is(.button, .cta)');
$browser->assertSeeIn('@' . $xpath, 'Submit');
}
Leverage the built-in LRU cache to avoid redundant conversions:
$converter = new CssSelectorConverter();
$xpath1 = $converter->toXPath('div.container'); // Cached
$xpath2 = $converter->toXPath('div.container'); // Retrieved from cache
Service Provider Binding:
Bind the converter as a singleton in AppServiceProvider for global access:
public function register()
{
$this->app->singleton(CssSelectorConverter::class, function () {
return new CssSelectorConverter();
});
}
Custom XPathNodeFinder Wrapper:
Extend XPathNodeFinder for Laravel-specific DOM handling (e.g., DOMDocument or SimpleXML):
use Symfony\Component\CssSelector\XPathNodeFinder;
use DOMDocument;
class LaravelXPathNodeFinder extends XPathNodeFinder
{
public function __construct(DOMDocument $dom)
{
parent::__construct($dom->documentElement);
}
}
Selector Validation:
Use the isValid() method to validate selectors before conversion:
if ($converter->isValid('div:is(.active, .inactive)')) {
$xpath = $converter->toXPath('div:is(.active, .inactive)');
}
Combining with Laravel Collections:
Convert selectors to XPath and use Laravel’s collect() for post-processing:
$nodes = collect($finder->find($xpath))->map->textContent;
Memory Exhaustion in Large-Scale Scraping:
$converter = new CssSelectorConverter();
$converter->setCache(new \Symfony\Component\Cache\Adapter\FilesystemAdapter());
Malformed HTML/XML:
:is()).DOMDocument::loadHTML() or use spatie/html for cleaning:
$dom = new DOMDocument();
@$dom->loadHTML($dirtyHtml, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
:is() and :where() Quirks:
:is(.a, .b):where(.active) may not work as expected in older versions.$xpath = $converter->toXPath('div:is(.header, .footer) > p:where(.highlight)');
Namespace Handling:
xmlns) may not map cleanly from CSS selectors.DOMDocument or use local-name() in XPath:
$dom->registerNamespace('ns', 'http://example.com/ns');
$xpath = '//ns:div[@class="content"]';
PHP Version Requirements:
Enable Debugging:
Set the DEBUG constant to log conversion steps:
define('DEBUG', true);
$converter = new CssSelectorConverter();
$xpath = $converter->toXPath('div > p'); // Logs parsing details
Validate XPath:
Use DOMXPath::evaluate() to test XPath manually:
$dom = new DOMDocument();
$dom->loadHTML('<div><p>Test</p></div>');
$xpath = new DOMXPath($dom);
$result = $xpath->evaluate('//div/p');
Common Selector Edge Cases:
[href="url"] vs. [href=url]).:nth-child(), :not(), and :has() for compatibility.>, +, and ~ work as expected in nested structures.Custom Selector Syntax:
Extend CssSelectorConverter to support domain-specific selectors:
class CustomCssSelectorConverter extends CssSelectorConverter
{
protected function parseCustomSelector(string $selector): array
{
// Add logic for custom syntax (e.g., `.my-custom[role=admin]`)
return parent::parseSelector($selector);
}
}
Post-Processing XPath: Modify XPath after conversion for Laravel-specific needs:
$xpath = $converter->toXPath('div');
$xpath = str_replace('//div', '//div[contains(@class, "module")]', $xpath);
Integration with Laravel Events: Cache converted selectors globally during boot:
public function boot()
{
$this->app->booted(function () {
$converter = app(CssSelectorConverter::class);
$converter->setCache(app('cache')->store('array'));
});
}
**Testing Selector
How can I help you explore Laravel packages today?