Issues (7)

src/Runner.php (1 issue)

Severity
1
<?php
2
3
/**
4
 * This file is part of error-handler
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Slick\ErrorHandler;
13
14
use ErrorException;
15
use Slick\ErrorHandler\Exception\ExceptionInspector;
16
use Slick\ErrorHandler\Handler\HandlerInterface;
17
use Slick\ErrorHandler\Util\SystemFacade;
18
use Throwable;
19
20
/**
21
 * Runner
22
 *
23
 * @package Slick\ErrorHandler
24
 */
25
final class Runner implements RunnerInterface
26
{
27
28
    /** @var array<callable|HandlerInterface> */
29
    private array $handlers = [];
30
31
    public function __construct(private readonly SystemFacade $system, private readonly ?string $path = '/')
32
    {
33
    }
34
35
    /**
36
     * @inheritDoc
37
     */
38
    public function pushHandler(callable|HandlerInterface $handler): self
39
    {
40
        $this->handlers[] = $handler;
41
        return $this;
42
    }
43
44
    /**
45
     * @inheritDoc
46
     */
47
    public function getHandlers(): array
48
    {
49
        return $this->handlers;
50
    }
51
52
    /**
53
     * @inheritDoc
54
     */
55
    public function clearHandlers(): self
56
    {
57
        $this->handlers = [];
58
        return $this;
59
    }
60
61
    /**
62
     * @inheritDoc
63
     */
64
    public function register(): self
65
    {
66
        $this->system->setExceptionHandler([$this, 'handleException']);
67
        $this->system->setErrorHandler([$this, 'handleError']);
68
        $this->system->registerShutdownFunction([$this, 'handleShutdown']);
69
        return $this;
70
    }
71
72
    /**
73
     * @inheritDoc
74
     */
75
    public function unregister(): self
76
    {
77
        $this->system->restoreExceptionHandler();
78
        $this->system->restoreErrorHandler();
79
        return $this;
80
    }
81
82
    /**
83
     * @inheritDoc
84
     */
85
    public function handleException(Throwable $exception): string
86
    {
87
        $inspector = new ExceptionInspector($exception, (string) $this->path);
88
        $this->system->startOutputBuffering();
89
        // Just in case there are no handlers:
90
        $handlerResponse = null;
91
92
        try {
93
            foreach (array_reverse($this->handlers) as $handler) {
94
                $handlerResponse = is_callable($handler)
95
                    ? $handler($exception, $inspector, $this)
96
                    : $handler->handle($exception, $inspector, $this);
97
98
                if (in_array($handlerResponse, [HandlerInterface::LAST_HANDLER, HandlerInterface::QUIT])) {
99
                    break;
100
                }
101
            }
102
        } finally {
103
            $output = $this->system->cleanOutputBuffer();
104
        }
105
106
        if ($handlerResponse === HandlerInterface::QUIT) {
107
            // Cleanup all other output buffers before sending our output:
108
            while ($this->system->getOutputBufferLevel() > 0) {
109
                $this->system->endOutputBuffering();
110
            }
111
112
            echo $output;
113
            $this->system->flushOutputBuffer();
114
            $this->system->stopExecution(1);
115
        }
116
117
        return is_string($output) ? $output : '';
0 ignored issues
show
The condition is_string($output) is always true.
Loading history...
118
    }
119
120
    /**
121
     * @inheritDoc
122
     */
123
    public function handleError(int $level, string $message, ?string $file = null, ?int $line = null): bool
124
    {
125
        $shouldHandle = $level & $this->system->getErrorReportingLevel();
126
        if (!($shouldHandle)) {
127
            // Propagate error to the next handler, allows error_get_last() to
128
            // work on silenced errors.
129
            return false;
130
        }
131
132
        $exception = new ErrorException($this->clearMessage($message), 0, $level, $file, $line);
133
        $this->handleException($exception);
134
        return true;
135
    }
136
137
    /**
138
     * @inheritDoc
139
     */
140
    public function handleShutdown(): void
141
    {
142
        $error = $this->system->getLastError();
143
        if ($error && $this->isLevelFatal($error['type'])) {
144
            $this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
145
        }
146
    }
147
148
    /**
149
     * Determine if an error level is fatal (halts execution)
150
     *
151
     * @param int $level
152
     * @return bool
153
     */
154
    private function isLevelFatal($level): bool
155
    {
156
        $errors = E_ERROR;
157
        $errors |= E_PARSE;
158
        $errors |= E_CORE_ERROR;
159
        $errors |= E_CORE_WARNING;
160
        $errors |= E_COMPILE_ERROR;
161
        $errors |= E_COMPILE_WARNING;
162
        return ($level & $errors) > 0;
163
    }
164
165
    /**
166
     * @inheritDoc
167
     * @param array<string, string> $headers
168
     */
169
    public function outputHeaders(array $headers): void
170
    {
171
        $this->system->sendHeaders($headers);
172
    }
173
174
    public function sendResponseCode(int $code): void
175
    {
176
        $this->system->setHttpResponseCode($code);
177
    }
178
179
    private function clearMessage(string $message): string
180
    {
181
        $parts = explode("in /", $message, 2);
182
        return trim($parts[0]);
183
    }
184
}
185