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
[](https://packagist.org/packages/beyondcode/laravel-query-detector)
-[](https://travis-ci.org/beyondcode/laravel-query-detector)
-[](https://scrutinizer-ci.com/g/beyondcode/laravel-query-detector)
[](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).
-
+
+
## 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
+}