Handler::emitException()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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