laramint / queryguard
Zero-config CI gate for Laravel: auto-instruments your test suite, baselines query counts, and fails PRs that introduce N+1s or query regressions.
Requires
- php: ^8.2
- illuminate/console: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^2.11 || ^3.9
- laravel/pint: ^1.0
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- pestphp/pest: ^2.0|^3.0|^4.0
- phpunit/phpunit: ^9.5.10|^10.5|^11.0|^12.0
README
Zero-config CI gate for Laravel query regressions.
Baseline query counts per test, surface N+1 patterns, and fail builds when tests drift — without manual per-test assertions.
QueryGuard
Zero-config CI gate for Laravel. Auto-instruments your test suite, baselines query counts per test, and fails PRs that introduce N+1s or query regressions — without you adding a single assertion.
Existing Laravel query tools either run only in dev mode (beyondcode/laravel-query-detector, Debugbar) or require devs to opt-in per test with manual assertions (mattiasgeniar/phpunit-query-count-assertions). Neither catches regressions automatically in CI on an existing suite.
QueryGuard does. Install, baseline once, and from then on every PR that pushes a test's query count past its baseline (or introduces a new N+1 pattern) fails the build with a precise diff.
Install
composer require --dev laramint/queryguard
Register the PHPUnit extension in phpunit.xml:
<extensions> <bootstrap class="QueryGuard\PHPUnit\QueryGuardExtension"/> </extensions>
Usage
# Record the baseline (do this once, commit the file). php artisan queryguard:baseline # In CI, this exits non-zero on regression: php artisan queryguard:check # Or run phpunit directly with the env var: QUERYGUARD_MODE=check vendor/bin/phpunit
Commit tests/.queryguard-baseline.json to git — PR diffs naturally show "this test went from 4 to 17 queries."
Per-test budgets (optional)
use QueryGuard\Attributes\QueryBudget; #[QueryBudget(max: 5)] public function test_index_is_fast(): void { /* ... */ }
GitHub Actions
- name: QueryGuard run: | php artisan queryguard:check --markdown > queryguard.md || EXIT=$? gh pr comment "$PR_NUMBER" --body-file queryguard.md exit ${EXIT:-0} env: PR_NUMBER: ${{ github.event.pull_request.number }} GH_TOKEN: ${{ github.token }}
Configuration
php artisan vendor:publish --tag=queryguard-config
Then edit config/queryguard.php for tolerances, ignore patterns, slow-query threshold, and N+1 detection threshold.
How it works
- The PHPUnit extension hooks
testPrepared/testFinishedand registers aDB::listencallback that records every query per-test. - Each query's SQL is normalized into a stable signature (literals stripped,
IN (?,?,?)collapsed, keywords lowercased) so the same logical query matches across runs. - At end of run, the recorded profiles are either written to
tests/.queryguard-baseline.json(inbaselinemode) or diffed against it (incheckmode). - Any of the following exits the run non-zero:
- A test's query count exceeds
baseline + tolerance - The same query signature is executed more than
n_plus_one.thresholdtimes in a single test - A
#[QueryBudget]is exceeded
- A test's query count exceeds
Slow queries and new query signatures are reported as warnings (non-fatal) by default.
License
MIT.
