ircmaxell/php-yacc
PHP port of kmyacc: a YACC/LALR(1) parser generator. Feed it a YACC grammar plus a parser template to generate a PHP parser. Useful for building fast parsers for structured languages; generation is resource-heavy, runtime parsing is efficient.
## Getting Started
1. **Installation**: Add to `composer.json` under `require-dev`:
```json
"ircmaxell/php-yacc": "^0.0.7"
Run composer install. Verify with vendor/bin/phpyacc --version.
First Grammar: Create app/Grammar/Simple.y with a minimal rule:
%token NUMBER
%%
input: NUMBER { echo "Parsed: " . $1; }
Template Setup: Copy vendor/ircmaxell/php-yacc/examples/php/template.php to app/Grammar/Template.php. Modify the parser_class and action_code sections to match your needs.
Generate Parser: Run in a build script (e.g., php artisan generate:parser):
vendor/bin/phpyacc -f app/Grammar/Simple.y -t app/Grammar/Template.php -o app/Generated/SimpleParser.php
Autoload: Add the generated file to composer.json autoload:
"autoload": {
"files": ["app/Generated/SimpleParser.php"]
}
First Parse: Use the generated parser in a Laravel service:
$parser = new \App\Generated\SimpleParser();
$result = $parser->parse(explode(' ', '42'));
Key Files:
app/Grammar/*.y: Your YACC grammars.app/Generated/*.php: Generated parser classes (commit these!).app/Grammar/Template.php: Custom parser template.// app/Console/Commands/GenerateParser.php
public function handle() {
$this->callSilently('vendor:publish', ['--tag' => 'php-yacc']);
$output = Artisan::call('phpyacc', [
'-f' => 'app/Grammar/MyGrammar.y',
'-t' => 'app/Grammar/Template.php',
'-o' => 'app/Generated/MyParser.php',
]);
$this->info("Parser generated successfully.");
}
# .github/workflows/parser.yml
jobs:
generate-parser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: composer install
- run: php artisan generate:parser
%token definitions. Use a shared Token class:
// app/Services/Token.php
class Token {
const NUMBER = 1;
const PLUS = 2;
// ...
}
[token_type, value, line_number]:
$lexer = new MyLexer($input);
$tokens = [];
while ($token = $lexer->next()) {
$tokens[] = [$token->type, $token->value, $token->line];
}
$parser->parse($tokens);
-n flag to avoid magic $1, $2 references:
%name expr
expr: expr '+' expr { $$ = new AddNode($expr_1, $expr_2); }
Template.php to include AST node classes:
// app/Grammar/Template.php
$parser_class .= "
class AddNode implements NodeInterface {
public function __construct(public NodeInterface \$left, public NodeInterface \$right) {}
}
";
#if directives in grammars to support multiple variants:
#if defined('PHP7')
%token T_ARRAY
#else
%token T_ARRAY_HASH
#endif
// app/Facades/ParserFacade.php
class ParserFacade {
public static function parse(string $input, string $grammar = 'default') {
$parserClass = "App\\Generated\\{$grammar}Parser";
return (new $parserClass())->parse(explode(' ', $input));
}
}
error() method in your template:
$parser_class .= "
public function error(\$message) {
throw new ParseError(\"Line \$this->line: \$message\");
}
";
sync() method to reset the parser state after errors:
$parser_class .= "
public function sync() {
\$this->lex = \$this->lexer;
\$this->token = null;
}
";
-v and -x flags broke in v0.0.4. Always pin to ~0.0.7 and test upgrades manually.phpyacc -v to generate conflict logs, but redirect stderr to a file:
vendor/bin/phpyacc -v -f grammar.y 2> conflicts.log
Template.php if using relative paths:
$parser_class .= "use App\\Generated\\Tokens;"
-v to expose warnings:
warning: shift/reduce conflict on token NUMBER
Fix: Add precedence rules manually (e.g., expr: expr '*' expr before expr: expr '+' expr).-p flag (undocumented; check phpyacc --help).<?php ?>) in the template’s action_code section. Use <?= for output:
$action_code = "<?= \$action ?? '' ?>";
Parser. Override in Template.php:
$parser_class = "class MyCustomParser extends Parser { ... }";
%union Support: Workaround: Use named semantic values (-n) and type-check manually:
%name expr
expr: NUMBER { $$ = (int)\$expr_1; }
%type: Annotate types in semantic actions:
expr: NUMBER { return (int)\$1; }
expr: NUMBER temp { $$ = process($temp_1); }
temp: NUMBER { $$ = $1; }
$tokens = explode(' ', $input);
file_put_contents('debug.tokens', print_r($tokens, true));
$parser_class .= "
public function debug() {
return [
'line' => \$this->line,
'lookahead' => \$this->lookahead,
'stack' => \$this->stack,
];
}
";
$this->line for error reporting.// app/Providers/ParserServiceProvider.php
public function register() {
$this->app->singleton('parser', function () {
return new \App\Generated\SimpleParser();
});
}
config/parser.php:
return [
'grammars' => [
'default' => app_path('Grammar/Default.y'),
'advanced' => app_path('Gr
How can I help you explore Laravel packages today?