Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Renderer
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
4 / 4
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 createEngine
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isJsonOnlyRequest
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\View\Boiler\Error;
6
7use Celemas\Boiler\Engine;
8use Celemas\Core\Error\Renderer as RendererInterface;
9use Celemas\Core\Exception\HttpError;
10use Override;
11use Psr\Http\Message\ResponseFactoryInterface as ResponseFactory;
12use Psr\Http\Message\ResponseInterface as Response;
13use Psr\Http\Message\ServerRequestInterface as Request;
14use Throwable;
15
16/**
17 * @psalm-import-type DirsInput from \Celemas\Boiler\Engine
18 */
19final class Renderer implements RendererInterface
20{
21    /**
22     * @param non-empty-string $template
23     * @param DirsInput $dirs
24     * @param array<string, mixed> $context
25     * @param list<class-string> $trusted
26     */
27    public function __construct(
28        private string $template,
29        private string|array $dirs,
30        private array $context = [],
31        private array $trusted = [],
32        private bool $autoescape = true,
33    ) {}
34
35    #[Override]
36    public function render(
37        Throwable $exception,
38        ResponseFactory $factory,
39        Request $request,
40        bool $debug,
41    ): Response {
42        $payload = null;
43
44        if ($exception instanceof HttpError) {
45            $payload = $exception->payload();
46            $request = $exception->request() ?: $request;
47        }
48        $dirs = $this->dirs;
49        $engine = $this->createEngine($dirs);
50
51        $code = (int) $exception->getCode();
52        $status = $code < 100 || $code > 599 ? 500 : $code;
53        $response = $factory->createResponse($status);
54
55        if ($this->isJsonOnlyRequest($request)) {
56            $response = $response->withHeader('Content-Type', 'application/json');
57            $json = json_encode($payload);
58            $response->getBody()->write($json === false ? '{}' : $json);
59        } else {
60            $response->getBody()->write($engine->render($this->template, array_merge($this->context, [
61                'request' => $request,
62                'exception' => $exception,
63                'payload' => $payload,
64            ])));
65        }
66
67        return $response;
68    }
69
70    /** @param DirsInput $dirs */
71    private function createEngine(string|array $dirs): Engine
72    {
73        return $this->autoescape
74            ? Engine::create($dirs, trusted: $this->trusted)
75            : Engine::unescaped($dirs, trusted: $this->trusted);
76    }
77
78    private function isJsonOnlyRequest(Request $request): bool
79    {
80        // Split the Accept header into individual media types
81        $acceptedTypes = array_map('trim', explode(',', $request->getHeaderLine('Accept')));
82
83        foreach ($acceptedTypes as $type) {
84            // Remove quality values if present
85            $mediaType = trim(explode(';', $type)[0]);
86
87            if ($mediaType !== 'application/json') {
88                return false;
89            }
90        }
91
92        return true;
93    }
94}