Skip to content
Merged
40 changes: 40 additions & 0 deletions src/Controllers/QueriesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Barryvdh\Debugbar\Controllers;

use Barryvdh\Debugbar\Support\Explain;
use Exception;
use Illuminate\Http\Request;

class QueriesController extends BaseController
{
/**
* Generate explain data for query.
*/
public function explain(Request $request)
{
if (!config('debugbar.options.db.explain.enabled', false)) {
return response()->json([
'success' => false,
'message' => 'EXPLAIN is currently disabled in the Debugbar.',
], 400);
}

try {
$data = match ($request->json('mode')) {
'visual' => (new Explain())->generateVisualExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
default => (new Explain())->generateRawExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')),
};

return response()->json([
'success' => true,
'data' => $data,
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage(),
], 400);
}
}
}
182 changes: 74 additions & 108 deletions src/DataCollector/QueryCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Barryvdh\Debugbar\DataCollector;

use Barryvdh\Debugbar\Support\Explain;
use DebugBar\DataCollector\PDO\PDOCollector;
use DebugBar\DataCollector\TimeDataCollector;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

/**
Expand Down Expand Up @@ -152,7 +154,6 @@ public function addQuery($query)
$limited = $this->softLimit && $this->queryCount > $this->softLimit;

$sql = (string) $query->sql;
$explainResults = [];
$time = $query->time / 1000;
$endTime = microtime(true);
$startTime = $endTime - $time;
Expand All @@ -168,41 +169,6 @@ public function addQuery($query)
} catch (\Throwable $e) {
// ignore error for non-pdo laravel drivers
}
$bindings = $query->connection->prepareBindings($query->bindings);

// Run EXPLAIN on this query (if needed)
if (!$limited && $this->explainQuery && $pdo && preg_match('/^\s*(' . implode('|', $this->explainTypes) . ') /i', $sql)) {
$statement = $pdo->prepare('EXPLAIN ' . $sql);
$statement->execute($bindings);
$explainResults = $statement->fetchAll(\PDO::FETCH_CLASS);
}

$bindings = $this->getDataFormatter()->checkBindings($bindings);
if (!empty($bindings) && $this->renderSqlWithParams) {
foreach ($bindings as $key => $binding) {
// This regex matches placeholders only, not the question marks,
// nested in quotes, while we iterate through the bindings
// and substitute placeholders by suitable values.
$regex = is_numeric($key)
? "/(?<!\?)\?(?=(?:[^'\\\']*'[^'\\']*')*[^'\\\']*$)(?!\?)/"
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";

// Mimic bindValue and only quote non-integer and non-float data types
if (!is_int($binding) && !is_float($binding)) {
if ($pdo) {
try {
$binding = $pdo->quote((string) $binding);
} catch (\Exception $e) {
$binding = $this->emulateQuote($binding);
}
} else {
$binding = $this->emulateQuote($binding);
}
}

$sql = preg_replace($regex, addcslashes($binding, '$'), $sql, 1);
}
}

$source = [];

Expand All @@ -213,16 +179,20 @@ public function addQuery($query)
}
}

$bindings = match (true) {
$limited && filled($query->bindings) => null,
default => $query->connection->prepareBindings($query->bindings),
};

$this->queries[] = [
'query' => $sql,
'type' => 'query',
'bindings' => !$limited ? $this->getDataFormatter()->escapeBindings($bindings) : null,
'bindings' => $bindings,
'start' => $startTime,
'time' => $time,
'memory' => $this->lastMemoryUsage ? memory_get_usage(false) - $this->lastMemoryUsage : 0,
'source' => $source,
'explain' => $explainResults,
'connection' => $query->connection->getDatabaseName(),
'connection' => $query->connection->getName(),
'driver' => $query->connection->getConfig('driver'),
'hints' => ($this->showHints && !$limited) ? $hints : null,
'show_copy' => $this->showCopyButton,
Expand Down Expand Up @@ -484,8 +454,7 @@ public function collectTransactionEvent($event, $connection)
'time' => 0,
'memory' => 0,
'source' => $source,
'explain' => [],
'connection' => $connection->getDatabaseName(),
'connection' => $connection->getName(),
'driver' => $connection->getConfig('driver'),
'hints' => null,
'show_copy' => false,
Expand Down Expand Up @@ -516,15 +485,21 @@ public function collect()
$totalTime += $query['time'];
$totalMemory += $query['memory'];

if (str_ends_with($query['connection'], '.sqlite')) {
$query['connection'] = $this->normalizeFilePath($query['connection']);
$connectionName = DB::connection($query['connection'])->getDatabaseName();
if (str_ends_with($connectionName, '.sqlite')) {
$connectionName = $this->normalizeFilePath($connectionName);
}

$canExplainQuery = match (true) {
in_array($query['driver'], ['mysql', 'pgsql']) => $query['bindings'] !== null && preg_match('/^\s*(' . implode('|', $this->explainTypes) . ') /i', $query['query']),
default => false,
};

$statements[] = [
'sql' => $this->getDataFormatter()->formatSql($query['query']),
'sql' => $this->getSqlQueryToDisplay($query),
'type' => $query['type'],
'params' => [],
'bindings' => $query['bindings'],
'bindings' => $query['bindings'] ?? [],
'hints' => $query['hints'],
'show_copy' => $query['show_copy'],
'backtrace' => array_values($query['source']),
Expand All @@ -536,69 +511,16 @@ public function collect()
'filename' => $this->getDataFormatter()->formatSource($source, true),
'source' => $this->getDataFormatter()->formatSource($source),
'xdebug_link' => is_object($source) ? $this->getXdebugLink($source->file ?: '', $source->line) : null,
'connection' => $query['connection'],
'connection' => $connectionName,
'explain' => $this->explainQuery && $canExplainQuery ? [
'url' => route('debugbar.queries.explain'),
'visual-confirm' => (new Explain())->confirm($query['connection']),
'driver' => $query['driver'],
'connection' => $query['connection'],
'query' => $query['query'],
'hash' => (new Explain())->hash($query['connection'], $query['query'], $query['bindings']),
] : null,
];

if ($query['explain']) {
// Add the results from the EXPLAIN as new rows
if ($query['driver'] === 'pgsql') {
$explainer = trim(implode("\n", array_map(function ($explain) {
return $explain->{'QUERY PLAN'};
}, $query['explain'])));

if ($explainer) {
$statements[] = [
'sql' => " - EXPLAIN: {$explainer}",
'type' => 'explain',
];
}
} elseif ($query['driver'] === 'sqlite') {
$vmi = '<table style="margin:-5px -11px !important;width: 100% !important">';
$vmi .= "<thead><tr>
<td>Address</td>
<td>Opcode</td>
<td>P1</td>
<td>P2</td>
<td>P3</td>
<td>P4</td>
<td>P5</td>
<td>Comment</td>
</tr></thead>";

foreach ($query['explain'] as $explain) {
$vmi .= "<tr>
<td>{$explain->addr}</td>
<td>{$explain->opcode}</td>
<td>{$explain->p1}</td>
<td>{$explain->p2}</td>
<td>{$explain->p3}</td>
<td>{$explain->p4}</td>
<td>{$explain->p5}</td>
<td>{$explain->comment}</td>
</tr>";
}

$vmi .= '</table>';

$statements[] = [
'sql' => " - EXPLAIN:",
'type' => 'explain',
'params' => [
'Virtual Machine Instructions' => $vmi,
]
];
} else {
foreach ($query['explain'] as $explain) {
$statements[] = [
'sql' => " - EXPLAIN # {$explain->id}: `{$explain->table}` ({$explain->select_type})",
'type' => 'explain',
'params' => $explain,
'row_count' => $explain->rows,
'stmt_id' => $explain->id,
];
}
}
}
}

if ($this->durationBackground) {
Expand Down Expand Up @@ -676,7 +598,7 @@ public function getWidgets()
return [
"queries" => [
"icon" => "database",
"widget" => "PhpDebugBar.Widgets.SQLQueriesWidget",
"widget" => "PhpDebugBar.Widgets.LaravelQueriesWidget",
"map" => "queries",
"default" => "[]"
],
Expand All @@ -686,4 +608,48 @@ public function getWidgets()
]
];
}

private function getSqlQueryToDisplay(array $query): string
{
$sql = $query['query'];
if ($query['type'] === 'query' && $this->renderSqlWithParams && method_exists(DB::connection($query['connection'])->getQueryGrammar(), 'substituteBindingsIntoRawSql')) {
$sql = DB::connection($query['connection'])->getQueryGrammar()->substituteBindingsIntoRawSql($sql, $query['bindings'] ?? []);
} elseif ($query['type'] === 'query' && $this->renderSqlWithParams) {
$bindings = $this->getDataFormatter()->checkBindings($query['bindings']);
if (!empty($bindings)) {
$pdo = null;
try {
$pdo = $query->connection->getPdo();
} catch (\Throwable) {
// ignore error for non-pdo laravel drivers
}

foreach ($bindings as $key => $binding) {
// This regex matches placeholders only, not the question marks,
// nested in quotes, while we iterate through the bindings
// and substitute placeholders by suitable values.
$regex = is_numeric($key)
? "/(?<!\?)\?(?=(?:[^'\\\']*'[^'\\']*')*[^'\\\']*$)(?!\?)/"
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";

// Mimic bindValue and only quote non-integer and non-float data types
if (!is_int($binding) && !is_float($binding)) {
if ($pdo) {
try {
$binding = $pdo->quote((string) $binding);
} catch (\Exception $e) {
$binding = $this->emulateQuote($binding);
}
} else {
$binding = $this->emulateQuote($binding);
}
}

$sql = preg_replace($regex, addcslashes($binding, '$'), $sql, 1);
}
}
}

return $this->getDataFormatter()->formatSql($sql);
}
}
15 changes: 0 additions & 15 deletions src/DataFormatter/QueryFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,6 @@ public function checkBindings($bindings)
return $bindings;
}

/**
* Make the bindings safe for outputting.
*
* @param array $bindings
* @return array
*/
public function escapeBindings($bindings)
{
foreach ($bindings as &$binding) {
$binding = htmlentities((string) $binding, ENT_QUOTES, 'UTF-8', false);
}

return $bindings;
}

/**
* Format a source object.
*
Expand Down
1 change: 1 addition & 0 deletions src/JavascriptRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function __construct(DebugBar $debugBar, $baseUrl = null, $basePath = nul

$this->cssFiles['laravel'] = __DIR__ . '/Resources/laravel-debugbar.css';
$this->jsFiles['laravel-cache'] = __DIR__ . '/Resources/cache/widget.js';
$this->jsFiles['laravel-queries'] = __DIR__ . '/Resources/queries/widget.js';

$theme = config('debugbar.theme', 'auto');
switch ($theme) {
Expand Down
67 changes: 67 additions & 0 deletions src/Resources/laravel-debugbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -924,3 +924,70 @@ pre.phpdebugbar-widgets-code-block ul.phpdebugbar-widgets-numbered-code li {
color: var(--color-gray-100) !important;
}
}

div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-copy-clipboard {
float: none !important;
}

.phpdebugbar-widgets-bg-measure .phpdebugbar-widgets-value {
height: 2px !important;
}

div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name {
width: 150px;
}

div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-connection {
font-size: 12px;
padding: 2px 4px;
background: #737373;
margin-left: 6px;
border-radius: 4px;
color: #fff !important;
}

div.phpdebugbar-widgets-sqlqueries button.phpdebugbar-widgets-explain-btn {
cursor: pointer;
background: #383838;
color: #fff;
font-size: 13px;
padding: 0 8px;
border-radius: 4px;
line-height: 1.25rem;
}

div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain {
margin: 0 !important;
}

div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain th {
border: 1px solid #ddd;
text-align: center;
}

div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-visual-explain {
display: inline-block;
font-weight: bold;
text-decoration: underline;
margin-top: 6px;
}

div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-visual-link {
color: #888;
margin-left: 6px;
}

div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-visual-explain:after {
content: "\f08e";
font-family: PhpDebugbarFontAwesome;
margin-left: 4px;
font-size: 12px;
}

div.phpdebugbar-widgets-sqlqueries li.phpdebugbar-widgets-list-item.phpdebugbar-widgets-expandable {
cursor: pointer;
}

div.phpdebugbar-widgets-sqlqueries li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-params {
cursor: default;
}
Loading
Loading