Skip to content

Commit 9c232e1

Browse files
authored
Merge pull request barryvdh#608 from sebdesign/backtrace-queries
Improve the queries tab
2 parents a964c7b + c89a3ad commit 9c232e1

File tree

6 files changed

+530
-87
lines changed

6 files changed

+530
-87
lines changed

src/DataCollector/QueryCollector.php

Lines changed: 140 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class QueryCollector extends PDOCollector
1414
protected $queries = [];
1515
protected $renderSqlWithParams = false;
1616
protected $findSource = false;
17+
protected $middleware = [];
1718
protected $explainQuery = false;
1819
protected $explainTypes = ['SELECT']; // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
1920
protected $showHints = false;
@@ -52,10 +53,12 @@ public function setShowHints($enabled = true)
5253
* Enable/disable finding the source
5354
*
5455
* @param bool $value
56+
* @param array $middleware
5557
*/
56-
public function setFindSource($value = true)
58+
public function setFindSource($value, array $middleware)
5759
{
5860
$this->findSource = (bool) $value;
61+
$this->middleware = $middleware;
5962
}
6063

6164
/**
@@ -97,7 +100,7 @@ public function addQuery($query, $bindings, $time, $connection)
97100
$explainResults = $statement->fetchAll(\PDO::FETCH_CLASS);
98101
}
99102

100-
$bindings = $this->checkBindings($bindings);
103+
$bindings = $this->getDataFormatter()->checkBindings($bindings);
101104
if (!empty($bindings) && $this->renderSqlWithParams) {
102105
foreach ($bindings as $key => $binding) {
103106
// This regex matches placeholders only, not the question marks,
@@ -110,7 +113,8 @@ public function addQuery($query, $bindings, $time, $connection)
110113
}
111114
}
112115

113-
$source = null;
116+
$source = [];
117+
114118
if ($this->findSource) {
115119
try {
116120
$source = $this->findSource();
@@ -120,7 +124,8 @@ public function addQuery($query, $bindings, $time, $connection)
120124

121125
$this->queries[] = [
122126
'query' => $query,
123-
'bindings' => $this->escapeBindings($bindings),
127+
'type' => 'query',
128+
'bindings' => $this->getDataFormatter()->escapeBindings($bindings),
124129
'time' => $time,
125130
'source' => $source,
126131
'explain' => $explainResults,
@@ -133,36 +138,6 @@ public function addQuery($query, $bindings, $time, $connection)
133138
}
134139
}
135140

136-
/**
137-
* Check bindings for illegal (non UTF-8) strings, like Binary data.
138-
*
139-
* @param $bindings
140-
* @return mixed
141-
*/
142-
protected function checkBindings($bindings)
143-
{
144-
foreach ($bindings as &$binding) {
145-
if (is_string($binding) && !mb_check_encoding($binding, 'UTF-8')) {
146-
$binding = '[BINARY DATA]';
147-
}
148-
}
149-
return $bindings;
150-
}
151-
152-
/**
153-
* Make the bindings safe for outputting.
154-
*
155-
* @param array $bindings
156-
* @return array
157-
*/
158-
protected function escapeBindings($bindings)
159-
{
160-
foreach ($bindings as &$binding) {
161-
$binding = htmlentities($binding, ENT_QUOTES, 'UTF-8', false);
162-
}
163-
return $bindings;
164-
}
165-
166141
/**
167142
* Explainer::performQueryAnalysis()
168143
*
@@ -200,39 +175,102 @@ protected function performQueryAnalysis($query)
200175
$hints[] = 'An argument has a leading wildcard character: <code>' . $matches[1]. '</code>.
201176
The predicate with this argument is not sargable and cannot use an index if one exists.';
202177
}
203-
return implode("<br />", $hints);
178+
return $hints;
204179
}
205180

206181
/**
207-
* Use a backtrace to search for the origin of the query.
182+
* Use a backtrace to search for the origins of the query.
183+
*
184+
* @return array
208185
*/
209186
protected function findSource()
210187
{
211-
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
212-
foreach ($traces as $trace) {
213-
if (isset($trace['class']) && isset($trace['file']) && strpos(
214-
$trace['file'],
215-
DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR
216-
) === false
217-
) {
218-
if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) {
219-
list($file, $line) = $this->getTwigInfo($trace);
220-
} elseif (strpos($trace['file'], storage_path()) !== false) {
221-
$hash = pathinfo($trace['file'], PATHINFO_FILENAME);
222-
$line = isset($trace['line']) ? $trace['line'] : '?';
223-
224-
if ($name = $this->findViewFromHash($hash)) {
225-
return 'view::' . $name . ':' . $line;
226-
}
227-
return 'view::' . $hash . ':' . $line;
188+
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
189+
190+
$sources = [];
191+
192+
foreach ($stack as $index => $trace) {
193+
$sources[] = $this->parseTrace($index, $trace);
194+
}
195+
196+
return array_filter($sources);
197+
}
198+
199+
/**
200+
* Parse a trace element from the backtrace stack.
201+
*
202+
* @param int $index
203+
* @param array $trace
204+
* @return array|bool
205+
*/
206+
protected function parseTrace($index, array $trace)
207+
{
208+
$frame = (object) [
209+
'index' => $index,
210+
'namespace' => null,
211+
'name' => null,
212+
'line' => isset($trace['line']) ? $trace['line'] : '?',
213+
];
214+
215+
if (isset($trace['function']) && $trace['function'] == 'substituteBindings') {
216+
$frame->name = 'Route binding';
217+
218+
return $frame;
219+
}
220+
221+
if (isset($trace['class']) && isset($trace['file']) && strpos(
222+
$trace['file'],
223+
DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR
224+
) === false
225+
) {
226+
$file = $trace['file'];
227+
228+
if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) {
229+
list($file, $frame->line) = $this->getTwigInfo($trace);
230+
} elseif (strpos($file, storage_path()) !== false) {
231+
$hash = pathinfo($file, PATHINFO_FILENAME);
232+
233+
if (! $frame->name = $this->findViewFromHash($hash)) {
234+
$frame->name = $hash;
235+
}
236+
237+
$frame->namespace = 'view';
238+
239+
return $frame;
240+
} elseif (strpos($file, 'Middleware') !== false) {
241+
$frame->name = $this->findMiddlewareFromFile($file);
242+
243+
if ($frame->name) {
244+
$frame->namespace = 'middleware';
228245
} else {
229-
$file = $trace['file'];
230-
$line = isset($trace['line']) ? $trace['line'] : '?';
246+
$frame->name = $this->normalizeFilename($file);
231247
}
232248

233-
return $this->normalizeFilename($file) . ':' . $line;
234-
} elseif (isset($trace['function']) && $trace['function'] == 'Illuminate\Routing\{closure}') {
235-
return 'Route binding';
249+
return $frame;
250+
}
251+
252+
$frame->name = $this->normalizeFilename($file);
253+
254+
return $frame;
255+
}
256+
257+
258+
return false;
259+
}
260+
261+
/**
262+
* Find the middleware alias from the file.
263+
*
264+
* @param string $file
265+
* @return string|null
266+
*/
267+
protected function findMiddlewareFromFile($file)
268+
{
269+
$filename = pathinfo($file, PATHINFO_FILENAME);
270+
271+
foreach ($this->middleware as $alias => $class) {
272+
if (strpos($class, $filename) !== false) {
273+
return $alias;
236274
}
237275
}
238276
}
@@ -298,6 +336,35 @@ protected function normalizeFilename($path)
298336
return str_replace(base_path(), '', $path);
299337
}
300338

339+
/**
340+
* Collect a database transaction event.
341+
* @param string $event
342+
* @param \Illuminate\Database\Connection $connection
343+
* @return array
344+
*/
345+
public function collectTransactionEvent($event, $connection)
346+
{
347+
$source = [];
348+
349+
if ($this->findSource) {
350+
try {
351+
$source = $this->findSource();
352+
} catch (\Exception $e) {
353+
}
354+
}
355+
356+
$this->queries[] = [
357+
'query' => $event,
358+
'type' => 'transaction',
359+
'bindings' => [],
360+
'time' => 0,
361+
'source' => $source,
362+
'explain' => [],
363+
'connection' => $connection->getDatabaseName(),
364+
'hints' => null,
365+
];
366+
}
367+
301368
/**
302369
* Reset the queries.
303370
*/
@@ -318,33 +385,37 @@ public function collect()
318385
foreach ($queries as $query) {
319386
$totalTime += $query['time'];
320387

321-
$bindings = $query['bindings'];
322-
if($query['hints']){
323-
$bindings['hints'] = $query['hints'];
324-
}
325-
326388
$statements[] = [
327-
'sql' => $this->formatSql($query['query']),
328-
'params' => (object) $bindings,
389+
'sql' => $this->getDataFormatter()->formatSql($query['query']),
390+
'type' => $query['type'],
391+
'params' => [],
392+
'bindings' => $query['bindings'],
393+
'hints' => $query['hints'],
394+
'backtrace' => array_values($query['source']),
329395
'duration' => $query['time'],
330-
'duration_str' => $this->formatDuration($query['time']),
331-
'stmt_id' => $query['source'],
396+
'duration_str' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']),
397+
'stmt_id' => $this->getDataFormatter()->formatSource(reset($query['source'])),
332398
'connection' => $query['connection'],
333399
];
334400

335401
//Add the results from the explain as new rows
336402
foreach($query['explain'] as $explain){
337403
$statements[] = [
338404
'sql' => ' - EXPLAIN #' . $explain->id . ': `' . $explain->table . '` (' . $explain->select_type . ')',
405+
'type' => 'explain',
339406
'params' => $explain,
340407
'row_count' => $explain->rows,
341408
'stmt_id' => $explain->id,
342409
];
343410
}
344411
}
345412

413+
$nb_statements = array_filter($queries, function ($query) {
414+
return $query['type'] == 'query';
415+
});
416+
346417
$data = [
347-
'nb_statements' => count($queries),
418+
'nb_statements' => count($nb_statements),
348419
'nb_failed_statements' => 0,
349420
'accumulated_duration' => $totalTime,
350421
'accumulated_duration_str' => $this->formatDuration($totalTime),
@@ -353,17 +424,6 @@ public function collect()
353424
return $data;
354425
}
355426

356-
/**
357-
* Removes extra spaces at the beginning and end of the SQL query and its lines.
358-
*
359-
* @param string $sql
360-
* @return string
361-
*/
362-
protected function formatSql($sql)
363-
{
364-
return trim(preg_replace("/\s*\n\s*/", "\n", $sql));
365-
}
366-
367427
/**
368428
* {@inheritDoc}
369429
*/
@@ -380,7 +440,7 @@ public function getWidgets()
380440
return [
381441
"queries" => [
382442
"icon" => "database",
383-
"widget" => "PhpDebugBar.Widgets.SQLQueriesWidget",
443+
"widget" => "PhpDebugBar.Widgets.LaravelSQLQueriesWidget",
384444
"map" => "queries",
385445
"default" => "[]"
386446
],

0 commit comments

Comments
 (0)