ApiDebugger::startDebug()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 3
eloc 13
c 2
b 0
f 1
nc 2
nop 0
dl 0
loc 18
rs 9.8333
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ka4ivan\ApiDebugger\Support;
6
7
use Illuminate\Http\Request;
0 ignored issues
show
Bug introduced by
The type Illuminate\Http\Request was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Facades\DB;
10
11
class ApiDebugger
12
{
13
    private array $queries = [];
14
15
    /**
16
     * Check if the debugger is active based on APP_DEBUG environment variable.
17
     *
18
     * @return bool
19
     */
20
    public function isActive(): bool
21
    {
22
        return (bool) env('APP_DEBUG');
23
    }
24
25
    /**
26
     * Start debugging by enabling query logging if the debugger is active.
27
     *
28
     * @return void
29
     */
30
    public function startDebug(): void
31
    {
32
        if (!$this->isActive()) {
33
            return;
34
        }
35
36
        $this->queries = [];
37
38
        DB::listen(function ($query) {
39
            $this->queries[] = [
40
                'sql' => $query->sql,
41
                'bindings' => $query->bindings,
42
                'time' => $query->time,
43
                'trace' => collect(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))
0 ignored issues
show
Bug introduced by
debug_backtrace(Ka4ivan\..._BACKTRACE_IGNORE_ARGS) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

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

43
                'trace' => collect(/** @scrutinizer ignore-type */ debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))
Loading history...
44
                    ->filter(fn ($trace) => isset($trace['file']) && !str_contains($trace['file'], 'vendor'))
45
                    ->map(fn ($trace) => "{$trace['file']}:{$trace['line']}")
46
                    ->values()
47
                    ->all(),
48
            ];
49
        });
50
    }
51
52
    /**
53
     * Get the debugging information including request data and queries executed.
54
     *
55
     * @param Request $request
56
     * @return array
57
     */
58
    public function getDebug(Request $request): array
59
    {
60
        return [
61
            'debugger' => [
62
                'queries' => $this->getQueriesInfo(),
63
                'request' => $this->getRequestInfo($request),
64
            ],
65
        ];
66
    }
67
68
    /**
69
     * Retrieve information about the request such as body and headers.
70
     *
71
     * @param Request $request
72
     * @return array
73
     */
74
    protected function getRequestInfo(Request $request): array
75
    {
76
        return [
77
            'body' => Arr::except($request->input(), ['password', 'confirm_password', 'password_confirmation', '_destination', '_method', '_token', '_modal', 'destination']),
78
            'headers' => $request->header(),
79
        ];
80
    }
81
82
    /**
83
     * Retrieve the query log information, including count, executed queries, and checks for N+1 and long queries.
84
     *
85
     * @return array
86
     */
87
    protected function getQueriesInfo(): array
88
    {
89
        $queries = $this->queries;
90
91
        $totalTime = round(array_reduce($queries, fn ($carry, $query) => $carry + $query['time'], 0), 2);
92
        $longQueries = $this->checkLongQueries($queries);
93
        $repeatedQueries = $this->checkRepeatedQueries($queries);
94
95
        return [
96
            'count' => count($queries),
97
            'time' => $totalTime,
98
            'data' => $queries,
99
            'long_queries' => $longQueries,
100
            'repeated_queries' => $repeatedQueries,
101
        ];
102
    }
103
104
    /**
105
     * Check for long queries (taking longer than a specified threshold).
106
     *
107
     * @param array $queries
108
     * @param float $threshold
109
     * @return array
110
     */
111
    protected function checkLongQueries(array $queries, float $threshold = 10.0): array
112
    {
113
        return collect($queries)
0 ignored issues
show
Bug introduced by
$queries of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

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

113
        return collect(/** @scrutinizer ignore-type */ $queries)
Loading history...
114
            ->filter(fn ($query) => $query['time'] > $threshold)
115
            ->sortByDesc('time')
116
            ->values()
117
            ->all();
118
    }
119
120
    /**
121
     * Check for N+1 query issues.
122
     *
123
     * This method compares queries to detect repeating queries with similar structures.
124
     *
125
     * @param array $queries
126
     * @return array
127
     */
128
    protected function checkRepeatedQueries(array $queries): array
129
    {
130
        $queryMap = [];
131
        $repeatedQueries = [];
132
133
        foreach ($queries as $query) {
134
            $queryKey = $query['sql'];
135
            $queryMap[$queryKey][] = $query;
136
        }
137
138
        foreach ($queryMap as $sql => $instances) {
139
            if (count($instances) > 1) {
140
                $backtraces = array_map(
141
                    fn ($q) => $q['trace'],
142
                    $instances
143
                );
144
145
                $repeatedQueries[] = [
146
                    'sql' => $sql,
147
                    'count' => count($instances),
148
                    'backtrace' => $backtraces,
149
                ];
150
            }
151
        }
152
153
        return $repeatedQueries;
154
    }
155
}
156