Passed
Pull Request — master (#25)
by Evgeniy
02:13
created

ErrorHandler::handleThrowable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 4
c 2
b 0
f 0
dl 0
loc 9
ccs 0
cts 5
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
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
final class ErrorHandler
27
{
28
    /**
29
     * @var int the size of the reserved memory. A portion of memory is pre-allocated so that
30
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
31
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
32
     * Defaults to 256KB.
33
     */
34
    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...
35
    private string $memoryReserve = '';
36
    private bool $debug = false;
37
38
    private LoggerInterface $logger;
39
    private ThrowableRendererInterface $defaultRenderer;
40
41 13
    public function __construct(LoggerInterface $logger, ThrowableRendererInterface $defaultRenderer)
42
    {
43 13
        $this->logger = $logger;
44 13
        $this->defaultRenderer = $defaultRenderer;
45 13
    }
46
47
    /**
48
     * Handles PHP execution errors such as warnings and notices.
49
     *
50
     * This method is used as a PHP error handler. It will raise an [[\ErrorException]].
51
     *
52
     * @param int $severity the level of the error raised.
53
     * @param string $message the error message.
54
     * @param string $file the filename that the error was raised in.
55
     * @param int $line the line number the error was raised at.
56
     *
57
     * @throws ErrorException
58
     */
59
    public function handleError(int $severity, string $message, string $file, int $line): void
60
    {
61
        if (!(error_reporting() & $severity)) {
62
            // This error code is not included in error_reporting
63
            return;
64
        }
65
66
        throw new ErrorException($message, $severity, $severity, $file, $line);
67
    }
68
69
    /**
70
     * Handle throwable and return output
71
     *
72
     * @param Throwable $t
73
     * @param ThrowableRendererInterface|null $renderer
74
     * @param ServerRequestInterface|null $request
75
     *
76
     * @return string
77
     */
78 10
    public function handleCaughtThrowable(
79
        Throwable $t,
80
        ThrowableRendererInterface $renderer = null,
81
        ServerRequestInterface $request = null
82
    ): string {
83 10
        if ($renderer === null) {
84 5
            $renderer = $this->defaultRenderer;
85
        }
86
87
        try {
88 10
            $this->log($t, $request);
89 10
            return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request);
90
        } catch (Throwable $t) {
91
            return (string) $t;
92
        }
93
    }
94
95
    /**
96
     * Handle throwable, echo output and exit
97
     *
98
     * @param Throwable $t
99
     */
100
    public function handleThrowable(Throwable $t): void
101
    {
102
        // disable error capturing to avoid recursive errors while handling exceptions
103
        $this->unregister();
104
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
105
        http_response_code(Status::INTERNAL_SERVER_ERROR);
106
107
        echo $this->handleCaughtThrowable($t);
108
        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...
109
    }
110
111
    /**
112
     * Enables and disables debugging mode.
113
     *
114
     * Debug mode need to enable in the design, and to disable in production.
115
     *
116
     * @param bool $enable Enable/disable debugging mode.
117
     */
118 2
    public function debug(bool $enable = true): void
119
    {
120 2
        $this->debug = $enable;
121 2
    }
122
123
    /**
124
     * Register this error handler.
125
     */
126
    public function register(): void
127
    {
128
        $this->disableDisplayErrors();
129
130
        set_exception_handler([$this, 'handleThrowable']);
131
        /** @psalm-suppress InvalidArgument */
132
        set_error_handler([$this, 'handleError']);
133
134
        if ($this->memoryReserveSize > 0) {
135
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
136
        }
137
138
        register_shutdown_function([$this, 'handleFatalError']);
139
    }
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
    public function handleFatalError(): void
151
    {
152
        unset($this->memoryReserve);
153
        $error = error_get_last();
154
155
        if ($error !== null && ErrorException::isFatalError($error)) {
156
            $exception = new ErrorException(
157
                $error['message'],
158
                $error['type'],
159
                $error['type'],
160
                $error['file'],
161
                $error['line']
162
            );
163
164
            $this->handleThrowable($exception);
165
            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...
166
        }
167
    }
168
169 10
    private function log(Throwable $t, ServerRequestInterface $request = null): void
170
    {
171 10
        $renderer = new PlainTextRenderer();
172
173 10
        $this->logger->error(
174 10
            $renderer->renderVerbose($t, $request),
175 10
            ['throwable' => $t]
176
        );
177 10
    }
178
179
    private function disableDisplayErrors(): void
180
    {
181
        if (function_exists('ini_set')) {
182
            ini_set('display_errors', '0');
183
        }
184
    }
185
}
186