Skip to content

Commit ac6adfe

Browse files
committed
Improve the queries tab
- Collect database transactions events (begin, commit, rollback). - Display the stack of the backtrace when a query item is expanded. - Model binding is detected explicitly in the `Router#substituteBindings` method. - The backtrace now detects middleware. - Redesigned the params table to display bindings, hints and backtrace nicely. - The data formatting methods have been extracted to a `QueryFormatter`. See barryvdh#483 P.S.: In the screenshot I have replace the fonts with a system font task. I plan to submit a PR to the [php-debugbar](https://github.com/maximebf/php-debugbar) repo about that.
1 parent a964c7b commit ac6adfe

File tree

4 files changed

+382
-83
lines changed

4 files changed

+382
-83
lines changed

src/DataCollector/QueryCollector.php

Lines changed: 128 additions & 76 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,7 @@ public function addQuery($query, $bindings, $time, $connection)
120124

121125
$this->queries[] = [
122126
'query' => $query,
123-
'bindings' => $this->escapeBindings($bindings),
127+
'bindings' => $this->getDataFormatter()->escapeBindings($bindings),
124128
'time' => $time,
125129
'source' => $source,
126130
'explain' => $explainResults,
@@ -133,36 +137,6 @@ public function addQuery($query, $bindings, $time, $connection)
133137
}
134138
}
135139

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-
166140
/**
167141
* Explainer::performQueryAnalysis()
168142
*
@@ -200,39 +174,103 @@ protected function performQueryAnalysis($query)
200174
$hints[] = 'An argument has a leading wildcard character: <code>' . $matches[1]. '</code>.
201175
The predicate with this argument is not sargable and cannot use an index if one exists.';
202176
}
203-
return implode("<br />", $hints);
177+
return $hints;
204178
}
205179

206180
/**
207-
* Use a backtrace to search for the origin of the query.
181+
* Use a backtrace to search for the origins of the query.
182+
*
183+
* @return array
208184
*/
209185
protected function findSource()
210186
{
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;
187+
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
188+
189+
$sources = [];
190+
191+
foreach ($stack as $index => $trace) {
192+
$sources[] = $this->parseTrace($index, $trace);
193+
}
194+
195+
return array_filter($sources);
196+
}
197+
198+
/**
199+
* Parse a trace element from the backtrace stack.
200+
*
201+
* @param int $index
202+
* @param array $trace
203+
* @return array|bool
204+
*/
205+
protected function parseTrace($index, array $trace)
206+
{
207+
$frame = (object) [
208+
'index' => $index,
209+
'namespace' => null,
210+
'name' => null,
211+
'file' => 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+
$frame->file = $trace['file'];
227+
228+
if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) {
229+
list($frame->file, $frame->line) = $this->getTwigInfo($trace);
230+
} elseif (strpos($frame->file, storage_path()) !== false) {
231+
$hash = pathinfo($frame->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($frame->file, 'Middleware') !== false) {
241+
$frame->name = $this->findMiddlewareFromFile($frame->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($frame->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($frame->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,34 @@ 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+
'bindings' => [],
359+
'time' => 0,
360+
'source' => $source,
361+
'explain' => [],
362+
'connection' => $connection->getDatabaseName(),
363+
'hints' => null,
364+
];
365+
}
366+
301367
/**
302368
* Reset the queries.
303369
*/
@@ -318,17 +384,14 @@ public function collect()
318384
foreach ($queries as $query) {
319385
$totalTime += $query['time'];
320386

321-
$bindings = $query['bindings'];
322-
if($query['hints']){
323-
$bindings['hints'] = $query['hints'];
324-
}
387+
$metadata = $this->getDataFormatter()->formatMetadata($query);
325388

326389
$statements[] = [
327-
'sql' => $this->formatSql($query['query']),
328-
'params' => (object) $bindings,
390+
'sql' => $this->getDataFormatter()->formatSql($query['query']),
391+
'params' => $metadata,
329392
'duration' => $query['time'],
330393
'duration_str' => $this->formatDuration($query['time']),
331-
'stmt_id' => $query['source'],
394+
'stmt_id' => $this->getDataFormatter()->formatSource(reset($query['source'])),
332395
'connection' => $query['connection'],
333396
];
334397

@@ -353,17 +416,6 @@ public function collect()
353416
return $data;
354417
}
355418

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-
367419
/**
368420
* {@inheritDoc}
369421
*/

0 commit comments

Comments
 (0)