diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bf83d90 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: php + +php: + - 7.0 + +cache: + directories: + - $HOME/.composer/cache + +before_script: + - composer install + +script: + - php ./vendor/bin/phpspec run --format=pretty --stop-on-failure --no-code-generation + + diff --git a/CHANGELOG.md b/CHANGELOG.md index c00f429..1a2b851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +--- + +## [1.2.0] - 2019-10-01 + +### Added + +- SQL Query Monitor show at the Page how many SQL queries have been fired. +- Measures the SQL execution time +- Testing framework [phpspec](https://www.phpspec.net/en/stable/manual/introduction.html) + +--- + ## [1.1.0] - 2019-09-20 ### Added @@ -13,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - add log message - add position specifications of the calling source code +--- + ## [1.0.0] - 2019-07-21 available in tumtum/oxid-sql-logger package only diff --git a/README.md b/README.md index f1bb8f4..e35db96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Oxid eShop SQL Logger --------------------- +[![Build Status](https://travis-ci.org/TumTum/oxid-sql-logger.svg?branch=master)](https://travis-ci.org/TumTum/oxid-sql-logger) + Returns all SQL queries into console of a Browser. ## Install @@ -30,6 +32,28 @@ CLI: ![Example CLI](https://raw.githubusercontent.com/d3datadevelopment/oxid-sql-logger/master/img/screenshot-cli.jpg) +## SQL Query Status Monitor + +![Example CLI](https://raw.githubusercontent.com/TumTum/oxid-sql-logger/master/img/sql-query-status-monitor.jpg) + +See how many queries and which types of queries have been added to the database. +To determine the amount. + +#### Switch on + +For this purpose, the parameter `$this->blSQLStatusBox = true;` must be stored in the file `config.inc.php`. +So you can turn it on and off temporarily. + +Unique: Insert, at the end, the Smarty tag: `[{tm_sql_status}]` in the `base.tpl` file. + +####### source/Application/views/flow/tpl/layout/base.tpl + +```html + [{tm_sql_status}] + + +``` + ## Credits -Many thanks to [Tobias Matthaiou](https://github.com/TumTum/oxid-sql-logger) for his inspiration. \ No newline at end of file +Many thanks to [Tobias Matthaiou](https://github.com/TumTum/oxid-sql-logger) for his inspiration. diff --git a/composer.json b/composer.json index 6b57bb4..4990a51 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,11 @@ "monolog/monolog": "^1", "nilportugues/sql-query-formatter": "^1.2.2" }, + "require-dev": { + "phpspec/phpspec": "^4", + "doctrine/dbal": "^2.5", + "bovigo/assert": "^2" + }, "license": "GPL-3.0", "autoload": { "psr-4": { diff --git a/img/sql-query-status-monitor.jpg b/img/sql-query-status-monitor.jpg new file mode 100644 index 0000000..9a199e6 Binary files /dev/null and b/img/sql-query-status-monitor.jpg differ diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 0000000..66f8981 --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,4 @@ +suites: + default: + namespace: "tm\\oxid\\sql\\logger\\" + psr4_prefix: "tm\\oxid\\sql\\logger\\" diff --git a/spec/OxidSQLLoggerSpec.php b/spec/OxidSQLLoggerSpec.php new file mode 100644 index 0000000..1e8834d --- /dev/null +++ b/spec/OxidSQLLoggerSpec.php @@ -0,0 +1,89 @@ +testLogger = new Monolog\Handler\TestHandler(); + Monolog\Registry::addLogger( + new Monolog\Logger('sql', [$this->testLogger]), + 'sql', + true + ); + } + + public function it_is_initializable() + { + $this->shouldHaveType(OxidSQLLogger::class); + } + + /** + * @throws FailureException + */ + public function it_should_log_the_sql_normally() + { + $this->startQuery('SELECT 1', ['param1'], ['master']); + for ($i = 0; $i <= 1000; $i++) @get_declared_classes(); + $this->stopQuery(); + + $this->assertExpectLog(); + $this->assertMatchMessage('/\[\d+ms\] SELECT 1/'); + + $this->assertContext('params', isSameAs(['param1'])); + $this->assertContext('types', isSameAs(['master'])); + $this->assertContext('time', isOfType('float')); + + } + + /** + * @throws FailureException + */ + public function it_should_log_without_time_specification() + { + $this->startQuery('SELECT 1'); + $this->startQuery('SELECT 2'); + + $this->assertExpectLog(); + $this->assertMatchMessage('/\[Statement canceled\] SELECT 1/'); + $this->assertContext('time', isSameAs('Statement canceled')); + + } + + private function assertExpectLog() + { + if (!$this->testLogger->hasRecords(Monolog\Logger::DEBUG)) { + throw new FailureException("No log entry was made"); + } + } + + private function assertMatchMessage($regex) + { + if (!$this->testLogger->hasRecordThatMatches($regex, Monolog\Logger::DEBUG)) { + $message = $this->testLogger->getRecords()[0]['message']; + throw new FailureException("Expect message '{$regex}' got '{$message}'"); + }; + } + + private function assertContext($param, $actual) + { + $record = $this->testLogger->getRecords()[0]; + + \bovigo\assert\assert($record['context'], hasKey($param), "Context '{$param}' not found"); + \bovigo\assert\assert($record['context'][$param], $actual, "Context '{$param} is not same"); + } +} diff --git a/spec/SQLQuerySpec.php b/spec/SQLQuerySpec.php new file mode 100644 index 0000000..5ef7009 --- /dev/null +++ b/spec/SQLQuerySpec.php @@ -0,0 +1,65 @@ +shouldHaveType(SQLQuery::class); + } + + public function it_hat_sql_setter_getter() + { + $this->setSQL('SELECT NOW()')->shouldHaveType(SQLQuery::class); + $this->getSQL()->shouldBe('SELECT NOW()'); + } + + public function it_hat_params_setter_getter() + { + $this->setParams(['param1', 'param2'])->shouldHaveType(SQLQuery::class); + $this->getParams()->shouldBe(['param1', 'param2']); + } + + public function it_hat_type_setter_getter() + { + $this->setTypes(['type1'])->shouldHaveType(SQLQuery::class); + $this->getTypes()->shouldBe(['type1']); + } + + public function it_should_indicate_an_aborted_time() + { + $this->setSql('SELECT 1'); + $this->setCanceled()->shouldHaveType(SQLQuery::class); + $this->getElapsedTime()->shouldBe('Statement canceled'); + } + + public function it_should_display_with_a_readable_time() + { + $this->setSql('SELECT 1'); + sleep(1); + $this->getReadableElapsedTime()->shouldMatch('/^1\.\d\d\ds/'); + } + + public function it_should_display_with_a_readable_time_in_ms() + { + $this->setSql('SELECT 1'); + for ($i = 0; $i <= 1000; $i++) @get_declared_classes(); + $this->getReadableElapsedTime()->shouldMatch('/^\d{1,3}ms/'); + } + + public function it_should_display_a_time_in_float() + { + $this->setSql('SELECT 1'); + for ($i = 0; $i <= 1000; $i++) @get_declared_classes(); + $this->getElapsedTime()->shouldBeFloat(); + } +} diff --git a/src/AutoInstallSmaryPlugin.php b/src/AutoInstallSmaryPlugin.php new file mode 100644 index 0000000..0e02a91 --- /dev/null +++ b/src/AutoInstallSmaryPlugin.php @@ -0,0 +1,57 @@ + + * Date: 2019-08-20 + * Time: 21:33 + */ + +namespace D3\OxidSqlLogger; + +/** + * Class AutoInstallSmaryPlugin + */ +class AutoInstallSmaryPlugin +{ + public function runInstall() + { + $oxideshop_ce = new \SplFileInfo(__DIR__ . '/../../../oxid-esales/oxideshop-ce/source/Core/Smarty/Plugin'); + $smartyPlugin = new \SplFileInfo(__DIR__ . '/Smarty/function.tm_sql_status.php'); + + if ($oxideshop_ce->isDir()) { + + $target = new \SplFileInfo($oxideshop_ce->getRealPath() . '/' . $smartyPlugin->getBasename()); + + if ($target->isFile() && $this->isSameFile($target, $smartyPlugin)) { + return; + } + + $this->createHardLink($smartyPlugin, $target); + + OxidUtilsView::clearSmarty(); + } + } + + /** + * @param \SplFileInfo $target + * @param \SplFileInfo $ + * @return bool + */ + protected function isSameFile(\SplFileInfo $target, \SplFileInfo $smartyPlugin) + { + return @md5_file($target->getPathname()) == @md5_file($smartyPlugin->getRealPath()); + } + + /** + * @param \SplFileInfo $smarty_func_tm_sql_status + * @param \SplFileInfo $target + */ + protected function createHardLink(\SplFileInfo $smartyPlugin, \SplFileInfo $target) + { + if ($target->isFile()) { + @unlink($target->getPathname()); + } + + link($smartyPlugin->getPathname(), $target->getPathname()); + } +} + diff --git a/src/OxidSQLLogger.php b/src/OxidSQLLogger.php index 8777c8a..6b068e1 100644 --- a/src/OxidSQLLogger.php +++ b/src/OxidSQLLogger.php @@ -21,6 +21,11 @@ class OxidSQLLogger implements SQLLogger public $logStartingClass; public $logStartingFunction; + /** + * @var SQLQuery + */ + private $SQLQuery = null; + /** * @inheritDoc */ @@ -44,19 +49,18 @@ class OxidSQLLogger implements SQLLogger { $formatter = new Formatter(); - Monolog\Registry::sql()->addDebug( - $this->message ? $this->message : $sql, - [ - 'query' => $formatter->format($sql), - 'params' => $params, - 'types' => $types, - 'logStartingFile' => $this->logStartingFile, - 'logStartingLine' => $this->logStartingLine, - 'logStartingClass' => $this->logStartingClass, - 'logStartingFunction' => $this->logStartingFunction, + if ($this->SQLQuery) { + $this->SQLQuery->setCanceled(); + $this->stopQuery(); + } - ] - ); + $this->SQLQuery = (new SQLQuery()) ->setSql($formatter->format($sql)) + ->setParams($params) + ->setTypes($types) + ->setLogStartingFile($this->logStartingFile) + ->setLogStartingLine($this->logStartingLine) + ->setLogStartingClass($this->logStartingClass) + ->setLogStartingFunction($this->logStartingFunction); } /** @@ -64,5 +68,21 @@ class OxidSQLLogger implements SQLLogger */ public function stopQuery() { + if ($this->SQLQuery) { + Monolog\Registry::sql()->addDebug( + '['.$this->SQLQuery->getReadableElapsedTime().'] ' . ( $this->message ? $this->message : $this->SQLQuery->getSql() ), + [ + 'params' => $this->SQLQuery->getParams(), + 'time' => $this->SQLQuery->getElapsedTime(), + 'types' => $this->SQLQuery->getTypes(), + 'logStartingFile' => $this->SQLQuery->getLogStartingFile(), + 'logStartingLine' => $this->SQLQuery->getLogStartingLine(), + 'logStartingClass' => $this->SQLQuery->getLogStartingClass(), + 'logStartingFunction' => $this->SQLQuery->getLogStartingFunction(), + ] + ); + } + + $this->SQLQuery = null; } } diff --git a/src/OxidUtilsView.php b/src/OxidUtilsView.php new file mode 100644 index 0000000..451e257 --- /dev/null +++ b/src/OxidUtilsView.php @@ -0,0 +1,24 @@ + + * Date: 2019-08-20 + * Time: 21:33 + */ + +namespace D3\OxidSqlLogger; + +/** + * Class OxidUtilsView + * @package tm\oxid\sql\logger + */ +class OxidUtilsView extends \OxidEsales\Eshop\Core\UtilsView +{ + + /** + * Removes existing Smarty instance + */ + public static function clearSmarty() + { + \OxidEsales\Eshop\Core\UtilsView::$_oSmarty = null; + } +} diff --git a/src/SQLQuery.php b/src/SQLQuery.php new file mode 100644 index 0000000..363dafd --- /dev/null +++ b/src/SQLQuery.php @@ -0,0 +1,215 @@ + + * Date: 2019-08-20 + * Time: 21:56 + */ + +namespace D3\OxidSqlLogger; + +/** + * Class SQLQuery + * @package tm\oxid\sql\logger + */ +class SQLQuery +{ + /** + * @var float|null + */ + private $start_time = null; + + /** + * @var float|null + */ + private $stop_time = null; + + /** + * @var string + */ + private $sql = ''; + + /** + * @var null + */ + private $params = null; + + /** + * @var null + */ + private $types = null; + + private $logStartingFile; + + private $logStartingLine; + + private $logStartingClass; + + private $logStartingFunction; + + /** + * @inheritDoc + */ + public function __construct() + { + $this->start_time = microtime(true); + } + + /** + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @param string $sql + * @return SQLQuery + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * @return null + */ + public function getParams() + { + return $this->params; + } + + /** + * @param null $params + * @return SQLQuery + */ + public function setParams($params) + { + $this->params = $params; + return $this; + } + + /** + * @return null + */ + public function getTypes() + { + return $this->types; + } + + /** + * @param null $types + * @return SQLQuery + */ + public function setTypes($types) + { + $this->types = $types; + return $this; + } + + /** + * @param $file + * @return SQLQuery + */ + public function setLogStartingFile($file) + { + $this->logStartingFile = $file; + return $this; + } + + /** + * @param $line + * @return SQLQuery + */ + public function setLogStartingLine($line) + { + $this->logStartingLine = $line; + return $this; + } + + /** + * @param $classname + * @return SQLQuery + */ + public function setLogStartingClass($classname) + { + $this->logStartingClass = $classname; + return $this; + } + + /** + * @param $functionname + * @return SQLQuery + */ + public function setLogStartingFunction($functionname) + { + $this->logStartingFunction = $functionname; + return $this; + } + + /** + * Statement was cancelled prematurely, an error was thrown. + * + * @return $this + */ + public function setCanceled() + { + $this->start_time = null; + return $this; + } + + /** + * Returns elapsed time + * @return float|string + */ + public function getElapsedTime() + { + if ($this->start_time === null) { + return 'Statement canceled'; + } + + if ($this->stop_time === null) { + $end_time = microtime(true); + $this->stop_time = $end_time - $this->start_time; + } + + return (float)$this->stop_time; + } + + /** + * Returns a human readable elapsed time + * + * @return string + */ + public function getReadableElapsedTime() + { + return $this->readableElapsedTime($this->getElapsedTime()); + } + + /** + * Returns a human readable elapsed time + * + * @param float $microtime + * @param string $format The format to display (printf format) + * @return string + */ + private function readableElapsedTime($microtime, $format = '%.3f%s', $round = 3) + { + if (is_string($microtime)) { + return $microtime; + } + + if ($microtime >= 1) { + $unit = 's'; + $time = round($microtime, $round); + } else { + $unit = 'ms'; + $time = round($microtime*1000); + + $format = preg_replace('/(%.[\d]+f)/', '%d', $format); + } + + return sprintf($format, $time, $unit); + } +} diff --git a/src/Smarty/function.tm_sql_status.php b/src/Smarty/function.tm_sql_status.php new file mode 100644 index 0000000..433bc98 --- /dev/null +++ b/src/Smarty/function.tm_sql_status.php @@ -0,0 +1,55 @@ +blSQLStatusBox = true +* ------------------------------------------------------------- +*/ +function smarty_function_tm_sql_status($aParams, &$smarty) +{ + $myConfig = \OxidEsales\Eshop\Core\Registry::getConfig(); + + // muss in config.inc.php gesetzt sein + + $box = $myConfig->getConfigParam('blSQLStatusBox'); + + if ($box == false) { + return ''; + } + + $db = \OxidEsales\Eshop\Core\DatabaseProvider::getDb(\OxidEsales\Eshop\Core\DatabaseProvider::FETCH_MODE_ASSOC); + $query = "SHOW STATUS WHERE Variable_name IN ( 'Com_select', 'Com_update', 'Com_insert', 'Com_delete' )"; + + $iSelects = $iDeletes = $iInserts = $iUpdates = 0; + $rows = $db->getAll($query); + foreach ($rows as $row) { + switch ($row['Variable_name']) { + case 'Com_select': + $iSelects = (int)$row['Value']; + break; + case 'Com_update': + $iUpdates = (int)$row['Value']; + break; + case 'Com_insert': + $iInserts = (int)$row['Value']; + break; + case 'Com_delete': + $iDeletes = (int)$row['Value']; + break; + default: + break; + } + } + + $iSum = $iSelects + $iDeletes + $iInserts + $iUpdates; + $sTable = ''; + $sTable .= ''; + $sTable .= ""; + $sTable .= '
All: $iSum SELECT: $iSelects UPDATE: $iUpdates INSERT: $iInserts DELETE: $iDeletes
'; + + return $sTable; +} diff --git a/src/functions.php b/src/functions.php index 0571331..9f44972 100644 --- a/src/functions.php +++ b/src/functions.php @@ -12,3 +12,5 @@ function D3StartSQLLog($message = null) { function D3StopSQLLog() { \D3\OxidSqlLogger\OxidEsalesDatabase::disableLogger(); } + +(new \D3\OxidSqlLogger\AutoInstallSmaryPlugin())->runInstall();