Passed
Push — main ( d3d78c...a401b7 )
by Thomas
03:23
created

Handler::getError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 2

Importance

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