Passed
Push — master ( 493fe1...4ac1f5 )
by Mihail
07:50
created

LaravelDatabaseCollector::setFindSource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Ffcms\Core\Debug;
4
5
6
use DebugBar\DataCollector\PDO\PDOCollector;
7
use DebugBar\DataCollector\TimeDataCollector;
8
9
10
/**
11
 * Collects data about SQL statements executed with PDO
12
 */
13
class LaravelDatabaseCollector extends PDOCollector
14
{
15
    protected $timeCollector;
16
    protected $queries = [];
17
    protected $renderSqlWithParams = false;
18
    protected $findSource = false;
19
    protected $middleware = [];
20
    protected $explainQuery = false;
21
    protected $explainTypes = ['SELECT']; // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
22
    protected $showHints = false;
23
    protected $reflection = [];
24
    /**
25
     * @param TimeDataCollector $timeCollector
26
     */
27
    public function __construct(TimeDataCollector $timeCollector = null)
28
    {
29
        $this->timeCollector = $timeCollector;
30
    }
31
    /**
32
     * Renders the SQL of traced statements with params embedded
33
     *
34
     * @param boolean $enabled
35
     * @param string $quotationChar NOT USED
36
     */
37
    public function setRenderSqlWithParams($enabled = true, $quotationChar = "'")
38
    {
39
        $this->renderSqlWithParams = $enabled;
40
    }
41
    /**
42
     * Show or hide the hints in the parameters
43
     *
44
     * @param boolean $enabled
45
     */
46
    public function setShowHints($enabled = true)
47
    {
48
        $this->showHints = $enabled;
49
    }
50
    /**
51
     * Enable/disable finding the source
52
     *
53
     * @param bool $value
54
     * @param array $middleware
55
     */
56
    public function setFindSource($value, array $middleware)
57
    {
58
        $this->findSource = (bool) $value;
59
        $this->middleware = $middleware;
60
    }
61
    /**
62
     * Enable/disable the EXPLAIN queries
63
     *
64
     * @param  bool $enabled
65
     * @param  array|null $types Array of types to explain queries (select/insert/update/delete)
66
     */
67
    public function setExplainSource($enabled, $types)
68
    {
69
        $this->explainQuery = $enabled;
70
        if($types){
71
            $this->explainTypes = $types;
72
        }
73
    }
74
    /**
75
     *
76
     * @param string $query
77
     * @param array $bindings
78
     * @param float $time
79
     * @param \Illuminate\Database\Connection $connection
80
     */
81
    public function addQuery($query, $bindings, $time, $connection)
82
    {
83
        $explainResults = [];
84
        $time = $time / 1000;
85
        $endTime = microtime(true);
86
        $startTime = $endTime - $time;
87
        $hints = $this->performQueryAnalysis($query);
88
        $pdo = $connection->getPdo();
89
        $bindings = $connection->prepareBindings($bindings);
90
        // Run EXPLAIN on this query (if needed)
91
        if ($this->explainQuery && preg_match('/^('.implode($this->explainTypes).') /i', $query)) {
92
            $statement = $pdo->prepare('EXPLAIN ' . $query);
93
            $statement->execute($bindings);
94
            $explainResults = $statement->fetchAll(\PDO::FETCH_CLASS);
95
        }
96
        $bindings = $this->getDataFormatter()->checkBindings($bindings);
0 ignored issues
show
Bug introduced by
The method checkBindings() does not exist on DebugBar\DataFormatter\DataFormatterInterface. ( Ignorable by Annotation )

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

96
        $bindings = $this->getDataFormatter()->/** @scrutinizer ignore-call */ checkBindings($bindings);

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...
97
        if (!empty($bindings) && $this->renderSqlWithParams) {
98
            foreach ($bindings as $key => $binding) {
99
                // This regex matches placeholders only, not the question marks,
100
                // nested in quotes, while we iterate through the bindings
101
                // and substitute placeholders by suitable values.
102
                $regex = is_numeric($key)
103
                    ? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
104
                    : "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
105
                $query = preg_replace($regex, $pdo->quote($binding), $query, 1);
106
            }
107
        }
108
        $source = [];
109
        if ($this->findSource) {
110
            try {
111
                $source = $this->findSource();
112
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
113
            }
114
        }
115
        $this->queries[] = [
116
            'query' => $query,
117
            'type' => 'query',
118
            'bindings' => $this->getDataFormatter()->escapeBindings($bindings),
0 ignored issues
show
Bug introduced by
The method escapeBindings() does not exist on DebugBar\DataFormatter\DataFormatterInterface. ( Ignorable by Annotation )

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

118
            'bindings' => $this->getDataFormatter()->/** @scrutinizer ignore-call */ escapeBindings($bindings),

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...
119
            'time' => $time,
120
            'source' => $source,
121
            'explain' => $explainResults,
122
            'connection' => $connection->getDatabaseName(),
123
            'hints' => $this->showHints ? $hints : null,
124
        ];
125
        if ($this->timeCollector !== null) {
126
            $this->timeCollector->addMeasure($query, $startTime, $endTime);
127
        }
128
    }
129
    /**
130
     * Explainer::performQueryAnalysis()
131
     *
132
     * Perform simple regex analysis on the code
133
     *
134
     * @package xplain (https://github.com/rap2hpoutre/mysql-xplain-xplain)
135
     * @author e-doceo
136
     * @copyright 2014
137
     * @version $Id$
138
     * @access public
139
     * @param string $query
140
     * @return string
141
     */
142
    protected function performQueryAnalysis($query)
143
    {
144
        $hints = [];
145
        if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $query)) {
146
            $hints[] = 'Use <code>SELECT *</code> only if you need all columns from table';
147
        }
148
        if (preg_match('/ORDER BY RAND()/i', $query)) {
149
            $hints[] = '<code>ORDER BY RAND()</code> is slow, try to avoid if you can.
150
				You can <a href="http://stackoverflow.com/questions/2663710/how-does-mysqls-order-by-rand-work" target="_blank">read this</a>
151
				or <a href="http://stackoverflow.com/questions/1244555/how-can-i-optimize-mysqls-order-by-rand-function" target="_blank">this</a>';
152
        }
153
        if (strpos($query, '!=') !== false) {
154
            $hints[] = 'The <code>!=</code> operator is not standard. Use the <code>&lt;&gt;</code> operator to test for inequality instead.';
155
        }
156
        if (stripos($query, 'WHERE') === false && preg_match('/^(SELECT) /i', $query)) {
157
            $hints[] = 'The <code>SELECT</code> statement has no <code>WHERE</code> clause and could examine many more rows than intended';
158
        }
159
        if (preg_match('/LIMIT\\s/i', $query) && stripos($query, 'ORDER BY') === false) {
160
            $hints[] = '<code>LIMIT</code> without <code>ORDER BY</code> causes non-deterministic results, depending on the query execution plan';
161
        }
162
        if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $query, $matches)) {
163
            $hints[] = 	'An argument has a leading wildcard character: <code>' . $matches[1]. '</code>.
164
								The predicate with this argument is not sargable and cannot use an index if one exists.';
165
        }
166
        return $hints;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $hints returns the type array|string[] which is incompatible with the documented return type string.
Loading history...
167
    }
168
    /**
169
     * Use a backtrace to search for the origins of the query.
170
     *
171
     * @return array
172
     */
173
    protected function findSource()
174
    {
175
        $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, 50);
176
        $sources = [];
177
        foreach ($stack as $index => $trace) {
178
            $sources[] = $this->parseTrace($index, $trace);
179
        }
180
        return array_filter($sources);
181
    }
182
    /**
183
     * Parse a trace element from the backtrace stack.
184
     *
185
     * @param  int    $index
186
     * @param  array  $trace
187
     * @return object|bool
188
     */
189
    protected function parseTrace($index, array $trace)
190
    {
191
        $frame = (object) [
192
            'index' => $index,
193
            'namespace' => null,
194
            'name' => null,
195
            'line' => isset($trace['line']) ? $trace['line'] : '?',
196
        ];
197
        if (isset($trace['function']) && $trace['function'] == 'substituteBindings') {
198
            $frame->name = 'Route binding';
199
            return $frame;
200
        }
201
        if (isset($trace['class']) &&
202
            isset($trace['file']) &&
203
            !$this->fileIsInExcludedPath($trace['file'])
204
        ) {
205
            $file = $trace['file'];
206
            if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) {
207
                list($file, $frame->line) = $this->getTwigInfo($trace);
208
            } elseif (strpos($file, storage_path()) !== false) {
0 ignored issues
show
Bug introduced by
The function storage_path was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

208
            } elseif (strpos($file, /** @scrutinizer ignore-call */ storage_path()) !== false) {
Loading history...
209
                $hash = pathinfo($file, PATHINFO_FILENAME);
210
                if (! $frame->name = $this->findViewFromHash($hash)) {
211
                    $frame->name = $hash;
212
                }
213
                $frame->namespace = 'view';
214
                return $frame;
215
            } elseif (strpos($file, 'Middleware') !== false) {
216
                $frame->name = $this->findMiddlewareFromFile($file);
217
                if ($frame->name) {
218
                    $frame->namespace = 'middleware';
219
                } else {
220
                    $frame->name = $this->normalizeFilename($file);
221
                }
222
                return $frame;
223
            }
224
            $frame->name = $this->normalizeFilename($file);
225
            return $frame;
226
        }
227
        return false;
228
    }
229
    /**
230
     * Check if the given file is to be excluded from analysis
231
     *
232
     * @param string $file
233
     * @return bool
234
     */
235
    protected function fileIsInExcludedPath($file)
236
    {
237
        $excludedPaths = [
238
            '/vendor/laravel/framework/src/Illuminate/Database',
239
            '/vendor/laravel/framework/src/Illuminate/Events',
240
            '/vendor/barryvdh/laravel-debugbar',
241
        ];
242
        $normalizedPath = str_replace('\\', '/', $file);
243
        foreach ($excludedPaths as $excludedPath) {
244
            if (strpos($normalizedPath, $excludedPath) !== false) {
245
                return true;
246
            }
247
        }
248
        return false;
249
    }
250
    /**
251
     * Find the middleware alias from the file.
252
     *
253
     * @param  string $file
254
     * @return string|null
255
     */
256
    protected function findMiddlewareFromFile($file)
257
    {
258
        $filename = pathinfo($file, PATHINFO_FILENAME);
259
        foreach ($this->middleware as $alias => $class) {
260
            if (strpos($class, $filename) !== false) {
261
                return $alias;
262
            }
263
        }
264
    }
265
    /**
266
     * Find the template name from the hash.
267
     *
268
     * @param  string $hash
269
     * @return null|string
270
     */
271
    protected function findViewFromHash($hash)
272
    {
273
        $finder = app('view')->getFinder();
0 ignored issues
show
Bug introduced by
The function app was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

273
        $finder = /** @scrutinizer ignore-call */ app('view')->getFinder();
Loading history...
274
        if (isset($this->reflection['viewfinderViews'])) {
275
            $property = $this->reflection['viewfinderViews'];
276
        } else {
277
            $reflection = new \ReflectionClass($finder);
278
            $property = $reflection->getProperty('views');
279
            $property->setAccessible(true);
280
            $this->reflection['viewfinderViews'] = $property;
281
        }
282
        foreach ($property->getValue($finder) as $name => $path){
283
            if (sha1($path) == $hash || md5($path) == $hash) {
284
                return $name;
285
            }
286
        }
287
    }
288
    /**
289
     * Get the filename/line from a Twig template trace
290
     *
291
     * @param array $trace
292
     * @return array The file and line
293
     */
294
    protected function getTwigInfo($trace)
295
    {
296
        $file = $trace['object']->getTemplateName();
297
        if (isset($trace['line'])) {
298
            foreach ($trace['object']->getDebugInfo() as $codeLine => $templateLine) {
299
                if ($codeLine <= $trace['line']) {
300
                    return [$file, $templateLine];
301
                }
302
            }
303
        }
304
        return [$file, -1];
305
    }
306
    /**
307
     * Shorten the path by removing the relative links and base dir
308
     *
309
     * @param string $path
310
     * @return string
311
     */
312
    protected function normalizeFilename($path)
313
    {
314
        if (file_exists($path)) {
315
            $path = realpath($path);
316
        }
317
        return str_replace(base_path(), '', $path);
0 ignored issues
show
Bug introduced by
The function base_path was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

317
        return str_replace(/** @scrutinizer ignore-call */ base_path(), '', $path);
Loading history...
318
    }
319
    /**
320
     * Collect a database transaction event.
321
     * @param  string $event
322
     * @param \Illuminate\Database\Connection $connection
323
     * @return array
324
     */
325
    public function collectTransactionEvent($event, $connection)
326
    {
327
        $source = [];
328
        if ($this->findSource) {
329
            try {
330
                $source = $this->findSource();
331
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
332
            }
333
        }
334
        $this->queries[] = [
335
            'query' => $event,
336
            'type' => 'transaction',
337
            'bindings' => [],
338
            'time' => 0,
339
            'source' => $source,
340
            'explain' => [],
341
            'connection' => $connection->getDatabaseName(),
342
            'hints' => null,
343
        ];
344
    }
345
    /**
346
     * Reset the queries.
347
     */
348
    public function reset()
349
    {
350
        $this->queries = [];
351
    }
352
    /**
353
     * {@inheritDoc}
354
     */
355
    public function collect()
356
    {
357
        $totalTime = 0;
358
        $queries = $this->queries;
359
        $statements = [];
360
        foreach ($queries as $query) {
361
            $totalTime += $query['time'];
362
            $statements[] = [
363
                'sql' => $this->getDataFormatter()->formatSql($query['query']),
0 ignored issues
show
Bug introduced by
The method formatSql() does not exist on DebugBar\DataFormatter\DataFormatterInterface. Did you maybe mean formatVar()? ( Ignorable by Annotation )

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

363
                'sql' => $this->getDataFormatter()->/** @scrutinizer ignore-call */ formatSql($query['query']),

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...
364
                'type' => $query['type'],
365
                'params' => [],
366
                'bindings' => $query['bindings'],
367
                'hints' => $query['hints'],
368
                'backtrace' => array_values($query['source']),
369
                'duration' => $query['time'],
370
                'duration_str' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']),
0 ignored issues
show
Deprecated Code introduced by
The function DebugBar\DataCollector\D...ector::formatDuration() has been deprecated. ( Ignorable by Annotation )

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

370
                'duration_str' => ($query['type'] == 'transaction') ? '' : /** @scrutinizer ignore-deprecated */ $this->formatDuration($query['time']),
Loading history...
371
                'stmt_id' => $this->getDataFormatter()->formatSource(reset($query['source'])),
0 ignored issues
show
Bug introduced by
The method formatSource() does not exist on DebugBar\DataFormatter\DataFormatterInterface. ( Ignorable by Annotation )

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

371
                'stmt_id' => $this->getDataFormatter()->/** @scrutinizer ignore-call */ formatSource(reset($query['source'])),

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...
372
                'connection' => $query['connection'],
373
            ];
374
            //Add the results from the explain as new rows
375
            foreach($query['explain'] as $explain){
376
                $statements[] = [
377
                    'sql' => ' - EXPLAIN #' . $explain->id . ': `' . $explain->table . '` (' . $explain->select_type . ')',
378
                    'type' => 'explain',
379
                    'params' => $explain,
380
                    'row_count' => $explain->rows,
381
                    'stmt_id' => $explain->id,
382
                ];
383
            }
384
        }
385
        $nb_statements = array_filter($queries, function ($query) {
386
            return $query['type'] == 'query';
387
        });
388
        $data = [
389
            'nb_statements' => count($nb_statements),
390
            'nb_failed_statements' => 0,
391
            'accumulated_duration' => $totalTime,
392
            'accumulated_duration_str' => $this->formatDuration($totalTime),
0 ignored issues
show
Deprecated Code introduced by
The function DebugBar\DataCollector\D...ector::formatDuration() has been deprecated. ( Ignorable by Annotation )

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

392
            'accumulated_duration_str' => /** @scrutinizer ignore-deprecated */ $this->formatDuration($totalTime),
Loading history...
393
            'statements' => $statements
394
        ];
395
        return $data;
396
    }
397
    /**
398
     * {@inheritDoc}
399
     */
400
    public function getName()
401
    {
402
        return 'queries';
403
    }
404
    /**
405
     * {@inheritDoc}
406
     */
407
    public function getWidgets()
408
    {
409
        return [
410
            "queries" => [
411
                "icon" => "database",
412
                "widget" => "PhpDebugBar.Widgets.SQLQueriesWidget",
413
                "map" => "queries",
414
                "default" => "[]"
415
            ],
416
            "queries:badge" => [
417
                "map" => "queries.nb_statements",
418
                "default" => 0
419
            ]
420
        ];
421
    }
422
}