From 163ed3895d65d362c8c474800a6f4db207b19106 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sun, 22 Sep 2024 18:12:27 +0700 Subject: [PATCH 01/10] Extract response generator from ErrorCatcher middleware --- config/di-web.php | 3 + src/Handler/ThrowableHandler.php | 188 +++++++++++++++++ src/Middleware/ErrorCatcher.php | 182 +--------------- src/ThrowableHandlerInterface.php | 17 ++ tests/Handler/ThrowableHandlerTest.php | 263 +++++++++++++++++++++++ tests/Middleware/ErrorCatcherTest.php | 280 ++++--------------------- 6 files changed, 515 insertions(+), 418 deletions(-) create mode 100644 src/Handler/ThrowableHandler.php create mode 100644 src/ThrowableHandlerInterface.php create mode 100644 tests/Handler/ThrowableHandlerTest.php diff --git a/config/di-web.php b/config/di-web.php index c0f814f..e0d37e4 100644 --- a/config/di-web.php +++ b/config/di-web.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use Yiisoft\ErrorHandler\ThrowableHandlerInterface; +use Yiisoft\ErrorHandler\Handler\ThrowableHandler; use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; use Yiisoft\ErrorHandler\ThrowableRendererInterface; @@ -10,5 +12,6 @@ */ return [ + ThrowableHandlerInterface::class => ThrowableHandler::class, ThrowableRendererInterface::class => HtmlRenderer::class, ]; diff --git a/src/Handler/ThrowableHandler.php b/src/Handler/ThrowableHandler.php new file mode 100644 index 0000000..105c600 --- /dev/null +++ b/src/Handler/ThrowableHandler.php @@ -0,0 +1,188 @@ +> + */ + private array $renderers = [ + 'application/json' => JsonRenderer::class, + 'application/xml' => XmlRenderer::class, + 'text/xml' => XmlRenderer::class, + 'text/plain' => PlainTextRenderer::class, + 'text/html' => HtmlRenderer::class, + '*/*' => HtmlRenderer::class, + ]; + private ?string $contentType = null; + + public function __construct( + private ResponseFactoryInterface $responseFactory, + private ErrorHandler $errorHandler, + private ContainerInterface $container, + HeadersProvider $headersProvider = null, + ) { + $this->headersProvider = $headersProvider ?? new HeadersProvider(); + } + + public function handle(Throwable $t, ServerRequestInterface $request): ResponseInterface + { + //throw new \Exception('11'); + $contentType = $this->contentType ?? $this->getContentType($request); + $renderer = $request->getMethod() === Method::HEAD ? new HeaderRenderer() : $this->getRenderer($contentType); + + $data = $this->errorHandler->handle($t, $renderer, $request); + $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR); + foreach ($this->headersProvider->getAll() as $name => $value) { + $response = $response->withHeader($name, $value); + } + return $data->addToResponse($response->withHeader(Header::CONTENT_TYPE, $contentType)); + } + + /** + * Returns a new instance with the specified content type and renderer class. + * + * @param string $contentType The content type to add associated renderers for. + * @param string $rendererClass The classname implementing the {@see ThrowableRendererInterface}. + */ + public function withRenderer(string $contentType, string $rendererClass): self + { + if (!is_subclass_of($rendererClass, ThrowableRendererInterface::class)) { + throw new InvalidArgumentException(sprintf( + 'Class "%s" does not implement "%s".', + $rendererClass, + ThrowableRendererInterface::class, + )); + } + + $new = clone $this; + $new->renderers[$this->normalizeContentType($contentType)] = $rendererClass; + return $new; + } + + /** + * Returns a new instance without renderers by the specified content types. + * + * @param string[] $contentTypes The content types to remove associated renderers for. + * If not specified, all renderers will be removed. + */ + public function withoutRenderers(string ...$contentTypes): self + { + $new = clone $this; + + if (count($contentTypes) === 0) { + $new->renderers = []; + return $new; + } + + foreach ($contentTypes as $contentType) { + unset($new->renderers[$this->normalizeContentType($contentType)]); + } + + return $new; + } + + /** + * Force content type to respond with regardless of request. + * + * @param string $contentType The content type to respond with regardless of request. + */ + public function forceContentType(string $contentType): self + { + $contentType = $this->normalizeContentType($contentType); + + if (!isset($this->renderers[$contentType])) { + throw new InvalidArgumentException(sprintf('The renderer for %s is not set.', $contentType)); + } + + $new = clone $this; + $new->contentType = $contentType; + return $new; + } + + /** + * Returns the renderer by the specified content type, or null if the renderer was not set. + * + * @param string $contentType The content type associated with the renderer. + */ + private function getRenderer(string $contentType): ?ThrowableRendererInterface + { + if (isset($this->renderers[$contentType])) { + /** @var ThrowableRendererInterface */ + return $this->container->get($this->renderers[$contentType]); + } + + return null; + } + + /** + * Returns the priority content type from the accept request header. + * + * @return string The priority content type. + */ + private function getContentType(ServerRequestInterface $request): string + { + try { + foreach (HeaderValueHelper::getSortedAcceptTypes($request->getHeader(Header::ACCEPT)) as $header) { + if (array_key_exists($header, $this->renderers)) { + return $header; + } + } + } catch (InvalidArgumentException) { + // The Accept header contains an invalid q factor. + } + + return '*/*'; + } + + /** + * Normalizes the content type. + * + * @param string $contentType The raw content type. + * + * @return string Normalized content type. + */ + private function normalizeContentType(string $contentType): string + { + if (!str_contains($contentType, '/')) { + throw new InvalidArgumentException('Invalid content type.'); + } + + return strtolower(trim($contentType)); + } +} diff --git a/src/Middleware/ErrorCatcher.php b/src/Middleware/ErrorCatcher.php index bbfd8ac..5d53fb1 100644 --- a/src/Middleware/ErrorCatcher.php +++ b/src/Middleware/ErrorCatcher.php @@ -4,128 +4,26 @@ namespace Yiisoft\ErrorHandler\Middleware; -use InvalidArgumentException; -use Psr\Container\ContainerInterface; +use Throwable; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Throwable; use Yiisoft\ErrorHandler\CompositeException; -use Yiisoft\ErrorHandler\ErrorHandler; use Yiisoft\ErrorHandler\Event\ApplicationError; -use Yiisoft\ErrorHandler\HeadersProvider; -use Yiisoft\ErrorHandler\Renderer\HeaderRenderer; -use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; -use Yiisoft\ErrorHandler\Renderer\JsonRenderer; -use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer; -use Yiisoft\ErrorHandler\Renderer\XmlRenderer; -use Yiisoft\ErrorHandler\ThrowableRendererInterface; -use Yiisoft\Http\Header; -use Yiisoft\Http\HeaderValueHelper; -use Yiisoft\Http\Method; -use Yiisoft\Http\Status; - -use function array_key_exists; -use function count; -use function is_subclass_of; -use function sprintf; -use function strtolower; -use function trim; +use Yiisoft\ErrorHandler\ThrowableHandlerInterface; /** - * `ErrorCatcher` catches all throwables from the next middlewares and renders it - * according to the content type passed by the client. + * `ErrorCatcher` catches all throwables from the next middlewares + * and renders it with a handler that implements the `ThrowableHandlerInterface`. */ final class ErrorCatcher implements MiddlewareInterface { - private HeadersProvider $headersProvider; - - /** - * @psalm-var array> - */ - private array $renderers = [ - 'application/json' => JsonRenderer::class, - 'application/xml' => XmlRenderer::class, - 'text/xml' => XmlRenderer::class, - 'text/plain' => PlainTextRenderer::class, - 'text/html' => HtmlRenderer::class, - '*/*' => HtmlRenderer::class, - ]; - private ?string $contentType = null; - public function __construct( - private ResponseFactoryInterface $responseFactory, - private ErrorHandler $errorHandler, - private ContainerInterface $container, + private ThrowableHandlerInterface $throwableHandler, private ?EventDispatcherInterface $eventDispatcher = null, - HeadersProvider $headersProvider = null, - ) { - $this->headersProvider = $headersProvider ?? new HeadersProvider(); - } - - /** - * Returns a new instance with the specified content type and renderer class. - * - * @param string $contentType The content type to add associated renderers for. - * @param string $rendererClass The classname implementing the {@see ThrowableRendererInterface}. - */ - public function withRenderer(string $contentType, string $rendererClass): self - { - if (!is_subclass_of($rendererClass, ThrowableRendererInterface::class)) { - throw new InvalidArgumentException(sprintf( - 'Class "%s" does not implement "%s".', - $rendererClass, - ThrowableRendererInterface::class, - )); - } - - $new = clone $this; - $new->renderers[$this->normalizeContentType($contentType)] = $rendererClass; - return $new; - } - - /** - * Returns a new instance without renderers by the specified content types. - * - * @param string[] $contentTypes The content types to remove associated renderers for. - * If not specified, all renderers will be removed. - */ - public function withoutRenderers(string ...$contentTypes): self - { - $new = clone $this; - - if (count($contentTypes) === 0) { - $new->renderers = []; - return $new; - } - - foreach ($contentTypes as $contentType) { - unset($new->renderers[$this->normalizeContentType($contentType)]); - } - - return $new; - } - - /** - * Force content type to respond with regardless of request. - * - * @param string $contentType The content type to respond with regardless of request. - */ - public function forceContentType(string $contentType): self - { - $contentType = $this->normalizeContentType($contentType); - - if (!isset($this->renderers[$contentType])) { - throw new InvalidArgumentException(sprintf('The renderer for %s is not set.', $contentType)); - } - - $new = clone $this; - $new->contentType = $contentType; - return $new; - } + ) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { @@ -137,74 +35,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } catch (Throwable $e) { $t = new CompositeException($e, $t); } - return $this->generateErrorResponse($t, $request); - } - } - - /** - * Generates a response with error information. - */ - private function generateErrorResponse(Throwable $t, ServerRequestInterface $request): ResponseInterface - { - $contentType = $this->contentType ?? $this->getContentType($request); - $renderer = $request->getMethod() === Method::HEAD ? new HeaderRenderer() : $this->getRenderer($contentType); - $data = $this->errorHandler->handle($t, $renderer, $request); - $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR); - foreach ($this->headersProvider->getAll() as $name => $value) { - $response = $response->withHeader($name, $value); + return $this->throwableHandler->handle($t, $request); } - return $data->addToResponse($response->withHeader(Header::CONTENT_TYPE, $contentType)); - } - - /** - * Returns the renderer by the specified content type, or null if the renderer was not set. - * - * @param string $contentType The content type associated with the renderer. - */ - private function getRenderer(string $contentType): ?ThrowableRendererInterface - { - if (isset($this->renderers[$contentType])) { - /** @var ThrowableRendererInterface */ - return $this->container->get($this->renderers[$contentType]); - } - - return null; - } - - /** - * Returns the priority content type from the accept request header. - * - * @return string The priority content type. - */ - private function getContentType(ServerRequestInterface $request): string - { - try { - foreach (HeaderValueHelper::getSortedAcceptTypes($request->getHeader(Header::ACCEPT)) as $header) { - if (array_key_exists($header, $this->renderers)) { - return $header; - } - } - } catch (InvalidArgumentException) { - // The Accept header contains an invalid q factor. - } - - return '*/*'; - } - - /** - * Normalizes the content type. - * - * @param string $contentType The raw content type. - * - * @return string Normalized content type. - */ - private function normalizeContentType(string $contentType): string - { - if (!str_contains($contentType, '/')) { - throw new InvalidArgumentException('Invalid content type.'); - } - - return strtolower(trim($contentType)); } } diff --git a/src/ThrowableHandlerInterface.php b/src/ThrowableHandlerInterface.php new file mode 100644 index 0000000..d58643a --- /dev/null +++ b/src/ThrowableHandlerInterface.php @@ -0,0 +1,17 @@ +createThrowableHandler() + ->handle( + $this->createThrowable(), + $this->createServerRequest('HEAD', ['Accept' => ['test/html']]) + ); + $response + ->getBody() + ->rewind(); + $content = $response + ->getBody() + ->getContents(); + + $this->assertEmpty($content); + $this->assertSame([HeaderRenderer::DEFAULT_ERROR_MESSAGE], $response->getHeader('X-Error-Message')); + } + + public function testHandleWithFailAcceptRequestHeader(): void + { + $response = $this + ->createThrowableHandler() + ->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => ['text/plain;q=2.0']]) + ); + $response + ->getBody() + ->rewind(); + $content = $response + ->getBody() + ->getContents(); + + $this->assertNotSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + $this->assertStringContainsString('createThrowableHandler() + ->withRenderer($mimeType, PlainTextRenderer::class); + $response = $handler->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => [$mimeType]]) + ); + $response + ->getBody() + ->rewind(); + $content = $response + ->getBody() + ->getContents(); + + $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + } + + public function testThrownExceptionWithRendererIsNotImplementThrowableRendererInterface() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Class "' . self::class . '" does not implement "' . ThrowableRendererInterface::class . '".', + ); + $this + ->createThrowableHandler() + ->withRenderer('test/test', self::class); + } + + public function testThrownExceptionWithInvalidContentType() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid content type.'); + $this + ->createThrowableHandler() + ->withRenderer('test invalid content type', PlainTextRenderer::class); + } + + public function testWithoutRenderers(): void + { + $handler = $this + ->createThrowableHandler() + ->withoutRenderers(); + $response = $handler->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => ['test/html']]) + ); + $response + ->getBody() + ->rewind(); + $content = $response + ->getBody() + ->getContents(); + + $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + } + + public function testWithoutRenderer(): void + { + $handler = $this + ->createThrowableHandler() + ->withoutRenderers('*/*'); + $response = $handler->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => ['test/html']]) + ); + $response + ->getBody() + ->rewind(); + $content = $response + ->getBody() + ->getContents(); + + $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + } + + public function testAdvancedAcceptHeader(): void + { + $contentType = 'text/html;version=2'; + $handler = $this + ->createThrowableHandler() + ->withRenderer($contentType, PlainTextRenderer::class); + $response = $handler->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => ['text/html', $contentType]]) + ); + $response + ->getBody() + ->rewind(); + $content = $response + ->getBody() + ->getContents(); + + $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + } + + public function testDefaultContentType(): void + { + $handler = $this + ->createThrowableHandler() + ->withRenderer('*/*', PlainTextRenderer::class); + $response = $handler->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => ['test/test']]) + ); + $response + ->getBody() + ->rewind(); + $content = $response + ->getBody() + ->getContents(); + + $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + } + + public function testForceContentType(): void + { + $handler = $this + ->createThrowableHandler() + ->forceContentType('application/json'); + $response = $handler->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => ['text/xml']]) + ); + $response + ->getBody() + ->rewind(); + + $this->assertSame('application/json', $response->getHeaderLine(Header::CONTENT_TYPE)); + } + + public function testForceContentTypeSetInvalidType(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The renderer for image/gif is not set.'); + $this + ->createThrowableHandler() + ->forceContentType('image/gif'); + } + + public function testAddedHeaders(): void + { + $provider = new HeadersProvider([ + 'X-Default' => 'default', + 'Content-Type' => 'incorrect', + ]); + $provider->add('X-Test', 'test'); + $provider->add('X-Test2', ['test2', 'test3']); + $handler = $this + ->createThrowableHandler(provider: $provider) + ->withRenderer('*/*', PlainTextRenderer::class); + $response = $handler->handle( + $this->createThrowable(), + $this->createServerRequest('GET', ['Accept' => ['test/test']]) + ); + $headers = $response->getHeaders(); + + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertNotEquals('incorrect', $headers['Content-Type']); + + $this->assertArrayHasKey('X-Default', $headers); + $this->assertEquals(['default'], $headers['X-Default']); + $this->assertArrayHasKey('X-Test', $headers); + $this->assertEquals(['test'], $headers['X-Test']); + $this->assertArrayHasKey('X-Test2', $headers); + $this->assertEquals(['test2', 'test3'], $headers['X-Test2']); + } + + private function createThrowableHandler( + HeadersProvider $provider = null, + ): ThrowableHandlerInterface { + $container = new SimpleContainer([], fn (string $className): object => new $className()); + return new ThrowableHandler( + new ResponseFactory(), + $this->createErrorHandler(), + $container, + $provider ?? new HeadersProvider() + ); + } + + private function createErrorHandler(): ErrorHandler + { + $logger = $this->createMock(LoggerInterface::class); + return new ErrorHandler($logger, new PlainTextRenderer()); + } + + private function createServerRequest(string $method, array $headers = []): ServerRequestInterface + { + return new ServerRequest([], [], [], [], [], $method, '/', $headers); + } + + private function createThrowable(): Throwable + { + return new RuntimeException(); + } +} diff --git a/tests/Middleware/ErrorCatcherTest.php b/tests/Middleware/ErrorCatcherTest.php index 134d252..5731197 100644 --- a/tests/Middleware/ErrorCatcherTest.php +++ b/tests/Middleware/ErrorCatcherTest.php @@ -5,281 +5,75 @@ namespace Yiisoft\ErrorHandler\Tests\Middleware; use Psr\EventDispatcher\EventDispatcherInterface; -use HttpSoft\Message\ResponseFactory; +use HttpSoft\Message\Response; use HttpSoft\Message\ServerRequest; -use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use Psr\Log\LoggerInterface; use RuntimeException; -use Yiisoft\ErrorHandler\ErrorHandler; -use Yiisoft\ErrorHandler\HeadersProvider; +use Throwable; use Yiisoft\ErrorHandler\Middleware\ErrorCatcher; -use Yiisoft\ErrorHandler\Renderer\HeaderRenderer; -use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer; -use Yiisoft\ErrorHandler\ThrowableRendererInterface; -use Yiisoft\Http\Header; -use Yiisoft\Test\Support\Container\SimpleContainer; +use Yiisoft\ErrorHandler\ThrowableHandlerInterface; +use Yiisoft\Http\Status; final class ErrorCatcherTest extends TestCase { - public function testProcessWithHeadRequestMethod(): void + public function testSuccess(): void { - $response = $this - ->createErrorCatcher() - ->process( - $this->createServerRequest('HEAD', ['Accept' => ['test/html']]), - $this->createRequestHandlerWithThrowable(), - ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - - $this->assertEmpty($content); - $this->assertSame([HeaderRenderer::DEFAULT_ERROR_MESSAGE], $response->getHeader('X-Error-Message')); - } - - public function testProcessWithFailAcceptRequestHeader(): void - { - $response = $this - ->createErrorCatcher() - ->process( - $this->createServerRequest('GET', ['Accept' => ['text/plain;q=2.0']]), - $this->createRequestHandlerWithThrowable(), - ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - - $this->assertNotSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); - $this->assertStringContainsString('createMock(EventDispatcherInterface::class); - $eventDispatcher->method('dispatch')->willThrowException(new \RuntimeException('Event dispatcher error')); - $container = new SimpleContainer([], fn (string $className): object => new $className()); $errorCatcher = new ErrorCatcher( - new ResponseFactory(), - $this->createErrorHandler(), - $container, - $eventDispatcher, + $this->createThrowableHandler(), ); + $handler = new class () implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new Response(); + } + }; $response = $errorCatcher->process( - $this->createServerRequest('GET', ['Accept' => ['text/plain;q=2.0']]), - $this->createRequestHandlerWithThrowable(), - ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - $this->assertNotSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); - $this->assertStringContainsString('createErrorCatcher() - ->withRenderer($mimeType, PlainTextRenderer::class); - $response = $catcher->process( - $this->createServerRequest('GET', ['Accept' => [$mimeType]]), - $this->createRequestHandlerWithThrowable(), - ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - - $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); - } - - public function testThrownExceptionWithRendererIsNotImplementThrowableRendererInterface() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage( - 'Class "' . self::class . '" does not implement "' . ThrowableRendererInterface::class . '".', - ); - $this - ->createErrorCatcher() - ->withRenderer('test/test', self::class); - } - - public function testThrownExceptionWithInvalidContentType() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid content type.'); - $this - ->createErrorCatcher() - ->withRenderer('test invalid content type', PlainTextRenderer::class); - } - - public function testWithoutRenderers(): void - { - $catcher = $this - ->createErrorCatcher() - ->withoutRenderers(); - $response = $catcher->process( - $this->createServerRequest('GET', ['Accept' => ['test/html']]), - $this->createRequestHandlerWithThrowable(), + new ServerRequest(), + $handler ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + $this->assertSame(200, $response->getStatusCode()); } - public function testWithoutRenderer(): void + public function testError(): void { - $catcher = $this - ->createErrorCatcher() - ->withoutRenderers('*/*'); - $response = $catcher->process( - $this->createServerRequest('GET', ['Accept' => ['test/html']]), - $this->createRequestHandlerWithThrowable(), - ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - - $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); - } - - public function testAdvancedAcceptHeader(): void - { - $contentType = 'text/html;version=2'; - $catcher = $this - ->createErrorCatcher() - ->withRenderer($contentType, PlainTextRenderer::class); - $response = $catcher->process( - $this->createServerRequest('GET', ['Accept' => ['text/html', $contentType]]), - $this->createRequestHandlerWithThrowable(), + $errorCatcher = new ErrorCatcher( + $this->createThrowableHandler(), ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - - $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); - } - - public function testDefaultContentType(): void - { - $catcher = $this - ->createErrorCatcher() - ->withRenderer('*/*', PlainTextRenderer::class); - $response = $catcher->process( - $this->createServerRequest('GET', ['Accept' => ['test/test']]), + $response = $errorCatcher->process( + new ServerRequest(), $this->createRequestHandlerWithThrowable(), ); - $response - ->getBody() - ->rewind(); - $content = $response - ->getBody() - ->getContents(); - $this->assertSame(PlainTextRenderer::DEFAULT_ERROR_MESSAGE, $content); + $this->assertSame(Status::INTERNAL_SERVER_ERROR, $response->getStatusCode()); } - public function testForceContentType(): void + public function testErrorWithEventDispatcher(): void { - $catcher = $this - ->createErrorCatcher() - ->forceContentType('application/json'); - $response = $catcher->process( - $this->createServerRequest('GET', ['Accept' => ['text/xml']]), - $this->createRequestHandlerWithThrowable(), + $eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $eventDispatcher->method('dispatch')->willThrowException(new \RuntimeException('Event dispatcher error')); + $errorCatcher = new ErrorCatcher( + $this->createThrowableHandler(), + $eventDispatcher, ); - $response - ->getBody() - ->rewind(); - - $this->assertSame('application/json', $response->getHeaderLine(Header::CONTENT_TYPE)); - } - - public function testForceContentTypeSetInvalidType(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The renderer for image/gif is not set.'); - $this - ->createErrorCatcher() - ->forceContentType('image/gif'); - } - - public function testAddedHeaders(): void - { - $provider = new HeadersProvider([ - 'X-Default' => 'default', - 'Content-Type' => 'incorrect', - ]); - $provider->add('X-Test', 'test'); - $provider->add('X-Test2', ['test2', 'test3']); - $catcher = $this - ->createErrorCatcher(provider: $provider) - ->withRenderer('*/*', PlainTextRenderer::class); - $response = $catcher->process( - $this->createServerRequest('GET', ['Accept' => ['test/test']]), + $response = $errorCatcher->process( + new ServerRequest(), $this->createRequestHandlerWithThrowable(), ); - $headers = $response->getHeaders(); - - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertNotEquals('incorrect', $headers['Content-Type']); - - $this->assertArrayHasKey('X-Default', $headers); - $this->assertEquals(['default'], $headers['X-Default']); - $this->assertArrayHasKey('X-Test', $headers); - $this->assertEquals(['test'], $headers['X-Test']); - $this->assertArrayHasKey('X-Test2', $headers); - $this->assertEquals(['test2', 'test3'], $headers['X-Test2']); + $this->assertSame(Status::INTERNAL_SERVER_ERROR, $response->getStatusCode()); } - private function createErrorCatcher( - HeadersProvider $provider = null, - ): ErrorCatcher { - $container = new SimpleContainer([], fn (string $className): object => new $className()); - return new ErrorCatcher( - new ResponseFactory(), - $this->createErrorHandler(), - $container, - null, - $provider ?? new HeadersProvider() - ); - } - - private function createErrorHandler(): ErrorHandler - { - $logger = $this->createMock(LoggerInterface::class); - return new ErrorHandler($logger, new PlainTextRenderer()); - } - - private function createServerRequest(string $method, array $headers = []): ServerRequestInterface + private function createThrowableHandler(): ThrowableHandlerInterface { - return new ServerRequest([], [], [], [], [], $method, '/', $headers); + return new class () implements ThrowableHandlerInterface { + public function handle(Throwable $t, ServerRequestInterface $request): ResponseInterface + { + return new Response(Status::INTERNAL_SERVER_ERROR); + } + }; } private function createRequestHandlerWithThrowable(): RequestHandlerInterface From de3ff822638abe208052ad9f308542fb4f9772bc Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sun, 22 Sep 2024 18:14:41 +0700 Subject: [PATCH 02/10] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 525049c..b4b843a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 3.3.1 under development - Enh #130: Pass exception message instead of rendered exception to logger in `ErrorHandler` (@olegbaturin) +- Enh #133: Extract response generator from `ErrorCatcher` middleware to separate class `ThrowableHandler` (@olegbaturin) ## 3.3.0 July 11, 2024 From 05926a07351290d7848b5615697cdd4cd6a5b288 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Mon, 23 Sep 2024 12:44:06 +0700 Subject: [PATCH 03/10] minor fixes --- src/Handler/ThrowableHandler.php | 1 - src/ThrowableHandlerInterface.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Handler/ThrowableHandler.php b/src/Handler/ThrowableHandler.php index 105c600..e2e0ff9 100644 --- a/src/Handler/ThrowableHandler.php +++ b/src/Handler/ThrowableHandler.php @@ -62,7 +62,6 @@ public function __construct( public function handle(Throwable $t, ServerRequestInterface $request): ResponseInterface { - //throw new \Exception('11'); $contentType = $this->contentType ?? $this->getContentType($request); $renderer = $request->getMethod() === Method::HEAD ? new HeaderRenderer() : $this->getRenderer($contentType); diff --git a/src/ThrowableHandlerInterface.php b/src/ThrowableHandlerInterface.php index d58643a..0a660d8 100644 --- a/src/ThrowableHandlerInterface.php +++ b/src/ThrowableHandlerInterface.php @@ -11,7 +11,7 @@ interface ThrowableHandlerInterface { /** - * Handles a Throwable and produces a response. + * Handles a `Throwable` object and produces a response. */ - public function handle(Throwable $t, ServerRequestInterface $request): ResponseInterface; + public function handle(Throwable $throwable, ServerRequestInterface $request): ResponseInterface; } From 469406986e42fb2637a511cac2a49366319fea49 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Mon, 23 Sep 2024 12:50:59 +0700 Subject: [PATCH 04/10] fix var name --- src/Handler/ThrowableHandler.php | 4 ++-- tests/Middleware/ErrorCatcherTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Handler/ThrowableHandler.php b/src/Handler/ThrowableHandler.php index e2e0ff9..6eaa64e 100644 --- a/src/Handler/ThrowableHandler.php +++ b/src/Handler/ThrowableHandler.php @@ -60,12 +60,12 @@ public function __construct( $this->headersProvider = $headersProvider ?? new HeadersProvider(); } - public function handle(Throwable $t, ServerRequestInterface $request): ResponseInterface + public function handle(Throwable $throwable, ServerRequestInterface $request): ResponseInterface { $contentType = $this->contentType ?? $this->getContentType($request); $renderer = $request->getMethod() === Method::HEAD ? new HeaderRenderer() : $this->getRenderer($contentType); - $data = $this->errorHandler->handle($t, $renderer, $request); + $data = $this->errorHandler->handle($throwable, $renderer, $request); $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR); foreach ($this->headersProvider->getAll() as $name => $value) { $response = $response->withHeader($name, $value); diff --git a/tests/Middleware/ErrorCatcherTest.php b/tests/Middleware/ErrorCatcherTest.php index 5731197..a965623 100644 --- a/tests/Middleware/ErrorCatcherTest.php +++ b/tests/Middleware/ErrorCatcherTest.php @@ -69,7 +69,7 @@ public function testErrorWithEventDispatcher(): void private function createThrowableHandler(): ThrowableHandlerInterface { return new class () implements ThrowableHandlerInterface { - public function handle(Throwable $t, ServerRequestInterface $request): ResponseInterface + public function handle(Throwable $throwable, ServerRequestInterface $request): ResponseInterface { return new Response(Status::INTERNAL_SERVER_ERROR); } From fb12fe9db80e57fc599b5ad8549b0c1ed2ac069f Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Fri, 27 Sep 2024 16:35:07 +0700 Subject: [PATCH 05/10] rename ThrowableHandler to ThrowableResponseFactory --- CHANGELOG.md | 2 +- composer.json | 1 + config/di-web.php | 6 +- .../ThrowableResponseFactory.php} | 11 +-- src/Middleware/ErrorCatcher.php | 8 +-- ... => ThrowableResponseFactoryInterface.php} | 4 +- .../ThrowableResponseFactoryTest.php} | 71 +++++++++---------- tests/Middleware/ErrorCatcherTest.php | 14 ++-- 8 files changed, 59 insertions(+), 58 deletions(-) rename src/{Handler/ThrowableHandler.php => Factory/ThrowableResponseFactory.php} (94%) rename src/{ThrowableHandlerInterface.php => ThrowableResponseFactoryInterface.php} (72%) rename tests/{Handler/ThrowableHandlerTest.php => Factory/ThrowableResponseFactoryTest.php} (83%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4b843a..5bccf17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 3.3.1 under development - Enh #130: Pass exception message instead of rendered exception to logger in `ErrorHandler` (@olegbaturin) -- Enh #133: Extract response generator from `ErrorCatcher` middleware to separate class `ThrowableHandler` (@olegbaturin) +- Enh #133: Extract response generator from `ErrorCatcher` middleware to separate class `ThrowableResponseFactory` (@olegbaturin) ## 3.3.0 July 11, 2024 diff --git a/composer.json b/composer.json index d5e5531..2a8772c 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,7 @@ "httpsoft/http-message": "^1.0.9", "maglnet/composer-require-checker": "^4.4", "phpunit/phpunit": "^9.5", + "psr/event-dispatcher": "^1.0", "rector/rector": "^1.2", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", diff --git a/config/di-web.php b/config/di-web.php index e0d37e4..b6bc5d6 100644 --- a/config/di-web.php +++ b/config/di-web.php @@ -2,16 +2,16 @@ declare(strict_types=1); -use Yiisoft\ErrorHandler\ThrowableHandlerInterface; -use Yiisoft\ErrorHandler\Handler\ThrowableHandler; +use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory; use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; use Yiisoft\ErrorHandler\ThrowableRendererInterface; +use Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface; /** * @var array $params */ return [ - ThrowableHandlerInterface::class => ThrowableHandler::class, ThrowableRendererInterface::class => HtmlRenderer::class, + ThrowableResponseFactoryInterface::class => ThrowableResponseFactory::class, ]; diff --git a/src/Handler/ThrowableHandler.php b/src/Factory/ThrowableResponseFactory.php similarity index 94% rename from src/Handler/ThrowableHandler.php rename to src/Factory/ThrowableResponseFactory.php index 6eaa64e..faf949f 100644 --- a/src/Handler/ThrowableHandler.php +++ b/src/Factory/ThrowableResponseFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Yiisoft\ErrorHandler\Handler; +namespace Yiisoft\ErrorHandler\Factory; use Throwable; use InvalidArgumentException; @@ -17,8 +17,8 @@ use Yiisoft\ErrorHandler\Renderer\JsonRenderer; use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer; use Yiisoft\ErrorHandler\Renderer\XmlRenderer; -use Yiisoft\ErrorHandler\ThrowableHandlerInterface; use Yiisoft\ErrorHandler\ThrowableRendererInterface; +use Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface; use Yiisoft\Http\Header; use Yiisoft\Http\HeaderValueHelper; use Yiisoft\Http\Method; @@ -32,9 +32,10 @@ use function trim; /** - * `ThrowableHandler` renders throwables according to the content type passed by the client. + * `ThrowableResponseFactory` renders `Throwable` object + * and produces a response according to the content type passed by the client. */ -final class ThrowableHandler implements ThrowableHandlerInterface +final class ThrowableResponseFactory implements ThrowableResponseFactoryInterface { private HeadersProvider $headersProvider; @@ -60,7 +61,7 @@ public function __construct( $this->headersProvider = $headersProvider ?? new HeadersProvider(); } - public function handle(Throwable $throwable, ServerRequestInterface $request): ResponseInterface + public function create(Throwable $throwable, ServerRequestInterface $request): ResponseInterface { $contentType = $this->contentType ?? $this->getContentType($request); $renderer = $request->getMethod() === Method::HEAD ? new HeaderRenderer() : $this->getRenderer($contentType); diff --git a/src/Middleware/ErrorCatcher.php b/src/Middleware/ErrorCatcher.php index 5d53fb1..13ece4e 100644 --- a/src/Middleware/ErrorCatcher.php +++ b/src/Middleware/ErrorCatcher.php @@ -12,16 +12,16 @@ use Psr\Http\Server\RequestHandlerInterface; use Yiisoft\ErrorHandler\CompositeException; use Yiisoft\ErrorHandler\Event\ApplicationError; -use Yiisoft\ErrorHandler\ThrowableHandlerInterface; +use Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface; /** * `ErrorCatcher` catches all throwables from the next middlewares - * and renders it with a handler that implements the `ThrowableHandlerInterface`. + * and renders it with a handler that implements the `ThrowableResponseFactoryInterface`. */ final class ErrorCatcher implements MiddlewareInterface { public function __construct( - private ThrowableHandlerInterface $throwableHandler, + private ThrowableResponseFactoryInterface $throwableResponseFactory, private ?EventDispatcherInterface $eventDispatcher = null, ) {} @@ -36,7 +36,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $t = new CompositeException($e, $t); } - return $this->throwableHandler->handle($t, $request); + return $this->throwableResponseFactory->create($t, $request); } } } diff --git a/src/ThrowableHandlerInterface.php b/src/ThrowableResponseFactoryInterface.php similarity index 72% rename from src/ThrowableHandlerInterface.php rename to src/ThrowableResponseFactoryInterface.php index 0a660d8..a201331 100644 --- a/src/ThrowableHandlerInterface.php +++ b/src/ThrowableResponseFactoryInterface.php @@ -8,10 +8,10 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -interface ThrowableHandlerInterface +interface ThrowableResponseFactoryInterface { /** * Handles a `Throwable` object and produces a response. */ - public function handle(Throwable $throwable, ServerRequestInterface $request): ResponseInterface; + public function create(Throwable $throwable, ServerRequestInterface $request): ResponseInterface; } diff --git a/tests/Handler/ThrowableHandlerTest.php b/tests/Factory/ThrowableResponseFactoryTest.php similarity index 83% rename from tests/Handler/ThrowableHandlerTest.php rename to tests/Factory/ThrowableResponseFactoryTest.php index 6a920f0..df40fe2 100644 --- a/tests/Handler/ThrowableHandlerTest.php +++ b/tests/Factory/ThrowableResponseFactoryTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Yiisoft\ErrorHandler\Tests\Handler; +namespace Yiisoft\ErrorHandler\Tests\Factory; use HttpSoft\Message\ResponseFactory; use HttpSoft\Message\ServerRequest; @@ -13,23 +13,22 @@ use RuntimeException; use Throwable; use Yiisoft\ErrorHandler\ErrorHandler; -use Yiisoft\ErrorHandler\Handler\ThrowableHandler; +use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory; use Yiisoft\ErrorHandler\HeadersProvider; -use Yiisoft\ErrorHandler\Middleware\ErrorCatcher; use Yiisoft\ErrorHandler\Renderer\HeaderRenderer; use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer; -use Yiisoft\ErrorHandler\ThrowableHandlerInterface; use Yiisoft\ErrorHandler\ThrowableRendererInterface; +use Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface; use Yiisoft\Http\Header; use Yiisoft\Test\Support\Container\SimpleContainer; -final class ThrowableHandlerTest extends TestCase +final class ThrowableResponseFactoryTest extends TestCase { public function testHandleWithHeadRequestMethod(): void { $response = $this - ->createThrowableHandler() - ->handle( + ->createThrowableResponseFactory() + ->create( $this->createThrowable(), $this->createServerRequest('HEAD', ['Accept' => ['test/html']]) ); @@ -47,8 +46,8 @@ public function testHandleWithHeadRequestMethod(): void public function testHandleWithFailAcceptRequestHeader(): void { $response = $this - ->createThrowableHandler() - ->handle( + ->createThrowableResponseFactory() + ->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => ['text/plain;q=2.0']]) ); @@ -66,10 +65,10 @@ public function testHandleWithFailAcceptRequestHeader(): void public function testAddedRenderer(): void { $mimeType = 'test/test'; - $handler = $this - ->createThrowableHandler() + $factory = $this + ->createThrowableResponseFactory() ->withRenderer($mimeType, PlainTextRenderer::class); - $response = $handler->handle( + $response = $factory->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => [$mimeType]]) ); @@ -90,7 +89,7 @@ public function testThrownExceptionWithRendererIsNotImplementThrowableRendererIn 'Class "' . self::class . '" does not implement "' . ThrowableRendererInterface::class . '".', ); $this - ->createThrowableHandler() + ->createThrowableResponseFactory() ->withRenderer('test/test', self::class); } @@ -99,16 +98,16 @@ public function testThrownExceptionWithInvalidContentType() $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid content type.'); $this - ->createThrowableHandler() + ->createThrowableResponseFactory() ->withRenderer('test invalid content type', PlainTextRenderer::class); } public function testWithoutRenderers(): void { - $handler = $this - ->createThrowableHandler() + $factory = $this + ->createThrowableResponseFactory() ->withoutRenderers(); - $response = $handler->handle( + $response = $factory->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => ['test/html']]) ); @@ -124,10 +123,10 @@ public function testWithoutRenderers(): void public function testWithoutRenderer(): void { - $handler = $this - ->createThrowableHandler() + $factory = $this + ->createThrowableResponseFactory() ->withoutRenderers('*/*'); - $response = $handler->handle( + $response = $factory->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => ['test/html']]) ); @@ -144,10 +143,10 @@ public function testWithoutRenderer(): void public function testAdvancedAcceptHeader(): void { $contentType = 'text/html;version=2'; - $handler = $this - ->createThrowableHandler() + $factory = $this + ->createThrowableResponseFactory() ->withRenderer($contentType, PlainTextRenderer::class); - $response = $handler->handle( + $response = $factory->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => ['text/html', $contentType]]) ); @@ -163,10 +162,10 @@ public function testAdvancedAcceptHeader(): void public function testDefaultContentType(): void { - $handler = $this - ->createThrowableHandler() + $factory = $this + ->createThrowableResponseFactory() ->withRenderer('*/*', PlainTextRenderer::class); - $response = $handler->handle( + $response = $factory->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => ['test/test']]) ); @@ -182,10 +181,10 @@ public function testDefaultContentType(): void public function testForceContentType(): void { - $handler = $this - ->createThrowableHandler() + $factory = $this + ->createThrowableResponseFactory() ->forceContentType('application/json'); - $response = $handler->handle( + $response = $factory->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => ['text/xml']]) ); @@ -201,7 +200,7 @@ public function testForceContentTypeSetInvalidType(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The renderer for image/gif is not set.'); $this - ->createThrowableHandler() + ->createThrowableResponseFactory() ->forceContentType('image/gif'); } @@ -213,10 +212,10 @@ public function testAddedHeaders(): void ]); $provider->add('X-Test', 'test'); $provider->add('X-Test2', ['test2', 'test3']); - $handler = $this - ->createThrowableHandler(provider: $provider) + $factory = $this + ->createThrowableResponseFactory(provider: $provider) ->withRenderer('*/*', PlainTextRenderer::class); - $response = $handler->handle( + $response = $factory->create( $this->createThrowable(), $this->createServerRequest('GET', ['Accept' => ['test/test']]) ); @@ -233,11 +232,11 @@ public function testAddedHeaders(): void $this->assertEquals(['test2', 'test3'], $headers['X-Test2']); } - private function createThrowableHandler( + private function createThrowableResponseFactory( HeadersProvider $provider = null, - ): ThrowableHandlerInterface { + ): ThrowableResponseFactoryInterface { $container = new SimpleContainer([], fn (string $className): object => new $className()); - return new ThrowableHandler( + return new ThrowableResponseFactory( new ResponseFactory(), $this->createErrorHandler(), $container, diff --git a/tests/Middleware/ErrorCatcherTest.php b/tests/Middleware/ErrorCatcherTest.php index a965623..30066ca 100644 --- a/tests/Middleware/ErrorCatcherTest.php +++ b/tests/Middleware/ErrorCatcherTest.php @@ -14,7 +14,7 @@ use RuntimeException; use Throwable; use Yiisoft\ErrorHandler\Middleware\ErrorCatcher; -use Yiisoft\ErrorHandler\ThrowableHandlerInterface; +use Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface; use Yiisoft\Http\Status; final class ErrorCatcherTest extends TestCase @@ -22,7 +22,7 @@ final class ErrorCatcherTest extends TestCase public function testSuccess(): void { $errorCatcher = new ErrorCatcher( - $this->createThrowableHandler(), + $this->createThrowableResponseFactory(), ); $handler = new class () implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface @@ -41,7 +41,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface public function testError(): void { $errorCatcher = new ErrorCatcher( - $this->createThrowableHandler(), + $this->createThrowableResponseFactory(), ); $response = $errorCatcher->process( new ServerRequest(), @@ -56,7 +56,7 @@ public function testErrorWithEventDispatcher(): void $eventDispatcher = $this->createMock(EventDispatcherInterface::class); $eventDispatcher->method('dispatch')->willThrowException(new \RuntimeException('Event dispatcher error')); $errorCatcher = new ErrorCatcher( - $this->createThrowableHandler(), + $this->createThrowableResponseFactory(), $eventDispatcher, ); $response = $errorCatcher->process( @@ -66,10 +66,10 @@ public function testErrorWithEventDispatcher(): void $this->assertSame(Status::INTERNAL_SERVER_ERROR, $response->getStatusCode()); } - private function createThrowableHandler(): ThrowableHandlerInterface + private function createThrowableResponseFactory(): ThrowableResponseFactoryInterface { - return new class () implements ThrowableHandlerInterface { - public function handle(Throwable $throwable, ServerRequestInterface $request): ResponseInterface + return new class () implements ThrowableResponseFactoryInterface { + public function create(Throwable $throwable, ServerRequestInterface $request): ResponseInterface { return new Response(Status::INTERNAL_SERVER_ERROR); } From 7ea52c8fe7edb79e8a5356bab782b92ee8bd539f Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Fri, 27 Sep 2024 16:44:33 +0700 Subject: [PATCH 06/10] fix style --- src/Middleware/ErrorCatcher.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Middleware/ErrorCatcher.php b/src/Middleware/ErrorCatcher.php index 13ece4e..73ec0a6 100644 --- a/src/Middleware/ErrorCatcher.php +++ b/src/Middleware/ErrorCatcher.php @@ -23,7 +23,8 @@ final class ErrorCatcher implements MiddlewareInterface public function __construct( private ThrowableResponseFactoryInterface $throwableResponseFactory, private ?EventDispatcherInterface $eventDispatcher = null, - ) {} + ) { + } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { From 1be155f749fb8c34f195874313227d5237f2b817 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Wed, 9 Oct 2024 16:23:48 +0700 Subject: [PATCH 07/10] update readme --- README.md | 62 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4d602c0..1b26eb3 100644 --- a/README.md +++ b/README.md @@ -121,47 +121,73 @@ $errorHandler = new ErrorHandler($logger, $renderer); For more information about creating your own renders and examples of rendering error data, [see here](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/handling-errors.md#rendering-error-data). -### Using middleware for catching unhandled errors +### Using a Factory to create a response -`Yiisoft\ErrorHandler\Middleware\ErrorCatcher` is a [PSR-15](https://www.php-fig.org/psr/psr-15/) middleware that -catches exceptions that appear during middleware stack execution and passes them to the handler. +`Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` renders `Throwable` object and produces a response according to the content type passed by the client. ```php -use Yiisoft\ErrorHandler\Middleware\ErrorCatcher; +use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory; /** + * @var \Throwable $throwable * @var \Psr\Container\ContainerInterface $container * @var \Psr\Http\Message\ResponseFactoryInterface $responseFactory * @var \Psr\Http\Message\ServerRequestInterface $request - * @var \Psr\Http\Server\RequestHandlerInterface $handler * @var \Yiisoft\ErrorHandler\ErrorHandler $errorHandler - * @var \Yiisoft\ErrorHandler\ThrowableRendererInterface $renderer */ -$errorCatcher = new ErrorCatcher($responseFactory, $errorHandler, $container); +$throwableResponseFactory = new ThrowableResponseFactory($responseFactory, $errorHandler, $container); -// In any case, it will return an instance of the `Psr\Http\Message\ResponseInterface`. -// Either the expected response, or a response with error information. -$response = $errorCatcher->process($request, $handler); +// Creating an instance of the `Psr\Http\Message\ResponseInterface` with error information. +$response = $throwableResponseFactory->create($throwable, $request); ``` -The error catcher chooses how to render an exception based on accept HTTP header. If it is `text/html` -or any unknown content type, it will use the error or exception HTML template to display errors. For other -mime types, the error handler will choose different renderer that is registered within the error catcher. +`Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` chooses how to render an exception based on accept HTTP header. +If it is `text/html` or any unknown content type, it will use the error or exception HTML template to display errors. +For other mime types, the error handler will choose different renderer that is registered within the error catcher. By default, JSON, XML and plain text are supported. You can change this behavior as follows: ```php // Returns a new instance without renderers by the specified content types. -$errorCatcher = $errorCatcher->withoutRenderers('application/xml', 'text/xml'); +$throwableResponseFactory = $throwableResponseFactory->withoutRenderers('application/xml', 'text/xml'); // Returns a new instance with the specified content type and renderer class. -$errorCatcher = $errorCatcher->withRenderer('my/format', new MyRenderer()); +$throwableResponseFactory = $throwableResponseFactory->withRenderer('my/format', new MyRenderer()); // Returns a new instance with the specified force content type to respond with regardless of request. -$errorCatcher = $errorCatcher->forceContentType('application/json'); +$throwableResponseFactory = $throwableResponseFactory->forceContentType('application/json'); +``` + +### Using a Middleware for catching unhandled errors + +`Yiisoft\ErrorHandler\Middleware\ErrorCatcher` is a [PSR-15](https://www.php-fig.org/psr/psr-15/) middleware that +catches exceptions that appear during middleware stack execution and passes them to the instance of `Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface` to create a response. + +```php +use Yiisoft\ErrorHandler\Middleware\ErrorCatcher; + +/** + * @var \Psr\EventDispatcher\EventDispatcherInterface $eventDispatcher + * @var \Psr\Http\Message\ServerRequestInterface $request + * @var \Psr\Http\Server\RequestHandlerInterface $handler + * @var \Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface $throwableResponseFactory + */ + +$errorCatcher = new ErrorCatcher($throwableResponseFactory); + +// In any case, it will return an instance of the `Psr\Http\Message\ResponseInterface`. +// Either the expected response, or a response with error information. +$response = $errorCatcher->process($request, $handler); +``` + +`Yiisoft\ErrorHandler\Middleware\ErrorCatcher` can be instantiated with [PSR-14](https://www.php-fig.org/psr/psr-15/) event dispatcher an optional dependency. +In this case `\Yiisoft\ErrorHandler\Event\ApplicationError` will be dispatched when `ErrorCatcher` catches an error. + +```php +$errorCatcher = new ErrorCatcher($throwableResponseFactory, $eventDispatcher); ``` -### Using middleware for mapping certain exceptions to custom responses +### Using a Middleware for mapping certain exceptions to custom responses `Yiisoft\ErrorHandler\Middleware\ExceptionResponder` is a [PSR-15](https://www.php-fig.org/psr/psr-15/) middleware that maps certain exceptions to custom responses. @@ -196,7 +222,7 @@ In the application middleware stack `Yiisoft\ErrorHandler\Middleware\ExceptionRe ## Events -- When `ErrorCatcher` catches an error it dispatches `\Yiisoft\ErrorHandler\Event\ApplicationError` event. +- When `ErrorCatcher` catches an error it optionally dispatches `\Yiisoft\ErrorHandler\Event\ApplicationError` event. Instance of `Psr\EventDispatcher\EventDispatcherInterface` must be provided to the `ErrorCatcher`. ## Friendly Exceptions From b38141465b811b0e5f46120b47a1954de0cf874b Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Thu, 10 Oct 2024 13:52:42 +0700 Subject: [PATCH 08/10] fixed readme --- CHANGELOG.md | 2 +- README.md | 14 +++++++------- src/Factory/ThrowableResponseFactory.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bccf17..9d03717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 3.3.1 under development - Enh #130: Pass exception message instead of rendered exception to logger in `ErrorHandler` (@olegbaturin) -- Enh #133: Extract response generator from `ErrorCatcher` middleware to separate class `ThrowableResponseFactory` (@olegbaturin) +- Enh #133: Extract response generator from `ErrorCatcher` middleware into separate `ThrowableResponseFactory` class (@olegbaturin) ## 3.3.0 July 11, 2024 diff --git a/README.md b/README.md index 1b26eb3..36fa6f3 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,9 @@ $errorHandler = new ErrorHandler($logger, $renderer); For more information about creating your own renders and examples of rendering error data, [see here](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/handling-errors.md#rendering-error-data). -### Using a Factory to create a response +### Using a factory to create a response -`Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` renders `Throwable` object and produces a response according to the content type passed by the client. +`Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` renders `Throwable` object and produces a response according to the content type provided by the client. ```php use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory; @@ -143,7 +143,7 @@ $response = $throwableResponseFactory->create($throwable, $request); ``` `Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` chooses how to render an exception based on accept HTTP header. -If it is `text/html` or any unknown content type, it will use the error or exception HTML template to display errors. +If it's `text/html` or any unknown content type, it will use the error or exception HTML template to display errors. For other mime types, the error handler will choose different renderer that is registered within the error catcher. By default, JSON, XML and plain text are supported. You can change this behavior as follows: @@ -158,10 +158,10 @@ $throwableResponseFactory = $throwableResponseFactory->withRenderer('my/format', $throwableResponseFactory = $throwableResponseFactory->forceContentType('application/json'); ``` -### Using a Middleware for catching unhandled errors +### Using a middleware for catching unhandled errors `Yiisoft\ErrorHandler\Middleware\ErrorCatcher` is a [PSR-15](https://www.php-fig.org/psr/psr-15/) middleware that -catches exceptions that appear during middleware stack execution and passes them to the instance of `Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface` to create a response. +catches exceptions raised during middleware stack execution and passes them to the instance of `Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface` to create a response. ```php use Yiisoft\ErrorHandler\Middleware\ErrorCatcher; @@ -180,14 +180,14 @@ $errorCatcher = new ErrorCatcher($throwableResponseFactory); $response = $errorCatcher->process($request, $handler); ``` -`Yiisoft\ErrorHandler\Middleware\ErrorCatcher` can be instantiated with [PSR-14](https://www.php-fig.org/psr/psr-15/) event dispatcher an optional dependency. +`Yiisoft\ErrorHandler\Middleware\ErrorCatcher` can be instantiated with [PSR-14](https://www.php-fig.org/psr/psr-14/) event dispatcher as an optional dependency. In this case `\Yiisoft\ErrorHandler\Event\ApplicationError` will be dispatched when `ErrorCatcher` catches an error. ```php $errorCatcher = new ErrorCatcher($throwableResponseFactory, $eventDispatcher); ``` -### Using a Middleware for mapping certain exceptions to custom responses +### Using a middleware for mapping certain exceptions to custom responses `Yiisoft\ErrorHandler\Middleware\ExceptionResponder` is a [PSR-15](https://www.php-fig.org/psr/psr-15/) middleware that maps certain exceptions to custom responses. diff --git a/src/Factory/ThrowableResponseFactory.php b/src/Factory/ThrowableResponseFactory.php index faf949f..dd9db73 100644 --- a/src/Factory/ThrowableResponseFactory.php +++ b/src/Factory/ThrowableResponseFactory.php @@ -33,7 +33,7 @@ /** * `ThrowableResponseFactory` renders `Throwable` object - * and produces a response according to the content type passed by the client. + * and produces a response according to the content type provided by the client. */ final class ThrowableResponseFactory implements ThrowableResponseFactoryInterface { From b3c21501ab3c4792a40cc206bde48bea58e50bb8 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Thu, 10 Oct 2024 15:57:45 +0700 Subject: [PATCH 09/10] update description --- src/ThrowableResponseFactoryInterface.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ThrowableResponseFactoryInterface.php b/src/ThrowableResponseFactoryInterface.php index a201331..13dab44 100644 --- a/src/ThrowableResponseFactoryInterface.php +++ b/src/ThrowableResponseFactoryInterface.php @@ -8,6 +8,9 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +/** + * ThrowableResponseFactoryInterface produces a response for `Throwable` object. + */ interface ThrowableResponseFactoryInterface { /** From e8f56839839edc6b8bbee02a74765c02dc27635e Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 10 Oct 2024 14:23:28 +0300 Subject: [PATCH 10/10] Update src/ThrowableResponseFactoryInterface.php --- src/ThrowableResponseFactoryInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ThrowableResponseFactoryInterface.php b/src/ThrowableResponseFactoryInterface.php index 13dab44..1a16cb9 100644 --- a/src/ThrowableResponseFactoryInterface.php +++ b/src/ThrowableResponseFactoryInterface.php @@ -9,7 +9,7 @@ use Psr\Http\Message\ServerRequestInterface; /** - * ThrowableResponseFactoryInterface produces a response for `Throwable` object. + * `ThrowableResponseFactoryInterface` produces a response for `Throwable` object. */ interface ThrowableResponseFactoryInterface {