Passed
Pull Request — master (#79)
by Mohannad
11:37
created

QueryDetector::parseTrace()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 18
rs 9.6111
cc 5
nc 4
nop 2
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
use BeyondCode\QueryDetector\Concerns\Bootable;
13
use BeyondCode\QueryDetector\Concerns\HasContext;
14
use BeyondCode\QueryDetector\Concerns\InteractsWithSourceFiles;
15
16
class QueryDetector
17
{
18
    use Bootable, HasContext, InteractsWithSourceFiles;
19
20
    /** @var Collection */
21
    private $queries;
22
23
    public function __construct()
24
    {
25
        $this->queries = Collection::make();
26
    }
27
28
    protected function boot()
29
    {
30
        DB::listen(function($query) {
31
            $backtrace = collect(debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50));
32
33
            $this->logQuery($query, $backtrace);
34
        });
35
36
        foreach ($this->getOutputTypes() as $outputType) {
37
            app()->singleton($outputType);
38
            app($outputType)->boot();
39
        }
40
    }
41
42
    public function isEnabled(): bool
43
    {
44
        $configEnabled = value(config('querydetector.enabled'));
45
46
        if ($configEnabled === null) {
47
            $configEnabled = config('app.debug');
48
        }
49
50
        return $configEnabled;
51
    }
52
53
    public function logQuery($query, Collection $backtrace)
54
    {
55
        $modelTrace = $backtrace->first(function ($trace) {
56
            return Arr::get($trace, 'object') instanceof Builder;
57
        });
58
59
        // Ensure the query is coming from an Eloquent model
60
        if (is_null($modelTrace)) {
61
            return ;
62
        }
63
64
        /*
65
         * Relations get resolved by either calling the "getRelationValue" method on the model,
66
         * or if the class itself is a Relation.
67
         */
68
        $relation = $backtrace->first(function ($trace) {
69
            return Arr::get($trace, 'function') === 'getRelationValue' || Arr::get($trace, 'class') === Relation::class ;
70
        });
71
72
        // We try to access a relation
73
        if (! is_array($relation) || ! isset($relation['object'])) {
74
            return ;
75
        }
76
77
        if ($relation['class'] === Relation::class) {
78
            $model = get_class($relation['object']->getParent());
79
            $relationName = get_class($relation['object']->getRelated());
80
            $relatedModel = $relationName;
81
        } else {
82
            $model = get_class($relation['object']);
83
            $relationName = $relation['args'][0];
84
            $relatedModel = $relationName;
85
        }
86
87
        $sources = $this->findSource($backtrace);
88
89
        $key = md5($this->context . $query->sql . $model . $relationName . $sources[0]->name . $sources[0]->line);
90
91
        $count = Arr::get($this->queries, $key.'.count', 0);
92
        $time = Arr::get($this->queries, $key.'.time', 0);
93
94
        $this->queries[$key] = [
95
            'count' => ++$count,
96
            'time' => $time + $query->time,
97
            'query' => $query->sql,
98
            'model' => $model,
99
            'relatedModel' => $relatedModel,
100
            'relation' => $relationName,
101
            'context' => $this->context,
102
            'sources' => $sources
103
        ];
104
    }
105
106
    public function getDetectedQueries(): Collection
107
    {
108
        $exceptions = config('querydetector.except', []);
109
110
        $queries = $this->queries
111
            ->values();
112
113
        foreach ($exceptions as $parentModel => $relations) {
114
            foreach ($relations as $relation) {
115
                $queries = $queries->reject(function ($query) use ($relation, $parentModel) {
116
                    return $query['model'] === $parentModel && $query['relatedModel'] === $relation;
117
                });
118
            }
119
        }
120
121
        $queries = $queries->where('count', '>', config('querydetector.threshold', 1))->values();
122
123
        if ($queries->isNotEmpty()) {
124
            event(new QueryDetected($queries));
125
        }
126
127
        return $queries;
128
    }
129
130
    protected function getOutputTypes()
131
    {
132
        $outputTypes = config('querydetector.output');
133
134
        if (! is_array($outputTypes)) {
135
            $outputTypes = [$outputTypes];
136
        }
137
138
        return $outputTypes;
139
    }
140
141
    protected function applyOutput(Collection $detectedQueries, Response $response)
142
    {
143
        foreach ($this->getOutputTypes() as $type) {
144
            app($type)->output($detectedQueries, $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

144
            app($type)->/** @scrutinizer ignore-call */ output($detectedQueries, $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...
145
        }
146
    }
147
148
    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

148
    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...
149
    {
150
        $detectedQueries = $this->getDetectedQueries();
151
152
        if ($detectedQueries->isNotEmpty()) {
153
            $this->applyOutput($detectedQueries, $response);
154
        }
155
156
        return $response;
157
    }
158
}
159