diff --git a/.github/workflows/tasks.yml b/.github/workflows/tasks.yml index afa9390..34bad86 100644 --- a/.github/workflows/tasks.yml +++ b/.github/workflows/tasks.yml @@ -9,19 +9,8 @@ jobs: strategy: fail-fast: false matrix: - php: [ '7.4', '8.0', '8.1', '8.2' ] - typo3: [ '10', '11', '12' ] - exclude: - - php: '8.0' - typo3: '10' - - php: '8.1' - typo3: '10' - - php: '8.2' - typo3: '10' - - php: '7.4' - typo3: '12' - - php: '8.0' - typo3: '12' + php: [ '8.1', '8.2' ] + typo3: [ '11', '12' ] steps: - name: Setup PHP with PECL extension uses: shivammathur/setup-php@v2 diff --git a/.gitignore b/.gitignore index 0e382f6..7bf2056 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ -.phpunit.cache composer.lock public/ vendor/ -.phpunit.result.cache var +Resources/Public/test-result diff --git a/.phpunit-watcher.yml b/.phpunit-watcher.yml new file mode 100644 index 0000000..63de359 --- /dev/null +++ b/.phpunit-watcher.yml @@ -0,0 +1,12 @@ +watch: + directories: + - Classes/ + - Tests/ + fileMask: '*.php' +notifications: + passingTests: false + failingTests: false +phpunit: + binaryPath: vendor/bin/phpunit + arguments: '--stop-on-failure' + timeout: 180 diff --git a/Classes/Dto/ScriptResult.php b/Classes/Dto/ScriptResult.php new file mode 100644 index 0000000..47474fe --- /dev/null +++ b/Classes/Dto/ScriptResult.php @@ -0,0 +1,41 @@ +request; + } +} diff --git a/Classes/Dto/StopWatch.php b/Classes/Dto/StopWatch.php index ee649af..4ba7a02 100644 --- a/Classes/Dto/StopWatch.php +++ b/Classes/Dto/StopWatch.php @@ -6,30 +6,33 @@ final class StopWatch { - /** @var string */ - public $key = ''; - /** @var string */ - public $info = ''; - /** @var ?float */ - public $startTime; - /** @var ?float */ - public $stopTime; - - public function __construct(string $key, string $info) + public float $startTime; + + public ?float $stopTime = null; + + public function __construct(public string $key, public string $info) { - $this->key = $key; - $this->info = $info; $this->startTime = microtime(true); } public function getDuration(): float { - $this->stopTime = $this->stopTime ?? microtime(true); + $this->stopTime ??= microtime(true); return $this->stopTime - $this->startTime; } - public function __invoke(): void + public function stop(): void { $this->stopTime = microtime(true); } + + public function __invoke(): void + { + $this->stop(); + } + + public function stopIfNot(): void + { + $this->stopTime ??= microtime(true); + } } diff --git a/Classes/EventListener/ConsoleCommandEventListener.php b/Classes/EventListener/ConsoleCommandEventListener.php new file mode 100644 index 0000000..60e2b27 --- /dev/null +++ b/Classes/EventListener/ConsoleCommandEventListener.php @@ -0,0 +1,24 @@ +getCommand()?->getName()); + } + + public function stop(ConsoleTerminateEvent $event): void + { + TimingUtility::end('cli'); + TimingUtility::getInstance()->shutdown(ScriptResult::fromCli($event->getExitCode())); + } +} diff --git a/Classes/Middleware/AdminpanelSqlLoggingMiddleware.php b/Classes/Middleware/AdminpanelSqlLoggingMiddleware.php index 6beadb7..ab9c4af 100644 --- a/Classes/Middleware/AdminpanelSqlLoggingMiddleware.php +++ b/Classes/Middleware/AdminpanelSqlLoggingMiddleware.php @@ -14,14 +14,13 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; -class AdminpanelSqlLoggingMiddleware implements MiddlewareInterface +/** + * @deprecated can be removed if only TYPO3 >=12 is compatible + */ +final class AdminpanelSqlLoggingMiddleware implements MiddlewareInterface { /** * Enable SQL Logging as early as possible to catch all queries if the admin panel is active - * - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { @@ -39,6 +38,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface ) ); } + return $handler->handle($request); } } diff --git a/Classes/Middleware/FirstMiddleware.php b/Classes/Middleware/FirstMiddleware.php index 4c57b41..df1be64 100644 --- a/Classes/Middleware/FirstMiddleware.php +++ b/Classes/Middleware/FirstMiddleware.php @@ -5,6 +5,7 @@ namespace Kanti\ServerTiming\Middleware; use Doctrine\DBAL\Logging\SQLLogger; +use Kanti\ServerTiming\Dto\ScriptResult; use Kanti\ServerTiming\Dto\StopWatch; use Kanti\ServerTiming\Utility\TimingUtility; use Psr\Http\Message\ResponseInterface; @@ -12,52 +13,62 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Http\ImmediateResponseException; +use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Utility\DebuggerUtility; -class FirstMiddleware implements MiddlewareInterface +final class FirstMiddleware implements MiddlewareInterface { - /** @var StopWatch|null */ - public static $stopWatchOutward = null; + public static ?StopWatch $stopWatchOutward = null; public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $stop = TimingUtility::stopWatch('middleware', 'Inward'); - $request = $request->withAttribute('server-timing:middleware:inward', $stop); + $inward = TimingUtility::stopWatch('middleware', 'Inward'); + $request = $request->withAttribute('server-timing:middleware:inward', $inward); + + $GLOBALS['TYPO3_REQUEST'] = $request; + $this->registerSqlLogger(); - $response = $handler->handle($request); - $stop = self::$stopWatchOutward; - if ($stop instanceof StopWatch) { - $stop(); + + try { + $response = $handler->handle($request); + } catch (ImmediateResponseException $immediateResponseException) { + $response = $immediateResponseException->getResponse(); } - return $response; + + $inward->stopIfNot(); + self::$stopWatchOutward?->stopIfNot(); + self::$stopWatchOutward = null; + + return TimingUtility::getInstance()->shutdown(ScriptResult::fromRequest($request, $response)) ?? $response; } - protected function registerSqlLogger(): void + /** + * @deprecated can be removed if only TYPO3 >=12 is compatible + */ + private function registerSqlLogger(): void { $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); $connection = $connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME); $connection->getConfiguration()->setSQLLogger( new class implements SQLLogger { - /** @var StopWatch|null */ - private $stopWatch = null; + private ?StopWatch $stopWatch = null; - public function startQuery($sql, ?array $params = null, ?array $types = null) + public function startQuery($sql, ?array $params = null, ?array $types = null): void { - $stop = $this->stopWatch; - if ($stop instanceof StopWatch) { - $stop(); + $this->stopWatch?->stopIfNot(); + + if ($sql === 'SELECT DATABASE()') { + return; } + $this->stopWatch = TimingUtility::stopWatch('sql', $sql); } - public function stopQuery() + public function stopQuery(): void { - $stop = $this->stopWatch; - if ($stop instanceof StopWatch) { - $stop(); - $this->stopWatch = null; - } + $this->stopWatch?->stopIfNot(); + $this->stopWatch = null; } } ); diff --git a/Classes/Middleware/LastMiddleware.php b/Classes/Middleware/LastMiddleware.php index 92f5af0..afd61c7 100644 --- a/Classes/Middleware/LastMiddleware.php +++ b/Classes/Middleware/LastMiddleware.php @@ -11,20 +11,22 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -class LastMiddleware implements MiddlewareInterface +final class LastMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - TimingUtility::getInstance()->checkBackendUserStatus(); - $stop = $request->getAttribute('server-timing:middleware:inward'); - if ($stop instanceof StopWatch) { - $stop(); - } + $stopWatch = $request->getAttribute('server-timing:middleware:inward'); + $stopWatch?->stop(); + $request = $request->withoutAttribute('server-timing:middleware:inward'); - $stop = TimingUtility::stopWatch('requestHandler'); - $response = $handler->handle($request); - $stop(); - \Kanti\ServerTiming\Middleware\FirstMiddleware::$stopWatchOutward = TimingUtility::stopWatch('middleware', 'Outward'); + $stopWatch = TimingUtility::stopWatch('requestHandler'); + try { + $response = $handler->handle($request); + } finally { + $stopWatch(); + FirstMiddleware::$stopWatchOutward = TimingUtility::stopWatch('middleware', 'Outward'); + } + return $response; } } diff --git a/Classes/Service/ConfigService.php b/Classes/Service/ConfigService.php new file mode 100644 index 0000000..51ea861 --- /dev/null +++ b/Classes/Service/ConfigService.php @@ -0,0 +1,35 @@ +getConfig('stop_watch_limit') ?: self::DEFAULT_STOP_WATCH_LIMIT); + } + + public function tracesSampleRate(): ?float + { + $tracesSampleRate = $this->getConfig(TimingUtility::IS_CLI ? 'sentry_cli_sample_rate' : 'sentry_sample_rate'); + return $tracesSampleRate === '' ? null : (float)$tracesSampleRate; + } + + public function enableTracing(): ?bool + { + $tracesSampleRate = $this->tracesSampleRate(); + return $tracesSampleRate === null ? null : (bool)$tracesSampleRate; + } + + private function getConfig(string $path): string + { + return (string)($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['server_timing'][$path] ?? ''); + } +} diff --git a/Classes/Service/RegisterShutdownFunction/RegisterShutdownFunction.php b/Classes/Service/RegisterShutdownFunction/RegisterShutdownFunction.php new file mode 100644 index 0000000..6751369 --- /dev/null +++ b/Classes/Service/RegisterShutdownFunction/RegisterShutdownFunction.php @@ -0,0 +1,13 @@ +callCount++; + } +} diff --git a/Classes/Service/SentryService.php b/Classes/Service/SentryService.php new file mode 100644 index 0000000..959ef31 --- /dev/null +++ b/Classes/Service/SentryService.php @@ -0,0 +1,113 @@ +getClient(); + if (!$client && class_exists(Sentry::class)) { + $client = Sentry::getInstance()->getClient(); + } + + if (!$client) { + return; + } + + $options = $client->getOptions(); + + $options->setTracesSampleRate($this->configService->tracesSampleRate() ?? $options->getTracesSampleRate()); + $options->setEnableTracing($this->configService->enableTracing() ?? $options->getEnableTracing()); + + $transactionContext = new TransactionContext(); + if ($result->isCli()) { + $this->setContextFromCli($transactionContext, $result); + } else { + $this->setContextFromRequest($transactionContext, $result); + } + + $transactionContext->setStartTimestamp($stopWatches[0]->startTime); + + $transaction = $hub->startTransaction($transactionContext); + $hub->setSpan($transaction); + + /** @var non-empty-list $stack */ + $stack = [$transaction]; + foreach ($stopWatches as $stopWatch) { + while (count($stack) > 1 && $stopWatch->stopTime > $stack[array_key_last($stack)]->getEndTimestamp()) { + array_pop($stack); + } + + $parent = $stack[array_key_last($stack)]; + $spanContext = new SpanContext(); + $spanContext->setOp($stopWatch->key); + $spanContext->setStartTimestamp($stopWatch->startTime); + $spanContext->setDescription($stopWatch->info); + $span = $parent->startChild($spanContext); + $span->finish($stopWatch->stopTime); + $hub->setSpan($span); + $stack[] = $span; + } + + $hub->setSpan($transaction); + + $should = $options->shouldAttachStacktrace(); + $options->setAttachStacktrace(false); + $transaction->finish($stopWatches[0]->stopTime); + $options->setAttachStacktrace($should); + } + + private function setContextFromRequest(TransactionContext $transactionContext, ScriptResult $result): void + { + $serverRequest = $result->request; + assert($serverRequest instanceof ServerRequestInterface); + $transactionContext->setName($serverRequest->getMethod() . ' ' . $serverRequest->getUri()); + $transactionContext->setOp('typo3.request'); + $transactionContext->setData([ + 'request.method' => $serverRequest->getMethod(), + 'request.query' => $serverRequest->getQueryParams(), + 'request.body' => $serverRequest->getParsedBody(), + 'request.headers' => $serverRequest->getHeaders(), + 'request.cookies' => $serverRequest->getCookieParams(), + 'request.url' => (string)$serverRequest->getUri(), + ]); + $statusCode = $result->response?->getStatusCode() ?? http_response_code(); + if (is_int($statusCode)) { + $transactionContext->setStatus(SpanStatus::createFromHttpStatusCode($statusCode)); + } + } + + private function setContextFromCli(TransactionContext $transactionContext, ScriptResult $result): void + { + $transactionContext->setName(implode(' ', $_SERVER['argv'])); + $transactionContext->setOp('typo3.cli'); + if ($result->cliExitCode !== null) { + $transactionContext->setStatus($result->cliExitCode ? SpanStatus::unknownError() : SpanStatus::ok()); + } + } +} diff --git a/Classes/SqlLogging/DoctrineSqlLogger.php b/Classes/SqlLogging/DoctrineSqlLogger.php new file mode 100644 index 0000000..ea5896d --- /dev/null +++ b/Classes/SqlLogging/DoctrineSqlLogger.php @@ -0,0 +1,32 @@ +=12 + +declare(strict_types=1); + +namespace Kanti\ServerTiming\SqlLogging; + +// +//use Kanti\ServerTiming\Dto\StopWatch; +//use Kanti\ServerTiming\Utility\TimingUtility; +// +//final class DoctrineSqlLogger +//{ +// private ?StopWatch $stopWatch = null; +// +// public function startQuery(string $sql): void +// { +// if ($sql === 'SELECT DATABASE()') { +// return; +// } +// +// $this->stopWatch?->stopIfNot(); +// $this->stopWatch = TimingUtility::stopWatch('sql', $sql); +// } +// +// public function stopQuery(): void +// { +// $this->stopWatch?->stopIfNot(); +// $this->stopWatch = null; +// } +//} diff --git a/Classes/SqlLogging/LoggingConnection.php b/Classes/SqlLogging/LoggingConnection.php new file mode 100644 index 0000000..4ae45bc --- /dev/null +++ b/Classes/SqlLogging/LoggingConnection.php @@ -0,0 +1,48 @@ +=12 + +declare(strict_types=1); + +namespace Kanti\ServerTiming\SqlLogging; + +// +//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; +// +//if (!class_exists(AbstractConnectionMiddleware::class)) { +// return; +//} +// +//final class LoggingConnection extends AbstractConnectionMiddleware +//{ +// public function __construct(ConnectionInterface $connection, private readonly DoctrineSqlLogger $logger) +// { +// parent::__construct($connection); +// } +// +// 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/Classes/SqlLogging/LoggingDriver.php b/Classes/SqlLogging/LoggingDriver.php new file mode 100644 index 0000000..bda8de4 --- /dev/null +++ b/Classes/SqlLogging/LoggingDriver.php @@ -0,0 +1,29 @@ +=12 + +declare(strict_types=1); + +namespace Kanti\ServerTiming\SqlLogging; + +// +//use Doctrine\DBAL\Driver as DriverInterface; +//use Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware; +//use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; +// +//if (!class_exists(AbstractDriverMiddleware::class)) { +// return; +//} +// +//final class LoggingDriver extends AbstractDriverMiddleware +//{ +// public function __construct(DriverInterface $driver, private readonly DoctrineSqlLogger $logger) +// { +// parent::__construct($driver); +// } +// +// public function connect(array $params) +// { +// return new LoggingConnection(parent::connect($params), $this->logger); +// } +//} diff --git a/Classes/SqlLogging/LoggingMiddleware.php b/Classes/SqlLogging/LoggingMiddleware.php new file mode 100644 index 0000000..21b9ab1 --- /dev/null +++ b/Classes/SqlLogging/LoggingMiddleware.php @@ -0,0 +1,24 @@ +=12 + +declare(strict_types=1); + +namespace Kanti\ServerTiming\SqlLogging; + +// +//use Doctrine\DBAL\Driver as DriverInterface; +//use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; +//use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; +// +//if (!interface_exists(MiddlewareInterface::class)) { +// return; +//} +// +//final class LoggingMiddleware implements MiddlewareInterface +//{ +// public function wrap(DriverInterface $driver): DriverInterface +// { +// return new LoggingDriver($driver, new DoctrineSqlLogger()); +// } +//} diff --git a/Classes/SqlLogging/LoggingStatement.php b/Classes/SqlLogging/LoggingStatement.php new file mode 100644 index 0000000..3c3dade --- /dev/null +++ b/Classes/SqlLogging/LoggingStatement.php @@ -0,0 +1,33 @@ +=12 + +declare(strict_types=1); + +namespace Kanti\ServerTiming\SqlLogging; + +// +//use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware; +//use Doctrine\DBAL\Driver\Result as ResultInterface; +//use Doctrine\DBAL\Driver\Statement as StatementInterface; +// +//if (!class_exists(AbstractStatementMiddleware::class)) { +// return; +//} +// +//final class LoggingStatement extends AbstractStatementMiddleware +//{ +// public function __construct(StatementInterface $statement, private readonly DoctrineSqlLogger $logger, private readonly string $sql) +// { +// parent::__construct($statement); +// } +// +// public function execute($params = null): ResultInterface +// { +// $this->logger->startQuery($this->sql); +// $result = parent::execute($params); +// $this->logger->stopQuery(); +// +// return $result; +// } +//} diff --git a/Classes/Utility/GuzzleUtility.php b/Classes/Utility/GuzzleUtility.php index 042daaa..fcc9750 100644 --- a/Classes/Utility/GuzzleUtility.php +++ b/Classes/Utility/GuzzleUtility.php @@ -7,19 +7,25 @@ use Closure; use GuzzleHttp\Promise\PromiseInterface; use Psr\Http\Message\RequestInterface; +use TYPO3\CMS\Core\Http\ServerRequestFactory; -class GuzzleUtility +final class GuzzleUtility { - public static function getHandler(): Closure + public static function getHandler(): ?Closure { - return static function (callable $handler): Closure { - return static function (RequestInterface $request, array $options) use ($handler): PromiseInterface { - $info = $request->getMethod() . ' ' . $request->getUri()->__toString(); - $stop = TimingUtility::stopWatch('guzzle', $info); - $response = $handler($request, $options); + if (str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/typo3/module/system/config')) { + // fix bug in Configuration Backend module + return null; + } + + return static fn(callable $handler): Closure => static function (RequestInterface $request, array $options) use ($handler): PromiseInterface { + $info = $request->getMethod() . ' ' . $request->getUri()->__toString(); + $stop = TimingUtility::stopWatch('guzzle', $info); + try { + return $handler($request, $options); + } finally { $stop(); - return $response; - }; + } }; } } diff --git a/Classes/Utility/TimingUtility.php b/Classes/Utility/TimingUtility.php index 4d47f96..c679db9 100644 --- a/Classes/Utility/TimingUtility.php +++ b/Classes/Utility/TimingUtility.php @@ -4,40 +4,48 @@ namespace Kanti\ServerTiming\Utility; +use Exception; +use Kanti\ServerTiming\Dto\ScriptResult; use Kanti\ServerTiming\Dto\StopWatch; +use Kanti\ServerTiming\Service\RegisterShutdownFunction\RegisterShutdownFunctionInterface; +use Kanti\ServerTiming\Service\SentryService; +use Kanti\ServerTiming\Service\ConfigService; +use Psr\Http\Message\ResponseInterface; +use SplObjectStorage; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; -final class TimingUtility +final class TimingUtility implements SingletonInterface { - /** @var TimingUtility|null */ - private static $instance = null; - /** @var bool */ - private static $registered = false; - /** @var bool|null */ - private static $isBackendUser = null; + private bool $registered = false; + /** @var bool */ - private static $isCli = PHP_SAPI === 'cli'; + public const IS_CLI = PHP_SAPI === 'cli'; - public static function getInstance(): TimingUtility + private bool $alreadyShutdown = false; + + public function __construct(private readonly RegisterShutdownFunctionInterface $registerShutdownFunction, private readonly ConfigService $configService) { - return self::$instance = self::$instance ?? new self(); } - /** - * only for tests - * @phpstan-ignore-next-line - */ - private static function resetInstance(): void + public static function getInstance(): TimingUtility { - self::$instance = null; + return GeneralUtility::makeInstance(TimingUtility::class); } /** @var StopWatch[] */ - private $order = []; + private array $order = []; + /** @var array */ - private $stopWatchStack = []; + private array $stopWatchStack = []; + + /** @return StopWatch[] */ + public function getStopWatches(): array + { + return $this->order; + } public static function start(string $key, string $info = ''): void { @@ -46,13 +54,15 @@ public static function start(string $key, string $info = ''): void public function startInternal(string $key, string $info = ''): void { - if (!$this->isActive()) { + if (!$this->shouldTrack()) { return; } + $stop = $this->stopWatchInternal($key, $info); if (isset($this->stopWatchStack[$key])) { - throw new \Exception('only one measurement at a time, use TimingUtility::stopWatch() for parallel measurements'); + throw new Exception('only one measurement at a time, use TimingUtility::stopWatch() for parallel measurements'); } + $this->stopWatchStack[$key] = $stop; } @@ -63,12 +73,14 @@ public static function end(string $key): void public function endInternal(string $key): void { - if (!$this->isActive()) { + if (!$this->shouldTrack()) { return; } + if (!isset($this->stopWatchStack[$key])) { - throw new \Exception('where is no measurement with this key'); + throw new Exception('where is no measurement with this key'); } + $stop = $this->stopWatchStack[$key]; $stop(); unset($this->stopWatchStack[$key]); @@ -83,40 +95,77 @@ public function stopWatchInternal(string $key, string $info = ''): StopWatch { $stopWatch = new StopWatch($key, $info); - if ($this->isActive()) { + if ($this->shouldTrack()) { if (!count($this->order)) { $phpStopWatch = new StopWatch('php', ''); $phpStopWatch->startTime = $_SERVER["REQUEST_TIME_FLOAT"]; $this->order[] = $phpStopWatch; } - $this->order[] = $stopWatch; - if (!self::$registered) { - register_shutdown_function(static function () { - self::getInstance()->shutdown(); - }); - self::$registered = true; + if (count($this->order) < $this->configService->stopWatchLimit()) { + $this->order[] = $stopWatch; + } + + if (!$this->registered) { + $this->registerShutdownFunction->register(fn(): ?ResponseInterface => $this->shutdown(ScriptResult::fromShutdown())); + $this->registered = true; } } return $stopWatch; } - private function shutdown(): void + public function shutdown(ScriptResult $result): ?ResponseInterface { - if (!$this->isActive()) { - return; + if (!$this->shouldTrack()) { + return $result->response; } + + $this->alreadyShutdown = true; + + + foreach (array_reverse($this->order) as $stopWatch) { + $stopWatch->stopIfNot(); + } + + GeneralUtility::makeInstance(SentryService::class)->sendSentryTrace($result, $this->order); + + if (!$this->shouldAddHeader()) { + return $result->response; + } + $timings = []; foreach ($this->combineIfToMuch($this->order) as $index => $time) { $timings[] = $this->timingString($index, trim($time->key . ' ' . $time->info), $time->getDuration()); } + if (count($timings) > 70) { $timings = [$this->timingString(0, 'To Many measurements ' . count($timings), 0.000001)]; } - if ($timings) { - header(sprintf('Server-Timing: %s', implode(',', $timings)), false); + + + $headerString = implode(',', $timings); + if (!$timings) { + return $result->response; } + + $memoryUsage = $this->humanReadableFileSize(memory_get_peak_usage()); + if ($result->response) { + return $result->response + ->withAddedHeader('Server-Timing', $headerString) + ->withAddedHeader('X-Max-Memory-Usage', $memoryUsage); + } + + header('Server-Timing: ' . $headerString, false); + header('X-Max-Memory-Usage: ' . $memoryUsage, false); + return null; + } + + private function humanReadableFileSize(int $size): string + { + $fileSizeNames = [" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"]; + $i = floor(log($size, 1024)); + return $size ? round($size / (1024 ** $i), 2) . $fileSizeNames[$i] : '0 Bytes'; } /** @@ -130,9 +179,11 @@ private function combineIfToMuch(array $initalStopWatches): array if (!isset($elementsByKey[$stopWatch->key])) { $elementsByKey[$stopWatch->key] = []; } + $elementsByKey[$stopWatch->key][] = $stopWatch; } - $keepStopWatches = new \SplObjectStorage(); + + $keepStopWatches = new SplObjectStorage(); $insertBefore = []; foreach ($elementsByKey as $key => $stopWatches) { @@ -141,14 +192,14 @@ private function combineIfToMuch(array $initalStopWatches): array foreach ($stopWatches as $stopWatch) { $keepStopWatches->attach($stopWatch); } + continue; } + $first = $stopWatches[0]; $sum = array_sum( array_map( - static function (StopWatch $stopWatch) { - return $stopWatch->getDuration(); - }, + static fn(StopWatch $stopWatch): float => $stopWatch->getDuration(), $stopWatches ) ); @@ -156,26 +207,28 @@ static function (StopWatch $stopWatch) { $insertBefore[$key]->startTime = $first->startTime; $insertBefore[$key]->stopTime = $insertBefore[$key]->startTime + $sum; - usort($stopWatches, static function (StopWatch $a, StopWatch $b) { - return $b->getDuration() <=> $a->getDuration(); - }); + usort($stopWatches, static fn(StopWatch $a, StopWatch $b): int => $b->getDuration() <=> $a->getDuration()); $biggestStopWatches = array_slice($stopWatches, 0, 3); foreach ($biggestStopWatches as $stopWatch) { $keepStopWatches->attach($stopWatch); } } + $result = []; foreach ($initalStopWatches as $stopWatch) { if (isset($insertBefore[$stopWatch->key])) { $result[] = $insertBefore[$stopWatch->key]; unset($insertBefore[$stopWatch->key]); } + if (!$keepStopWatches->contains($stopWatch)) { continue; } + $result[] = $stopWatch; } + return $result; } @@ -186,25 +239,26 @@ private function timingString(int $index, string $description, float $durationIn return sprintf('%03d;desc="%s";dur=%0.2f', $index, $description, $durationInSeconds * 1000); } - public function isActive(): bool + public function shouldAddHeader(): bool { - if (self::$isCli) { + if (self::IS_CLI) { return false; } - if (self::$isBackendUser === false && Environment::getContext()->isProduction()) { - return false; + + if ($this->isBackendUser()) { + return true; } - return true; + + return !Environment::getContext()->isProduction(); } - /** - * @internal - */ - public function checkBackendUserStatus(): void + public function shouldTrack(): bool { - self::$isBackendUser = (bool)GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('backend.user', 'isLoggedIn'); - if (!$this->isActive()) { - self::$instance = null; - } + return !$this->alreadyShutdown; + } + + private function isBackendUser(): bool + { + return (bool)GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('backend.user', 'isLoggedIn'); } } diff --git a/Classes/XClass/ExtbaseDispatcherV11.php b/Classes/XClass/ExtbaseDispatcher.php similarity index 72% rename from Classes/XClass/ExtbaseDispatcherV11.php rename to Classes/XClass/ExtbaseDispatcher.php index 5032107..ee5675c 100644 --- a/Classes/XClass/ExtbaseDispatcherV11.php +++ b/Classes/XClass/ExtbaseDispatcher.php @@ -6,19 +6,11 @@ use Kanti\ServerTiming\Utility\TimingUtility; use Psr\Http\Message\ResponseInterface; -use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Extbase\Mvc\Dispatcher; use TYPO3\CMS\Extbase\Mvc\Request; use TYPO3\CMS\Extbase\Mvc\RequestInterface; -use function class_alias; - -if (version_compare((new Typo3Version())->getBranch(), '11.0', '<')) { - class_alias(Dispatcher::class, ExtbaseDispatcherV11::class); - return; -} - -class ExtbaseDispatcherV11 extends Dispatcher +final class ExtbaseDispatcher extends Dispatcher { public function dispatch(RequestInterface $request): ResponseInterface { @@ -26,6 +18,7 @@ public function dispatch(RequestInterface $request): ResponseInterface if ($request instanceof Request) { $info .= '->' . $request->getControllerActionName(); } + $stop = TimingUtility::stopWatch('extbase', $info); $response = parent::dispatch($request); $stop(); diff --git a/Classes/XClass/ExtbaseDispatcherLegacy.php b/Classes/XClass/ExtbaseDispatcherLegacy.php deleted file mode 100644 index 5b2ac61..0000000 --- a/Classes/XClass/ExtbaseDispatcherLegacy.php +++ /dev/null @@ -1,33 +0,0 @@ -getBranch(), '11.0', '>=')) { - class_alias(Dispatcher::class, ExtbaseDispatcherLegacy::class); - return; -} - -class ExtbaseDispatcherLegacy extends Dispatcher -{ - public function dispatch(RequestInterface $request, ResponseInterface $response) - { - $info = str_replace('\\', '_', $request->getControllerObjectName()); - if ($request instanceof Request) { - $info .= '->' . $request->getControllerActionName(); - } - $stop = TimingUtility::stopWatch('extbase', $info); - parent::dispatch($request, $response); - $stop(); - } -} diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index a0568e5..b9506c0 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -1,16 +1,19 @@ [ 'server-timing/first' => [ - 'target' => \Kanti\ServerTiming\Middleware\FirstMiddleware::class, + 'target' => FirstMiddleware::class, 'before' => [ 'staticfilecache/fallback', 'typo3/cms-frontend/timetracker', ], ], 'server-timing/last' => [ - 'target' => \Kanti\ServerTiming\Middleware\LastMiddleware::class, + 'target' => LastMiddleware::class, 'after' => [ 'solr/service/pageexporter', 'typo3/cms-frontend/output-compression', @@ -23,14 +26,14 @@ ], 'backend' => [ 'server-timing/first' => [ - 'target' => \Kanti\ServerTiming\Middleware\FirstMiddleware::class, + 'target' => FirstMiddleware::class, 'before' => [ 'typo3/cms-core/normalized-params-attribute', 'typo3/cms-backend/locked-backend', ], ], 'server-timing/last' => [ - 'target' => \Kanti\ServerTiming\Middleware\LastMiddleware::class, + 'target' => LastMiddleware::class, 'after' => [ 'typo3/cms-frontend/output-compression', 'typo3/cms-backend/response-headers', diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..8f666ca --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,26 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: true + + Kanti\ServerTiming\: + resource: '../Classes/*' + # SqlLogging can be removed from exclude if only TYPO3 12 and above are required + exclude: '../Classes/{Dto,SqlLogging}/*' + + Kanti\ServerTiming\Service\RegisterShutdownFunction\RegisterShutdownFunctionInterface: + class: Kanti\ServerTiming\Service\RegisterShutdownFunction\RegisterShutdownFunction + + Kanti\ServerTiming\EventListener\ConsoleCommandEventListener: + tags: + - + name: event.listener + identifier: kanti/server-timing/console-command-event-listener + event: Symfony\Component\Console\Event\ConsoleCommandEvent + method: start + - + name: event.listener + identifier: kanti/server-timing/console-terminate-event-listener + event: Symfony\Component\Console\Event\ConsoleTerminateEvent + method: stop diff --git a/README.md b/README.md index a74abb8..ba25786 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ at the moment there is nothing to configure ````php - $stop = \Kanti\ServerTiming\Utility\TimingUtility::stopWatch('doSomething', 'additinal Information'); + $stop = \Kanti\ServerTiming\Utility\TimingUtility::stopWatch('doSomething', 'additional Information'); $result = $this->doSomethingExpensive(); $stop(); @@ -39,7 +39,7 @@ at the moment there is nothing to configure ````php - \Kanti\ServerTiming\Utility\TimingUtility::start('doSomething', 'additinal Information'); + \Kanti\ServerTiming\Utility\TimingUtility::start('doSomething', 'additional Information'); $result = $this->doSomethingExpensive(); \Kanti\ServerTiming\Utility\TimingUtility::end('doSomething'); diff --git a/Tests/TimingUtilityTest.php b/Tests/TimingUtilityTest.php index 01ef7be..5afb2c4 100644 --- a/Tests/TimingUtilityTest.php +++ b/Tests/TimingUtilityTest.php @@ -4,23 +4,43 @@ namespace Kanti\ServerTiming\Tests; +use Generator; +use Kanti\ServerTiming\Service\ConfigService; +use Kanti\ServerTiming\Service\RegisterShutdownFunction\RegisterShutdownFunctionNoop; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use Kanti\ServerTiming\Dto\StopWatch; use Kanti\ServerTiming\Utility\TimingUtility; use PHPUnit\Framework\TestCase; +use ReflectionClass; +use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * @coversDefaultClass \Kanti\ServerTiming\Utility\TimingUtility - */ -class TimingUtilityTest extends TestCase +#[CoversClass(TimingUtility::class)] +#[CoversClass(StopWatch::class)] +#[CoversClass(ConfigService::class)] +#[CoversClass(RegisterShutdownFunctionNoop::class)] +final class TimingUtilityTest extends TestCase { - /** - * @test - * @covers ::getInstance - * @covers ::stopWatch - * @covers ::stopWatchInternal - * @covers ::isActive - * @covers \Kanti\ServerTiming\Dto\StopWatch - */ + protected function setUp(): void + { + GeneralUtility::setSingletonInstance(TimingUtility::class, $this->getTestInstance()); + } + + protected function tearDown(): void + { + unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['server_timing']); + GeneralUtility::resetSingletonInstances([]); + } + + #[Test] + public function getInstance(): void + { + self::assertInstanceOf(TimingUtility::class, $firstCall = TimingUtility::getInstance()); + self::assertSame($firstCall, TimingUtility::getInstance()); + } + + #[Test] public function stopWatch(): void { $stopWatch = TimingUtility::stopWatch('testStopWatch'); @@ -34,26 +54,79 @@ public function stopWatch(): void self::assertLessThan(1, $stopWatch->getDuration()); } - /** - * @test - * @covers ::stopWatchInternal - * @covers ::isActive - * @covers \Kanti\ServerTiming\Dto\StopWatch - */ + #[Test] + public function stopWatchStop(): void + { + $stopWatch = TimingUtility::stopWatch('testStopWatch'); + self::assertNull($stopWatch->stopTime); + $stopWatch->stop(); + $stopTime = $stopWatch->stopTime; + self::assertIsFloat($stopTime); + self::assertIsFloat($stopWatch->getDuration()); + self::assertSame($stopTime, $stopWatch->stopTime); + self::assertGreaterThan(0.0, $stopWatch->getDuration()); + self::assertLessThan(1.0, $stopWatch->getDuration()); + } + + #[Test] + public function stopWatchStopIfNot(): void + { + $stopWatch = TimingUtility::stopWatch('testStopWatch'); + self::assertNull($stopWatch->stopTime); + $stopWatch->stopIfNot(); + self::assertGreaterThan(0.0, $stopWatch->stopTime); + $stopWatch->stopTime = 0.0; + $stopWatch->stopIfNot(); + self::assertSame(0.0, $stopWatch->stopTime); + } + + #[Test] public function stopWatchInternal(): void { - (new TimingUtility())->stopWatchInternal('test'); + TimingUtility::getInstance()->stopWatchInternal('test'); self::assertTrue(true, 'isCallable'); } - /** - * @test - * @covers ::getInstance - * @covers ::stopWatch - * @covers ::stopWatchInternal - * @covers ::isActive - * @covers \Kanti\ServerTiming\Dto\StopWatch - */ + #[Test] + public function stopWatchFirstIsAlwaysPhp(): void + { + $timingUtility = $this->getTestInstance(); + $timingUtility->stopWatchInternal('test'); + + $watches = $timingUtility->getStopWatches(); + self::assertCount(2, $watches); + self::assertSame('php', $watches[0]->key); + self::assertSame('test', $watches[1]->key); + } + + #[Test] + public function stopWatchLimit(): void + { + $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['server_timing']['stop_watch_limit'] = 3; + $timingUtility = $this->getTestInstance(); + $timingUtility->stopWatchInternal('test'); + $timingUtility->stopWatchInternal('test'); + $timingUtility->stopWatchInternal('test'); + + $watches = $timingUtility->getStopWatches(); + self::assertCount(3, $watches); + self::assertSame('php', $watches[0]->key); + self::assertSame('test', $watches[1]->key); + self::assertSame('test', $watches[2]->key); + } + + #[Test] + public function didRegisterShutdownFunctionOnce(): void + { + $timingUtility = new TimingUtility($registerShutdownFunction = new RegisterShutdownFunctionNoop(), new ConfigService()); + $timingUtility->stopWatchInternal('test'); + $timingUtility->stopWatchInternal('test'); + $timingUtility->stopWatchInternal('test'); + $timingUtility->stopWatchInternal('test'); + self::assertSame(1, $registerShutdownFunction->callCount); + } + + #[Test] public function stopWatchGetDuration(): void { $stopWatch = TimingUtility::stopWatch('testStopWatch'); @@ -65,22 +138,20 @@ public function stopWatchGetDuration(): void } /** - * @test - * @covers ::timingString - * @dataProvider dataProviderTimingString - * * @param array{0:int, 1:string, 2:float} $args */ + #[Test] + #[DataProvider('dataProviderTimingString')] public function timingString(string $expected, array $args): void { - $reflection = new \ReflectionClass(TimingUtility::class); + $reflection = new ReflectionClass(TimingUtility::class); $reflectionMethod = $reflection->getMethod('timingString'); - $reflectionMethod->setAccessible(true); - $result = $reflectionMethod->invoke(new TimingUtility(), ...$args); - self::assertEquals($expected, $result); + + $result = $reflectionMethod->invoke($this->getTestInstance(), ...$args); + self::assertSame($expected, $result); } - public function dataProviderTimingString(): \Generator + public static function dataProviderTimingString(): Generator { yield 'simple' => [ '000;desc="key";dur=12210.00', @@ -101,27 +172,22 @@ public function dataProviderTimingString(): \Generator } /** - * @test - * @covers ::combineIfToMuch - * @covers \Kanti\ServerTiming\Dto\StopWatch - * @dataProvider dataProviderCombineIfToMuch - * * @param StopWatch[] $expected * @param StopWatch[] $initalStopWatches */ + #[Test] + #[DataProvider('dataProviderCombineIfToMuch')] public function combineIfToMuch(array $expected, array $initalStopWatches): void { - $reflection = new \ReflectionClass(TimingUtility::class); - $initalStopWatches = array_map(static function (StopWatch $el) { - return clone $el; - }, $initalStopWatches); + $reflection = new ReflectionClass(TimingUtility::class); + $initalStopWatches = array_map(static fn(StopWatch $el): StopWatch => clone $el, $initalStopWatches); $reflectionMethod = $reflection->getMethod('combineIfToMuch'); - $reflectionMethod->setAccessible(true); - $result = $reflectionMethod->invoke(new TimingUtility(), $initalStopWatches); + + $result = $reflectionMethod->invoke($this->getTestInstance(), $initalStopWatches); self::assertEqualsWithDelta($expected, $result, 0.00001); } - public function dataProviderCombineIfToMuch(): \Generator + public static function dataProviderCombineIfToMuch(): Generator { $stopWatchX = new StopWatch('x', 'info'); $stopWatchX->startTime = 100001.0001; @@ -187,52 +253,23 @@ public function dataProviderCombineIfToMuch(): \Generator ]; } - /** - * @test - * @covers ::isActive - */ - public function isActive(): void + #[Test] + public function shouldTrack(): void { - $reflection = new \ReflectionClass(TimingUtility::class); - $isCli = $reflection->getProperty('isCli'); - $isCli->setAccessible(true); - - $isBackendUser = $reflection->getProperty('isBackendUser'); - $isBackendUser->setAccessible(true); + $reflection = new ReflectionClass(TimingUtility::class); + $isAlreadyShutdown = $reflection->getProperty('alreadyShutdown'); - $timingUtility = new TimingUtility(); + $timingUtility = $this->getTestInstance(); - $isCli->setValue(false); - $isBackendUser->setValue(true); - self::assertTrue($timingUtility->isActive()); + $isAlreadyShutdown->setValue($timingUtility, false); + self::assertTrue($timingUtility->shouldTrack()); - $isCli->setValue(true); - $isBackendUser->setValue(true); - self::assertFalse($timingUtility->isActive()); - - $isCli->setValue(true); - $isBackendUser->setValue(false); - self::assertFalse($timingUtility->isActive()); - - $isCli->setValue(true); - $isBackendUser->setValue(null); - self::assertFalse($timingUtility->isActive()); + $isAlreadyShutdown->setValue($timingUtility, true); + self::assertFalse($timingUtility->shouldTrack()); } - /** - * @test - * @covers ::getInstance - * @covers ::resetInstance - */ - public function getInstance(): void + private function getTestInstance(): TimingUtility { - $firstInstance = TimingUtility::getInstance(); - self::assertSame($firstInstance, TimingUtility::getInstance()); - - $reflection = new \ReflectionClass(TimingUtility::class); - $resetInstance = $reflection->getMethod('resetInstance'); - $resetInstance->setAccessible(true); - $resetInstance->invoke(null); - self::assertNotSame($firstInstance, TimingUtility::getInstance()); + return new TimingUtility(new RegisterShutdownFunctionNoop(), new ConfigService()); } } diff --git a/composer.json b/composer.json index 160c49f..b473349 100644 --- a/composer.json +++ b/composer.json @@ -1,63 +1,66 @@ { - "name" : "kanti/server-timing", - "description" : "Show timings of Database and HTTP Calls", - "type" : "typo3-cms-extension", - "minimum-stability" : "stable", - "license" : "GPL-2.0-or-later", - "authors" : [ + "name": "kanti/server-timing", + "description": "Show timings of Database and HTTP Calls (send them to Sentry)", + "license": "GPL-2.0-or-later", + "type": "typo3-cms-extension", + "authors": [ { - "name" : "Matthias Vogel", - "email" : "git@kanti.de" + "name": "Matthias Vogel", + "email": "git@kanti.de" } ], - "autoload" : { - "psr-4" : { - "Kanti\\ServerTiming\\" : "Classes/" - } + "require": { + "php": "~8.1.0 || ~8.2.0", + "composer-runtime-api": "^2.0.0", + "typo3/cms-core": "^11.0 || ^12.0", + "typo3/cms-extbase": "^11.0 || ^12.0" + }, + "require-dev": { + "andersundsehr/resource-watcher": "dev-master", + "infection/infection": "^0.26.13", + "phpstan/extension-installer": "^1.1", + "phpunit/phpunit": "^10", + "pluswerk/grumphp-config": "^6.8.0", + "saschaegerer/phpstan-typo3": "^1.1", + "sentry/sdk": "^3.5", + "spatie/phpunit-watcher": "^1.23", + "ssch/typo3-rector": "^1.1.3", + "typo3/cms-adminpanel": "^11.0 || ^12.0" }, - "autoload-dev" : { - "psr-4" : { - "Kanti\\ServerTiming\\Tests\\" : "Tests/" + "minimum-stability": "stable", + "autoload": { + "psr-4": { + "Kanti\\ServerTiming\\": "Classes/" } }, - "extra" : { - "typo3/cms" : { - "extension-key" : "server_timing" - }, - "pluswerk/grumphp-config" : { - "auto-setting": false - }, - "grumphp" : { - "config-default-path" : "grumphp.yml" + "autoload-dev": { + "psr-4": { + "Kanti\\ServerTiming\\Tests\\": "Tests/" } }, - "config" : { - "allow-plugins" : { + "config": { + "allow-plugins": { "typo3/class-alias-loader": true, "typo3/cms-composer-installers": true, "phpro/grumphp": true, "pluswerk/grumphp-config": true, "infection/extension-installer": true, - "phpstan/extension-installer": true + "phpstan/extension-installer": true, + "ergebnis/composer-normalize": true, + "php-http/discovery": false } }, - "scripts" : { - "test" : "@php -d pcov.enabled=1 ./vendor/bin/phpunit", - "infection" : "infection --only-covered" - }, - "require" : { - "php" : "~7.4 || ~8.0 || ~8.1 || ~8.2", - "typo3/cms-core" : "10.* || 11.* || 12.*", - "typo3/cms-extbase" : "10.* || 11.* || 12.*", - "ocramius/package-versions": "^2.1", - "composer-runtime-api": "^2.0.0" + "extra": { + "typo3/cms": { + "extension-key": "server_timing" + } }, - "require-dev" : { - "pluswerk/grumphp-config" : "^4.0 || ^5.0", - "typo3/cms-adminpanel" : "10.* || 11.* || 12.*", - "phpunit/phpunit" : "^9.5", - "infection/infection" : "^0.18.2 || ^0.26.13", - "saschaegerer/phpstan-typo3": "^1.1", - "phpstan/extension-installer": "^1.1" + "scripts": { + "infection": "infection --only-covered", + "test": "@php -d pcov.enabled=1 ./vendor/bin/phpunit --display-warnings", + "test:watch": [ + "Composer\\Config::disableProcessTimeout", + "@php -d pcov.enabled=1 ./vendor/bin/phpunit-watcher watch < /dev/tty" + ] } } diff --git a/ext_conf_template.txt b/ext_conf_template.txt new file mode 100644 index 0000000..31d4735 --- /dev/null +++ b/ext_conf_template.txt @@ -0,0 +1,8 @@ +# cat=sentry; type=integer; label=StopWatch Limit +stop_watch_limit = 100000 + +# cat=sentry; type=string; label=Sentry Sample Rate between 0.0 and 1.0 (empty: keep default) +sentry_sample_rate = + +# cat=sentry; type=string; label=Sentry CLI Sample Rate between 0.0 and 1.0 (empty: keep default) +sentry_cli_sample_rate = diff --git a/ext_emconf.php b/ext_emconf.php index 5224466..9cef13e 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,5 +1,7 @@ 'Kanti: server-timing', @@ -12,10 +14,10 @@ 'uploadfolder' => '0', 'createDirs' => '', 'clearCacheOnLoad' => 0, - 'version' => \Composer\InstalledVersions::getPrettyVersion('kanti/server-timing'), + 'version' => InstalledVersions::getPrettyVersion('kanti/server-timing'), 'constraints' => [ 'depends' => [ - 'typo3' => '10.0.0-12.1.999', + 'typo3' => '11.0.0-12.1.999', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index 169e992..880f7b6 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,26 +1,21 @@ getBranch(), '11.0', '>=')) { - $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][Dispatcher::class] = [ - 'className' => ExtbaseDispatcherV11::class, - ]; -} else { - $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][Dispatcher::class] = [ - 'className' => ExtbaseDispatcherLegacy::class, - ]; -} +// can be used instead after TYPO3 support is set to >=12 +//if (version_compare((new Typo3Version())->getBranch(), '12.3', '>=')) { +// $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverMiddlewares']['server_timing_logging'] = LoggingMiddleware::class; +//} else { $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][SqlLogging::class] = [ 'className' => AdminpanelSqlLoggingMiddleware::class, ]; +//} -$GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler']['server_timing'] = GuzzleUtility::getHandler(); +$handler = GuzzleUtility::getHandler(); +if ($handler) { + $GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler']['server_timing'] = $handler; +} diff --git a/grumphp.yml b/grumphp.yml index c49711f..9ad8ce8 100644 --- a/grumphp.yml +++ b/grumphp.yml @@ -1,7 +1,16 @@ imports: - { resource: vendor/pluswerk/grumphp-config/grumphp.yml } - - parameters: - convention.phpstan_level: ~ + convention.phpstan_level: null convention.security_checker_blocking: false + convention.process_timeout: 240 + convention.jsonlint_ignore_pattern: { } + convention.xmllint_ignore_pattern: { } + convention.yamllint_ignore_pattern: { } + convention.phpcslint_ignore_pattern: { } + convention.phpcslint_exclude: { } + convention.xlifflint_ignore_pattern: { } + convention.rector_ignore_pattern: { } + convention.rector_enabled: true + convention.rector_config: rector.php + convention.rector_clear-cache: false diff --git a/infection.json b/infection.json index ab68748..b15a1fc 100644 --- a/infection.json +++ b/infection.json @@ -9,10 +9,10 @@ "configDir": "." }, "logs": { - "text": "var/infection.log", - "html": "var/infection.html" + "text": "Resources/Public/test-result/infection.log", + "html": "Resources/Public/test-result/infection.html" }, - "minCoveredMsi": 88, + "minCoveredMsi": 99, "initialTestsPhpOptions": "-d pcov.enabled=1", "mutators": { "@default": true diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..96b782d --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$loggers of class Doctrine\\\\DBAL\\\\Logging\\\\LoggerChain constructor expects iterable\\, array\\ given\\.$#" + count: 1 + path: Classes/Middleware/AdminpanelSqlLoggingMiddleware.php diff --git a/phpstan.neon b/phpstan.neon index bdeda83..b004e3d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,11 +1,10 @@ +includes: + - phpstan-baseline.neon + - vendor/andersundsehr/phpstan-git-files/extension.php + parameters: - level: max - paths: - - Classes/ - - Tests/ - excludePaths: - - Classes/XClass/ExtbaseDispatcherLegacy.php - - Classes/XClass/ExtbaseDispatcherV11.php + level: 8 + reportUnmatchedIgnoredErrors: false typo3: requestGetAttributeMapping: server-timing:middleware:inward: Kanti\ServerTiming\Dto\StopWatch|null diff --git a/phpunit.xml b/phpunit.xml index 8741629..72c176a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,31 +1,28 @@ + cacheDirectory="var/.phpunit.cache" + requireCoverageMetadata="true" + beStrictAboutCoverageMetadata="true"> Tests - - - - Classes - + - - + + + + + Classes + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..969ebaa --- /dev/null +++ b/rector.php @@ -0,0 +1,51 @@ +parallel(); + $rectorConfig->importNames(); + $rectorConfig->importShortClasses(); + $rectorConfig->cacheClass(FileCacheStorage::class); + $rectorConfig->cacheDirectory('./var/cache/rector'); + + $rectorConfig->paths( + array_filter(explode("\n", (string)shell_exec("git ls-files | xargs ls -d 2>/dev/null | grep -E '\.(php|html|typoscript)$'"))) + ); + + // define sets of rules + $rectorConfig->sets( + [ + ...RectorSettings::sets(true), + ...RectorSettings::setsTypo3(false), + PHPUnitLevelSetList::UP_TO_PHPUNIT_100, + PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES, + PHPUnitSetList::PHPUNIT_CODE_QUALITY, + PHPUnitSetList::PHPUNIT_YIELD_DATA_PROVIDER, + PHPUnitSetList::PHPUNIT_EXCEPTION, + PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, + PHPUnitSetList::REMOVE_MOCKS, + ] + ); + + // remove some rules + // ignore some files + $rectorConfig->skip( + [ + ...RectorSettings::skip(), + ...RectorSettings::skipTypo3(), + + /** + * rector should not touch these files + */ + //__DIR__ . '/src/Example', + //__DIR__ . '/src/Example.php', + ] + ); +};