Passed
Pull Request — master (#97)
by
unknown
02:17
created

QueryDetector::emptyQueries()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace BeyondCode\QueryDetector;
4
5
use Illuminate\Support\Facades\DB;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Collection;
8
use Illuminate\Database\Eloquent\Builder;
9
use Symfony\Component\HttpFoundation\Response;
10
use Illuminate\Database\Eloquent\Relations\Relation;
11
use BeyondCode\QueryDetector\Events\QueryDetected;
12
13
class QueryDetector
14
{
15
    /** @var Collection */
16
    private $queries;
17
18
    public function __construct()
19
    {
20
        $this->queries = Collection::make();
21
    }
22
23
    public function boot()
24
    {
25
        DB::listen(function($query) {
26
            $backtrace = collect(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50));
27
28
            $this->logQuery($query, $backtrace);
29
        });
30
31
        foreach ($this->getOutputTypes() as $outputType) {
32
            app()->singleton($outputType);
33
            app($outputType)->boot();
34
        }
35
    }
36
37
    public function isEnabled(): bool
38
    {
39
        $configEnabled = value(config('querydetector.enabled'));
40
41
        if ($configEnabled === null) {
42
            $configEnabled = config('app.debug');
43
        }
44
45
        return $configEnabled;
46
    }
47
48
    public function logQuery($query, Collection $backtrace)
49
    {
50
        $modelTrace = $backtrace->first(function ($trace) {
51
            return Arr::get($trace, 'object') instanceof Builder;
52
        });
53
54
        // The query is coming from an Eloquent model
55
        if (! is_null($modelTrace)) {
56
            /*
57
             * Relations get resolved by either calling the "getRelationValue" method on the model,
58
             * or if the class itself is a Relation.
59
             */
60
            $relation = $backtrace->first(function ($trace) {
61
                return Arr::get($trace, 'function') === 'getRelationValue' || Arr::get($trace, 'class') === Relation::class ;
62
            });
63
64
            // We try to access a relation
65
            if (is_array($relation) && isset($relation['object'])) {
66
                if ($relation['class'] === Relation::class) {
67
                    $model = get_class($relation['object']->getParent());
68
                    $relationName = get_class($relation['object']->getRelated());
69
                    $relatedModel = $relationName;
70
                } else {
71
                    $model = get_class($relation['object']);
72
                    $relationName = $relation['args'][0];
73
                    $relatedModel = $relationName;
74
                }
75
76
                $sources = $this->findSource($backtrace);
77
78
                $key = md5($query->sql . $model . $relationName . $sources[0]->name . $sources[0]->line);
79
80
                $count = Arr::get($this->queries, $key.'.count', 0);
81
                $time = Arr::get($this->queries, $key.'.time', 0);
82
83
                $this->queries[$key] = [
84
                    'count' => ++$count,
85
                    'time' => $time + $query->time,
86
                    'query' => $query->sql,
87
                    'model' => $model,
88
                    'relatedModel' => $relatedModel,
89
                    'relation' => $relationName,
90
                    'sources' => $sources
91
                ];
92
            }
93
        }
94
    }
95
96
    protected function findSource($stack)
97
    {
98
        $sources = [];
99
100
        foreach ($stack as $index => $trace) {
101
            $sources[] = $this->parseTrace($index, $trace);
102
        }
103
104
        return array_values(array_filter($sources));
105
    }
106
107
    public function parseTrace($index, array $trace)
108
    {
109
        $frame = (object) [
110
            'index' => $index,
111
            'name' => null,
112
            'line' => isset($trace['line']) ? $trace['line'] : '?',
113
        ];
114
115
        if (isset($trace['class']) &&
116
            isset($trace['file']) &&
117
            !$this->fileIsInExcludedPath($trace['file'])
118
        ) {
119
            $frame->name = $this->normalizeFilename($trace['file']);
120
121
            return $frame;
122
        }
123
124
        return false;
125
    }
126
127
    /**
128
     * Check if the given file is to be excluded from analysis
129
     *
130
     * @param string $file
131
     * @return bool
132
     */
133
    protected function fileIsInExcludedPath($file)
134
    {
135
        $excludedPaths = [
136
            '/vendor/laravel/framework/src/Illuminate/Database',
137
            '/vendor/laravel/framework/src/Illuminate/Events',
138
        ];
139
140
        $normalizedPath = str_replace('\\', '/', $file);
141
142
        foreach ($excludedPaths as $excludedPath) {
143
            if (strpos($normalizedPath, $excludedPath) !== false) {
144
                return true;
145
            }
146
        }
147
148
        return false;
149
    }
150
151
    /**
152
     * Shorten the path by removing the relative links and base dir
153
     *
154
     * @param string $path
155
     * @return string
156
     */
157
    protected function normalizeFilename($path): string
158
    {
159
        if (file_exists($path)) {
160
            $path = realpath($path);
161
        }
162
163
        return str_replace(base_path(), '', $path);
164
    }
165
166
    public function getDetectedQueries(): Collection
167
    {
168
        $exceptions = config('querydetector.except', []);
169
170
        $queries = $this->queries
171
            ->values();
172
173
        foreach ($exceptions as $parentModel => $relations) {
174
            foreach ($relations as $relation) {
175
                $queries = $queries->reject(function ($query) use ($relation, $parentModel) {
176
                    return $query['model'] === $parentModel && $query['relatedModel'] === $relation;
177
                });
178
            }
179
        }
180
181
        $queries = $queries->where('count', '>', config('querydetector.threshold', 1))->values();
182
183
        if ($queries->isNotEmpty()) {
184
            event(new QueryDetected($queries));
185
        }
186
187
        return $queries;
188
    }
189
190
    protected function getOutputTypes()
191
    {
192
        $outputTypes = config('querydetector.output');
193
194
        if (! is_array($outputTypes)) {
195
            $outputTypes = [$outputTypes];
196
        }
197
198
        return $outputTypes;
199
    }
200
201
    protected function applyOutput(Response $response)
202
    {
203
        foreach ($this->getOutputTypes() as $type) {
204
            app($type)->output($this->getDetectedQueries(), $response);
0 ignored issues
show
Bug introduced by
The method output() does not exist on Illuminate\Contracts\Foundation\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

204
            app($type)->/** @scrutinizer ignore-call */ output($this->getDetectedQueries(), $response);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
205
        }
206
    }
207
208
    public function output($request, $response)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

208
    public function output(/** @scrutinizer ignore-unused */ $request, $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
209
    {
210
        if ($this->getDetectedQueries()->isNotEmpty()) {
211
            $this->applyOutput($response);
212
        }
213
214
        return $response;
215
    }
216
217
    public function emptyQueries()
218
    {
219
        $this->queries = Collection::make();
220
    }
221
}
222