From a71db2b359a8128e37b58ff1798b4cd5ed4555d4 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Jan 2026 11:35:09 +0300 Subject: [PATCH 1/3] Remove deprecated `ThrowableResponseFactory` --- CHANGELOG.md | 4 +- config/di-web.php | 19 +- src/Factory/ThrowableResponseFactory.php | 190 ------------- .../Factory/ThrowableResponseFactoryTest.php | 258 ------------------ 4 files changed, 19 insertions(+), 452 deletions(-) delete mode 100644 src/Factory/ThrowableResponseFactory.php delete mode 100644 tests/Factory/ThrowableResponseFactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b0cf1..0271ea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Yii Error Handler Change Log -## 4.3.3 under development +## 5.0.0 under development -- no changes in this release. +- Chg #162: Replace deprecated `ThrowableResponseFactory` class usage to new one, and remove it (@vjik) ## 4.3.2 January 09, 2026 diff --git a/config/di-web.php b/config/di-web.php index b6bc5d6..b5367a7 100644 --- a/config/di-web.php +++ b/config/di-web.php @@ -2,9 +2,14 @@ declare(strict_types=1); -use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory; +use Psr\Container\ContainerInterface; +use Yiisoft\Definitions\DynamicReference; use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; +use Yiisoft\ErrorHandler\RendererProvider\CompositeRendererProvider; +use Yiisoft\ErrorHandler\RendererProvider\ContentTypeRendererProvider; +use Yiisoft\ErrorHandler\RendererProvider\HeadRendererProvider; use Yiisoft\ErrorHandler\ThrowableRendererInterface; +use Yiisoft\ErrorHandler\ThrowableResponseFactory; use Yiisoft\ErrorHandler\ThrowableResponseFactoryInterface; /** @@ -13,5 +18,15 @@ return [ ThrowableRendererInterface::class => HtmlRenderer::class, - ThrowableResponseFactoryInterface::class => ThrowableResponseFactory::class, + ThrowableResponseFactoryInterface::class => [ + 'class' => ThrowableResponseFactory::class, + '__construct()' => [ + 'rendererProvider' => DynamicReference::to( + static fn(ContainerInterface $container) => new CompositeRendererProvider( + new HeadRendererProvider(), + new ContentTypeRendererProvider($container), + ) + ), + ], + ], ]; diff --git a/src/Factory/ThrowableResponseFactory.php b/src/Factory/ThrowableResponseFactory.php deleted file mode 100644 index 6a16c1b..0000000 --- a/src/Factory/ThrowableResponseFactory.php +++ /dev/null @@ -1,190 +0,0 @@ -> - */ - 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 readonly ResponseFactoryInterface $responseFactory, - private readonly ErrorHandler $errorHandler, - private readonly ContainerInterface $container, - ?HeadersProvider $headersProvider = null, - ) { - $this->headersProvider = $headersProvider ?? new HeadersProvider(); - } - - public function create(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($throwable, $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/tests/Factory/ThrowableResponseFactoryTest.php b/tests/Factory/ThrowableResponseFactoryTest.php deleted file mode 100644 index 0c9ae08..0000000 --- a/tests/Factory/ThrowableResponseFactoryTest.php +++ /dev/null @@ -1,258 +0,0 @@ -createThrowableResponseFactory() - ->create( - $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 - ->createThrowableResponseFactory() - ->create( - $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('createThrowableResponseFactory() - ->withRenderer($mimeType, PlainTextRenderer::class); - $response = $factory->create( - $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 - ->createThrowableResponseFactory() - ->withRenderer('test/test', self::class); - } - - public function testThrownExceptionWithInvalidContentType() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid content type.'); - $this - ->createThrowableResponseFactory() - ->withRenderer('test invalid content type', PlainTextRenderer::class); - } - - public function testWithoutRenderers(): void - { - $factory = $this - ->createThrowableResponseFactory() - ->withoutRenderers(); - $response = $factory->create( - $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 - { - $factory = $this - ->createThrowableResponseFactory() - ->withoutRenderers('*/*'); - $response = $factory->create( - $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'; - $factory = $this - ->createThrowableResponseFactory() - ->withRenderer($contentType, PlainTextRenderer::class); - $response = $factory->create( - $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 - { - $factory = $this - ->createThrowableResponseFactory() - ->withRenderer('*/*', PlainTextRenderer::class); - $response = $factory->create( - $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 - { - $factory = $this - ->createThrowableResponseFactory() - ->forceContentType('application/json'); - $response = $factory->create( - $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 - ->createThrowableResponseFactory() - ->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']); - $factory = $this - ->createThrowableResponseFactory(provider: $provider) - ->withRenderer('*/*', PlainTextRenderer::class); - $response = $factory->create( - $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 createThrowableResponseFactory( - ?HeadersProvider $provider = null, - ): ThrowableResponseFactoryInterface { - $container = new SimpleContainer([], fn (string $className): object => new $className()); - return new ThrowableResponseFactory( - 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(); - } -} From 5fe600cfb7c01e9d8b4d5cb1634b01e7390a7ad4 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Jan 2026 11:46:03 +0300 Subject: [PATCH 2/3] Add `UPGRADE.md` --- UPGRADE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 UPGRADE.md diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..4e05f57 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,13 @@ +# Upgrading Instructions for my package + +This file contains the upgrade notes. These notes highlight changes that could break your +application when you upgrade the package from one version to another. + +> **Important!** The following upgrading instructions are cumulative. That is, if you want +> to upgrade from version A to version C and there is version B between A and C, you need +> to follow the instructions for both A and B. + +## Upgrade from 4.x + +- `Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` was removed, use + `Yiisoft\ErrorHandler\ThrowableResponseFactory` instead. From 03b83e2920d0047ad24bf36317500f8b31595b1d Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Jan 2026 12:25:13 +0300 Subject: [PATCH 3/3] Update UPGRADE.md Co-authored-by: Alexander Makarov --- UPGRADE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE.md b/UPGRADE.md index 4e05f57..7dbab11 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,4 +1,4 @@ -# Upgrading Instructions for my package +# Upgrading Instructions for Yii Error Handler This file contains the upgrade notes. These notes highlight changes that could break your application when you upgrade the package from one version to another.