yosymfony/parser-utils
A small PHP toolkit for building parsers, providing tokens, token streams, AST nodes, source locations, and syntax errors. It offers reusable utilities for lexers and recursive-descent parsers, and underpins libraries such as yosymfony/toml.
Installation:
composer require yosymfony/parser-utils
Add to composer.json under require-dev if only for testing/parsing tools.
First Use Case:
Parse a simple arithmetic expression (e.g., "3 + 5 * 2").
use Yosymfony\ParserUtils\Parser;
$parser = new Parser();
$parser->addRule('expr', function($parser) {
return $parser->exprAdd();
});
$parser->addRule('exprAdd', function($parser) {
$left = $parser->exprMul();
while ($parser->peek() === '+') {
$parser->consume();
$left += $parser->exprMul();
}
return $left;
});
$parser->addRule('exprMul', function($parser) {
$left = $parser->number();
while ($parser->peek() === '*') {
$parser->consume();
$left *= $parser->number();
}
return $left;
});
$parser->addRule('number', function($parser) {
return (int)$parser->consume();
});
$result = $parser->parse('3 + 5 * 2'); // Returns 13
Where to Look First:
peek(), consume(), parse()).Parser class docs (if any) for core methods like addRule(), parse(), and error handling.Define Grammar Rules:
Break input into hierarchical rules (e.g., expr → exprAdd → exprMul → number).
$parser->addRule('ruleName', function($parser) {
// Recursive calls to sub-rules or terminal matches
});
Terminal vs. Non-Terminal Rules:
number, identifier).
$parser->addRule('identifier', function($parser) {
return $parser->consume(); // Assumes input is alphanumeric
});
exprAdd combining exprMul with +).Error Handling:
Use Parser::expect() or custom logic to handle mismatches:
try {
$result = $parser->parse($input);
} catch (\RuntimeException $e) {
// Log or rethrow with context
}
Integration with Laravel:
$request->validate([
'query' => 'required|string',
]);
$parsed = $parser->parse($request->query);
--filter="type:active").Parser as a singleton for reuse:
$this->app->singleton(Parser::class, function() {
return new Parser();
});
Performance Tips:
Parser instance).Left Recursion:
The package doesn’t natively handle left recursion (e.g., expr → expr '+' term). Rewrite rules to avoid it:
// Bad (left-recursive):
$parser->addRule('expr', function($parser) {
return $parser->expr() + $parser->term(); // Infinite loop
});
// Good (right-recursive):
$parser->addRule('expr', function($parser) {
$left = $parser->term();
while ($parser->peek() === '+') {
$parser->consume();
$left += $parser->term();
}
return $left;
});
Input Consumption:
consume() removes the matched token from the input stream. Over-consumption causes RuntimeException.peek() to check ahead without consuming:
if ($parser->peek() === '(') {
$parser->consume(); // Safe to consume after peek
}
Whitespace Handling: The parser treats whitespace as significant by default. Strip it manually:
$input = preg_replace('/\s+/', '', $input);
No Built-in Tokenization: You must pre-process input into tokens or handle raw strings carefully:
// Risky: Assumes input is tokenized
$parser->addRule('number', function($parser) {
return (int)$parser->consume(); // Fails on "123abc"
});
// Safer: Use regex or a tokenizer
Log Parser State: Add debug output to track input position and matched rules:
$parser->addRule('debug', function($parser) {
logger()->debug('Position:', $parser->getPosition());
return null;
});
Test Incrementally:
Start with a single rule (e.g., number) and expand gradually. Use try-catch to isolate failures:
try {
$parser->parse('123');
} catch (\RuntimeException $e) {
dd($e->getMessage(), $parser->getPosition());
}
Edge Cases:
parse() in checks.Parser::expect() or custom validation:
if (!$parser->peek()) {
throw new \InvalidArgumentException("Unexpected end of input");
}
Custom Error Messages:
Override default error messages by catching RuntimeException and rethrowing with context:
try {
$parser->parse($input);
} catch (\RuntimeException $e) {
throw new \RuntimeException("Parse error at position {$parser->getPosition()}: {$e->getMessage()}");
}
Tokenization Layer: Extend the parser with a tokenizer for complex grammars (e.g., SQL-like queries):
class QueryTokenizer {
public function tokenize(string $input): array {
// Implement lexing logic (e.g., split into [SELECT, FROM, users])
}
}
Laravel Service Integration: Create a facade or helper for common parsing tasks:
// app/Helpers/ParserHelper.php
class ParserHelper {
public static function parseQuery(string $query) {
$parser = app(Parser::class);
// ... setup rules
return $parser->parse($query);
}
}
Performance Optimization:
For high-frequency parsing (e.g., API request validation), memoize Parser instances:
$parserPool = [];
function getParser(string $grammarName) {
if (!isset($parserPool[$grammarName])) {
$parserPool[$grammarName] = new Parser();
// Setup rules...
}
return $parserPool[$grammarName];
}
How can I help you explore Laravel packages today?