From 8144b13ef8149b9bfedefce4b2ff1d433e336058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Guti=C3=A9rrez?= Date: Wed, 27 Sep 2023 22:31:16 +0200 Subject: [PATCH 1/5] drop php 7.4, use whoops fram filtering, improve templates, improve types --- .github/workflows/code-analysis.yml | 5 +- .github/workflows/unit-tests.yml | 2 +- Makefile | 7 +- README.md | 2 +- composer.json | 30 ++-- ecs.php | 10 +- phpstan.neon.dist | 5 +- phpunit.xml.dist | 2 +- src/ExceptionHandler.php | 48 +++--- src/Handler/ErrorHandler.php | 18 +-- src/Renderer/AbstractRenderer.php | 13 +- src/Renderer/HtmlRenderer.php | 2 +- src/Renderer/JsonRenderer.php | 4 +- src/Renderer/PlainTextRenderer.php | 7 +- src/Renderer/XmlRenderer.php | 2 +- src/Whoops/Handler/ErrorHandler.php | 29 ++-- src/Whoops/Inspector.php | 64 -------- src/Whoops/Renderer/HtmlRenderer.php | 19 +-- src/Whoops/Renderer/JsonRenderer.php | 55 ++++--- src/Whoops/Renderer/PlainTextRenderer.php | 56 +++++-- src/Whoops/Renderer/RendererTrait.php | 53 +++--- src/Whoops/Renderer/XmlRenderer.php | 27 ++-- tests/Exception/ExceptionHandlerTest.php | 2 - tests/Exception/Handler/ErrorHandlerTest.php | 14 +- tests/Exception/Renderer/HtmlRendererTest.php | 151 ++++++++++++++---- tests/Exception/Renderer/JsonRendererTest.php | 24 +-- .../Renderer/PlainTextRendererTest.php | 33 ++-- tests/Exception/Renderer/XmlRendererTest.php | 73 +++++---- tests/Exception/Stubs/ErrorHandlerStub.php | 3 + .../Exception/Stubs/ExceptionHandlerStub.php | 21 +-- tests/Exception/Stubs/InspectorStub.php | 3 + tests/Exception/Stubs/RendererStub.php | 11 +- .../Stubs/WhoopsErrorHandlerStub.php | 3 + tests/Exception/Whoops/InspectorTest.php | 52 ------ .../Whoops/Renderer/JsonRendererTest.php | 54 +++++-- .../Whoops/Renderer/PlainTextRendererTest.php | 48 +++--- .../Whoops/Renderer/XmlRendererTest.php | 72 ++++++--- 37 files changed, 519 insertions(+), 505 deletions(-) delete mode 100644 src/Whoops/Inspector.php delete mode 100644 tests/Exception/Whoops/InspectorTest.php diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml index 782853b..b1d01ad 100644 --- a/.github/workflows/code-analysis.yml +++ b/.github/workflows/code-analysis.yml @@ -18,7 +18,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.2 coverage: none - name: Composer install @@ -66,9 +66,6 @@ jobs: - name: Run check style run: make lint-ecs - - name: Run copy/paste detection - run: make qa-phpcpd - - name: Run mess detection run: make qa-phpmd diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 4ab476d..f909e2f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -18,7 +18,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.2 coverage: none - name: Composer install diff --git a/Makefile b/Makefile index b11660a..43d2471 100644 --- a/Makefile +++ b/Makefile @@ -24,10 +24,6 @@ fix: make --no-print-directory fix-ecs -.PHONY: qa-phpcpd -qa-phpcpd: - vendor/bin/phpcpd src tests - .PHONY: qa-phpmd qa-phpmd: vendor/bin/phpmd src,tests ansi unusedcode,naming,design,controversial,codesize @@ -38,7 +34,7 @@ qa-phpmnd: .PHONY: qa-compatibility qa-compatibility: - vendor/bin/phpcs --standard=PHPCompatibility --runtime-set testVersion 8.1- src tests + vendor/bin/phpcs --standard=PHPCompatibility --runtime-set testVersion 8.2- src tests .PHONY: qa-phpstan qa-phpstan: @@ -46,7 +42,6 @@ qa-phpstan: .PHONY: qa qa: - make --no-print-directory qa-phpcpd && \ make --no-print-directory qa-phpmd && \ make --no-print-directory qa-phpmnd && \ make --no-print-directory qa-compatibility && \ diff --git a/README.md b/README.md index 5c688ea..ee49d0f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![PHP version](https://img.shields.io/badge/PHP-%3E%3D7.4-8892BF.svg?style=flat-square)](http://php.net) +[![PHP version](https://img.shields.io/badge/PHP-%3E%3D8.1-8892BF.svg?style=flat-square)](http://php.net) [![Latest Version](https://img.shields.io/packagist/v/juliangut/slim-exception.svg?style=flat-square)](https://packagist.org/packages/juliangut/slim-exception) [![License](https://img.shields.io/github/license/juliangut/slim-exception.svg?style=flat-square)](https://github.com/juliangut/slim-exception/blob/master/LICENSE) diff --git a/composer.json b/composer.json index 18e9dde..d7d9e21 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": "^7.4|^8.0", + "php": "^8.0", "ext-mbstring": "*", "slim/slim": "^4.11", "willdurand/negotiation": "^3.0" @@ -32,23 +32,18 @@ "ext-dom": "*", "ext-json": "*", "ext-simplexml": "*", - "grifart/phpstan-oneline": "~0.4", "filp/whoops": "^2.15", - "infection/infection": "~0.24", - "juliangut/easy-coding-standard-config": "^1.0", - "laminas/laminas-diactoros": "^2.2", - "overtrue/phplint": "^3.0|^4.0", + "infection/infection": "~0.25|~0.27", + "juliangut/easy-coding-standard-config": "dev-master", + "juliangut/phpstan-config": "^1.1", + "laminas/laminas-diactoros": "^2.20", + "overtrue/phplint": "^4.0|^5.0|^6.0", "phpcompatibility/php-compatibility": "^9.3", - "phpmd/phpmd": "^2.10", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "povils/phpmnd": "^2.5", - "roave/security-advisories": "dev-master", - "sebastian/phpcpd": "^6.0", - "thecodingmachine/phpstan-strict-rules": "^1.0" + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6.13|^10.3", + "povils/phpmnd": "^3.2", + "roave/security-advisories": "dev-master" }, "suggest": { "filp/whoops": "Enhance development error reporting and production stack-traces", @@ -69,8 +64,7 @@ "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "infection/extension-installer": true, - "phpstan/extension-installer": true + "infection/extension-installer": true } } } diff --git a/ecs.php b/ecs.php index 8945774..66d7062 100644 --- a/ecs.php +++ b/ecs.php @@ -11,7 +11,7 @@ declare(strict_types=1); -use Jgut\ECS\Config\ConfigSet74; +use Jgut\ECS\Config\ConfigSet80; use SlevomatCodingStandard\Sniffs\Exceptions\ReferenceThrowableOnlySniff; use Symplify\EasyCodingStandard\Config\ECSConfig; @@ -31,15 +31,11 @@ __DIR__ . '/tests', ]); - $configSet = new ConfigSet74(); - - $configSet + (new ConfigSet80()) ->setHeader($header) ->enablePhpUnitRules() ->setAdditionalSkips([ - ReferenceThrowableOnlySniff::class . '.ReferencedGeneralException' => [ - __DIR__ . '/src/ExceptionHandler.php', - ], + ReferenceThrowableOnlySniff::class . '.ReferencedGeneralException' => __DIR__ . '/src/ExceptionHandler.php', ]) ->configure($ecsConfig); }; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 701bea7..d09a38f 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,17 +1,16 @@ includes: - - vendor/phpstan/phpstan/conf/bleedingEdge.neon + - %rootDir%/../../juliangut/phpstan-config/phpstan-phpunit.neon parameters: level: max checkMissingCallableSignature: true - tipsOfTheDay: false - errorFormat: compact parallel: maximumNumberOfProcesses: 7 paths: - src typeAliases: TraceLine: 'array{file: ?string, line: ?int, function: ?string, class: ?string, args: array}' + ExceptionData: 'array{type: class-string, message: string, code: int, file: string, line: int, trace?: array}' ignoreErrors: - message: '/^Call to function is_subclass_of\(\) with Whoops\\Handler\\HandlerInterface and .Jgut\\\\Slim\\\\Exception\\\\Whoops\\\\Renderer\\\\HtmlRenderer. will always evaluate to false\.$/' path: src/Whoops/Handler/ErrorHandler.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 19aa97e..827f5ed 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ request = $request; - $this->errorHandler = $errorHandler; - $this->displayErrorDetails = $displayErrorDetails; - $this->logErrors = $logErrors; - $this->logErrorDetails = $logErrorDetails; - } + protected ServerRequestInterface $request, + protected ErrorHandlerInterface $errorHandler, + protected bool $displayErrorDetails, + protected bool $logErrors, + protected bool $logErrorDetails, + ) {} /** * Register exception handling. @@ -110,7 +93,7 @@ public function handleShutdown(): void // @codeCoverageIgnoreStart if (!\defined('PHPUNIT_TEST')) { - exit; + exit; // @phpstan-ignore-line } // @codeCoverageIgnoreEnd } @@ -150,9 +133,16 @@ private function getFatalException(array $error): HttpException $trace = $this->getBackTrace(); if (\count($trace) !== 0) { - $reflection = new ReflectionProperty(Exception::class, 'trace'); - $reflection->setAccessible(true); - $reflection->setValue($exception, $trace); + $reflectionClass = new ReflectionClass($exception); + while ($reflectionClass->getParentClass() !== false) { + $reflectionClass = $reflectionClass->getParentClass(); + } + + if ($reflectionClass->hasProperty('trace')) { + $reflectionProperty = $reflectionClass->getProperty('trace'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($exception, $trace); + } } return $exception; @@ -171,7 +161,7 @@ private function getBackTrace(): array fn(array $frame): array => $this->normalizeFrame($frame), xdebug_get_function_stack(), ); - } catch (ErrorException $exception) { + } catch (ErrorException) { // @ignoreException } diff --git a/src/Handler/ErrorHandler.php b/src/Handler/ErrorHandler.php index f241bde..b9f9bdb 100644 --- a/src/Handler/ErrorHandler.php +++ b/src/Handler/ErrorHandler.php @@ -61,8 +61,6 @@ class ErrorHandler extends SlimErrorHandler \E_USER_DEPRECATED => LogLevel::WARNING, ]; - protected Negotiator $negotiator; - /** * @var ErrorRendererInterface|string|callable(Throwable, bool): string */ @@ -88,16 +86,14 @@ class ErrorHandler extends SlimErrorHandler public function __construct( CallableResolverInterface $callableResolver, ResponseFactoryInterface $responseFactory, - Negotiator $negotiator, - ?LoggerInterface $logger = null + protected Negotiator $negotiator, + ?LoggerInterface $logger = null, ) { - $this->negotiator = $negotiator; - parent::__construct($callableResolver, $responseFactory, $logger); } /** - * @param array $errorRenderers + * @param array> $errorRenderers */ public function setErrorRenderers(array $errorRenderers): void { @@ -109,9 +105,9 @@ public function setErrorRenderers(array $errorRenderers): void } /** - * @param string|ErrorRendererInterface $errorRenderer + * @param ErrorRendererInterface|class-string $errorRenderer */ - public function setErrorRenderer(string $contentType, $errorRenderer): void + public function setErrorRenderer(string $contentType, ErrorRendererInterface|string $errorRenderer): void { $this->errorRenderers[$contentType] = $errorRenderer; } @@ -133,12 +129,12 @@ protected function determineContentType(ServerRequestInterface $request): ?strin if ($selected instanceof BaseAccept) { $contentType = $selected->getType(); } - } catch (NegotiateException $exception) { + } catch (NegotiateException) { // @ignoreException } } - if (mb_strpos($contentType, '/*+') !== false) { + if (str_contains($contentType, '/*+')) { $contentType = str_replace('/*+', '/', $contentType); } diff --git a/src/Renderer/AbstractRenderer.php b/src/Renderer/AbstractRenderer.php index 9bca97c..07b4f83 100644 --- a/src/Renderer/AbstractRenderer.php +++ b/src/Renderer/AbstractRenderer.php @@ -19,17 +19,10 @@ abstract class AbstractRenderer implements ErrorRendererInterface { - protected string $defaultTitle; - - protected string $defaultDescription; - public function __construct( - string $defaultTitle = 'Slim Application error', - string $defaultDescription = 'A website error has occurred. Sorry for the temporary inconvenience.' - ) { - $this->defaultTitle = $defaultTitle; - $this->defaultDescription = $defaultDescription; - } + protected string $defaultTitle = 'Slim Application error', + protected string $defaultDescription = 'An error has occurred. Sorry for the temporary inconvenience.', + ) {} protected function getErrorTitle(Throwable $exception): string { diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index 6d3930c..e9fe024 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -68,7 +68,7 @@ private function formatException(Throwable $exception): string return sprintf( $outputString, - \get_class($exception), + $exception::class, $exception->getCode(), htmlentities($exception->getMessage()), $exception->getFile(), diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index df87600..fe750a4 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -40,8 +40,6 @@ public function setPrettify(bool $prettify): void } /** - * @inheritDoc - * * @throws RuntimeException */ public function __invoke(Throwable $exception, bool $displayErrorDetails): string @@ -89,7 +87,7 @@ protected function getJsonFlags(): int private function formatException(Throwable $exception): array { return [ - 'type' => \get_class($exception), + 'type' => $exception::class, 'code' => $exception->getCode(), 'message' => $exception->getMessage(), 'file' => $exception->getFile(), diff --git a/src/Renderer/PlainTextRenderer.php b/src/Renderer/PlainTextRenderer.php index 7a73945..d08122d 100644 --- a/src/Renderer/PlainTextRenderer.php +++ b/src/Renderer/PlainTextRenderer.php @@ -22,7 +22,7 @@ public function __invoke(Throwable $exception, bool $displayErrorDetails): strin $output = $this->getErrorTitle($exception); if ($displayErrorDetails) { - $output .= $this->formatException($exception); + $output .= "\n" . $this->formatException($exception); while ($exception = $exception->getPrevious()) { $output .= "\nPrevious Error:\n"; @@ -41,12 +41,13 @@ private function formatException(Throwable $exception): string Message: %s File: %s Line: %s - Trace: %s + Trace: + %s OUTPUT; return sprintf( $outputString, - \get_class($exception), + $exception::class, $exception->getCode(), $exception->getMessage(), $exception->getFile(), diff --git a/src/Renderer/XmlRenderer.php b/src/Renderer/XmlRenderer.php index e6fb0f1..23003bc 100644 --- a/src/Renderer/XmlRenderer.php +++ b/src/Renderer/XmlRenderer.php @@ -36,7 +36,7 @@ public function __invoke(Throwable $exception, bool $displayErrorDetails): strin if ($displayErrorDetails) { do { $errorParts[] = ' '; - $errorParts[] = ' ' . \get_class($exception) . ''; + $errorParts[] = ' ' . $exception::class . ''; $errorParts[] = ' ' . $exception->getCode() . ''; $errorParts[] = ' ' . $this->createCdataSection($exception->getMessage()) . ''; $errorParts[] = ' ' . $exception->getFile() . ''; diff --git a/src/Whoops/Handler/ErrorHandler.php b/src/Whoops/Handler/ErrorHandler.php index 2f7cabc..bc4080d 100644 --- a/src/Whoops/Handler/ErrorHandler.php +++ b/src/Whoops/Handler/ErrorHandler.php @@ -27,10 +27,14 @@ use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\ErrorRendererInterface; use Throwable; +use Whoops\Exception\Frame; use Whoops\Handler\HandlerInterface; use Whoops\Handler\HandlerInterface as WhoopsHandler; use Whoops\Run as Whoops; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ErrorHandler extends BaseErrorHandler { protected const REQUEST_DATA_TABLE_LABEL = 'Slim Application (Request)'; @@ -69,21 +73,24 @@ public function __construct( ResponseFactoryInterface $responseFactory, Negotiator $negotiator, Whoops $whoops, - ?LoggerInterface $logger = null + ?LoggerInterface $logger = null, ) { parent::__construct($callableResolver, $responseFactory, $negotiator, $logger); - $whoops = clone $whoops; - $whoops->clearHandlers(); - $whoops->allowQuit(false); - $whoops->writeToOutput(false); + $clonedWhoops = clone $whoops; + $clonedWhoops->clearHandlers(); + $clonedWhoops->allowQuit(false); + $clonedWhoops->writeToOutput(false); + + $excludedPathRegex = sprintf('!^%s/.+\.php$!', \dirname(__DIR__, 3)); + $clonedWhoops->addFrameFilter( + static fn(Frame $frame): bool => preg_match($excludedPathRegex, $frame->getFile() ?? '') !== 1, + ); - $this->whoops = $whoops; + $this->whoops = $clonedWhoops; } /** - * {@inheritDoc} - * * @throws InvalidArgumentException * @throws RuntimeException * @@ -95,8 +102,6 @@ protected function determineRenderer(): callable } /** - * {@inheritDoc} - * * @throws InvalidArgumentException * @throws RuntimeException * @@ -116,12 +121,12 @@ protected function determineLogRenderer(): callable * * @return callable(Throwable): string */ - protected function getRenderer($renderer): callable + protected function getRenderer(mixed $renderer): callable { if (!$renderer instanceof WhoopsHandler) { throw new InvalidArgumentException(sprintf( 'Renderer "%s" for Whoops error handler should implement "%s".', - \is_object($renderer) ? \get_class($renderer) : \gettype($renderer), + \is_object($renderer) ? $renderer::class : \gettype($renderer), WhoopsHandler::class, )); } diff --git a/src/Whoops/Inspector.php b/src/Whoops/Inspector.php deleted file mode 100644 index 26e3565..0000000 --- a/src/Whoops/Inspector.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Jgut\Slim\Exception\Whoops; - -use Whoops\Exception\Frame; -use Whoops\Exception\FrameCollection; -use Whoops\Exception\Inspector as BaseInspector; - -class Inspector extends BaseInspector -{ - /** - * Get stack trace frames. - * - * Exception handling frames are removed. - * - * @return FrameCollection - */ - public function getTraceFrames(): FrameCollection - { - $frames = new FrameCollection([]); - $frames->prependFrames($this->filterTraceFrames()); - - return $frames; - } - - /** - * Filter stack frame list. - * - * Remove internal frames. - * - * @return array - */ - protected function filterTraceFrames(): array - { - /** @var array $frames */ - $frames = $this->getFrames() - ->getArray(); - - $excludedPathRegex = sprintf('!^%s/.+\.php$!', \dirname(__DIR__, 2)); - - $firstFrame = 0; - for ($i = 0, $length = \count($frames); $i < $length; ++$i) { - if (preg_match($excludedPathRegex, $frames[$i]->getFile() ?? '') === 1) { - continue; - } - - $firstFrame = $i; - break; - } - - return array_values(\array_slice($frames, $firstFrame)); - } -} diff --git a/src/Whoops/Renderer/HtmlRenderer.php b/src/Whoops/Renderer/HtmlRenderer.php index a71ba35..f86c318 100644 --- a/src/Whoops/Renderer/HtmlRenderer.php +++ b/src/Whoops/Renderer/HtmlRenderer.php @@ -13,8 +13,6 @@ namespace Jgut\Slim\Exception\Whoops\Renderer; -use Jgut\Slim\Exception\Whoops\Inspector; -use Whoops\Handler\Handler; use Whoops\Handler\PrettyPageHandler; /** @@ -24,22 +22,11 @@ class HtmlRenderer extends PrettyPageHandler { use RendererTrait; - public function __construct(string $defaultTitle = 'Slim Application error') - { + public function __construct( + protected string $defaultTitle = 'Slim Application error', + ) { parent::__construct(); - $this->defaultTitle = $defaultTitle; $this->setPageTitle($defaultTitle); } - - /** - * @inheritDoc - */ - public function handle() - { - $exception = $this->getException(); - $this->setInspector(new Inspector($exception)); - - return parent::handle() ?? Handler::QUIT; - } } diff --git a/src/Whoops/Renderer/JsonRenderer.php b/src/Whoops/Renderer/JsonRenderer.php index 2e93868..0734251 100644 --- a/src/Whoops/Renderer/JsonRenderer.php +++ b/src/Whoops/Renderer/JsonRenderer.php @@ -13,13 +13,12 @@ namespace Jgut\Slim\Exception\Whoops\Renderer; -use Jgut\Slim\Exception\Whoops\Inspector; use JsonException; use RuntimeException; +use Whoops\Exception\Frame; use Whoops\Handler\Handler; -use Whoops\Handler\JsonResponseHandler; -class JsonRenderer extends JsonResponseHandler +class JsonRenderer extends Handler { use RendererTrait; @@ -38,37 +37,51 @@ class JsonRenderer extends JsonResponseHandler protected bool $prettify = true; - public function __construct(string $defaultTitle = 'Slim Application error') - { - $this->defaultTitle = $defaultTitle; + protected bool $returnFrames = true; - $this->addTraceToOutput(true); - } + public function __construct( + protected string $defaultTitle = 'Slim Application error', + private bool $jsonApi = false, + ) {} public function setPrettify(bool $prettify): void { $this->prettify = $prettify; } + public function addTraceToOutput(bool $returnFrames = null): bool + { + if ($returnFrames !== null) { + $this->returnFrames = $returnFrames; + } + + return $this->returnFrames; + } + /** - * @inheritDoc - * * @throws RuntimeException */ public function handle() { - $exception = $this->getException(); - - $inspector = new Inspector($exception); - $this->setInspector($inspector); - - /** @var bool $addTrace */ - $addTrace = $this->addTraceToOutput(); - - $error = $this->getExceptionData($inspector, $addTrace); + /** @var list $frameFilters */ + $frameFilters = array_values($this->getRun()->getFrameFilters()); + + if ($this->jsonApi === true) { + $response = [ + 'errors' => [ + $this->getExceptionData($this->getInspector(), $this->addTraceToOutput(), $frameFilters), + ], + ]; + } else { + $response = [ + 'error' => [ + $this->getExceptionData($this->getInspector(), $this->addTraceToOutput(), $frameFilters), + ], + ]; + } try { - $json = json_encode($error, $this->getJsonFlags() | \JSON_THROW_ON_ERROR); + $output = json_encode($response, $this->getJsonFlags() | \JSON_THROW_ON_ERROR); } catch (JsonException $exception) { // @codeCoverageIgnoreStart throw new RuntimeException( @@ -79,7 +92,7 @@ public function handle() // @codeCoverageIgnoreEnd } - echo $json; + echo $output; // @phpstan-ignore-line return Handler::QUIT; } diff --git a/src/Whoops/Renderer/PlainTextRenderer.php b/src/Whoops/Renderer/PlainTextRenderer.php index dda8dc6..53216f1 100644 --- a/src/Whoops/Renderer/PlainTextRenderer.php +++ b/src/Whoops/Renderer/PlainTextRenderer.php @@ -13,40 +13,62 @@ namespace Jgut\Slim\Exception\Whoops\Renderer; -use Jgut\Slim\Exception\Whoops\Inspector; use Psr\Log\LoggerInterface; +use Whoops\Exception\Frame; use Whoops\Handler\PlainTextHandler; class PlainTextRenderer extends PlainTextHandler { use RendererTrait; - public function __construct(string $defaultTitle = 'Slim Application error', ?LoggerInterface $logger = null) - { + public function __construct( + protected string $defaultTitle = 'Slim Application error', + ?LoggerInterface $logger = null, + ) { parent::__construct($logger); - $this->defaultTitle = $defaultTitle; - $this->addTraceFunctionArgsToOutput(true); } public function generateResponse(): string { - $exception = $this->getException(); - - $inspector = new Inspector($exception); - $this->setInspector($inspector); - /** @var bool $addTrace */ $addTrace = $this->addTraceToOutput(); - $error = $this->getExceptionData($inspector, $addTrace); - $stackTrace = $addTrace ? "\n" . $this->getStackTraceOutput($error['trace'] ?? []) : ''; + /** @var list $frameFilters */ + $frameFilters = array_values($this->getRun()->getFrameFilters()); + + $error = $this->getExceptionData($this->getInspector(), $addTrace, $frameFilters); - $type = $addTrace ? ($error['type'] ?? '') . ': ' : ''; - $message = $error['message']; + return !$addTrace + ? $error['message'] + : $this->formatError($error); + } - return sprintf("%s%s%s\n", $type, $message, $stackTrace); + /** + * @param ExceptionData $error + */ + private function formatError(array $error): string + { + $outputString = <<<'OUTPUT' + Type: %s + Code: %s + Message: %s + File: %s + Line: %s + Trace: + %s + OUTPUT; + + return sprintf( + $outputString, + $error['type'], + $error['code'], + $error['message'], + $error['file'], + $error['line'], + $this->getStackTraceOutput($error['trace'] ?? []), + ); } /** @@ -76,7 +98,7 @@ function (array $stack) use ($argsOutputLimit, &$line): string { $stackFrames, ); - return "Stack trace:\n" . implode('', $stackTrace); + return implode('', $stackTrace); } /** @@ -126,7 +148,7 @@ protected function flattenArguments(array $args): array return array_map( function ($arg) { if (\is_object($arg)) { - $class = \get_class($arg); + $class = $arg::class; return $class . (mb_strpos($class, 'class@anonymous') !== 0 ? '::class' : ''); } diff --git a/src/Whoops/Renderer/RendererTrait.php b/src/Whoops/Renderer/RendererTrait.php index 4da376e..e06ac55 100644 --- a/src/Whoops/Renderer/RendererTrait.php +++ b/src/Whoops/Renderer/RendererTrait.php @@ -13,57 +13,40 @@ namespace Jgut\Slim\Exception\Whoops\Renderer; -use Jgut\Slim\Exception\Whoops\Inspector; use Slim\Exception\HttpException; use Throwable; +use Whoops\Exception\Formatter; +use Whoops\Exception\Frame; use Whoops\Exception\Inspector as WhoopsInspector; use Whoops\Handler\Handler; +use Whoops\Inspector\InspectorInterface; use Whoops\RunInterface; trait RendererTrait { - protected string $defaultTitle; - /** - * @return array{message: string, type?: class-string, trace?: array} + * @param list $frameFilters + * + * @return ExceptionData */ - protected function getExceptionData(Inspector $inspector, bool $addTrace = false): array - { + protected function getExceptionData( + InspectorInterface $inspector, + bool $shouldAddTrace, + array $frameFilters, + ): array { $exception = $inspector->getException(); - $error = [ - 'message' => $exception instanceof HttpException ? $exception->getTitle() : $this->defaultTitle, - ]; - - if ($addTrace) { - $error['type'] = \get_class($exception); - $error['trace'] = $this->getExceptionStack($inspector); - } + /** @var ExceptionData $error */ + $error = Formatter::formatExceptionAsDataArray( + $inspector, + $shouldAddTrace, + $frameFilters, + ); + $error['message'] = $exception instanceof HttpException ? $exception->getTitle() : $this->defaultTitle; return $error; } - /** - * @return array - */ - protected function getExceptionStack(Inspector $inspector): array - { - $frames = $inspector->getTraceFrames(); - $stackFrames = []; - - foreach ($frames as $frame) { - $stackFrames[] = [ - 'file' => $frame->getFile(true), - 'line' => $frame->getLine(), - 'function' => $frame->getFunction(), - 'class' => $frame->getClass(), - 'args' => $frame->getArgs(), - ]; - } - - return $stackFrames; - } - final public function __invoke(Throwable $exception, WhoopsInspector $inspector, RunInterface $run): int { $this->setException($exception); diff --git a/src/Whoops/Renderer/XmlRenderer.php b/src/Whoops/Renderer/XmlRenderer.php index 616834a..ca85b25 100644 --- a/src/Whoops/Renderer/XmlRenderer.php +++ b/src/Whoops/Renderer/XmlRenderer.php @@ -15,8 +15,8 @@ use DOMDocument; use DOMElement; -use Jgut\Slim\Exception\Whoops\Inspector; use SimpleXMLElement; +use Whoops\Exception\Frame; use Whoops\Handler\Handler; use Whoops\Handler\XmlResponseHandler; @@ -26,10 +26,9 @@ class XmlRenderer extends XmlResponseHandler protected bool $prettify = true; - public function __construct(string $defaultTitle = 'Slim Application error') - { - $this->defaultTitle = $defaultTitle; - + public function __construct( + protected string $defaultTitle = 'Slim Application error', + ) { $this->addTraceToOutput(true); } @@ -40,23 +39,23 @@ public function setPrettify(bool $prettify): void public function handle() { - $exception = $this->getException(); - - $inspector = new Inspector($exception); - $this->setInspector($inspector); - /** @var bool $addTrace */ $addTrace = $this->addTraceToOutput(); - $error = $this->getExceptionData($inspector, $addTrace); + /** @var list $frameFilters */ + $frameFilters = array_values($this->getRun()->getFrameFilters()); - echo $this->getFormattedXml($error); + $response = $this->getExceptionData($this->getInspector(), $addTrace, $frameFilters); + + $output = $this->getFormattedXml($response); + + echo $output; // @phpstan-ignore-line return Handler::QUIT; } /** - * @param array $data + * @param ExceptionData $data */ protected function getFormattedXml(array $data): string { @@ -93,7 +92,7 @@ protected function addDataNodes(SimpleXMLElement $node, array $data, string $nod $this->addDataNodes($node->addChild($key), $value, $key); } else { if (\is_object($value)) { - $value = \get_class($value); + $value = $value::class; } elseif (!\is_scalar($value)) { $value = \gettype($value); } diff --git a/tests/Exception/ExceptionHandlerTest.php b/tests/Exception/ExceptionHandlerTest.php index 56dd764..f1dcf8d 100644 --- a/tests/Exception/ExceptionHandlerTest.php +++ b/tests/Exception/ExceptionHandlerTest.php @@ -28,8 +28,6 @@ class ExceptionHandlerTest extends TestCase protected static bool $exitShutDown = false; /** - * {@inheritDoc} - * * Hack to prevent shutdown function to be triggered after PHPUnit has finished. */ public static function setUpBeforeClass(): void diff --git a/tests/Exception/Handler/ErrorHandlerTest.php b/tests/Exception/Handler/ErrorHandlerTest.php index 6427268..86d813f 100644 --- a/tests/Exception/Handler/ErrorHandlerTest.php +++ b/tests/Exception/Handler/ErrorHandlerTest.php @@ -81,7 +81,12 @@ public function testLoggingError(): void ->getMock(); $callableResolver->expects(static::any()) ->method('resolve') - ->withConsecutive([PlainTextRenderer::class], [HtmlRenderer::class]) + ->willReturnCallback(static function ($class) { + return match (true) { + $class === PlainTextRenderer::class => new PlainTextRenderer(), + $class === HtmlRenderer::class => new HtmlRenderer(), + }; + }) ->willReturnOnConsecutiveCalls(new PlainTextRenderer(), new HtmlRenderer()); $logger = $this->getMockBuilder(LoggerInterface::class) @@ -105,7 +110,12 @@ public function testLoggingHttpError(): void ->getMock(); $callableResolver->expects(static::any()) ->method('resolve') - ->withConsecutive([PlainTextRenderer::class], [HtmlRenderer::class]) + ->willReturnCallback(static function ($class) { + return match (true) { + $class === PlainTextRenderer::class => new PlainTextRenderer(), + $class === HtmlRenderer::class => new HtmlRenderer(), + }; + }) ->willReturnOnConsecutiveCalls(new PlainTextRenderer(), new HtmlRenderer()); $logger = $this->getMockBuilder(LoggerInterface::class) diff --git a/tests/Exception/Renderer/HtmlRendererTest.php b/tests/Exception/Renderer/HtmlRendererTest.php index 4342121..8391c3b 100644 --- a/tests/Exception/Renderer/HtmlRendererTest.php +++ b/tests/Exception/Renderer/HtmlRendererTest.php @@ -24,59 +24,140 @@ */ class HtmlRendererTest extends TestCase { - /** - * @var \PHPUnit\Framework\MockObject\MockObject|ServerRequestInterface - */ - protected $request; - - protected HtmlRenderer $renderer; - - protected function setUp(): void - { - $this->request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); - $this->renderer = new HtmlRenderer(); - } - public function testDefaultHttpExceptionOutput(): void { - $exception = new HttpForbiddenException($this->request); - $output = ($this->renderer)($exception, false); + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $renderer = new HtmlRenderer(); + $exception = new HttpForbiddenException($request); + $output = ($renderer)($exception, false); + + $expected /** @lang html */ + = <<<'EXPECTED' + + + + + + + 403 Forbidden - static::assertStringContainsString('403 Forbidden', $output); - static::assertStringContainsString('

403 Forbidden

', $output); - static::assertStringNotContainsString('

Details

', $output); - static::assertStringContainsString('

Forbidden.

', $output); + + + +

403 Forbidden

+

Forbidden.

+ Go Back + + + EXPECTED; + static::assertEquals($expected, $output); } public function testMessageHttpExceptionOutput(): void { - $exception = new HttpForbiddenException($this->request, 'No access'); - $output = ($this->renderer)($exception, false); + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $renderer = new HtmlRenderer(); + $exception = new HttpForbiddenException($request, 'No access'); + $output = ($renderer)($exception, false); - static::assertStringContainsString('403 Forbidden', $output); - static::assertStringContainsString('

403 Forbidden

', $output); - static::assertStringNotContainsString('

Details

', $output); - static::assertStringContainsString('

No access

', $output); + $expected /** @lang html */ + = <<<'EXPECTED' + + + + + + + 403 Forbidden + + + + +

403 Forbidden

+

No access

+ Go Back + + + EXPECTED; + static::assertEquals($expected, $output); } public function testDescriptionHttpExceptionOutput(): void { - $exception = new HttpForbiddenException($this->request, ''); - $output = ($this->renderer)($exception, false); + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $renderer = new HtmlRenderer(); + $exception = new HttpForbiddenException($request, ''); + $output = ($renderer)($exception, false); + + $expected /** @lang html */ + = <<<'EXPECTED' + + + + + + + 403 Forbidden - static::assertStringContainsString('403 Forbidden', $output); - static::assertStringContainsString('

403 Forbidden

', $output); - static::assertStringNotContainsString('

Details

', $output); - static::assertStringContainsString('

You are not permitted to perform the requested operation.

', $output); + + + +

403 Forbidden

+

You are not permitted to perform the requested operation.

+ Go Back + + + EXPECTED; + static::assertEquals($expected, $output); } public function testOutputWithTrace(): void { + $renderer = new HtmlRenderer(); $exception = new RuntimeException(); - $output = ($this->renderer)($exception, true); + $output = ($renderer)($exception, true); + + $file = __FILE__; + + $expected /** @lang html */ + = << + + + + + + Slim Application error - static::assertStringContainsString('Slim Application error', $output); - static::assertStringContainsString('

Slim Application error

', $output); - static::assertStringContainsString('

Details

', $output); + + + +

Slim Application error

+

The application could not run because of the following error:

+

Details

+
Type: RuntimeException
+
Code: 0
+
Message:
+
File: {$file}
+
Line: 129
+

Trace

+ EXPECTED; + static::assertStringContainsString($expected, $output); } } diff --git a/tests/Exception/Renderer/JsonRendererTest.php b/tests/Exception/Renderer/JsonRendererTest.php index bcef469..c27be27 100644 --- a/tests/Exception/Renderer/JsonRendererTest.php +++ b/tests/Exception/Renderer/JsonRendererTest.php @@ -23,17 +23,11 @@ */ class JsonRendererTest extends TestCase { - protected HttpForbiddenException $exception; - - protected function setUp(): void - { - $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); - $this->exception = new HttpForbiddenException($request, 'Forbidden action'); - } - public function testOutput(): void { - $output = (new JsonRenderer())($this->exception, false); + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); + $output = (new JsonRenderer())($exception, false); $expected = <<<'EXPECTED' { @@ -47,17 +41,21 @@ public function testOutput(): void public function testNotPrettifiedOutput(): void { + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); $renderer = new JsonRenderer(); $renderer->setPrettify(false); - $output = $renderer($this->exception, false); + $output = $renderer($exception, false); static::assertEquals('{"error":{"message":"403 Forbidden"}}', $output); } public function testOutputWithTrace(): void { - $output = (new JsonRenderer())($this->exception, true); + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); + $output = (new JsonRenderer())($exception, true); $expected = <<<'EXPECTED' { @@ -71,10 +69,12 @@ public function testOutputWithTrace(): void public function testNotPrettifiedOutputWithTrace(): void { + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); $renderer = new JsonRenderer(); $renderer->setPrettify(false); - $output = $renderer($this->exception, true); + $output = $renderer($exception, true); static::assertStringContainsString('{"error":{"message":"403 Forbidden","exception":[', $output); } diff --git a/tests/Exception/Renderer/PlainTextRendererTest.php b/tests/Exception/Renderer/PlainTextRendererTest.php index 7057e91..06f5d03 100644 --- a/tests/Exception/Renderer/PlainTextRendererTest.php +++ b/tests/Exception/Renderer/PlainTextRendererTest.php @@ -27,25 +27,38 @@ class PlainTextRendererTest extends TestCase protected PlainTextRenderer $renderer; - protected function setUp(): void + public function testOutput(): void { $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); - $this->exception = new HttpForbiddenException($request, 'Forbidden action'); - $this->renderer = new PlainTextRenderer(); - } + $exception = new HttpForbiddenException($request, 'Forbidden action'); + $renderer = new PlainTextRenderer(); - public function testOutput(): void - { - $output = ($this->renderer)($this->exception, false); + $output = $renderer($exception, false); static::assertEquals('403 Forbidden', $output); } public function testOutputWithTrace(): void { - $output = ($this->renderer)($this->exception, true); + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); + $renderer = new PlainTextRenderer(); + + $output = $renderer($exception, true); + + $file = __FILE__; + + $expected /** @lang text */ + = <<getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); - $this->exception = new HttpForbiddenException($request, 'Forbidden action'); - } - public function testOutput(): void { - $output = (new XmlRenderer())($this->exception, false); - - $expected = <<<'EXPECTED' - - - - - EXPECTED; + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); + $output = (new XmlRenderer())($exception, false); + + $expected /** @lang xml */ + = <<<'EXPECTED' + + + + + EXPECTED; static::assertEquals($expected, $output); } public function testNotPrettifiedOutput(): void { + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); $renderer = new XmlRenderer(); $renderer->setPrettify(false); - $output = $renderer($this->exception, false); + $output = $renderer($exception, false); - $expected = <<<'EXPECTED' - - - EXPECTED; + $expected /** @lang xml */ + = <<<'EXPECTED' + + + EXPECTED; static::assertEquals($expected, $output); } public function testOutputWithTrace(): void { - $output = (new XmlRenderer())($this->exception, true); - - $expected = <<<'EXPECTED' - - - - - - EXPECTED; + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); + $output = (new XmlRenderer())($exception, true); + + $expected /** @lang xml */ + = <<<'EXPECTED' + + + + + EXPECTED; static::assertStringContainsString($expected, $output); } public function testNotPrettifiedOutputWithTrace(): void { + $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); + $exception = new HttpForbiddenException($request, 'Forbidden action'); $renderer = new XmlRenderer(); $renderer->setPrettify(false); - $output = $renderer($this->exception, true); + $output = $renderer($exception, true); - $expected = <<<'EXPECTED' - - - EXPECTED; + $expected /** @lang xml */ + = <<<'EXPECTED' + + + EXPECTED; static::assertStringContainsString($expected, $output); } } diff --git a/tests/Exception/Stubs/ErrorHandlerStub.php b/tests/Exception/Stubs/ErrorHandlerStub.php index 2dc6f32..648d6bb 100644 --- a/tests/Exception/Stubs/ErrorHandlerStub.php +++ b/tests/Exception/Stubs/ErrorHandlerStub.php @@ -15,6 +15,9 @@ use Jgut\Slim\Exception\Handler\ErrorHandler; +/** + * @internal + */ class ErrorHandlerStub extends ErrorHandler { protected function inCli(): bool diff --git a/tests/Exception/Stubs/ExceptionHandlerStub.php b/tests/Exception/Stubs/ExceptionHandlerStub.php index 2d951c7..b81f71b 100644 --- a/tests/Exception/Stubs/ExceptionHandlerStub.php +++ b/tests/Exception/Stubs/ExceptionHandlerStub.php @@ -17,32 +17,27 @@ use Psr\Http\Message\ServerRequestInterface; use Slim\Interfaces\ErrorHandlerInterface; +/** + * @internal + */ class ExceptionHandlerStub extends ExceptionHandler { - protected array $lastError; - + /** + * @param array{type: int, message: string, file: string, line: int}|null $lastError + */ public function __construct( ServerRequestInterface $request, ErrorHandlerInterface $errorHandler, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails, - array $lastError = [] + protected ?array $lastError = null, ) { parent::__construct($request, $errorHandler, $displayErrorDetails, $logErrors, $logErrorDetails); - - $this->lastError = $lastError; } - /** - * @inheritDoc - */ protected function getLastError(): ?array { - if (\count($this->lastError)) { - return $this->lastError; - } - - return parent::getLastError(); + return $this->lastError ?? parent::getLastError(); } } diff --git a/tests/Exception/Stubs/InspectorStub.php b/tests/Exception/Stubs/InspectorStub.php index a4ad393..1ec5983 100644 --- a/tests/Exception/Stubs/InspectorStub.php +++ b/tests/Exception/Stubs/InspectorStub.php @@ -17,6 +17,9 @@ use Whoops\Exception\Frame; use Whoops\Exception\FrameCollection; +/** + * @internal + */ class InspectorStub extends Inspector { public function getFrames(array $frameFilters = []): FrameCollection diff --git a/tests/Exception/Stubs/RendererStub.php b/tests/Exception/Stubs/RendererStub.php index be46e2b..4b71050 100644 --- a/tests/Exception/Stubs/RendererStub.php +++ b/tests/Exception/Stubs/RendererStub.php @@ -16,20 +16,17 @@ use Jgut\Slim\Exception\Whoops\Renderer\HtmlRenderer; use Whoops\Handler\Handler; +/** + * @internal + */ class RendererStub extends HtmlRenderer { - /** - * @inheritDoc - */ public function getContentTypes(): array { return []; } - /** - * @inheritDoc - */ - public function handle() + public function handle(): ?int { echo $this->getException() ->getMessage(); diff --git a/tests/Exception/Stubs/WhoopsErrorHandlerStub.php b/tests/Exception/Stubs/WhoopsErrorHandlerStub.php index 7969d09..0202557 100644 --- a/tests/Exception/Stubs/WhoopsErrorHandlerStub.php +++ b/tests/Exception/Stubs/WhoopsErrorHandlerStub.php @@ -15,6 +15,9 @@ use Jgut\Slim\Exception\Whoops\Handler\ErrorHandler; +/** + * @internal + */ class WhoopsErrorHandlerStub extends ErrorHandler { protected function inCli(): bool diff --git a/tests/Exception/Whoops/InspectorTest.php b/tests/Exception/Whoops/InspectorTest.php deleted file mode 100644 index e02278a..0000000 --- a/tests/Exception/Whoops/InspectorTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Jgut\Slim\Exception\Tests\Whoops; - -use InvalidArgumentException; -use Jgut\Slim\Exception\Tests\Stubs\InspectorStub; -use Jgut\Slim\Exception\Whoops\Inspector; -use PHPUnit\Framework\TestCase; -use Psr\Http\Message\ServerRequestInterface; -use Slim\Exception\HttpInternalServerErrorException; - -/** - * @internal - */ -class InspectorTest extends TestCase -{ - public function testAssign(): void - { - $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); - $originalException = new InvalidArgumentException(); - $exception = new HttpInternalServerErrorException($request, null, $originalException); - - $inspector = new Inspector($exception); - - static::assertEquals($exception, $inspector->getException()); - } - - public function testStackTraceFrames(): void - { - $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); - $originalException = new InvalidArgumentException(); - $exception = new HttpInternalServerErrorException($request, null, $originalException); - - $inspector = new InspectorStub($exception); - - $frames = $inspector->getTraceFrames(); - - static::assertEquals(__CLASS__, $frames[\count($frames) - 1]->getClass()); - static::assertEquals(__FUNCTION__, $frames[\count($frames) - 1]->getFunction()); - } -} diff --git a/tests/Exception/Whoops/Renderer/JsonRendererTest.php b/tests/Exception/Whoops/Renderer/JsonRendererTest.php index bf9202c..f302d05 100644 --- a/tests/Exception/Whoops/Renderer/JsonRendererTest.php +++ b/tests/Exception/Whoops/Renderer/JsonRendererTest.php @@ -18,6 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpForbiddenException; use Whoops\Exception\Inspector; +use Whoops\Run as Whoops; /** * @internal @@ -31,20 +32,27 @@ public function testOutput(): void $inspector = new Inspector($exception); $renderer = new JsonRenderer(); - $renderer->addTraceToOutput(true); $renderer->setException($exception); $renderer->setInspector($inspector); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = <<<'EXPECTED' - { - "message": "403 Forbidden", - "type": "Slim\\Exception\\HttpForbiddenException", - "trace": [ + $file = __FILE__; + $expected = <<addTraceToOutput(true); $renderer->setException($exception); $renderer->setInspector($inspector); $renderer->setPrettify(false); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = '{"message":"403 Forbidden","type":"Slim\\\Exception\\\HttpForbiddenException","trace":['; + $expected = '{"error":[{' + . '"type":"Slim\\\\Exception\\\\HttpForbiddenException",' + . '"message":"403 Forbidden",' + . '"code":403,' + . '"file":"' . __FILE__ . '",' + . '"line":63,' + . '"trace":[{'; static::assertStringContainsString($expected, $output); } @@ -79,14 +93,25 @@ public function testNoTraceOutput(): void $renderer->addTraceToOutput(false); $renderer->setException($exception); $renderer->setInspector($inspector); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = <<<'EXPECTED' + $file = __FILE__; + + $expected = <<setException($exception); $renderer->setInspector($inspector); $renderer->setPrettify(false); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = '{"message":"403 Forbidden"}'; + $expected = '{"error":[{' + . '"type":"Slim\\\\Exception\\\\HttpForbiddenException",' + . '"message":"403 Forbidden",' + . '"code":403,' + . '"file":"' . __FILE__ . '",' + . '"line":123' + . '}]}'; static::assertEquals($expected, $output); } } diff --git a/tests/Exception/Whoops/Renderer/PlainTextRendererTest.php b/tests/Exception/Whoops/Renderer/PlainTextRendererTest.php index 785c3f9..9e1e32e 100644 --- a/tests/Exception/Whoops/Renderer/PlainTextRendererTest.php +++ b/tests/Exception/Whoops/Renderer/PlainTextRendererTest.php @@ -19,19 +19,13 @@ use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpNotImplementedException; use Whoops\Exception\Inspector; +use Whoops\Run as Whoops; /** * @internal */ class PlainTextRendererTest extends TestCase { - protected PlainTextRenderer $renderer; - - protected function setUp(): void - { - $this->renderer = new PlainTextRenderer(); - } - public function testOutput(): void { $request = $this->getMockBuilder(ServerRequestInterface::class)->disableOriginalConstructor()->getMock(); @@ -39,20 +33,27 @@ public function testOutput(): void $exception = new HttpNotImplementedException($request, null, $originalException); $inspector = new Inspector($exception); - $handler = new PlainTextRenderer(); - $handler->addTraceFunctionArgsToOutput(true); - $handler->setException($exception); - $handler->setInspector($inspector); + $renderer = new PlainTextRenderer(); + $renderer->addTraceFunctionArgsToOutput(true); + $renderer->setException($exception); + $renderer->setInspector($inspector); + $renderer->setRun(new Whoops()); ob_start(); - $handler->handle(); + $renderer->handle(); $output = ob_get_clean(); - static::assertStringContainsString( - 'Slim\\Exception\\HttpNotImplementedException: 501 Not Implemented', - $output, - ); - static::assertStringContainsString('Stack trace:', $output); + $file = __FILE__; + + $expected = <<renderer->addTraceToOutput(false); - $this->renderer->setException($exception); - $this->renderer->setInspector($inspector); + $renderer = new PlainTextRenderer(); + $renderer->addTraceToOutput(false); + $renderer->setException($exception); + $renderer->setInspector($inspector); + $renderer->setRun(new Whoops()); ob_start(); - $this->renderer->handle(); + $renderer->handle(); $output = ob_get_clean(); - static::assertStringContainsString('501 Not Implemented', $output); - static::assertStringNotContainsString('Stack trace:', $output); + static::assertEquals('501 Not Implemented', $output); } } diff --git a/tests/Exception/Whoops/Renderer/XmlRendererTest.php b/tests/Exception/Whoops/Renderer/XmlRendererTest.php index a1fa1fc..53f354b 100644 --- a/tests/Exception/Whoops/Renderer/XmlRendererTest.php +++ b/tests/Exception/Whoops/Renderer/XmlRendererTest.php @@ -18,6 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpForbiddenException; use Whoops\Exception\Inspector; +use Whoops\Run as Whoops; /** * @internal @@ -31,22 +32,27 @@ public function testOutput(): void $inspector = new Inspector($exception); $renderer = new XmlRenderer(); - $renderer->addTraceToOutput(true); $renderer->setException($exception); $renderer->setInspector($inspector); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = <<<'EXPECTED' - - - - Slim\Exception\HttpForbiddenException - - - EXPECTED; + $file = __FILE__; + + $expected /** @lang xml */ + = << + + Slim\\Exception\\HttpForbiddenException + + 403 + {$file} + 31 + + EXPECTED; static::assertStringContainsString($expected, $output); } @@ -57,19 +63,23 @@ public function testNotPrettifiedOutput(): void $inspector = new Inspector($exception); $renderer = new XmlRenderer(); - $renderer->addTraceToOutput(true); $renderer->setException($exception); $renderer->setInspector($inspector); $renderer->setPrettify(false); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = <<<'EXPECTED' - - Slim\Exception\HttpForbiddenException - EXPECTED; + $expected = '' . "\n" + . '' + . 'Slim\Exception\HttpForbiddenException' + . '' + . '403' + . '' . __FILE__ . '' + . '62' + . ''; static::assertStringContainsString($expected, $output); } @@ -83,18 +93,26 @@ public function testNoTraceOutput(): void $renderer->addTraceToOutput(false); $renderer->setException($exception); $renderer->setInspector($inspector); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = <<<'EXPECTED' - - - - + $file = __FILE__; + + $expected /** @lang xml */ + = << + + Slim\\Exception\\HttpForbiddenException + + 403 + {$file} + 89 + - EXPECTED; + EXPECTED; static::assertEquals($expected, $output); } @@ -109,16 +127,20 @@ public function testNotPrettifiedNoTraceOutput(): void $renderer->setException($exception); $renderer->setInspector($inspector); $renderer->setPrettify(false); + $renderer->setRun(new Whoops()); ob_start(); $renderer->handle(); $output = ob_get_clean(); - $expected = <<<'EXPECTED' - - - - EXPECTED; + $expected = '' . "\n" + . '' + . 'Slim\Exception\HttpForbiddenException' + . '' + . '403' + . '' . __FILE__ . '' + . '122' + . '' . "\n"; static::assertEquals($expected, $output); } } From 3fc772a5c4bf5dc291d0c704229f1bb3d6ff2926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Guti=C3=A9rrez?= Date: Thu, 28 Sep 2023 00:08:16 +0200 Subject: [PATCH 2/5] update dev dependency --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee49d0f..7f08f98 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![PHP version](https://img.shields.io/badge/PHP-%3E%3D8.1-8892BF.svg?style=flat-square)](http://php.net) +[![PHP version](https://img.shields.io/badge/PHP-%3E%3D8.0-8892BF.svg?style=flat-square)](http://php.net) [![Latest Version](https://img.shields.io/packagist/v/juliangut/slim-exception.svg?style=flat-square)](https://packagist.org/packages/juliangut/slim-exception) [![License](https://img.shields.io/github/license/juliangut/slim-exception.svg?style=flat-square)](https://github.com/juliangut/slim-exception/blob/master/LICENSE) diff --git a/composer.json b/composer.json index d7d9e21..8b6eda8 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "ext-simplexml": "*", "filp/whoops": "^2.15", "infection/infection": "~0.25|~0.27", - "juliangut/easy-coding-standard-config": "dev-master", + "juliangut/easy-coding-standard-config": "^1.12", "juliangut/phpstan-config": "^1.1", "laminas/laminas-diactoros": "^2.20", "overtrue/phplint": "^4.0|^5.0|^6.0", From 808d10b53c43ac59e53f69c356e281aebd652a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Guti=C3=A9rrez?= Date: Tue, 3 Oct 2023 17:38:12 +0200 Subject: [PATCH 3/5] update docs --- README.md | 10 +++------- composer.json | 9 +++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7f08f98..1a130c3 100644 --- a/README.md +++ b/README.md @@ -112,14 +112,10 @@ $app->run($request); trigger_error('This is embarrassing', \E_USER_ERROR); ``` -## Upgrade from 1.x +## Upgrade from 2.x -Overall usage has been drastically simplified due to Slim 4 migration to exception based error handling, basically what slim-exception was doing in 1.x. - -* Minimum Slim version is now 4.11 -* ExceptionManager has been removed as its functionality is now integrated into Slim -* Exceptions no longer uses juliangut/http-exception, and thus they have no identifier -* Global error/exception handling has been moved from a trait (meant for App) to its own class ExceptionHandler +* Minimum PHP version is now 8.0 +* Minimum Whoops version is now 2.15 as custom Inspector has been removed in favor of Whoop's frame filters ## Contributing diff --git a/composer.json b/composer.json index 8b6eda8..a4e4961 100644 --- a/composer.json +++ b/composer.json @@ -40,14 +40,15 @@ "overtrue/phplint": "^4.0|^5.0|^6.0", "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "^2.13", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.11", "phpunit/phpunit": "^9.6.13|^10.3", "povils/phpmnd": "^3.2", - "roave/security-advisories": "dev-master" + "roave/security-advisories": "dev-master", + "symfony/var-dumper": "^5.1|6.0" }, "suggest": { - "filp/whoops": "Enhance development error reporting and production stack-traces", - "symfony/var-dumper": "Help filp/whoops enhancing stack-traces" + "filp/whoops": "Enhance development error reporting (>=2.15)", + "symfony/var-dumper": "Help filp/whoops enhancing stack-traces (>=5.1)" }, "autoload": { "psr-4": { From cfd4a939eb05ef99d32c60d98f71ac21a7291a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Guti=C3=A9rrez?= Date: Tue, 3 Oct 2023 17:49:44 +0200 Subject: [PATCH 4/5] update var-dumper dep --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a4e4961..9b0a7e3 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "phpunit/phpunit": "^9.6.13|^10.3", "povils/phpmnd": "^3.2", "roave/security-advisories": "dev-master", - "symfony/var-dumper": "^5.1|6.0" + "symfony/var-dumper": "^6.0.4" }, "suggest": { "filp/whoops": "Enhance development error reporting (>=2.15)", From 5b9930a3faa99ede2bd94283c7e263c69cc67929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Guti=C3=A9rrez?= Date: Wed, 18 Oct 2023 23:15:29 +0200 Subject: [PATCH 5/5] update github actions --- .github/workflows/code-analysis.yml | 21 +++++---------------- .github/workflows/unit-tests.yml | 21 +++++---------------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml index b1d01ad..196c730 100644 --- a/.github/workflows/code-analysis.yml +++ b/.github/workflows/code-analysis.yml @@ -16,24 +16,12 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: shivammathur/setup-php@v2 - with: - php-version: 8.2 - coverage: none - - - name: Composer install - uses: ramsey/composer-install@v2 - with: - composer-options: "--prefer-dist" - - - name: Install Symplify easy-ci - run: composer require --dev symplify/easy-ci:11.1.5 --no-interaction --no-progress --ansi --prefer-stable --prefer-dist - - - id: output_php - run: echo "matrix=$(vendor/bin/easy-ci php-versions-json)" >> $GITHUB_OUTPUT + - id: composer-versions-matrix + uses: WyriHaximus/github-action-composer-php-versions-in-range@v1 outputs: - php: ${{ steps.output_php.outputs.matrix }} + php: ${{ steps.composer-versions-matrix.outputs.version }} + extensions: ${{ steps.composer-versions-matrix.outputs.extensions }} code_analysis: needs: prepare_env @@ -53,6 +41,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: ${{ join(fromJson(needs.prepare_env.outputs.extensions), ', ') }} coverage: none - name: Composer install diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index f909e2f..cd9bdeb 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -16,24 +16,12 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: shivammathur/setup-php@v2 - with: - php-version: 8.2 - coverage: none - - - name: Composer install - uses: ramsey/composer-install@v2 - with: - composer-options: "--prefer-dist" - - - name: Install Symplify easy-ci - run: composer require --dev symplify/easy-ci:11.1.5 --no-interaction --no-progress --ansi --prefer-stable --prefer-dist - - - id: output_php - run: echo "matrix=$(vendor/bin/easy-ci php-versions-json)" >> $GITHUB_OUTPUT + - id: composer-versions-matrix + uses: WyriHaximus/github-action-composer-php-versions-in-range@v1 outputs: - php: ${{ steps.output_php.outputs.matrix }} + php: ${{ steps.composer-versions-matrix.outputs.version }} + extensions: ${{ steps.composer-versions-matrix.outputs.extensions }} unit_tests: needs: prepare_env @@ -53,6 +41,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: ${{ join(fromJson(needs.prepare_env.outputs.extensions), ', ') }} coverage: none - name: Composer install