From 1d625cd1cbc1fb6226855bc856f3578c083dd967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20E=C3=9Fl?= <indy.essl@gmail.com> Date: Sat, 4 Mar 2023 21:23:57 +0100 Subject: [PATCH] [FEATURE] Introduce Doctrine DBAL v3 driver middlewares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. A new setting is introduced to register custom driver middlewares that will automatically be added to a newly established Doctrine database connection. Resolves: #100089 Releases: main Change-Id: I4cf8e0252c3241f49ce1b9adb68cb85d978e164b Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/78029 Tested-by: core-ci <typo3@b13.com> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Benni Mack <benni@typo3.org> --- .../Classes/Log/DoctrineSqlLogger.php | 30 +++++---- .../Log/DoctrineSqlLoggingMiddleware.php | 48 ++++++++++++++ .../Classes/Log/LoggingConnection.php | 63 +++++++++++++++++++ .../adminpanel/Classes/Log/LoggingDriver.php | 43 +++++++++++++ .../Classes/Log/LoggingStatement.php | 61 ++++++++++++++++++ .../Classes/Middleware/SqlLogging.php | 10 ++- .../Modules/Debug/QueryInformation.php | 16 +++-- typo3/sysext/adminpanel/ext_localconf.php | 6 ++ .../core/Classes/Database/ConnectionPool.php | 26 +++++++- ...troduceDoctrineDBALv3drivermiddlewares.rst | 38 +++++++++++ 10 files changed, 321 insertions(+), 20 deletions(-) create mode 100644 typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLoggingMiddleware.php create mode 100644 typo3/sysext/adminpanel/Classes/Log/LoggingConnection.php create mode 100644 typo3/sysext/adminpanel/Classes/Log/LoggingDriver.php create mode 100644 typo3/sysext/adminpanel/Classes/Log/LoggingStatement.php create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Feature-100089-IntroduceDoctrineDBALv3drivermiddlewares.rst diff --git a/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLogger.php b/typo3/sysext/adminpanel/Classes/Log/DoctrineSqlLogger.php index 63406d70e1b7..20cba821f686 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 000000000000..a38af566c537 --- /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 000000000000..f5e2df1a679f --- /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 000000000000..9ae883b34159 --- /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 000000000000..0be233c17128 --- /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 7c9146fe2bfa..65623b82bdbd 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 e435c513b9f6..f7ec173bcaf3 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 312d03b582a5..1b6f01160dd3 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 ac1a2f88f808..7b7f85e28776 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 000000000000..495eb882b0e1 --- /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 -- GitLab