Passed
Push — master ( ab4e12...f06992 )
by Evgeniy
02:22
created

ErrorHandler::handleFatalError()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
dl 0
loc 15
ccs 0
cts 11
cp 0
rs 9.9332
c 1
b 0
f 0
cc 3
nc 2
nop 0
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler;
6
7
use Psr\Http\Message\ServerRequestInterface;
8
use Psr\Log\LoggerInterface;
9
use Throwable;
10
use Yiisoft\ErrorHandler\Exception\ErrorException;
11
use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer;
12
use Yiisoft\Http\Status;
13
14
use function error_get_last;
15
use function error_reporting;
16
use function function_exists;
17
use function ini_set;
18
use function http_response_code;
19
use function register_shutdown_function;
20
use function restore_error_handler;
21
use function restore_exception_handler;
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
The constant Yiisoft\ErrorHandler\262_144 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
38
    private string $memoryReserve = '';
39
    private bool $debug = false;
40
41
    private LoggerInterface $logger;
42
    private ThrowableRendererInterface $defaultRenderer;
43
44 16
    public function __construct(LoggerInterface $logger, ThrowableRendererInterface $defaultRenderer)
45
    {
46 16
        $this->logger = $logger;
47 16
        $this->defaultRenderer = $defaultRenderer;
48 16
    }
49
50
    /**
51
     * Handles throwable and returns error data.
52
     *
53
     * @param Throwable $t
54
     * @param ThrowableRendererInterface|null $renderer
55
     * @param ServerRequestInterface|null $request
56
     *
57
     * @return ErrorData
58
     */
59 12
    public function handleThrowable(
60
        Throwable $t,
61
        ThrowableRendererInterface $renderer = null,
62
        ServerRequestInterface $request = null
63
    ): ErrorData {
64 12
        if ($renderer === null) {
65 5
            $renderer = $this->defaultRenderer;
66
        }
67
68
        try {
69 12
            $this->logger->error((string) (new PlainTextRenderer())->renderVerbose($t, $request), ['throwable' => $t]);
70 12
            return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request);
71 4
        } catch (Throwable $t) {
72 4
            return new ErrorData((string) $t);
73
        }
74
    }
75
76
    /**
77
     * Enables and disables debug mode.
78
     *
79
     * Ensure that is is disabled in production environment since debug mode exposes sensitive details.
80
     *
81
     * @param bool $enable Enable/disable debugging mode.
82
     */
83 2
    public function debug(bool $enable = true): void
84
    {
85 2
        $this->debug = $enable;
86 2
    }
87
88
    /**
89
     * Sets the size of the reserved memory.
90
     *
91
     * @param int $size The size of the reserved memory.
92
     *
93
     * @see $memoryReserveSize
94
     */
95 5
    public function memoryReserveSize(int $size): void
96
    {
97 5
        $this->memoryReserveSize = $size;
98 5
    }
99
100
    /**
101
     * Register this error handler.
102
     */
103 1
    public function register(): void
104
    {
105
        // Disables the display of error.
106 1
        if (function_exists('ini_set')) {
107 1
            ini_set('display_errors', '0');
108
        }
109
110
        // Handles throwable, echo output and exit.
111 1
        set_exception_handler(function (Throwable $t): void {
112
            $this->renderThrowableAndTerminate($t);
113 1
        });
114
115
        // Handles PHP execution errors such as warnings and notices.
116 1
        set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
117 1
            if (!(error_reporting() & $severity)) {
118
                // This error code is not included in error_reporting.
119
                return true;
120
            }
121
122 1
            throw new ErrorException($message, $severity, $severity, $file, $line);
123 1
        });
124
125 1
        if ($this->memoryReserveSize > 0) {
126
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
127
        }
128
129
        // Handles fatal error.
130 1
        register_shutdown_function(function (): void {
131
            $this->memoryReserve = '';
132
            $e = error_get_last();
133
134
            if ($e !== null && ErrorException::isFatalError($e)) {
135
                $error = new ErrorException($e['message'], $e['type'], $e['type'], $e['file'], $e['line']);
136
                $this->renderThrowableAndTerminate($error);
137
            }
138 1
        });
139 1
    }
140
141
    /**
142
     * Unregisters this error handler by restoring the PHP error and exception handlers.
143
     */
144
    public function unregister(): void
145
    {
146
        restore_error_handler();
147
        restore_exception_handler();
148
    }
149
150
    /**
151
     * Renders the throwable and terminates the script.
152
     *
153
     * @param Throwable $t
154
     */
155
    private function renderThrowableAndTerminate(Throwable $t): void
156
    {
157
        // disable error capturing to avoid recursive errors while handling exceptions
158
        $this->unregister();
159
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
160
        http_response_code(Status::INTERNAL_SERVER_ERROR);
161
162
        echo $this->handleThrowable($t);
163
        exit(1);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
164
    }
165
}
166