guanguans/soar-php
guanguans/soar-php is a PHP wrapper for Xiaomi's SOAR SQL optimizer and rewriter. It provides a simple, Laravel-friendly API to analyze, score, rewrite, and optimize SQL queries, with configurable options and support for multiple SQL dialects.
guanguans/soar-php is a thin, fluent PHP wrapper around Xiaomi's Go-based SOAR binary. The PHP package does not optimize anything itself — it shells out to the soar executable, so the binary is the real dependency.
Minimal steps:
composer require guanguans/soar-php
# install the SOAR binary separately (GitHub releases), then either put it on PATH
# or point the package at it via config/env
php artisan vendor:publish --provider="Guanguans\SoarPHP\SoarServiceProvider" # Laravel
use Guanguans\SoarPHP\Soar;
$soar = Soar::make();
echo $soar->score('SELECT * FROM users WHERE id = 1'); // heuristic score
echo $soar->suggest('SELECT * FROM users WHERE id = 1'); // optimization advice
Where to look first:
config/soar.php — binary path, default options, -test-dsn, log settings, OS-specific binary names.src/Soar.php — the fluent API surface (score, suggest, explain, rewrite, ast, fingerprint, html, md2html, pretty, exec).First real use case: wrap score() + suggest() in an Artisan command that takes a SQL string or file and prints a report. That gives immediate value with zero database connectivity required. Add explain()/rewrite() only once you have a safe -test-dsn configured.
Fluent one-shot calls. Treat the Soar instance as a value object per analysis:
$result = Soar::make()
->setOptions(['-test-dsn' => $dsn, '-online-dsn' => $dsn])
->suggest($sql);
Create a fresh instance (or clone) per request/command rather than reusing a mutated singleton; SOAR options are positional and stale options leak between calls.
DSN from your existing connection. Don't hardcode credentials. In Laravel, derive the SOAR -test-dsn from config('database.connections.mysql'):
$dsn = sprintf('%s:%s@%s:%s/%s',
$c['username'], $c['password'], $c['host'], $c['port'], $c['database']);
Point it at a read-only replica. SOAR uses it to run EXPLAIN, so the account needs SELECT/EXPLAIN only.
Machine-readable output for pipelines. For CI gates or dashboards, force JSON and parse:
$json = Soar::make()->setOutput('json')->suggest($sql);
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
Gate a deployment on score() >= $threshold or on the presence of severity-tagged suggestions.
Batch analysis of .sql files / slow-query logs. Split on ; carefully (or use SOAR's own delimiter handling), loop, and aggregate by fingerprint(). Fingerprinting normalizes literals, so it is the right key for grouping “same query shape, different values” in a slow-query dashboard.
Rewrite as a review artifact, not auto-apply. rewrite() gives you candidate SQL. Store the original + rewrite + score delta, surface it in a PR comment or admin panel (html() / md2html() output is display-ready), and let a human approve. Never pipe rewrites straight into production migrations.
Raw escape hatch. When a SOAR flag isn't exposed by a named helper, use exec() with the raw argument array. Keep this centralized in one adapter class so an eventual SOAR upgrade only touches one file.
Caching. Analysis of the same SQL + same options is deterministic. Cache by sha1($sql . serialize($options)) for 24h; this matters when an admin UI re-renders the same recommendations repeatedly.
soar: command not found or an incompatible binary (Linux amd64 vs Darwin arm64 vs Windows .exe). Set an absolute path in soar_binary, and verify with Soar::make()->version() in a health check.-test-dsn expects user:password@host:port/database. A Laravel-style mysql://... URL or an empty DSN will cause explain/rewrite to fail or silently degrade to heuristic-only output.score, suggest, fingerprint, ast, pretty are static-analysis. explain and parts of rewrite need live schema access. Design your feature so a missing DSN degrades gracefully.--no-color (or strip escape sequences) before json_decode, or parsing will fail intermittently.$, or newlines is dangerous if interpolated into a shell command. Prefer the package's stdin support, or at minimum pass arguments as an array so Symfony Process handles escaping. Very long SQL can exceed OS ARG_MAX — another reason to use stdin.score() returns a string. Cast to int before comparisons; PHP's loose comparison can surprise you at "0" boundaries.try/catch for ProcessFailedException/ProcessTimedOutException, set an explicit timeout, and log the command (minus credentials) for debugging..exe, and path separators in config are a frequent source of “works on my Mac” bugs. Normalize via DIRECTORY_SEPARATOR.-test-dsn, and don't expose raw exec() through an endpoint.-config file for heuristics (blacklists, thresholds). Ship your own soar.toml alongside config/soar.php and pass -config in the options array when you need team-wide tuning.How can I help you explore Laravel packages today?