guanguans/laravel-soar
Laravel SOAR brings Xiaomi's SOAR SQL optimizer and rewriter to Laravel, offering SQL health scores, index and performance suggestions, and automatic query rewriting via artisan commands, facades, and query logging, so you can catch slow or risky SQL before it hits production.
Install as a dev dependency (you rarely want SQL rewriting available in production):
composer require guanguans/laravel-soar --dev
php artisan vendor:publish --tag=soar-config
The package is a thin Laravel wrapper around guanguans/soar-php, which in turn shells out to Xiaomi's SOAR Go binary. So there are two moving parts to set up:
config/soar.php — this is where the binary path, default SOAR CLI options, and (for explain) database connection details live.php artisan soar:install (downloads a release for the current platform), or point SOAR_BIN / soar.bin at a binary you manage yourself.Verify the wiring before writing any code:
php artisan list soar # see the commands your version registers
php artisan soar:score --sql="select * from users where email = '[email protected]'"
Or from php artisan tinker:
Soar::score("select * from orders where status = 'paid' order by created_at desc");
First real use case: grab a slow query from storage/logs/laravel.log, the MySQL slow query log, or a Sentry/New Relic trace, and run it through soar:score (for a 0–100 health score plus alerts) and soar:rewrite (for a suggested rewrite). That feedback loop — score → rewrite → re-score — is the core value of the package.
Look first at: config/soar.php, php artisan list soar, and the Guanguans\LaravelSoar\Facades\Soar facade, which mirrors the soar-php API (score, syntacticCheck, rewrite, explain, md2html).
Ad-hoc analysis via the facade. The most common day-to-day pattern is dropping suspicious queries into Tinker or a throwaway command:
Soar::score($sql); // markdown report by default
Soar::syntacticCheck($sql); // just parse/validate
Soar::rewrite($sql); // candidate rewrite
Soar::explain($sql, 'mysql_read'); // needs a configured connection
Option overrides per call. Rather than editing config for one-off reports, layer options:
Soar::withOptions(['-report-type' => 'json', '-max-index-parts' => 3])->score($sql);
This is useful when a command needs machine-readable output while interactive use still wants markdown.
CI / pre-deploy SQL linting. Keep schema changes or hand-written reports in database/sql/ and run a lint step that fails the build on parse errors or low scores:
php artisan soar:lint database/sql/*.sql
# or a scripted loop that exits non-zero on alerts
This catches SELECT *, missing WHERE clauses, and non-sargable predicates before they ship.
Local query listener. In a local-only service provider, hook DB::listen() and score queries that exceed a duration threshold, logging the report to a dedicated channel. This turns SOAR into a passive reviewer that surfaces problems while you develop rather than only when you remember to run it:
DB::listen(function ($query) {
if ($query->time > 200 && app()->isLocal()) {
Log::channel('soar')->info(Soar::score($query->sql));
}
});
Gate this strictly on isLocal() or a feature flag — it shells out to a subprocess per query.
Test assertions. If you generate SQL dynamically (query builders, filters, reporting modules), assert against SOAR in a test:
$sql = Order::where('status', 'paid')->toSql();
$this->assertStringNotContainsString('ALERT', Soar::score($sql));
Prefer asserting on specific known-bad patterns (missing index hints, SELECT *) over a raw numeric score, which drifts between SOAR versions.
Explain on staging, not production. soar:explain (and Soar::explain) executes against a real MySQL connection. Configure a dedicated, read-only connection in config/soar.php pointing at a staging replica, and treat it as a separate environment-specific config from the scoring functionality, which needs no DB at all.
Rewrite → verify loop. Treat rewrite output as a candidate, never as an automatic patch. The pattern that works: copy the rewrite into your query, run EXPLAIN (or soar:explain) to confirm the plan improved, then run the feature's tests to confirm semantics are unchanged.
Wrap it for team workflows. Because it's just a facade, it's easy to build a small App\Support\SqlReview service that combines syntacticCheck, score, and a few opinionated checks, so engineers run one command instead of remembering five.
EXPLAIN and tests before applying.-1 means parse failure, not "terrible query". Distinguish parse errors from low scores when scripting; otherwise a typo in a SQL file silently looks like a performance problem.soar:install downloads at runtime; in locked-down CI you often need to install it in the image or vendor the binary. Check whether your config caches the resolved path.SOAR_BIN and other env-driven values are frozen after php artisan config:cache. Run php artisan config:clear when changing the binary path or connection details.withOptions() override the SOAR config file, which overrides SOAR defaults. The Laravel config/soar.php only forwards a subset — if an option seems ignored, check whether it's being passed through to the underlying soar-php call at all.explain requires credentials and permissions. It opens a real connection. Point it at a read replica, scope the user to read-only, and never expose it behind a public endpoint — it leaks schema information and can be used to run expensive queries.-report-type=json (or the equivalent so-php option) when parsing programmatically, and don't scrape the pretty output.IN (...) lists and generated SQL can exceed SOAR's parser limits or produce unhelpful rewrites.soar-php surfaces stderr from the binary, but it's easy to miss. When a command returns nothing useful, run the SOAR binary directly with -v against the same SQL to see whether the problem is the wrapper, the options, or the parser.How can I help you explore Laravel packages today?