phpmyadmin/sql-parser
phpmyadmin/sql-parser is a validating SQL lexer and parser for PHP with a focus on MySQL. It lets you parse, lint, tokenize, format, and analyze SQL queries, and ships with CLI utilities and a Composer-installable library for query discovery and formatting.
Installation
composer require phpmyadmin/sql-parser:^6.0
The package ships its own PSR-4 autoloading (PMA\SQLParser\ → src/), so do not add a manual mapping for vendor/phpmyadmin/sql-parser/src/ in your composer.json — duplicate mappings cause class-resolution confusion after composer dump-autoload.
First Use Case: Parsing a Query
use PhpMyAdmin\SqlParser\Parser;
$parser = new Parser('SELECT * FROM users WHERE id = 1');
if ($parser->errors) {
foreach ($parser->errors as $error) {
// $error->getMessage(), $error->token->position
}
}
foreach ($parser->statements as $statement) {
// $statement is a Statements\Statement subclass (SelectStatement, etc.)
}
Where to Look First
src/Parser.php (entry point), src/Statements/ (one class per statement type), src/Components/ (clauses: options, fields, joins, where…).tests/ — the most reliable documentation of node shapes and formatter output.src/Utils/Formatter.php and the Formatter on statements — this, not the AST alone, is what most Laravel use cases actually need.Version 6.0.1 Notes
E_ALL.Use Case: Reject malformed or unsupported SQL before it hits a raw DB::select().
$parser = new Parser($sql);
if ($parser->errors) {
abort(400, 'Invalid SQL: ' . $parser->errors[0]->getMessage());
}
6.0.1 fixed a crash (Attempt to read property "keyword" on null, #609) for truncated input like SELECT * FROM — previously that path could throw a PHP error instead of populating $parser->errors, so validation now degrades cleanly.
Use Case: Extract tables/aliases for caching keys, audit logging, or multi-tenant guardrails.
$parser = new Parser($sql);
$statement = $parser->statements[0];
$tables = $statement->getTables(); // Table components
$aliases = $statement->getAliases(); // alias => name map
6.0.1 (#658) made getAliases() skip null/absent aliases instead of using null as an array offset. If you previously worked around this with array_filter($aliases, fn($k) => $k !== '', ARRAY_FILTER_USE_KEY), you can drop it — but keep the guard if you're on 6.0.0.
Use Case: Normalise or pretty-print SQL for logs, admin tools, or generated migrations.
use PhpMyAdmin\SqlParser\Utils\Formatter;
$parser = new Parser($sql);
$output = (new Formatter())->format($sql);
Formatter fixes in 6.0.1 that matter in Laravel logs:
->, ->>) are no longer mangled with stray spaces — important for MySQL JSON queries copied into an admin panel.Use Case: Validate user-supplied schema changes (e.g. a "create index" admin form).
$parser = new Parser('CREATE INDEX idx ON users (created_at DESC)');
if ($parser->errors) { /* reject */ }
6.0.1 (#636) fixed a hard error when validating indexes with a sort order (ASC/DESC). Before this patch, DESC indexes produced a failure; if you had worked around it by stripping sort orders before parsing, remove that hack.
Middleware for raw-SQL endpoints: parse $request->input('sql') and return 422 on $parser->errors.
Query logging: in DB::listen(), run the formatter to normalise and mask SQL before writing to your log channel.
Eloquent Builder extension: expose ->parseRawSql($sql) on a custom builder that returns $parser->statements — useful for read-only introspection, not for executing.
MySQL Dialect Only
The parser is MySQL-specific. TOP (SQL Server) or PostgreSQL-only casts/RETURNING will fail. Pre-process or guard by connection driver before parsing.
Errors go to $parser->errors, not exceptions
Parsing is non-throwing by default; check the property. Only Builder/Utils code paths throw PhpMyAdmin\SqlParser\Exceptions\*. A try/catch around new Parser() alone will silently let bad SQL through.
AST shape is undocumented
Inspect with var_dump($statement) or read tests/ before mutating nodes. Component lists are plain arrays of objects, not a traversable visitor tree — the "rewrite with a callback" style shown in many tutorials does not exist in this package.
Performance on large queries
Deeply nested subqueries and huge INSERT ... VALUES batches are slow. Cache parse results by md5($sql) if you parse the same shape repeatedly.
PHP version churn
6.0.1 is clean on 8.5/8.6, but 6.0.0 emits deprecation notices under E_ALL. If your app treats deprecations as failures (Laravel's HandleExceptions + error_reporting(-1)), pin ^6.0.1.
$parser->errors and $parser->statements — partial parses still populate statements.vendor/bin/phpunit vendor/phpmyadmin/sql-parser/tests (6.0.1 targets PHPUnit 13; you may need a separate PHPUnit install outside your app's constraint).Builder / Component subclasses for reusable fragments (e.g. a pagination component that always emits LIMIT x OFFSET y).Context quote style — set the identifier quote character on your Context so INSERT building matches your connection's settings (#631).AppServiceProvider.How can I help you explore Laravel packages today?