generated from yiisoft/package-template
-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extract response generator from
ErrorCatcher
middleware (#133)
Co-authored-by: Sergei Predvoditelev <[email protected]>
- Loading branch information
1 parent
f2c23eb
commit e98f687
Showing
9 changed files
with
562 additions
and
434 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Yiisoft\ErrorHandler\Factory; | ||
|
||
use Throwable; | ||
use InvalidArgumentException; | ||
use Psr\Container\ContainerInterface; | ||
use Psr\Http\Message\ResponseFactoryInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Yiisoft\ErrorHandler\ErrorHandler; | ||
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\ErrorHandler\ThrowableResponseFactoryInterface; | ||
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; | ||
|
||
/** | ||
* `ThrowableResponseFactory` renders `Throwable` object | ||
* and produces a response according to the content type provided by the client. | ||
*/ | ||
final class ThrowableResponseFactory implements ThrowableResponseFactoryInterface | ||
{ | ||
private HeadersProvider $headersProvider; | ||
|
||
/** | ||
* @psalm-var array<string,class-string<ThrowableRendererInterface>> | ||
*/ | ||
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 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)); | ||
} | ||
} |
Oops, something went wrong.