Passed
Push — main ( a1a461...2097df )
by Thomas
12:50
created

Handler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Chuck\Error;
6
7
use Conia\Chuck\Config;
8
use Conia\Chuck\Exception\HttpBadRequest;
9
use Conia\Chuck\Exception\HttpError;
10
use Conia\Chuck\Exception\HttpForbidden;
11
use Conia\Chuck\Exception\HttpMethodNotAllowed;
12
use Conia\Chuck\Exception\HttpNotFound;
13
use Conia\Chuck\Exception\HttpServerError;
14
use Conia\Chuck\Exception\HttpUnauthorized;
15
use Conia\Chuck\Http\Emitter;
16
use Conia\Chuck\Middleware;
17
use Conia\Chuck\Registry;
18
use Conia\Chuck\Renderer\Render;
19
use Conia\Chuck\Request;
20
use Conia\Chuck\Response;
21
use ErrorException;
22
use Psr\Log\LoggerInterface as PsrLogger;
23
use Throwable;
24
25
/**
26
 * @psalm-suppress MixedPropertyFetch, MixedArgument, MixedReturnStatement
27
 * @psalm-suppress MixedMethodCall, MixedAssignment, MixedInferredReturnType
28
 */
29
class Handler implements Middleware
30
{
31
    public const RENDERER = 'errorRenderer';
32
33 42
    public function __construct(protected Config $config, protected Registry $registry)
34
    {
35 42
        set_error_handler([$this, 'handleError'], E_ALL);
36 42
        set_exception_handler([$this, 'emitException']);
37
    }
38
39 12
    public function __destruct()
40
    {
41 12
        restore_error_handler();
42 12
        restore_exception_handler();
43
    }
44
45 10
    public function __invoke(Request $request, callable $next): Response
46
    {
47
        try {
48 10
            $response = $next($request);
49 8
            assert($response instanceof Response);
50
51 8
            return $response;
52 2
        } catch (Throwable $e) {
53 2
            return $this->handleException($e, $request);
54
        }
55
    }
56
57 15
    public function handleError(
58
        int $level,
59
        string $message,
60
        string $file = '',
61
        int $line = 0,
62
    ): bool {
63 15
        if ($level & error_reporting()) {
64 14
            throw new ErrorException($message, $level, $level, $file, $line);
65
        }
66
67 1
        return false;
68
    }
69
70 2
    public function emitException(Throwable $exception): void
71
    {
72 2
        $response = $this->handleException($exception, null);
73 2
        (new Emitter())->emit($response->psr());
74
    }
75
76 11
    public function handleException(Throwable $exception, ?Request $request): Response
77
    {
78 11
        if ($exception instanceof HttpError) {
79 6
            $code = $exception->getCode();
80 6
            $title = htmlspecialchars($exception->getTitle());
81 6
            $description = 'HTTP Error';
82
        } else {
83 5
            $code = 500;
84 5
            $title = '500 Internal Server Error';
85 5
            $description = $exception->getMessage();
86
        }
87
88 11
        $error = new Error(
89 11
            $title,
90 11
            $description,
91 11
            $exception->getTraceAsString(),
92 11
            $this->config->debug()
93 11
        );
94
95 11
        $this->log($exception);
96
97 11
        $accepted = $request ? $this->getAcceptedContentType($request) : 'text/html';
98 11
        $renderConfig = $this->registry->tag(self::class)->get($accepted);
99 11
        $render = new Render($renderConfig->renderer, ...$renderConfig->args);
100 11
        $response = $render->response($this->registry, $error);
101 11
        $response->status($code);
102
103 11
        return $response;
104
    }
105
106 11
    public function log(Throwable $exception): void
107
    {
108 11
        if ($this->registry->has(PsrLogger::class)) {
109 9
            $logger = $this->registry->get(PsrLogger::class);
110 9
            assert($logger instanceof PsrLogger);
111 9
            $method = $this->getLoggerMethod($exception);
112 9
            ([$logger, $method])('Uncaught Exception:', ['exception' => $exception]);
113
        }
114
    }
115
116 9
    protected function getLoggerMethod(Throwable $exception): string
117
    {
118 9
        return match ($exception::class) {
119 9
            HttpNotFound::class => 'info',
120 9
            HttpMethodNotAllowed::class => 'info',
121 9
            HttpForbidden::class => 'notice',
122 9
            HttpUnauthorized::class => 'notice',
123 9
            HttpBadRequest::class => 'warning',
124 9
            HttpServerError::class => 'error',
125 9
            default => 'error',
126 9
        };
127
    }
128
129 8
    protected function getAcceptedContentType(Request $request): string
130
    {
131 8
        $tag = $this->registry->tag(self::class);
132 8
        $accepted = $request->accept();
133 8
        $renderers = $tag->entries();
134 8
        $available = array_intersect($accepted, $renderers);
135
136 8
        return array_shift($available) ?? 'text/plain';
137
    }
138
}
139