carica/phpcss
carica/phpcss is a lightweight PHP library for parsing, manipulating, and rendering CSS. It turns stylesheets into an object model so you can inspect or modify rules and declarations, then output clean or optimized CSS for dynamic styles and build tools.
PhpCss is a pure tokenizer/parser for CSS selectors. It turns a string like ul.nav > li:first-child a[href^="https"] into a tree of typed PHP objects. It does not query a DOM, match nodes, or generate XPath — that is your job (or a companion package's). Understand that boundary first; it shapes everything below.
Minimal setup:
composer require carica/phpcss
use Carica\Css\Parser\Selector as SelectorParser;
$parser = new SelectorParser();
// Returns a Carica\Css\Selector\Group
$group = $parser->parse('ul.nav > li:first-child a[href^="https"]');
foreach ($group->getSelectors() as $sequence) {
// $sequence is Carica\Css\Selector\Sequence
foreach ($sequence->getParts() as $part) {
// Simple types: \Selector\Simple\Type, \Simple\Id,
// \Simple\ClassName, \Simple\Attribute, \Simple\PseudoClass
// Combinations: \Selector\Combination\Child,
// \Selector\Combination\Descendant, \Selector\Combination\Next,
// \Selector\Combination\Previous
echo get_class($part), PHP_EOL;
}
}
Where to look first:
Carica\Css\Parser\Selector and its parent Carica\Css\Parser — the entry point and lexer glue.Carica\Css\Selector\Group and Selector\Sequence — the two container types. A Group holds one Sequence per comma-separated selector; a Sequence holds Simple and Combination nodes.Carica\Css\Selector\Simple\* — one class per atomic selector construct.Carica\Css\Selector\Combination\* — the four CSS combinators.First use case: validating a selector before feeding it to a matcher, or compiling it into XPath. If you want the latter, look at carica/phpcss-to-xpath, which walks this AST and emits XPath 1.0 expressions.
The intended workflow. Parse, then convert, then hand to DOMXPath:
use Carica\Css\Parser\Selector as SelectorParser;
use Carica\Css\ToXpath\Converter;
$parser = new SelectorParser();
$converter = new Converter();
$group = $parser->parse('.card > h2.title');
$xpath = $converter($group); // e.g. "descendant-or-self::*[contains(concat(' ', @class, ' '), ' card ')]/h2[...]"
$dom = new DOMDocument();
$dom->loadHTML($html);
$nodes = (new DOMXPath($dom))->query($xpath);
Selectors are parsed constantly and are pure functions of the input string. Parse once, reuse the AST:
final class SelectorCache
{
private array $cache = [];
private SelectorParser $parser;
public function __construct()
{
$this->parser = new SelectorParser();
}
public function get(string $selector): \Carica\Css\Selector\Group
{
return $this->cache[$selector] ??= $this->parser->parse($selector);
}
}
For long-lived processes, swap the in-memory array for APCu/PSR-16 keyed on the selector string.
If your UI accepts a selector, the parser is the cheapest validator you can get:
try {
(new SelectorParser())->parse($userInput);
} catch (\Carica\Css\Parser\Exception $e) {
// "Invalid selector near ..." — surface this cleanly
}
Catch Carica\Css\Parser\Exception specifically; don't let generic \Exception hide a programming error.
Rather than instanceof chains, write a visitor over the node classes to build whatever you need — a matcher, a linter, a re-writer, a minifier:
class XPathCompiler implements \Carica\Css\Parser\Visitor
{
private string $out = '';
// one method per node type: visitType, visitId, visitClassName,
// visitAttribute, visitPseudoClass, visitChild, visitDescendant, ...
}
Walk the group and ->accept($visitor), or recurse manually using getParts(). Nodes are value objects — safe to memoize or serialize between requests.
If you're maintaining a browser-emulation layer or a scraping pipeline, use PhpCss to normalize selectors once at bootstrap, then reuse the compiled result for every document. This pairs well with a DOMXPath instance kept alive per document.
querySelectorAll. Budget for the companion package (carica/phpcss-to-xpath) or your own visitor. This is the single most common surprise.:first-child, :nth-child(2n+1), :not(...) come back as Simple\PseudoClass nodes with the argument stored as a string. Semantics (especially :nth-* formulas) are your responsibility when compiling.:is(), :where(), :has(), ::part(), and some attribute operators. Test the exact selectors you need in a unit test before committing to the dependency.Carica\Css\..., not PhpCss\.... The repo is ThomasWeinert/PhpCss but the package name and namespaces are carica/phpcss — autoload paths in examples from older blog posts may differ.composer require carica/phpcss and, if needed, add #[\AllowDynamicProperties] or use --ignore-platform-reqs deliberately, not by accident.Parser section or pre-process the selector string, then produce your own Simple node. Don't monkey-patch the lexer.Group/Sequence/Simple nodes are plain objects with simple properties, so serialize() (or var_export for tests) works for debugging — dump the AST when a selector behaves unexpectedly rather than guessing at the generated XPath.How can I help you explore Laravel packages today?