diff --git a/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLogger.php b/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLogger.php index 63406d70e1b74698c26a37c2845498729c95b502..20cba821f686b285fc55d915e096e2e2d7544c89 100644 --- a/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLogger.php +++ b/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLogger.php @@ -17,25 +17,31 @@ declare(strict_types=1); namespace TYPO3\CMS\Adminpanel\Log; -use Doctrine\DBAL\Logging\SQLLogger; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Adminpanel\Utility\MemoryUtility; /** * Doctrine SQL Logger implementation for recording queries for the admin panel + * + * @internal */ -class DoctrineSqlLogger implements SQLLogger, LoggerAwareInterface +class DoctrineSqlLogger implements LoggerAwareInterface { use LoggerAwareTrait; /** Executed SQL queries. */ protected array $queries = []; /** If Debug Stack is enabled (log queries) or not. */ - protected bool $enabled = true; + protected bool $enabled = false; protected float $start; protected int $currentQuery = 0; + public function enable(): void + { + $this->enabled = true; + } + public function startQuery($sql, array $params = null, array $types = null): void { if ($this->enabled && MemoryUtility::isMemoryConsumptionTooHigh()) { @@ -43,18 +49,18 @@ class DoctrineSqlLogger implements SQLLogger, LoggerAwareInterface $this->logger->warning('SQL Logging consumed too much memory, aborted. Not all queries have been recorded.'); } if ($this->enabled) { + $visibleBacktraceLength = 4; + $removeFromBacktraceLength = 4; + $this->start = microtime(true); - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7); - // remove this method - array_shift($backtrace); - // remove doctrine execute query - array_shift($backtrace); - // remove queryBuilder execute - array_shift($backtrace); + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $visibleBacktraceLength + $removeFromBacktraceLength); + // remove internal doctrine and logging methods from the visible backtrace + array_splice($backtrace, 0, $removeFromBacktraceLength); + $this->queries[++$this->currentQuery] = [ 'sql' => $sql, - 'params' => $params, - 'types' => $types, + 'params' => $params ?? [], + 'types' => $types ?? [], 'executionMS' => 0, 'backtrace' => $backtrace, ]; diff --git a/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLoggingMiddleware.php b/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLoggingMiddleware.php new file mode 100644 index 0000000000000000000000000000000000000000..a38af566c537d89f66c97102601e2b6fb9e5560d --- /dev/null +++ b/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLoggingMiddleware.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Adminpanel\Log; + +use Doctrine\DBAL\Driver as DriverInterface; +use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Wrapper around the Doctrine SQL Driver for logging sql queries + * + * @internal + */ +class DoctrineSqlLoggingMiddleware implements MiddlewareInterface +{ + protected DoctrineSqlLogger $logger; + + public function wrap(DriverInterface $driver): DriverInterface + { + $this->logger = GeneralUtility::makeInstance(DoctrineSqlLogger::class); + return new LoggingDriver($driver, $this->logger); + } + + public function enable(): void + { + $this->logger->enable(); + } + + public function getQueries(): array + { + return $this->logger->getQueries(); + } +} diff --git a/typo3/sysext/adminpanel/Classes/Log/LoggingConnection.php b/typo3/sysext/adminpanel/Classes/Log/LoggingConnection.php new file mode 100644 index 0000000000000000000000000000000000000000..f5e2df1a679ff0addc1ce5740b22e9b0860de152 --- /dev/null +++ b/typo3/sysext/adminpanel/Classes/Log/LoggingConnection.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Adminpanel\Log; + +use Doctrine\DBAL\Driver\Connection as ConnectionInterface; +use Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware; +use Doctrine\DBAL\Driver\Result; +use Doctrine\DBAL\Driver\Statement as DriverStatement; + +/** + * Part of the Doctrine SQL Logging Driver Adapter + * + * @internal + */ +final class LoggingConnection extends AbstractConnectionMiddleware +{ + private DoctrineSqlLogger $logger; + + public function __construct(ConnectionInterface $connection, DoctrineSqlLogger $logger) + { + parent::__construct($connection); + + $this->logger = $logger; + } + + public function prepare(string $sql): DriverStatement + { + return new LoggingStatement(parent::prepare($sql), $this->logger, $sql); + } + + public function query(string $sql): Result + { + $this->logger->startQuery($sql); + $query = parent::query($sql); + $this->logger->stopQuery(); + + return $query; + } + + public function exec(string $sql): int + { + $this->logger->startQuery($sql); + $query = parent::exec($sql); + $this->logger->stopQuery(); + + return $query; + } +} diff --git a/typo3/sysext/adminpanel/Classes/Log/LoggingDriver.php b/typo3/sysext/adminpanel/Classes/Log/LoggingDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..9ae883b34159a66d0888da1d8c6cc3b287a0fceb --- /dev/null +++ b/typo3/sysext/adminpanel/Classes/Log/LoggingDriver.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Adminpanel\Log; + +use Doctrine\DBAL\Driver as DriverInterface; +use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; + +/** + * Part of the Doctrine SQL Logging Driver Adapter + * + * @internal + */ +final class LoggingDriver extends AbstractDriverMiddleware +{ + private DoctrineSqlLogger $logger; + + public function __construct(DriverInterface $driver, DoctrineSqlLogger $logger) + { + parent::__construct($driver); + + $this->logger = $logger; + } + + public function connect(array $params) + { + return new LoggingConnection(parent::connect($params), $this->logger); + } +} diff --git a/typo3/sysext/adminpanel/Classes/Log/LoggingStatement.php b/typo3/sysext/adminpanel/Classes/Log/LoggingStatement.php new file mode 100644 index 0000000000000000000000000000000000000000..0be233c17128663c9f2515f02bb817367cae516d --- /dev/null +++ b/typo3/sysext/adminpanel/Classes/Log/LoggingStatement.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Adminpanel\Log; + +use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware; +use Doctrine\DBAL\Driver\Result as ResultInterface; +use Doctrine\DBAL\Driver\Statement as StatementInterface; +use Doctrine\DBAL\ParameterType; + +/** + * Part of the Doctrine SQL Logging Driver Adapter + * + * @internal + */ +final class LoggingStatement extends AbstractStatementMiddleware +{ + private DoctrineSqlLogger $logger; + private string $sql; + private array $params = []; + private array $types = []; + + public function __construct(StatementInterface $statement, DoctrineSqlLogger $logger, string $sql) + { + parent::__construct($statement); + + $this->logger = $logger; + $this->sql = $sql; + } + + public function bindValue($param, $value, $type = ParameterType::STRING) + { + $this->params[$param] = $value; + $this->types[$param] = $type; + + return parent::bindValue($param, $value, $type); + } + + public function execute($params = null): ResultInterface + { + $this->logger->startQuery($this->sql, $params ?? $this->params, $this->types); + $result = parent::execute($params); + $this->logger->stopQuery(); + + return $result; + } +} diff --git a/typo3/sysext/adminpanel/Classes/Middleware/SqlLogging.php b/typo3/sysext/adminpanel/Classes/Middleware/SqlLogging.php index 7c9146fe2bfa1f9cd3791db4fef4234e4fde31f3..65623b82bdbd5b4dd26d43a1edbeb8a008c3c32c 100644 --- a/typo3/sysext/adminpanel/Classes/Middleware/SqlLogging.php +++ b/typo3/sysext/adminpanel/Classes/Middleware/SqlLogging.php @@ -21,10 +21,9 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use TYPO3\CMS\Adminpanel\Log\DoctrineSqlLogger; +use TYPO3\CMS\Adminpanel\Log\DoctrineSqlLoggingMiddleware; use TYPO3\CMS\Adminpanel\Utility\StateUtility; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Enable sql logging for the admin panel @@ -45,7 +44,12 @@ class SqlLogging implements MiddlewareInterface { if (StateUtility::isActivatedForUser() && StateUtility::isOpen()) { $connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME); - $connection->getConfiguration()->setSQLLogger(GeneralUtility::makeInstance(DoctrineSqlLogger::class)); + foreach ($connection->getConfiguration()->getMiddlewares() as $middleware) { + if ($middleware instanceof DoctrineSqlLoggingMiddleware) { + $middleware->enable(); + break; + } + } } return $handler->handle($request); } diff --git a/typo3/sysext/adminpanel/Classes/Modules/Debug/QueryInformation.php b/typo3/sysext/adminpanel/Classes/Modules/Debug/QueryInformation.php index e435c513b9f61f995793f98434ca9b7fde486f6f..f7ec173bcaf3cc399622482cb039e5dccf46c179 100644 --- a/typo3/sysext/adminpanel/Classes/Modules/Debug/QueryInformation.php +++ b/typo3/sysext/adminpanel/Classes/Modules/Debug/QueryInformation.php @@ -18,7 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Adminpanel\Modules\Debug; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Adminpanel\Log\DoctrineSqlLogger; +use TYPO3\CMS\Adminpanel\Log\DoctrineSqlLoggingMiddleware; use TYPO3\CMS\Adminpanel\ModuleApi\AbstractSubModule; use TYPO3\CMS\Adminpanel\ModuleApi\DataProviderInterface; use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData; @@ -56,10 +56,18 @@ class QueryInformation extends AbstractSubModule implements DataProviderInterfac { $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); $connection = $connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME); - $logger = $connection->getConfiguration()->getSQLLogger(); + + $loggingMiddleware = null; + foreach ($connection->getConfiguration()->getMiddlewares() as $middleware) { + if ($middleware instanceof DoctrineSqlLoggingMiddleware) { + $loggingMiddleware = $middleware; + break; + } + } + $data = []; - if ($logger instanceof DoctrineSqlLogger) { - $queries = $logger->getQueries(); + if ($loggingMiddleware instanceof DoctrineSqlLoggingMiddleware) { + $queries = $loggingMiddleware->getQueries(); $data['totalQueries'] = count($queries); $data['queries'] = $this->groupQueries($queries); $data['totalTime'] = array_sum(array_column($queries, 'executionMS')) * 1000; diff --git a/typo3/sysext/adminpanel/ext_localconf.php b/typo3/sysext/adminpanel/ext_localconf.php index 312d03b582a568aa2d8be7ec8459838b43168da9..1b6f01160dd3396482e59641d65f5831457c4155 100644 --- a/typo3/sysext/adminpanel/ext_localconf.php +++ b/typo3/sysext/adminpanel/ext_localconf.php @@ -87,3 +87,9 @@ $GLOBALS['TYPO3_CONF_VARS']['LOG']['writerConfiguration'][LogLevel::DEBUG][InMem if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['adminpanel_requestcache'] ?? null)) { $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['adminpanel_requestcache'] = []; } + +if (!is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares'] ?? null)) { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['DB']['Connections']['Default']['driverMiddlewares'] = []; +} + +$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['adminpanel_loggingmiddleware'] = \TYPO3\CMS\Adminpanel\Log\DoctrineSqlLoggingMiddleware::class; diff --git a/typo3/sysext/core/Classes/Database/ConnectionPool.php b/typo3/sysext/core/Classes/Database/ConnectionPool.php index ac1a2f88f808b34d5b89f7c9e22e9cdf11d03d10..7b7f85e287763636493cee603d169ade200078e5 100644 --- a/typo3/sysext/core/Classes/Database/ConnectionPool.php +++ b/typo3/sysext/core/Classes/Database/ConnectionPool.php @@ -17,6 +17,8 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Database; +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Driver\Middleware as DriverMiddleware; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Events; use Doctrine\DBAL\Types\Type; @@ -174,6 +176,8 @@ class ConnectionPool /** * Map custom driver class for certain driver + * + * @internal */ protected function mapCustomDriver(array $connectionParams): array { @@ -185,6 +189,24 @@ class ConnectionPool return $connectionParams; } + /** + * Return any doctrine driver middlewares, that may have been set up in: + * $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares'] + */ + protected function getDriverMiddlewares(array $connectionParams): array + { + $middlewares = []; + + foreach ($connectionParams['driverMiddlewares'] ?? [] as $className) { + if (!in_array(DriverMiddleware::class, class_implements($className) ?: [], true)) { + throw new \UnexpectedValueException('Doctrine Driver Middleware must implement \Doctrine\DBAL\Driver\Middleware', 1677958727); + } + $middlewares[] = GeneralUtility::makeInstance($className); + } + + return $middlewares; + } + /** * Creates a connection object based on the specified parameters */ @@ -198,9 +220,11 @@ class ConnectionPool } $connectionParams = $this->mapCustomDriver($connectionParams); + $middlewares = $this->getDriverMiddlewares($connectionParams); + $configuration = $middlewares ? (new Configuration())->setMiddlewares($middlewares) : null; /** @var Connection $conn */ - $conn = DriverManager::getConnection($connectionParams); + $conn = DriverManager::getConnection($connectionParams, $configuration); $conn->prepareConnection($connectionParams['initCommands'] ?? ''); // Register all custom data types in the type mapping diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100089-IntroduceDoctrineDBALv3drivermiddlewares.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100089-IntroduceDoctrineDBALv3drivermiddlewares.rst new file mode 100644 index 0000000000000000000000000000000000000000..495eb882b0e17536fdeb855a2ae5c393f1cb0da8 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100089-IntroduceDoctrineDBALv3drivermiddlewares.rst @@ -0,0 +1,38 @@ +.. include:: /Includes.rst.txt + +.. _feature-100089-1677961107: + +================================================================ +Feature: #100089 - Introduce Doctrine DBAL v3 driver middlewares +================================================================ + +See :issue:`100089` + +Description +=========== + +Since v3, Doctrine DBAL supports adding custom driver middlewares. These +middlewares act as a decorator around the actual `Driver` component. +Subsequently, the `Connection`, `Statement` and `Result` components can be +decorated as well. These middlewares must implement the +:php:`\Doctrine\DBAL\Driver\Middleware` interface. +A common use case would be a middleware for implementing sql logging capabilities. + +For more information on driver middlewares, +see https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/architecture.html. +Furthermore, you can look up the implementation of the +:php:`\TYPO3\CMS\Adminpanel\Log\DoctrineSqlLoggingMiddleware` in ext:adminpanel +as an example. + +Registering a new driver middleware +=================================== + +.. code-block:: php + + $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['adminpanel_loggingmiddleware'] + = \TYPO3\CMS\Adminpanel\Log\DoctrineSqlLoggingMiddleware::class; + +Impact +====== + +.. index:: Database, ext:core