Test Failed
Pull Request — master (#952)
by Maxim
16:49 queued 07:48
created

ExceptionHandler::report()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

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