Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.89% covered (warning)
88.89%
48 / 54
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Handler
88.89% covered (warning)
88.89%
48 / 54
70.00% covered (warning)
70.00%
7 / 10
25.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 views
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 trusted
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 create
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
5
 customRenderer
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
6.60
 errorViews
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 projectViewPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolvePath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 builtinViewPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 trustedClasses
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\View\Boiler\Error;
6
7use Celemas\Core\Error\Handler as ErrorHandler;
8use Celemas\Core\Error\Renderer as ErrorRenderer;
9use Celemas\Core\Exception\HttpError;
10use Celemas\Core\Factory\Factory;
11use Celemas\Core\Request;
12use Cosray\Cms;
13use Cosray\Config;
14use Cosray\Exception\RuntimeException;
15use Cosray\Locale;
16use Cosray\Locales;
17use Cosray\Node\Node;
18use Psr\Log\LoggerInterface as Logger;
19
20/** @psalm-api */
21final class Handler
22{
23    /** @var string|list<string>|null */
24    private string|array|null $views = null;
25
26    /** @var list<class-string> */
27    private array $trusted = [
28        Node::class,
29        Cms::class,
30        Locales::class,
31        Locale::class,
32        Config::class,
33        Request::class,
34    ];
35
36    public function __construct(
37        private Config $config,
38        private Factory $factory,
39        private Logger $logger,
40    ) {}
41
42    /** @param string|list<string> $views */
43    public function views(string|array $views): self
44    {
45        $this->views = $views;
46
47        return $this;
48    }
49
50    /** @param list<class-string> $trusted */
51    public function trusted(array $trusted, bool $replace = false): self
52    {
53        if ($replace) {
54            $this->trusted = $trusted;
55        } else {
56            $this->trusted = array_merge($this->trusted, $trusted);
57        }
58
59        return $this;
60    }
61
62    public function create(): ErrorHandler
63    {
64        $debug = $this->config->debug();
65        $handler = new ErrorHandler($this->factory->responseFactory(), $debug);
66        $handler->logger($this->logger);
67
68        $renderer = $this->customRenderer();
69
70        if ($renderer) {
71            $handler->renderer($renderer, HttpError::class);
72            $handler->renderer($renderer);
73        } else {
74            $rendererFactory = new RendererFactory(
75                dirs: $this->errorViews(),
76                autoescape: true,
77                context: [
78                    'debug' => $debug,
79                    'env' => $this->config->env(),
80                ],
81                trusted: $this->trustedClasses(),
82            );
83            $handler->renderer($rendererFactory->withTemplate('http-error'), HttpError::class);
84            $handler->renderer($rendererFactory->withTemplate('http-server-error'));
85        }
86
87        if ($debug && $this->config->error->whoops && WhoopsHandler::available()) {
88            $handler->debugHandler(new WhoopsHandler());
89        }
90
91        return $handler;
92    }
93
94    private function customRenderer(): ?ErrorRenderer
95    {
96        $renderer = $this->config->error->renderer;
97
98        if ($renderer === null) {
99            return null;
100        }
101
102        if (is_string($renderer)) {
103            if (!is_a($renderer, ErrorRenderer::class, true)) {
104                throw new RuntimeException('Error renderer must implement ' . ErrorRenderer::class);
105            }
106
107            $renderer = new $renderer();
108        }
109
110        if ($renderer instanceof ErrorRenderer) {
111            return $renderer;
112        }
113
114        throw new RuntimeException('Error renderer must implement ' . ErrorRenderer::class);
115    }
116
117    /** @return non-empty-list<string> */
118    private function errorViews(): array
119    {
120        $views = $this->views ?? $this->config->error->views ?? $this->projectViewPath();
121        $dirs = [];
122
123        foreach ((array) $views as $view) {
124            if (!is_string($view) || $view === '') {
125                continue;
126            }
127
128            $path = $this->resolvePath($view);
129
130            if (is_dir($path)) {
131                $dirs[] = $path;
132            }
133        }
134
135        $dirs[] = $this->builtinViewPath();
136
137        return array_values(array_unique($dirs));
138    }
139
140    private function projectViewPath(): string
141    {
142        return $this->resolvePath($this->config->path->views);
143    }
144
145    private function resolvePath(string $path): string
146    {
147        if (str_starts_with($path, '/') && is_dir($path)) {
148            return $path;
149        }
150
151        return rtrim($this->config->path->root, '/') . '/' . ltrim($path, '/');
152    }
153
154    private function builtinViewPath(): string
155    {
156        return dirname(__DIR__, 4) . '/resources/error';
157    }
158
159    /** @return list<class-string> */
160    private function trustedClasses(): array
161    {
162        return array_values(array_unique(array_merge($this->trusted, $this->config->error->trusted)));
163    }
164}