Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve look Tracy bar #89

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/DI/Pass/ConnectionPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ public function loadConnectionConfiguration(string $connectionName, mixed $conne

// Middlewares: debug
if ($config->debug->panel) {
$sourcePaths = $config->debug->sourcePaths;
$builder->addDefinition($this->prefix(sprintf('connections.%s.middleware.internal.debug.stack', $connectionName)))
->setFactory(DebugStack::class)
->setFactory(DebugStack::class, ['sourcePaths' => $sourcePaths])
->setAutowired(false);
$builder->addDefinition($this->prefix(sprintf('connections.%s.middleware.internal.debug', $connectionName)))
->setFactory(DebugMiddleware::class, [$this->prefix(sprintf('@connections.%s.middleware.internal.debug.stack', $connectionName)), $connectionName])
Expand Down
12 changes: 12 additions & 0 deletions src/Middleware/Debug/DebugStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Nettrine\DBAL\Middleware\Debug;

use Nettrine\DBAL\Utils\QueryUtils;

/**
* @see https://github.com/symfony/doctrine-bridge
* @internal
Expand All @@ -12,13 +14,23 @@ class DebugStack
/** @var array<string, array<int, array{sql: string, params: mixed[], types: mixed[], duration: callable|float }>> */
private array $data = [];

public function __construct(
/** @var array<string> */
private array $sourcePaths
)
{
}

public function addQuery(string $connectionName, DebugQuery $query): void
{
$backtrace = debug_backtrace();
$backtrace = QueryUtils::getSource($this->sourcePaths, $backtrace);
$this->data[$connectionName][] = [
'sql' => $query->getSql(),
'params' => $query->getParams(),
'types' => $query->getTypes(),
'duration' => $query->getDuration(...), // stop() may not be called at this point
'source' => $backtrace,
];
}

Expand Down
27 changes: 25 additions & 2 deletions src/Tracy/ConnectionPanel.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Query\QueryException;
use Nettrine\DBAL\Middleware\Debug\DebugStack;
use Nettrine\DBAL\Utils\Formatter;
use PDO;
use PDOException;
use Throwable;
Expand All @@ -22,23 +23,27 @@ class ConnectionPanel implements IBarPanel

protected string $connectionName;

private function __construct(DebugStack $stack, string $connectionName)
protected Connection $connection;

private function __construct(DebugStack $stack, string $connectionName, Connection $connection)
{
$this->stack = $stack;
$this->connectionName = $connectionName;
$this->connection = $connection;
}

public static function initialize(
DebugStack $stack,
string $connectionName,
Connection $connection,
?Bar $bar = null,
?BlueScreen $blueScreen = null,
): self
{
$blueScreen ??= Debugger::getBlueScreen();
$blueScreen->addPanel(self::renderException(...));

$panel = new self($stack, $connectionName);
$panel = new self($stack, $connectionName, $connection);
$bar ??= Debugger::getBar();
$bar->addPanel($panel);

Expand Down Expand Up @@ -107,6 +112,7 @@ public function getPanel(): string
{
// phpcs:disable
return Helpers::capture(function (): void {
$connection = $this->connection;
$queries = $this->stack->getDataBy($this->connectionName);
$queriesNum = count($queries);
$totalTime = 0;
Expand All @@ -115,6 +121,23 @@ public function getPanel(): string
$totalTime += $query['duration'];
}

$formatter = new Formatter();

/**
* @var string[] $params
*/
$params = $this->connection->getParams();

$dbParams = [
'driver' => $params['driver'],
'host' => $params['host'],
'user' => $params['user'],
'password' => '*****',
'dbname' => $params['dbname'],
'port' => $params['port'],
'charset' => $params['charset'],
];

require __DIR__ . '/templates/panel.phtml';
});
// phpcs:enable
Expand Down
57 changes: 56 additions & 1 deletion src/Tracy/templates/panel.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

namespace Nettrine\DBAL\Tracy;

use Doctrine\DBAL\Connection;
use Nettrine\DBAL\Utils\Formatter;
use Nettrine\DBAL\Utils\QueryUtils;
use Tracy\Dumper;
use Tracy\Helpers;

/** @var Connection $connection */
/** @var Formatter $formatter */
/** @var int $queriesNum */
/** @var int $totalTime */
/** @var array $dbParams */
/** @var array<array{sql: string, params: mixed[], types: mixed[], duration: float }> $queries */
?>
<style>
Expand Down Expand Up @@ -38,29 +45,77 @@ use Tracy\Dumper;
</h1>
<div class="tracy-inner nettrine-dbal">
<?php if ($queries !== []): ?>
<h2>Queries with params</h2>
<table class="tracy-sortable">
<tr>
<th>Time&nbsp;ms</th>
<th>SQL</th>
<th>Params</th>
<th>Types</th>
</tr>

<?php foreach ($queries as $q): ?>
<tr>
<td>
<?= sprintf('%0.2f', $q['duration'] * 1000); ?>
<?php if (count($q['source']) !== 0): ?>
<br><a class="tracy-toggle tracy-collapsed" data-tracy-ref="^tr .nettrine-dbal-backtrace"></a>
<?php endif; ?>
</td>
<td class="tracy-dbal-sql">
<?= $q['sql']; ?>
<?= QueryUtils::highlight($q['sql']); ?>
<?php if (count($q['source']) !== 0): ?>
<table class="nettrine-dbal-backtrace tracy-collapsed">
<?php foreach ($q['source'] as $s): ?>
<tr><td><?= Helpers::editorLink($s['file'], $s['line']); ?></td></tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</td>
<td class="tracy-dbal-sql">
<?= Dumper::toHtml($q['params']); ?>
</td>
<td class="tracy-dbal-sql">
<?= Dumper::toHtml($q['types'], [Dumper::COLLAPSE => true]); ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<?php if ($queries !== []): ?>
<h2>Plain queries</h2>
<table class="tracy-sortable">
<tr>
<th>Time&nbsp;ms</th>
<th>SQL</th>
</tr>

<?php foreach ($queries as $q): ?>
<tr>
<td>
<?= sprintf('%0.2f', $q['duration'] * 1000); ?>
<?php if (count($q['source']) !== 0): ?>
<br><a class="tracy-toggle tracy-collapsed" data-tracy-ref="^tr .nettrine-dbal-backtrace"></a>
<?php endif; ?>
</td>
<td class="tracy-dbal-sql">
<?php $sql = $formatter->formatSql($connection, $q['sql'], $q['params']); ?>
<?= QueryUtils::highlight($sql); ?>
<?php if (count($q['source']) !== 0): ?>
<table class="nettrine-dbal-backtrace tracy-collapsed">
<?php foreach ($q['source'] as $s): ?>
<tr><td><?= Helpers::editorLink($s['file'], $s['line']); ?></td></tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>

<div style="margin-top: 25px;">
<?= Dumper::toHtml($dbParams, [Dumper::COLLAPSE => true]); ?>
</div>
</div>
<?php } ?>
87 changes: 87 additions & 0 deletions src/Utils/Formatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace Nettrine\DBAL\Utils;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ExpandArrayParameters;
use Doctrine\DBAL\Types\Type;

class Formatter
{

/**
* @param ?array<mixed> $params
*/
public function formatSql(Connection $connection, string $sql, ?array $params = null): string
{
if ($params) {

Check failure on line 18 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Only booleans are allowed in an if condition, array<mixed>|null given.
[$sql, $params] = self::expandListParameters($connection, $sql, $params, []);

// Escape % before vsprintf (example: LIKE '%ant%')
$sql = str_replace(['%', '?'], ['%%', '%s'], $sql);

$query = vsprintf(
$sql,
call_user_func(function () use ($connection, $params) {
$quotedParams = [];
foreach ($params as $value) {
$quotedParams[] = $connection->quote((string) $value);

Check failure on line 29 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Cannot cast mixed to string.
}

return $quotedParams;
})
);
} else {
$query = $sql;
}

return $query;
}

/**
* @param array<int, mixed>|array<string, mixed> $params
* @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
*/
private static function needsArrayParameterConversion(array $params, array $types): bool
{
if (is_string(key($params))) {
return true;
}

foreach ($types as $type) {
if (
$type === ArrayParameterType::INTEGER

Check failure on line 54 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Result of || is always false.

Check failure on line 54 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Result of || is always false.

Check failure on line 54 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Strict comparison using === between Doctrine\DBAL\Types\Type|int|string|null and Doctrine\DBAL\ArrayParameterType::INTEGER will always evaluate to false.
|| $type === ArrayParameterType::STRING

Check failure on line 55 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Strict comparison using === between Doctrine\DBAL\Types\Type|int|string|null and Doctrine\DBAL\ArrayParameterType::STRING will always evaluate to false.
|| $type === ArrayParameterType::ASCII

Check failure on line 56 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Strict comparison using === between Doctrine\DBAL\Types\Type|int|string|null and Doctrine\DBAL\ArrayParameterType::ASCII will always evaluate to false.
) {
return true;
}
}

return false;
}

/**
* @param array<int, mixed>|array<string, mixed> $params
* @param array<int,Type|int|string|null>|array<string,Type|int|string|null> $types
* @return array{string, array<int|string, mixed>, array<int|string,Type|int|string|null>}
*/
private function expandListParameters(Connection $connection, string $query, array $params, array $types): array
{
if (!self::needsArrayParameterConversion($params, $types)) {
return [$query, $params, $types];
}

$parser = $connection->getDatabasePlatform()->createSQLParser();
$visitor = new ExpandArrayParameters($params, $types);

Check failure on line 77 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Parameter #2 $types of class Doctrine\DBAL\ExpandArrayParameters constructor expects array<int<0, max>|string, Doctrine\DBAL\ArrayParameterType|Doctrine\DBAL\ParameterType|Doctrine\DBAL\Types\Type|string>, array<int|string, Doctrine\DBAL\Types\Type|int|string|null> given.
$parser->parse($query, $visitor);

return [

Check failure on line 80 in src/Utils/Formatter.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Method Nettrine\DBAL\Utils\Formatter::expandListParameters() should return array{string, array<int|string, mixed>, array<int|string, Doctrine\DBAL\Types\Type|int|string|null>} but returns array{string, list<mixed>, array<int<0, max>, Doctrine\DBAL\ParameterType|Doctrine\DBAL\Types\Type|string>}.
$visitor->getSQL(),
$visitor->getParameters(),
$visitor->getTypes(),
];
}

}
63 changes: 63 additions & 0 deletions src/Utils/QueryUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace Nettrine\DBAL\Utils;

final class QueryUtils
{

/**
* Highlight given SQL parts
*/
public static function highlight(string $sql): string
{
static $keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|SHOW|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE|START\s+TRANSACTION|COMMIT|ROLLBACK|(?:RELEASE\s+|ROLLBACK\s+TO\s+)?SAVEPOINT';
static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|[RI]?LIKE|REGEXP|TRUE|FALSE';
$sql = ' ' . $sql . ' ';
$sql = htmlspecialchars($sql, ENT_IGNORE, 'UTF-8');
$sql = preg_replace_callback(sprintf('#(/\\*.+?\\*/)|(?<=[\\s,(])(%s)(?=[\\s,)])|(?<=[\\s,(=])(%s)(?=[\\s,)=])#is', $keywords1, $keywords2), function ($matches) {

Check warning on line 17 in src/Utils/QueryUtils.php

View check run for this annotation

Codecov / codecov/patch

src/Utils/QueryUtils.php#L13-L17

Added lines #L13 - L17 were not covered by tests
if (!empty($matches[1])) { // comment

Check failure on line 18 in src/Utils/QueryUtils.php

View workflow job for this annotation

GitHub Actions / Phpstan / Phpstan (8.3)

Construct empty() is not allowed. Use more strict comparison.
return '<em style="color:gray">' . $matches[1] . '</em>';
}

if (!empty($matches[2])) { // most important keywords
return '<strong style="color:#2D44AD">' . $matches[2] . '</strong>';
}

if (!empty($matches[3])) { // other keywords
return '<strong>' . $matches[3] . '</strong>';
}
}, $sql);

Check warning on line 29 in src/Utils/QueryUtils.php

View check run for this annotation

Codecov / codecov/patch

src/Utils/QueryUtils.php#L29

Added line #L29 was not covered by tests

return trim((string) $sql);

Check warning on line 31 in src/Utils/QueryUtils.php

View check run for this annotation

Codecov / codecov/patch

src/Utils/QueryUtils.php#L31

Added line #L31 was not covered by tests
}

/**
* @param string[] $sourcePaths
* @param mixed[] $backtrace
* @return mixed[]
*/
public static function getSource(array $sourcePaths, array $backtrace): array
{
$result = [];
if (count($sourcePaths) === 0) {
return $result;
}

foreach ($backtrace as $i) {
if (!isset($i['file'], $i['line'])) {
continue;

Check warning on line 48 in src/Utils/QueryUtils.php

View check run for this annotation

Codecov / codecov/patch

src/Utils/QueryUtils.php#L46-L48

Added lines #L46 - L48 were not covered by tests
}

foreach ($sourcePaths as $path) {
$path = realpath($path);

Check warning on line 52 in src/Utils/QueryUtils.php

View check run for this annotation

Codecov / codecov/patch

src/Utils/QueryUtils.php#L51-L52

Added lines #L51 - L52 were not covered by tests

if (str_contains($i['file'], $path)) {
$result[] = $i;

Check warning on line 55 in src/Utils/QueryUtils.php

View check run for this annotation

Codecov / codecov/patch

src/Utils/QueryUtils.php#L54-L55

Added lines #L54 - L55 were not covered by tests
}
}
}

return $result;

Check warning on line 60 in src/Utils/QueryUtils.php

View check run for this annotation

Codecov / codecov/patch

src/Utils/QueryUtils.php#L60

Added line #L60 was not covered by tests
}

}