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

ErrorHandler::disableDisplayErrors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 1
b 0
f 0
cc 2
nc 2
nop 0
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 $exposeDetails = 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->exposeDetails ? $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
        $exposeDetails = $this->exposeDetails;
104
        $this->unregister();
105
        $this->exposeDetails = $exposeDetails;
106
107
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
108
        http_response_code(Status::INTERNAL_SERVER_ERROR);
109
110
        echo $this->handleCaughtThrowable($t);
111
        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...
112
    }
113
114
    /**
115
     * Register this error handler.
116
     *
117
     * @param bool $exposeDetails
118
     */
119 2
    public function register(bool $exposeDetails = false): void
120
    {
121 2
        $this->exposeDetails = $exposeDetails;
122 2
        $this->disableDisplayErrors();
123
124 2
        set_exception_handler([$this, 'handleThrowable']);
125
        /** @psalm-suppress InvalidArgument */
126 2
        set_error_handler([$this, 'handleError']);
127
128 2
        if ($this->memoryReserveSize > 0) {
129 2
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
130
        }
131
132 2
        register_shutdown_function([$this, 'handleFatalError']);
133 2
    }
134
135
    /**
136
     * Unregisters this error handler by restoring the PHP error and exception handlers.
137
     */
138 2
    public function unregister(): void
139
    {
140 2
        restore_error_handler();
141 2
        restore_exception_handler();
142 2
        $this->exposeDetails = false;
143 2
    }
144
145
    public function handleFatalError(): void
146
    {
147
        unset($this->memoryReserve);
148
        $error = error_get_last();
149
150
        if ($error !== null && ErrorException::isFatalError($error)) {
151
            $exception = new ErrorException(
152
                $error['message'],
153
                $error['type'],
154
                $error['type'],
155
                $error['file'],
156
                $error['line']
157
            );
158
159
            $this->handleThrowable($exception);
160
            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...
161
        }
162
    }
163
164 10
    private function log(Throwable $t, ServerRequestInterface $request = null): void
165
    {
166 10
        $renderer = new PlainTextRenderer();
167
168 10
        $this->logger->error(
169 10
            $renderer->renderVerbose($t, $request),
170 10
            ['throwable' => $t]
171
        );
172 10
    }
173
174 2
    private function disableDisplayErrors(): void
175
    {
176 2
        if (function_exists('ini_set')) {
177 2
            ini_set('display_errors', '0');
178
        }
179 2
    }
180
}
181