Skip to content

Commit

Permalink
StatementInfo - move "parsing" and param substitution to new Utility/…
Browse files Browse the repository at this point in the history
…Sql class
  • Loading branch information
bkdotcom committed Jul 22, 2024
1 parent 95f4917 commit c3327d4
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 100 deletions.
102 changes: 7 additions & 95 deletions src/Debug/Collector/StatementInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,64 +229,6 @@ public function setParams($params = array())
$this->params = $params ?: array();
}

/**
* Replace param holders with param values
*
* @param string $sql SQL statement
*
* @return string
*/
private function doParamSubstitution($sql)
{
if ($this->debug->arrayUtil->isList($this->params) === false) {
// named params
foreach ($this->params as $name => $value) {
$value = $this->doParamSubstitutionValue($value);
$sql = \str_replace($name, $value, $sql);
}
return $sql;
}
// anonymous params
if (\substr_count($sql, '?') !== count($this->params)) {
return $sql;
}
$strposOffset = 0;
foreach ($this->params as $value) {
$value = $this->doParamSubstitutionValue($value);
$pos = \strpos($sql, '?', $strposOffset);
$sql = \substr_replace($sql, $value, $pos, 1);
$strposOffset = $pos + \strlen($value);
}
return $sql;
}

/**
* Get param value for injection into SQL statement
*
* @param mixed $value Param value
*
* @return int|string
*/
private function doParamSubstitutionValue($value)
{
if (\is_string($value)) {
return "'" . \addslashes($value) . "'";
}
if (\is_numeric($value)) {
return $value;
}
if (\is_array($value)) {
return \implode(', ', \array_map(array($this, __FUNCTION__), $value));
}
if (\is_bool($value)) {
return (int) $value;
}
if ($value === null) {
return 'null';
}
return \call_user_func(array($this, __FUNCTION__), (string) $value);
}

/**
* Returns the exception's code
*
Expand Down Expand Up @@ -320,20 +262,22 @@ private function getGroupLabel()
{
$label = $this->sql;
$label = \preg_replace('/[\r\n\s]+/', ' ', $label);
$label = $this->doParamSubstitution($label);
$parsed = self::parseSqlForLabel($label);
$label = $this->debug->sql->replaceParams($label, $this->params);
$parsed = $this->debug->sql->parse($label);
if ($parsed === false) {
return $label;
}
$label = $parsed['method']; // method + table
$afterWhereKeys = array('groupBy', 'having', 'window', 'orderBy', 'limit', 'for');
$afterWhereValues = \array_intersect_key($parsed, \array_flip($afterWhereKeys));
$haveAfterWhere = \strlen(\implode('', $afterWhereValues)) > 0;
$haveMore = \count($afterWhereValues) > 0;
if ($parsed['where']) {
$label .= $parsed['afterMethod'] ? ' (…)' : '';
$label .= ' WHERE ' . $parsed['where'];
$label .= $haveAfterWhere ? '' : '';
} elseif ($parsed['afterMethod'] || $haveAfterWhere) {
} elseif ($parsed['afterMethod']) {
$haveMore = true;
}
if ($haveMore) {
$label .= '';
}
if (\strlen($label) > 100 && $parsed['select']) {
Expand Down Expand Up @@ -427,38 +371,6 @@ private function logQuery($label)
$this->debug->setCfg('stringMaxLen', $stringMaxLenBak, Debug::CONFIG_NO_PUBLISH | Debug::CONFIG_NO_RETURN);
}

/**
* "Parse" the sql statement to get a label
*
* @param string $sql SQL statement
*
* @return array|false
*/
private function parseSqlForLabel($sql)
{
$regex = '/^(?<method>
(?:DROP|SHOW).+|
CREATE(?:\sTEMPORARY)?\s+TABLE(?:\sIF\sNOT\sEXISTS)?\s+\S+|
DELETE.*?FROM\s+\S+|
INSERT(?:\s+(?:LOW_PRIORITY|DELAYED|HIGH_PRIORITY|IGNORE|INTO))*\s+\S+|
SELECT\s+(?P<select>.*?)\s+FROM\s+(?<from>\S+)|
UPDATE\s+\S+
)
(?P<afterMethod>.*?)
(?:\s+WHERE\s+(?P<where>.*?))?
(?:\s+GROUP BY\s+(?P<groupBy>.*?))?
(?:\s+HAVING\s+(?P<having>.*?))?
(?:\s+WINDOW\s+(?P<window>.*?))?
(?:\s+ORDER BY\s+(?P<orderBy>.*?))?
(?:\s+LIMIT\s+(?P<limit>.*?))?
(?:\s+FOR\s+(?P<for>.*?))?
$/six';
$keys = array('method', 'select', 'from', 'afterMethod', 'where');
return \preg_match($regex, $sql, $matches) === 1
? \array_merge(\array_fill_keys($keys, ''), $matches)
: false;
}

/**
* Find common query performance issues
*
Expand Down
3 changes: 3 additions & 0 deletions src/Debug/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ public function register(Container $container) // phpcs:ignore SlevomatCodingSta
// Psr\Http\Message\ServerRequestInterface
return \bdk\HttpMessage\ServerRequest::fromGlobals();
};
$container['sql'] = static function (Container $container) {
return new \bdk\Debug\Utility\Sql();
};
$container['sqlQueryAnalysis'] = static function (Container $container) {
return new \bdk\Debug\Utility\SqlQueryAnalysis($container['debug']);
};
Expand Down
114 changes: 114 additions & 0 deletions src/Debug/Utility/Sql.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

/**
* This file is part of PHPDebugConsole
*
* @package PHPDebugConsole
* @author Brad Kent <[email protected]>
* @license http://opensource.org/licenses/MIT MIT
* @copyright 2014-2024 Brad Kent
* @version v3.1
*/

namespace bdk\Debug\Utility;

use bdk\Debug\Utility\ArrayUtil;

/**
* Utilities for formatting SQL statements
*/
class Sql
{
/**
* "Parse" the sql statement to get a label
*
* This "parser" has a *very* limited scope. For internal use only.
*
* @param string $sql SQL statement
*
* @return array|false
*/
public static function parse($sql)
{
$regex = '/^(?<method>
(?:DROP|SHOW).+|
CREATE(?:\sTEMPORARY)?\s+TABLE(?:\sIF\sNOT\sEXISTS)?\s+\S+|
DELETE.*?FROM\s+\S+|
INSERT(?:\s+(?:LOW_PRIORITY|DELAYED|HIGH_PRIORITY|IGNORE|INTO))*\s+\S+|
SELECT\s+(?P<select>.*?)\s+FROM\s+(?<from>\S+)|
UPDATE\s+\S+
)
(?P<afterMethod>.*?)
(?:\s+WHERE\s+(?P<where>.*?))?
(?:\s+GROUP BY\s+(?P<groupBy>.*?))?
(?:\s+HAVING\s+(?P<having>.*?))?
(?:\s+WINDOW\s+(?P<window>.*?))?
(?:\s+ORDER BY\s+(?P<orderBy>.*?))?
(?:\s+LIMIT\s+(?P<limit>.*?))?
(?:\s+FOR\s+(?P<for>.*?))?
$/six';
$keysAlwaysReturn = array('method', 'select', 'from', 'afterMethod', 'where');
return \preg_match($regex, $sql, $matches) === 1
? \array_merge(\array_fill_keys($keysAlwaysReturn, ''), $matches)
: false;
}

/**
* Replace param holders with param values
*
* @param string $sql SQL statement
* @param array $params Bound Parameters
*
* @return string
*/
public static function replaceParams($sql, array $params)
{
if (ArrayUtil::isList($params) === false) {
// named params
foreach ($params as $name => $value) {
$value = self::doParamSubstitutionValue($value);
$sql = \str_replace($name, $value, $sql);
}
return $sql;
}
// anonymous params
if (\substr_count($sql, '?') !== \count($params)) {
return $sql;
}
$strposOffset = 0;
foreach ($params as $value) {
$value = self::doParamSubstitutionValue($value);
$pos = \strpos($sql, '?', $strposOffset);
$sql = \substr_replace($sql, $value, $pos, 1);
$strposOffset = $pos + \strlen($value);
}
return $sql;
}

/**
* Get param value for injection into SQL statement
*
* @param mixed $value Param value
*
* @return int|string
*/
private static function doParamSubstitutionValue($value)
{
if (\is_string($value)) {
return "'" . \addslashes($value) . "'";
}
if (\is_numeric($value)) {
return $value;
}
if (\is_array($value)) {
return \implode(', ', \array_map(array(__CLASS__, __FUNCTION__), $value));
}
if (\is_bool($value)) {
return (int) $value;
}
if ($value === null) {
return 'null';
}
return \call_user_func(array(__CLASS__, __FUNCTION__), (string) $value);
}
}
1 change: 1 addition & 0 deletions tests/Debug/Collector/DoctrineLoggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

/**
* @covers \bdk\Debug\Collector\DoctrineLogger
* @covers \bdk\Debug\Utility\Sql
*/
class DoctrineLoggerTest extends DebugTestFramework
{
Expand Down
1 change: 1 addition & 0 deletions tests/Debug/Collector/MysqliTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* @covers \bdk\Debug\Collector\MySqli
* @covers \bdk\Debug\Collector\MySqli\MySqliStmt
* @covers \bdk\Debug\Collector\StatementInfo
* @covers \bdk\Debug\Utility\Sql
*/
class MysqliTest extends DebugTestFramework
{
Expand Down
1 change: 1 addition & 0 deletions tests/Debug/Collector/PdoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* @covers \bdk\Debug\Collector\Pdo
* @covers \bdk\Debug\Collector\Pdo\Statement
* @covers \bdk\Debug\Collector\StatementInfo
* @covers \bdk\Debug\Utility\Sql
*/
class PdoTest extends DebugTestFramework
{
Expand Down
1 change: 1 addition & 0 deletions tests/Debug/Collector/StatementInfoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

/**
* @covers \bdk\Debug\Collector\StatementInfo
* @covers \bdk\Debug\Utility\Sql
* @covers \bdk\Debug\Utility\SqlQueryAnalysis
*/
class StatementInfoTest extends DebugTestFramework
Expand Down
10 changes: 5 additions & 5 deletions tests/ErrorHandler/EmailerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function tearDown(): void
parent::tearDown();
$this->errorHandler->setCfg(array(
'stats' => array(
'errorStatsFile' => __DIR__ . '/../../src/ErrorHandler/Plugin/error_stats.json',
'errorStatsFile' => __DIR__ . '/../../tmp/error_stats.json',
),
));
}
Expand Down Expand Up @@ -407,8 +407,8 @@ public function testDataWriteFail()

public function testPostSetCfg()
{
$fileNew = __DIR__ . '/statStore.json';
\file_put_contents($fileNew, \json_encode(array(
$statsFileNew = __DIR__ . '/statStore.json';
\file_put_contents($statsFileNew, \json_encode(array(
'errors' => array(
'notarealhash' => array(
'info' => array(),
Expand All @@ -418,11 +418,11 @@ public function testPostSetCfg()
), JSON_PRETTY_PRINT));
$this->errorHandler->setCfg(array(
'stats' => array(
'errorStatsFile' => $fileNew,
'errorStatsFile' => $statsFileNew,
),
));
$stats = $this->errorHandler->stats->find('notarealhash');
\unlink($fileNew);
\unlink($statsFileNew);
self::assertSame(array(
'info' => array(),
'foo' => 'bar',
Expand Down

0 comments on commit c3327d4

Please sign in to comment.