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