diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..62d0391 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,74 @@ +name: run-tests + +on: + push: + branches: + - master + - dev + pull_request: + branches: + - master + +jobs: + php-tests: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.3', '8.2', '8.1', '8.0'] + laravel: ['8.*', '9.*', '10.*', '11.*', '12.*'] + dependency-version: [prefer-stable] + exclude: + - php: 8.0 + laravel: 10.* + - php: 8.0 + laravel: 11.* + - php: 8.0 + laravel: 12.* + - php: 8.1 + laravel: 11.* + - php: 8.1 + laravel: 12.* + - php: 8.2 + laravel: 8.* + - php: 8.3 + laravel: 8.* + - php: 8.3 + laravel: 9.* + - php: 8.4 + laravel: 8.* + - php: 8.4 + laravel: 9.* + include: + - laravel: 8.* + testbench: 6.23 + - laravel: 9.* + testbench: 7.* + - laravel: 10.* + testbench: 8.* + - laravel: 11.* + testbench: 9.* + - laravel: 12.* + testbench: 10.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b99cf4..219f39f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build composer.lock -docs vendor coverage -.idea \ No newline at end of file +.idea +nbproject +.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index df16b68..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,19 +0,0 @@ -filter: - excluded_paths: [tests/*] - -checks: - php: - remove_extra_empty_lines: true - remove_php_closing_tag: true - remove_trailing_whitespace: true - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - order_alphabetically: true - fix_php_opening_tag: true - fix_linefeed: true - fix_line_ending: true - fix_identation_4spaces: true - fix_doc_comments: true - diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index f4d3cbc..0000000 --- a/.styleci.yml +++ /dev/null @@ -1,4 +0,0 @@ -preset: laravel - -disabled: - - single_class_element_per_statement diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4084157..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: php - -php: - - 7.1 - - 7.2 - -env: - matrix: - - COMPOSER_FLAGS="--prefer-lowest" - - COMPOSER_FLAGS="" - -before_script: - - travis_retry composer self-update - - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source - -script: - - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover - -after_script: - - php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover diff --git a/README.md b/README.md index 8c3629f..dee3863 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # Laravel N+1 Query Detector [![Latest Version on Packagist](https://img.shields.io/packagist/v/beyondcode/laravel-query-detector.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-query-detector) -[![Build Status](https://img.shields.io/travis/beyondcode/laravel-query-detector/master.svg?style=flat-square)](https://travis-ci.org/beyondcode/laravel-query-detector) -[![Quality Score](https://img.shields.io/scrutinizer/g/beyondcode/laravel-query-detector.svg?style=flat-square)](https://scrutinizer-ci.com/g/beyondcode/laravel-query-detector) [![Total Downloads](https://img.shields.io/packagist/dt/beyondcode/laravel-query-detector.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-query-detector) The Laravel N+1 query detector helps you to increase your application's performance by reducing the number of queries it executes. This package monitors your queries in real-time, while you develop your application and notify you when you should add eager loading (N+1 queries). -![Example alert](https://beyondco.de/github/n+1/alert.png) +![Example alert](https://beyondco.de/github/n+1/alert.png) + ## Installation @@ -19,66 +18,10 @@ composer require beyondcode/laravel-query-detector --dev The package will automatically register itself. -## Usage - -If you run your application in the `debug` mode, the query monitor will be automatically active. So there is nothing you have to do. +## Documentation -By default, this package will display an `alert()` message to notify you about an N+1 query found in the current request. If you rather want this information to be written to your `laravel.log` file, you can publish the configuration and change the output behaviour (see example below). +You can find the documentation on our [website](http://beyondco.de/docs/laravel-query-detector). -You can publish the package's configuration using this command: - -```bash -php artisan vendor:publish --provider=BeyondCode\\QueryDetector\\QueryDetectorServiceProvider -``` - -This will add the `querydetector.php` file in your config directory with the following contents: - -```php - env('QUERY_DETECTOR_ENABLED', null), - - /* - * Threshold level for the N+1 query detection. If a relation query will be - * executed more then this amount, the detector will notify you about it. - */ - 'threshold' => 1, - - /* - * Here you can whitelist model relations. - * - * Right now, you need to define the model relation both as the class name and the attribute name on the model. - * So if an "Author" model would have a "posts" relation that points to a "Post" class, you need to add both - * the "posts" attribute and the "Post::class", since the relation can get resolved in multiple ways. - */ - 'except' => [ - //Author::class => [ - // Post::class, - // 'posts', - //] - ], - - /* - * Define the output format that you want to use. - * Available options are: - * - * Alert: - * Displays an alert on the website - * \BeyondCode\QueryDetector\Outputs\Alert::class - * - * Log: - * Writes the N+1 queries into the Laravel.log file - * \BeyondCode\QueryDetector\Outputs\Log::class - */ - 'output' => \BeyondCode\QueryDetector\Outputs\Alert::class, - -]; -``` ### Testing diff --git a/composer.json b/composer.json index 116389e..e6da0e9 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,13 @@ } ], "require": { - "php": "^7.1", - "illuminate/support": "5.5.*|5.6.*" + "php": "^7.1 || ^8.0", + "illuminate/support": "^5.5 || ^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0 || ^11.0 || ^12.0" }, "require-dev": { - "orchestra/testbench": "3.6.*", - "phpunit/phpunit": "^7.0" + "laravel/legacy-factories": "^1.0", + "orchestra/testbench": "^3.0 || ^4.0 || ^5.0 || ^6.0|^8.0 || ^9.0 || ^10.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0 || ^10.5 || ^11.5.3" }, "autoload": { "psr-4": { @@ -36,7 +37,6 @@ "scripts": { "test": "vendor/bin/phpunit", "test-coverage": "vendor/bin/phpunit --coverage-html coverage" - }, "config": { "sort-packages": true diff --git a/config/config.php b/config/config.php index 7a1fa5e..84f1765 100644 --- a/config/config.php +++ b/config/config.php @@ -7,6 +7,12 @@ */ 'enabled' => env('QUERY_DETECTOR_ENABLED', null), + /* + * Threshold level for the N+1 query detection. If a relation query will be + * executed more than this amount, the detector will notify you about it. + */ + 'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1), + /* * Here you can whitelist model relations. * @@ -22,17 +28,42 @@ ], /* - * Define the output format that you want to use. + * Here you can set a specific log channel to write to + * in case you are trying to isolate queries or have a lot + * going on in the laravel.log. Defaults to laravel.log though. + */ + 'log_channel' => env('QUERY_DETECTOR_LOG_CHANNEL', 'daily'), + + /* + * Define the output format that you want to use. Multiple classes are supported. * Available options are: * * Alert: * Displays an alert on the website * \BeyondCode\QueryDetector\Outputs\Alert::class * + * Console: + * Writes the N+1 queries into your browsers console log + * \BeyondCode\QueryDetector\Outputs\Console::class + * + * Clockwork: (make sure you have the itsgoingd/clockwork package installed) + * Writes the N+1 queries warnings to Clockwork log + * \BeyondCode\QueryDetector\Outputs\Clockwork::class + * + * Debugbar: (make sure you have the barryvdh/laravel-debugbar package installed) + * Writes the N+1 queries into a custom messages collector of Debugbar + * \BeyondCode\QueryDetector\Outputs\Debugbar::class + * + * JSON: + * Writes the N+1 queries into the response body of your JSON responses + * \BeyondCode\QueryDetector\Outputs\Json::class + * * Log: * Writes the N+1 queries into the Laravel.log file * \BeyondCode\QueryDetector\Outputs\Log::class */ - 'output' => \BeyondCode\QueryDetector\Outputs\Alert::class, - -]; \ No newline at end of file + 'output' => [ + \BeyondCode\QueryDetector\Outputs\Alert::class, + \BeyondCode\QueryDetector\Outputs\Log::class, + ] +]; diff --git a/docs/_index.md b/docs/_index.md new file mode 100644 index 0000000..ed8a55e --- /dev/null +++ b/docs/_index.md @@ -0,0 +1,4 @@ +--- +packageName: Laravel Query Detector +githubUrl: https://github.com/beyondcode/laravel-query-detector +--- \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..3559ca2 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,17 @@ +--- +title: Installation +order: 1 +--- +# Laravel N+1 Query Detector + +The Laravel N+1 query detector helps you to increase your application's performance by reducing the number of queries it executes. This package monitors your queries in real-time, while you develop your application and notify you when you should add eager loading (N+1 queries). + +# Installation + +You can install the package via composer: + +``` +composer require beyondcode/laravel-query-detector --dev +``` + +The package will automatically register itself. \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..be34cfd --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,92 @@ +--- +title: Usage +order: 2 +--- + +## Usage + +If you run your application in the `debug` mode, the query monitor will be automatically active. So there is nothing you have to do. + +By default, this package will display an `alert()` message to notify you about an N+1 query found in the current request. + +If you rather want this information to be written to your `laravel.log` file, written to your browser's console log as a warning or listed in a new tab for the [Laravel Debugbar (barryvdh/laravel-debugbar)](https://github.com/barryvdh/laravel-debugbar), you can publish the configuration and change the output behaviour (see example below). + +You can publish the package's configuration using this command: + +```bash +php artisan vendor:publish --provider="BeyondCode\QueryDetector\QueryDetectorServiceProvider" +``` + +This will add the `querydetector.php` file in your config directory with the following contents: + +```php +return [ + /* + * Enable or disable the query detection. + * If this is set to "null", the app.debug config value will be used. + */ + 'enabled' => env('QUERY_DETECTOR_ENABLED', null), + + /* + * Threshold level for the N+1 query detection. If a relation query will be + * executed more then this amount, the detector will notify you about it. + */ + 'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1), + + /* + * Here you can whitelist model relations. + * + * Right now, you need to define the model relation both as the class name and the attribute name on the model. + * So if an "Author" model would have a "posts" relation that points to a "Post" class, you need to add both + * the "posts" attribute and the "Post::class", since the relation can get resolved in multiple ways. + */ + 'except' => [ + //Author::class => [ + // Post::class, + // 'posts', + //] + ], + + /* + * Define the output format that you want to use. Multiple classes are supported. + * Available options are: + * + * Alert: + * Displays an alert on the website + * \BeyondCode\QueryDetector\Outputs\Alert::class + * + * Console: + * Writes the N+1 queries into your browsers console log + * \BeyondCode\QueryDetector\Outputs\Console::class + * + * Clockwork: (make sure you have the itsgoingd/clockwork package installed) + * Writes the N+1 queries warnings to Clockwork log + * \BeyondCode\QueryDetector\Outputs\Clockwork::class + * + * Debugbar: (make sure you have the barryvdh/laravel-debugbar package installed) + * Writes the N+1 queries into a custom messages collector of Debugbar + * \BeyondCode\QueryDetector\Outputs\Debugbar::class + * + * JSON: + * Writes the N+1 queries into the response body of your JSON responses + * \BeyondCode\QueryDetector\Outputs\Json::class + * + * Log: + * Writes the N+1 queries into the Laravel.log file + * \BeyondCode\QueryDetector\Outputs\Log::class + */ + 'output' => [ + \BeyondCode\QueryDetector\Outputs\Log::class, + \BeyondCode\QueryDetector\Outputs\Alert::class, + ] + +]; +``` + +If you use **Lumen**, you need to copy the config file manually and register the Lumen Service Provider in `bootstrap/app.php` file + +```php +$app->register(\BeyondCode\QueryDetector\LumenQueryDetectorServiceProvider::class); +``` + +If you need additional logic to run when the package detects unoptimized queries, you can listen to the `\BeyondCode\QueryDetector\Events\QueryDetected` event and write a listener to run your own handler. (e.g. send warning to Sentry/Bugsnag, send Slack notification, etc.) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1eef57c..5119f0b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,12 +1,7 @@ @@ -14,16 +9,4 @@ tests - - - src/ - - - - - - - - - diff --git a/src/Events/QueryDetected.php b/src/Events/QueryDetected.php new file mode 100644 index 0000000..635ac4f --- /dev/null +++ b/src/Events/QueryDetected.php @@ -0,0 +1,26 @@ +queries = $queries; + } + + /** + * @return Collection + */ + public function getQueries() + { + return $this->queries; + } +} diff --git a/src/LumenQueryDetectorServiceProvider.php b/src/LumenQueryDetectorServiceProvider.php new file mode 100644 index 0000000..c73202e --- /dev/null +++ b/src/LumenQueryDetectorServiceProvider.php @@ -0,0 +1,21 @@ +app->configure('querydetector'); + $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'querydetector'); + + $this->app->middleware([ + QueryDetectorMiddleware::class + ]); + + $this->app->singleton(QueryDetector::class); + $this->app->alias(QueryDetector::class, 'querydetector'); + } +} diff --git a/src/Outputs/Alert.php b/src/Outputs/Alert.php index e951d74..08eb3c7 100644 --- a/src/Outputs/Alert.php +++ b/src/Outputs/Alert.php @@ -7,9 +7,14 @@ class Alert implements Output { + public function boot() + { + // + } + public function output(Collection $detectedQueries, Response $response) { - if ($response->isRedirection()) { + if (stripos($response->headers->get('Content-Type'), 'text/html') !== 0 || $response->isRedirection()) { return; } @@ -36,8 +41,8 @@ protected function getOutputContent(Collection $detectedQueries) $output = ''; + + return $output; + } +} diff --git a/src/Outputs/Debugbar.php b/src/Outputs/Debugbar.php new file mode 100644 index 0000000..ea887aa --- /dev/null +++ b/src/Outputs/Debugbar.php @@ -0,0 +1,34 @@ +collector = new MessagesCollector('N+1 Queries'); + + if (!LaravelDebugbar::hasCollector($this->collector->getName())) { + LaravelDebugbar::addCollector($this->collector); + } + } + + public function output(Collection $detectedQueries, Response $response) + { + foreach ($detectedQueries as $detectedQuery) { + $this->collector->addMessage(sprintf('Model: %s => Relation: %s - You should add `with(%s)` to eager-load this relation.', + $detectedQuery['model'], + $detectedQuery['relation'], + $detectedQuery['relation'] + )); + } + } +} diff --git a/src/Outputs/Json.php b/src/Outputs/Json.php new file mode 100644 index 0000000..e2f5714 --- /dev/null +++ b/src/Outputs/Json.php @@ -0,0 +1,27 @@ +getData(true); + if (! is_array($data)){ + $data = [ $data ]; + } + + $data['warning_queries'] = $detectedQueries; + $response->setData($data); + } + } +} diff --git a/src/Outputs/Log.php b/src/Outputs/Log.php index b8a386d..7563530 100644 --- a/src/Outputs/Log.php +++ b/src/Outputs/Log.php @@ -2,25 +2,40 @@ namespace BeyondCode\QueryDetector\Outputs; -use Log as LaravelLog; +use Illuminate\Support\Facades\Log as LaravelLog; use Illuminate\Support\Collection; use Symfony\Component\HttpFoundation\Response; class Log implements Output { + public function boot() + { + // + } + public function output(Collection $detectedQueries, Response $response) { - LaravelLog::info('Detected N+1 Query'); + $this->log('Detected N+1 Query'); + foreach ($detectedQueries as $detectedQuery) { - LaravelLog::info('Model: '.$detectedQuery['model']); - LaravelLog::info('Relation: '.$detectedQuery['relation']); - LaravelLog::info('Num-Called: '.$detectedQuery['count']); + $logOutput = 'Model: '.$detectedQuery['model'] . PHP_EOL; + + $logOutput .= 'Relation: '.$detectedQuery['relation'] . PHP_EOL; - LaravelLog::info('Call-Stack:'); + $logOutput .= 'Num-Called: '.$detectedQuery['count'] . PHP_EOL; + + $logOutput .= 'Call-Stack:' . PHP_EOL; foreach ($detectedQuery['sources'] as $source) { - LaravelLog::info('#'.$source->index.' '.$source->name.':'.$source->line); + $logOutput .= '#'.$source->index.' '.$source->name.':'.$source->line . PHP_EOL; } + + $this->log($logOutput); } } -} \ No newline at end of file + + private function log(string $message) + { + LaravelLog::channel(config('querydetector.log_channel'))->info($message); + } +} diff --git a/src/Outputs/Output.php b/src/Outputs/Output.php index accb26c..0dbe531 100644 --- a/src/Outputs/Output.php +++ b/src/Outputs/Output.php @@ -7,5 +7,7 @@ interface Output { + public function boot(); + public function output(Collection $detectedQueries, Response $response); -} \ No newline at end of file +} diff --git a/src/QueryDetector.php b/src/QueryDetector.php index 001f986..f48e5d2 100755 --- a/src/QueryDetector.php +++ b/src/QueryDetector.php @@ -2,29 +2,52 @@ namespace BeyondCode\QueryDetector; -use DB; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; use Symfony\Component\HttpFoundation\Response; use Illuminate\Database\Eloquent\Relations\Relation; +use BeyondCode\QueryDetector\Events\QueryDetected; class QueryDetector { /** @var Collection */ private $queries; + /** + * @var bool + */ + private $booted = false; - public function __construct() + private function resetQueries() { $this->queries = Collection::make(); } + public function __construct() + { + $this->resetQueries(); + } + public function boot() { - DB::listen(function($query) { + if ($this->booted) { + $this->resetQueries(); + return; + } + + DB::listen(function ($query) { $backtrace = collect(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50)); $this->logQuery($query, $backtrace); }); + + foreach ($this->getOutputTypes() as $outputType) { + app()->singleton($outputType); + app($outputType)->boot(); + } + + $this->booted = true; } public function isEnabled(): bool @@ -41,21 +64,21 @@ public function isEnabled(): bool public function logQuery($query, Collection $backtrace) { $modelTrace = $backtrace->first(function ($trace) { - return array_get($trace, 'object') instanceof Builder; + return Arr::get($trace, 'object') instanceof Builder; }); // The query is coming from an Eloquent model - if (! is_null($modelTrace)) { + if (!is_null($modelTrace)) { /* * Relations get resolved by either calling the "getRelationValue" method on the model, * or if the class itself is a Relation. */ $relation = $backtrace->first(function ($trace) { - return array_get($trace, 'function') === 'getRelationValue' || array_get($trace, 'class') === Relation::class ; + return Arr::get($trace, 'function') === 'getRelationValue' || Arr::get($trace, 'class') === Relation::class; }); // We try to access a relation - if (is_array($relation)) { + if (is_array($relation) && isset($relation['object'])) { if ($relation['class'] === Relation::class) { $model = get_class($relation['object']->getParent()); $relationName = get_class($relation['object']->getRelated()); @@ -66,17 +89,25 @@ public function logQuery($query, Collection $backtrace) $relatedModel = $relationName; } - $key = md5($query->sql . $model . $relationName); + $sources = $this->findSource($backtrace); + + if (empty($sources)) { + return; + } + + $key = md5($query->sql . $model . $relationName . $sources[0]->name . $sources[0]->line); - $count = array_get($this->queries, $key.'.count', 0); + $count = Arr::get($this->queries, $key . '.count', 0); + $time = Arr::get($this->queries, $key . '.time', 0); $this->queries[$key] = [ 'count' => ++$count, + 'time' => $time + $query->time, 'query' => $query->sql, 'model' => $model, 'relatedModel' => $relatedModel, 'relation' => $relationName, - 'sources' => $this->findSource($backtrace) + 'sources' => $sources ]; } } @@ -90,12 +121,12 @@ protected function findSource($stack) $sources[] = $this->parseTrace($index, $trace); } - return array_filter($sources); + return array_values(array_filter($sources)); } public function parseTrace($index, array $trace) { - $frame = (object) [ + $frame = (object)[ 'index' => $index, 'name' => null, 'line' => isset($trace['line']) ? $trace['line'] : '?', @@ -167,13 +198,31 @@ public function getDetectedQueries(): Collection } } - return $queries->where('count', '>', config('querydetector.threshold', 1))->values(); + $queries = $queries->where('count', '>', config('querydetector.threshold', 1))->values(); + + if ($queries->isNotEmpty()) { + event(new QueryDetected($queries)); + } + + return $queries; + } + + protected function getOutputTypes() + { + $outputTypes = config('querydetector.output'); + + if (!is_array($outputTypes)) { + $outputTypes = [$outputTypes]; + } + + return $outputTypes; } protected function applyOutput(Response $response) { - $outputType = app(config('querydetector.output')); - $outputType->output($this->getDetectedQueries(), $response); + foreach ($this->getOutputTypes() as $type) { + app($type)->output($this->getDetectedQueries(), $response); + } } public function output($request, $response) @@ -184,4 +233,9 @@ public function output($request, $response) return $response; } + + public function emptyQueries() + { + $this->queries = Collection::make(); + } } diff --git a/src/QueryDetectorServiceProvider.php b/src/QueryDetectorServiceProvider.php index 8d62987..4efac27 100644 --- a/src/QueryDetectorServiceProvider.php +++ b/src/QueryDetectorServiceProvider.php @@ -15,7 +15,7 @@ public function boot() if ($this->app->runningInConsole()) { $this->publishes([ __DIR__.'/../config/config.php' => config_path('querydetector.php'), - ], 'config'); + ], 'query-detector-config'); } $this->registerMiddleware(QueryDetectorMiddleware::class); diff --git a/tests/QueryDetectorTest.php b/tests/QueryDetectorTest.php index 48405e0..f2dbe0d 100644 --- a/tests/QueryDetectorTest.php +++ b/tests/QueryDetectorTest.php @@ -3,7 +3,9 @@ namespace BeyondCode\QueryDetector\Tests; use Route; +use Illuminate\Support\Facades\Event; use BeyondCode\QueryDetector\QueryDetector; +use BeyondCode\QueryDetector\Events\QueryDetected; use BeyondCode\QueryDetector\Tests\Models\Post; use BeyondCode\QueryDetector\Tests\Models\Author; use BeyondCode\QueryDetector\Tests\Models\Comment; @@ -32,6 +34,54 @@ public function it_detects_n1_query_on_properties() $this->assertSame('profile', $queries[0]['relation']); } + /** @test */ + public function it_detects_n1_query_on_multiple_requests() + { + Route::get('/', function (){ + $authors = Author::get(); + + foreach ($authors as $author) { + $author->profile; + } + }); + + // first request + $this->get('/'); + $queries = app(QueryDetector::class)->getDetectedQueries(); + $this->assertCount(1, $queries); + $this->assertSame(Author::count(), $queries[0]['count']); + $this->assertSame(Author::class, $queries[0]['model']); + $this->assertSame('profile', $queries[0]['relation']); + + // second request + $this->get('/'); + $queries = app(QueryDetector::class)->getDetectedQueries(); + $this->assertCount(1, $queries); + $this->assertSame(Author::count(), $queries[0]['count']); + $this->assertSame(Author::class, $queries[0]['model']); + $this->assertSame('profile', $queries[0]['relation']); + } + + /** @test */ + public function it_does_not_detect_a_false_n1_query_on_multiple_requests() + { + Route::get('/', function (){ + $authors = Author::with("profile")->get(); + + foreach ($authors as $author) { + $author->profile; + } + }); + + // first request + $this->get('/'); + $this->assertCount(0, app(QueryDetector::class)->getDetectedQueries()); + + // second request + $this->get('/'); + $this->assertCount(0, app(QueryDetector::class)->getDetectedQueries()); + } + /** @test */ public function it_ignores_eager_loaded_relationships() { @@ -224,4 +274,89 @@ public function it_ignores_redirects() $this->assertCount(1, $queries); } + + /** @test */ + public function it_fires_an_event_if_detects_n1_query() + { + Event::fake(); + + Route::get('/', function (){ + $authors = Author::all(); + + foreach ($authors as $author) { + $author->profile; + } + }); + + $this->get('/'); + + Event::assertDispatched(QueryDetected::class); + } + + /** @test */ + public function it_does_not_fire_an_event_if_there_is_no_n1_query() + { + Event::fake(); + + Route::get('/', function (){ + $authors = Author::with('profile')->get(); + + foreach ($authors as $author) { + $author->profile; + } + }); + + $this->get('/'); + + Event::assertNotDispatched(QueryDetected::class); + } + /** @test */ + public function it_uses_the_trace_line_to_detect_queries() + { + Route::get('/', function (){ + $authors = Author::all(); + $authors2 = Author::all(); + + foreach ($authors as $author) { + $author->profile->city; + } + + foreach ($authors2 as $author) { + $author->profile->city; + } + }); + + $this->get('/'); + + $queries = app(QueryDetector::class)->getDetectedQueries(); + + $this->assertCount(2, $queries); + + $this->assertSame(Author::count(), $queries[0]['count']); + $this->assertSame(Author::class, $queries[0]['model']); + $this->assertSame('profile', $queries[0]['relation']); + } + + /** @test */ + public function it_empty_queries() + { + Route::get('/', function (){ + $authors = Author::all(); + + foreach ($authors as $author) { + $author->profile; + } + }); + + $this->get('/'); + + $queryDetector = app(QueryDetector::class); + + $queries = $queryDetector->getDetectedQueries(); + $this->assertCount(1, $queries); + + $queryDetector->emptyQueries(); + $queries = $queryDetector->getDetectedQueries(); + $this->assertCount(0, $queries); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6f669c4..b981a0a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,7 +12,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase { protected static $setUpRun = false; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -79,4 +79,4 @@ protected function setUpDatabase() $table->morphs('commentable'); }); } -} \ No newline at end of file +}