Passed
Pull Request — master (#168)
by Alexander
11:02
created

ErrorHandler::withoutExposedDetails()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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