Passed
Pull Request — master (#326)
by Dmitriy
23:25 queued 08:25
created

ErrorHandler::withoutExposedDetails()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\ErrorHandler;
6
7
use Psr\Log\LoggerAwareInterface;
8
use Psr\Log\LoggerAwareTrait;
9
use Psr\Log\LoggerInterface;
10
use Yiisoft\Http\Status;
11
12
final class ErrorHandler implements LoggerAwareInterface
13
{
14
    use LoggerAwareTrait;
15
16
    /**
17
     * @var int the size of the reserved memory. A portion of memory is pre-allocated so that
18
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
19
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
20
     * Defaults to 256KB.
21
     */
22
    private int $memoryReserveSize = 262_144;
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Yii\Web\ErrorHandler\262_144 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
23
    private string $memoryReserve = '';
24
    private bool $exposeDetails = true;
25 18
26
    private ThrowableRendererInterface $defaultRenderer;
27 18
28 18
    public function __construct(LoggerInterface $logger, ThrowableRendererInterface $defaultRenderer)
29 18
    {
30
        $this->logger = $logger;
31
        $this->defaultRenderer = $defaultRenderer;
32
    }
33
34
    /**
35
     * Handles PHP execution errors such as warnings and notices.
36
     *
37
     * This method is used as a PHP error handler. It will raise an [[\ErrorException]].
38
     *
39
     * @param int $severity the level of the error raised.
40
     * @param string $message the error message.
41
     * @param string $file the filename that the error was raised in.
42
     * @param int $line the line number the error was raised at.
43
     *
44
     * @throws ErrorException
45
     */
46
    public function handleError(int $severity, string $message, string $file, int $line): void
47
    {
48
        if (!(error_reporting() & $severity)) {
49
            // This error code is not included in error_reporting
50
            return;
51
        }
52
53
        throw new ErrorException($message, $severity, $severity, $file, $line);
54
    }
55
56
    /**
57
     * Handle throwable and return output
58
     *
59
     * @param \Throwable $t
60 10
     * @param ThrowableRendererInterface|null $renderer
61
     * @return string
62 10
     */
63 5
    public function handleCaughtThrowable(\Throwable $t, ThrowableRendererInterface $renderer = null): string
64
    {
65
        if ($renderer === null) {
66
            $renderer = $this->defaultRenderer;
67 10
        }
68 10
69
        try {
70
            $this->log($t);
71
            return $this->exposeDetails ? $renderer->renderVerbose($t) : $renderer->render($t);
72
        } catch (\Throwable $t) {
73
            return (string)$t;
74
        }
75
    }
76
77
    /**
78
     * Handle throwable, echo output and exit
79
     *
80
     * @param \Throwable $t
81
     */
82
    public function handleThrowable(\Throwable $t): void
83
    {
84
        // disable error capturing to avoid recursive errors while handling exceptions
85
        $this->unregister();
86
87
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
88
        http_response_code(Status::INTERNAL_SERVER_ERROR);
89
90
        echo $this->handleCaughtThrowable($t);
91
        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...
92
    }
93
94 1
    /**
95
     * Register this error handler.
96 1
     */
97 1
    public function register(): void
98
    {
99 1
        $this->disableDisplayErrors();
100
        set_exception_handler([$this, 'handleThrowable']);
101 1
        /** @psalm-suppress InvalidArgument */
102 1
        set_error_handler([$this, 'handleError']);
103
104 1
        if ($this->memoryReserveSize > 0) {
105 1
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
106
        }
107 1
        register_shutdown_function([$this, 'handleFatalError']);
108
    }
109 1
110 1
    private function disableDisplayErrors(): void
111
    {
112 1
        if (function_exists('ini_set')) {
113
            ini_set('display_errors', '0');
114
        }
115
    }
116
117
    /**
118
     * Unregisters this error handler by restoring the PHP error and exception handlers.
119
     */
120
    public function unregister(): void
121
    {
122
        restore_error_handler();
123
        restore_exception_handler();
124
    }
125
126
    public function handleFatalError(): void
127
    {
128
        unset($this->memoryReserve);
129
        $error = error_get_last();
130
        if ($error !== null && ErrorException::isFatalError($error)) {
131
            $exception = new ErrorException(
132
                $error['message'],
133
                $error['type'],
134
                $error['type'],
135
                $error['file'],
136
                $error['line']
137
            );
138
            $this->handleThrowable($exception);
139
            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...
140 10
        }
141
    }
142 10
143 10
    private function log(\Throwable $t/*, ServerRequestInterface $request*/): void
144 10
    {
145
        $renderer = new PlainTextRenderer();
146 10
        $this->logger->error(
147
            $renderer->renderVerbose($t),
148
            [
149
                'throwable' => $t,
150 10
                //'request' => $request,
151
            ]
152 1
        );
153
    }
154 1
155 1
    public function withExposedDetails(): self
156 1
    {
157
        $new = clone $this;
158
        $new->exposeDetails = true;
159 1
        return $new;
160
    }
161 1
162 1
    public function withoutExposedDetails(): self
163 1
    {
164
        $new = clone $this;
165
        $new->exposeDetails = false;
166
        return $new;
167
    }
168
169
    public function setRenderer(ThrowableRendererInterface $defaultRenderer): void
170
    {
171
        $this->defaultRenderer = $defaultRenderer;
172
    }
173
}
174