Passed
Pull Request — master (#1044)
by Maxim
19:32
created

ExceptionHandler::shouldNotReport()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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