salman053/laravel-query-spy

Real-time Eloquent query debugging tool with N+1 detection, backtrace tracking, and performance insights for Laravel.

Maintainers

Package info

github.com/Salman053/query-spy

Homepage

pkg:composer/salman053/laravel-query-spy

Transparency log

Fund package maintenance!

Salman053

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-master 2026-07-23 06:31 UTC

This package is auto-updated.

Last update: 2026-07-23 07:25:47 UTC


README

Real-time database query debugging with N+1 detection for Laravel. Captures every query with its exact file and line origin, detects N+1 patterns with suggested eager-loading fixes, and tracks duplicate/slow queries.

Installation

composer require salman053/laravel-query-spy

Laravel auto-discovers the service provider. Optionally publish the config:

php artisan vendor:publish --tag="eloquent-query-spy-config"

Quick Start

Use the QuerySpy facade (aliased automatically via composer.json):

use QuerySpy;

QuerySpy::boot();

$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count(); // triggers N+1
}

$report = QuerySpy::analyse();

Or via dependency injection:

use EloquentQuerySpy\EloquentQuerySpy\QuerySpy;

Route::get('/users', function (QuerySpy $spy) {
    $spy->boot();

    $users = User::all();
    foreach ($users as $user) {
        $user->posts->each(fn ($post) => $post->comments);
    }

    return response()->json($spy->analyse());
});

Features

Query Interception

Every query executed through Laravel's DB facade or Eloquent is captured with backtrace info:

QuerySpy::boot();

User::find(1);

$queries = QuerySpy::getQueries();
// $queries[0]->sql       // "select * from "users" where "id" = ? limit 1"
// $queries[0]->bindings  // [1]
// $queries[0]->time      // 2.45 (ms)
// $queries[0]->file      // "app/Http/Controllers/UserController.php"
// $queries[0]->line      // 42
// $queries[0]->caller    // "UserController::show"
// $queries[0]->hash      // "ab12cd34"
// $queries[0]->occurrence // 1

The backtrace parser skips vendor frames and spy internals to report the actual application call site.

N+1 Detection

Repeated similar SELECT queries are flagged as N+1 issues with the suggested fix:

QuerySpy::getDetector()->setThreshold(3);
QuerySpy::boot();

$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count();
}

$issues = QuerySpy::getNPlusOneIssues();

foreach ($issues as $issue) {
    echo $issue->relation;       // "posts"
    echo $issue->count;          // number of repeated queries
    echo $issue->suggestedFix(); // "->with('posts')"
    echo "$issue->file:$issue->line"; // file and line to fix
    echo $issue->parentQuery->sql;    // the initial query before N+1
    echo $issue->parentQuery->file;   // where the parent query was triggered
}

How it works: The detector groups SELECT queries by their table. If a table has repeated identical queries (after normalizing bindings) above the threshold, it reports the table name, relation name, suggested ->with() call, and the suspicious file/line.

Duplicate Query Detection

Same SQL pattern executed multiple times in a request:

$duplicates = QuerySpy::getDuplicates();
// [[$query1, $query2], [$query3, $query4, $query5]]

Slow Query Detection

Queries exceeding a threshold in milliseconds:

$slow = QuerySpy::getSlowQueries(thresholdMs: 200);
// Only queries with time > 200ms

Full Analysis Report

$report = QuerySpy::analyse();
// [
//   'total_queries'     => 12,
//   'total_time'        => 45.23,
//   'n_plus_one_issues' => [...],
//   'duplicate_groups'  => 2,
//   'slow_queries'      => 0,
//   'queries'           => [...],
// ]

CLI Commands

# Report for the current request
php artisan spy:report

# Analyse a specific route by making a real HTTP request
php artisan spy:analyse /users

# JSON output (useful for CI or tooling)
php artisan spy:report --json
php artisan spy:analyse /users --json

# Clear collected data between requests
php artisan spy:clear

The report displays total queries, time, N+1 issues, duplicate groups, slow queries, and per-issue details with suggested fixes.

Middleware

Register the middleware alias in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias('query-spy', \EloquentQuerySpy\Http\Middleware\SpyMiddleware::class);
})

Or enable globally via config with console output:

// config/eloquent-query-spy.php
'middleware' => [
    'enabled' => true,
    'output' => 'console',
],

DebugBar Integration

If barryvdh/laravel-debugbar is installed, Query Spy automatically registers a collector showing query count, N+1 warnings, and per-query details. Disable via config:

'debugbar' => [
    'enabled' => false,
],

Configuration

// config/eloquent-query-spy.php
return [

    'enabled' => true,

    'n_plus_one' => [
        'enabled' => true,
        'threshold' => 3,         // ignore patterns below this count
    ],

    'slow_query_threshold' => 100, // milliseconds

    'exclude' => [
        'patterns' => [
            '%telescope_entries%',
            '%telescope_monitoring%',
        ],
        'tables' => [],
    ],

    'collect' => [
        'backtrace' => true,       // capture file/line for each query
        'bindings' => true,        // include query bindings
        'memory' => true,          // track memory before/after
    ],

    'debugbar' => [
        'enabled' => true,
    ],

    'middleware' => [
        'enabled' => false,
        'output' => 'console',
    ],

];

API Reference

All methods are available via the QuerySpy facade or by resolving EloquentQuerySpy\EloquentQuerySpy\QuerySpy from the container.

Method Returns Description
boot() void Start listening for queries (idempotent)
stopListening() void Stop listening
isListening() bool Check if actively listening
getQueries() QueryExecution[] All captured queries
getQueryCount() int Total query count
getTotalTime() float Total query time in ms
getNPlusOneIssues() NPlusOneIssue[] Detected N+1 issues
getDuplicates() QueryExecution[][] Groups of duplicate queries
getSlowQueries(int $thresholdMs) QueryExecution[] Slow queries
clear() void Reset all collected data
analyse() array Full analysis report
getCollector() QueryCollector Raw collector instance
getDetector() NPlusOneDetector Detector instance (for threshold config)

Development & Workbench

This package uses Orchestra Testbench for development. Run the workbench server to test in a browser:

composer serve

Available workbench routes:

  • /spy — demonstrates N+1 with lazy loading
  • /spy-eager — same queries with eager loading (zero issues)

Run tests:

composer test          # full validation (static analysis + lint + type coverage + unit)
composer test:unit     # Pest tests only
composer lint:check    # Pint formatting check
composer analyse       # PHPStan static analysis
composer build         # rebuild workbench migrations

Changelog

See CHANGELOG for release history.

Contributing

See CONTRIBUTING for guidelines.

License

MIT — see LICENSE.