Passed
Pull Request — master (#89)
by Sergei
03:18 queued 55s
created

ErrorHandler::handle()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 4
nop 3
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler;
6
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Log\LoggerInterface;
10
use Throwable;
11
use Yiisoft\ErrorHandler\Event\ApplicationError;
12
use Yiisoft\ErrorHandler\Exception\ErrorException;
13
use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer;
14
use Yiisoft\Http\Status;
15
16
use function error_get_last;
17
use function error_reporting;
18
use function function_exists;
19
use function http_response_code;
20
use function ini_set;
21
use function register_shutdown_function;
22
use function set_error_handler;
23
use function set_exception_handler;
24
use function str_repeat;
25
26
/**
27
 * `ErrorHandler` handles out of memory errors, fatals, warnings, notices and exceptions.
28
 */
29
final class ErrorHandler
30
{
31
    /**
32
     * @var int The size of the reserved memory. A portion of memory is pre-allocated so that
33
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
34
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
35
     * Defaults to 256KB.
36
     */
37
    private int $memoryReserveSize = 262_144;
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ';' on line 37 at column 40
Loading history...
38
    private string $memoryReserve = '';
39
    private bool $debug = false;
40
    private ?string $workingDirectory = null;
41
    private bool $enabled = false;
42
    private bool $initialized = false;
43
44 19
    public function __construct(
45
        private LoggerInterface $logger,
46
        private ThrowableRendererInterface $defaultRenderer,
47
        private ?EventDispatcherInterface $eventDispatcher = null,
48
    ) {
49 19
    }
50
51
    /**
52
     * Handles throwable and returns error data.
53
     *
54
     * @param ThrowableRendererInterface|null $renderer
55
     * @param ServerRequestInterface|null $request
56
     */
57 14
    public function handle(
58
        Throwable $t,
59
        ThrowableRendererInterface $renderer = null,
60
        ServerRequestInterface $request = null
61
    ): ErrorData {
62 14
        $renderer ??= $this->defaultRenderer;
63
64
        try {
65 14
            $this->logger->error((string) (new PlainTextRenderer())->renderVerbose($t, $request), ['throwable' => $t]);
66 14
            return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request);
67 4
        } catch (Throwable $t) {
68 4
            return new ErrorData((string) $t);
69
        }
70
    }
71
72
    /**
73
     * Enables and disables debug mode.
74
     *
75
     * Ensure that is is disabled in production environment since debug mode exposes sensitive details.
76
     *
77
     * @param bool $enable Enable/disable debugging mode.
78
     */
79 2
    public function debug(bool $enable = true): void
80
    {
81 2
        $this->debug = $enable;
82
    }
83
84
    /**
85
     * Sets the size of the reserved memory.
86
     *
87
     * @param int $size The size of the reserved memory.
88
     *
89
     * @see $memoryReserveSize
90
     */
91 6
    public function memoryReserveSize(int $size): void
92
    {
93 6
        $this->memoryReserveSize = $size;
94
    }
95
96
    /**
97
     * Register PHP exception and error handlers and enable this error handler.
98
     */
99 3
    public function register(): void
100
    {
101 2
        if ($this->enabled) {
102
            return;
103
        }
104
105 2
        if ($this->memoryReserveSize > 0) {
106
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
107
        }
108
109 2
        $this->initializeOnce();
110
111
        // Handles throwable, echo output and exit.
112 2
        set_exception_handler(function (Throwable $t): void {
113
            if (!$this->enabled) {
114
                return;
115
            }
116
117
            $this->renderThrowableAndTerminate($t);
118 2
        });
119
120
        // Handles PHP execution errors such as warnings and notices.
121 2
        set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
122 3
            if (!$this->enabled) {
123 1
                return false;
124
            }
125
126 2
            if (!(error_reporting() & $severity)) {
127
                // This error code is not included in error_reporting.
128
                return true;
129
            }
130
131 2
            throw new ErrorException($message, $severity, $severity, $file, $line);
132 2
        });
133
134 2
        $this->enabled = true;
135
    }
136
137
    /**
138
     * Disable this error handler.
139
     */
140 1
    public function unregister(): void
141
    {
142 1
        if (!$this->enabled) {
143
            return;
144
        }
145
146 1
        $this->memoryReserve = '';
147
148 1
        $this->enabled = false;
149
    }
150
151 2
    private function initializeOnce(): void
152
    {
153 2
        if ($this->initialized) {
154
            return;
155
        }
156
157
        // Disables the display of error.
158 2
        if (function_exists('ini_set')) {
159 2
            ini_set('display_errors', '0');
160
        }
161
162
        // Handles fatal error.
163 2
        register_shutdown_function(function (): void {
164
            if (!$this->enabled) {
165
                return;
166
            }
167
168
            $this->memoryReserve = '';
169
            $e = error_get_last();
170
171
            if ($e !== null && ErrorException::isFatalError($e)) {
172
                $error = new ErrorException($e['message'], $e['type'], $e['type'], $e['file'], $e['line']);
173
                $this->renderThrowableAndTerminate($error);
174
            }
175 2
        });
176
177 2
        if (!(PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) {
178
            $this->workingDirectory = getcwd();
179
        }
180
181 2
        $this->initialized = true;
182
    }
183
184
    /**
185
     * Renders the throwable and terminates the script.
186
     */
187
    private function renderThrowableAndTerminate(Throwable $t): void
188
    {
189
        if (!empty($this->workingDirectory)) {
190
            chdir($this->workingDirectory);
191
        }
192
        // disable error capturing to avoid recursive errors while handling exceptions
193
        $this->unregister();
194
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
195
        http_response_code(Status::INTERNAL_SERVER_ERROR);
196
197
        echo $this->handle($t);
198
        $this->eventDispatcher?->dispatch(new ApplicationError($t));
199
200
        register_shutdown_function(static function (): void {
201
            exit(1);
202
        });
203
    }
204
}
205