Passed
Push — master ( 44459b...dc1fe0 )
by butschster
20:27 queued 10:11
created

ExceptionHandler   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 174
Duplicated Lines 0 %

Test Coverage

Coverage 80%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 32
eloc 59
c 1
b 0
f 0
dl 0
loc 174
ccs 48
cts 60
cp 0.8
rs 9.84

15 Methods

Rating   Name   Duplication   Size   Complexity  
A addRenderer() 0 3 1
A canRender() 0 3 1
A handleShutdown() 0 10 3
A handleError() 0 11 2
A report() 0 14 5
A __construct() 0 3 1
A render() 0 6 1
A getRenderer() 0 10 5
A register() 0 5 1
A bootBasicHandlers() 0 3 1
A addReporter() 0 3 1
A dontReport() 0 3 1
A shouldNotReport() 0 11 3
A setOutput() 0 3 1
A handleGlobalException() 0 18 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Exceptions;
6
7
use Closure;
8
use Spiral\Exceptions\Attribute\NonReportable;
9
use Spiral\Exceptions\Renderer\PlainRenderer;
10
use Spiral\Filters\Exception\AuthorizationException;
11
use Spiral\Filters\Exception\ValidationException;
12
use Spiral\Http\Exception\ClientException;
13
14
/**
15
 * The class is responsible for:
16
 *   - Global error handling (outside of dispatchers) using the {@see handleGlobalException()} method.
17
 *     Use the {@see register()} method to register the handler as a global exception/error catcher.
18
 *   - Runtime error handling (in a dispatcher after booting) using reporters and renderers.
19
 *     Use the {@see render()} method to prepare a formatted exception output.
20
 *     Use the {@see report()} method to send a debug information to configured channels.
21
 */
22
class ExceptionHandler implements ExceptionHandlerInterface
23
{
24
    public ?Verbosity $verbosity = Verbosity::BASIC;
25
26
    /** @var array<int, ExceptionRendererInterface> */
27
    protected array $renderers = [];
28
    /** @var array<int, ExceptionReporterInterface|Closure> */
29
    protected array $reporters = [];
30
    protected mixed $output = null;
31
    protected array $nonReportableExceptions = [
32
        ClientException::class,
33
        AuthorizationException::class,
34
        ValidationException::class,
35
    ];
36
37 459
    public function __construct()
38
    {
39 459
        $this->bootBasicHandlers();
40
    }
41
42 40
    public function register(): void
43
    {
44 40
        \register_shutdown_function($this->handleShutdown(...));
45 40
        \set_error_handler($this->handleError(...));
46 40
        \set_exception_handler($this->handleGlobalException(...));
47
    }
48
49 7
    public function getRenderer(?string $format = null): ?ExceptionRendererInterface
50
    {
51 7
        if ($format !== null) {
52 5
            foreach ($this->renderers as $renderer) {
53 5
                if ($renderer->canRender($format)) {
54 5
                    return $renderer;
55
                }
56
            }
57
        }
58 2
        return \end($this->renderers) ?: null;
59
    }
60
61 4
    public function render(
62
        \Throwable $exception,
63
        ?Verbosity $verbosity = null,
64
        string $format = null,
65
    ): string {
66 4
        return (string)$this->getRenderer($format)?->render($exception, $verbosity ?? $this->verbosity, $format);
67
    }
68
69
    public function canRender(string $format): bool
70
    {
71
        return $this->getRenderer($format) !== null;
72
    }
73
74 23
    public function report(\Throwable $exception): void
75
    {
76 23
        if ($this->shouldNotReport($exception)) {
77 14
            return;
78
        }
79
80 9
        foreach ($this->reporters as $reporter) {
81
            try {
82 3
                if ($reporter instanceof ExceptionReporterInterface) {
83 3
                    $reporter->report($exception);
84
                } else {
85 3
                    $reporter($exception);
86
                }
87 1
            } catch (\Throwable) {
88
                // Do nothing
89
            }
90
        }
91
    }
92
93 1
    public function handleGlobalException(\Throwable $e): void
94
    {
95 1
        if (\in_array(PHP_SAPI, ['cli', 'cli-server'], true)) {
96 1
            $this->output ??= \defined('STDERR') ? \STDERR : \fopen('php://stderr', 'wb+');
97 1
            $format = 'cli';
98
        } else {
99
            $this->output ??= \defined('STDOUT') ? \STDOUT : \fopen('php://stdout', 'wb+');
100
            $format = 'html';
101
        }
102
103
        // we are safe to handle global exceptions (system level) with maximum verbosity
104 1
        $this->report($e);
105
106
        // There is possible an exception on the application termination
107
        try {
108 1
            \fwrite($this->output, $this->render($e, verbosity: $this->verbosity, format: $format));
109
        } catch (\Throwable) {
110
            $this->output = null;
111
        }
112
    }
113
114
    /**
115
     * Add renderer to the beginning of the renderers list
116
     */
117 440
    public function addRenderer(ExceptionRendererInterface $renderer): void
118
    {
119 440
        \array_unshift($this->renderers, $renderer);
120
    }
121
122
    /**
123
     * @param class-string<\Throwable> $exception
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<\Throwable> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<\Throwable>.
Loading history...
124
     */
125 1
    public function dontReport(string $exception): void
126
    {
127 1
        $this->nonReportableExceptions[] = $exception;
128
    }
129
130
    /**
131
     * @param ExceptionReporterInterface|Closure(\Throwable):void $reporter
132
     */
133 15
    public function addReporter(ExceptionReporterInterface|Closure $reporter): void
134
    {
135 15
        $this->reporters[] = $reporter;
136
    }
137
138
    /**
139
     * @param resource $output
140
     */
141 1
    public function setOutput(mixed $output): void
142
    {
143 1
        $this->output = $output;
144
    }
145
146
    /**
147
     * Handle php shutdown and search for fatal errors.
148
     */
149
    protected function handleShutdown(): void
150
    {
151
        if (empty($error = \error_get_last())) {
152
            return;
153
        }
154
155
        try {
156
            $this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
157
        } catch (\Throwable $e) {
158
            $this->handleGlobalException($e);
159
        }
160
    }
161
162
    /**
163
     * Convert application error into exception.
164
     * Handler for the {@see \set_error_handler()}.
165
     * @throws \ErrorException
166
     */
167 10
    protected function handleError(
168
        int $errno,
169
        string $errstr,
170
        string $errfile = '',
171
        int $errline = 0,
172
    ): bool {
173 10
        if (!(\error_reporting() & $errno)) {
174 2
            return false;
175
        }
176
177 10
        throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
178
    }
179
180 438
    protected function bootBasicHandlers(): void
181
    {
182 438
        $this->addRenderer(new PlainRenderer());
183
    }
184
185 23
    protected function shouldNotReport(\Throwable $exception): bool
186
    {
187 23
        foreach ($this->nonReportableExceptions as $nonReportableException) {
188 23
            if ($exception instanceof $nonReportableException) {
189 13
                return true;
190
            }
191
        }
192
193 10
        $attribute = (new \ReflectionClass($exception))->getAttributes(NonReportable::class)[0] ?? null;
194
195 10
        return $attribute !== null;
196
    }
197
}
198