1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Contract\Core\Exception\Exception;
6: use Salient\Contract\Core\Facade\FacadeAwareInterface;
7: use Salient\Contract\Core\Instantiable;
8: use Salient\Contract\Core\Unloadable;
9: use Salient\Core\Concern\FacadeAwareTrait;
10: use Salient\Core\Facade\Console;
11: use Salient\Utility\File;
12: use Salient\Utility\Regex;
13: use ErrorException;
14: use LogicException;
15: use Throwable;
16:
17: /**
18: * @api
19: *
20: * @implements FacadeAwareInterface<self>
21: */
22: final class ErrorHandler implements FacadeAwareInterface, Instantiable, Unloadable
23: {
24: /** @use FacadeAwareTrait<self> */
25: use FacadeAwareTrait;
26:
27: private const DEFAULT_EXIT_STATUS = 16;
28:
29: private const FATAL_ERRORS =
30: \E_ERROR
31: | \E_PARSE
32: | \E_CORE_ERROR
33: | \E_CORE_WARNING
34: | \E_COMPILE_ERROR
35: | \E_COMPILE_WARNING;
36:
37: /**
38: * [ [ Path regex, levels ], ... ]
39: *
40: * @var array<array{string,int}>
41: */
42: private array $Silenced = [];
43:
44: private int $ExitStatus = 0;
45: private bool $IsRegistered = false;
46: private bool $ShutdownIsRegistered = false;
47: private bool $IsShuttingDown = false;
48: private bool $IsShuttingDownOnFatalError = false;
49: private bool $IsShuttingDownOnUncaughtException = false;
50: private bool $IsShuttingDownOnExitSignal = false;
51:
52: /**
53: * @internal
54: */
55: public function __construct() {}
56:
57: /**
58: * @internal
59: */
60: public function unload(): void
61: {
62: $this->deregister();
63: }
64:
65: /**
66: * Register error, exception and shutdown handlers
67: *
68: * @return $this
69: */
70: public function register(): self
71: {
72: if ($this->IsRegistered) {
73: return $this;
74: }
75:
76: set_exception_handler([$this, 'handleException']);
77: set_error_handler([$this, 'handleError']);
78:
79: $this->IsRegistered = true;
80:
81: if ($this->ShutdownIsRegistered) {
82: return $this;
83: }
84:
85: register_shutdown_function([$this, 'handleShutdown']);
86:
87: $this->ShutdownIsRegistered = true;
88:
89: return $this;
90: }
91:
92: /**
93: * Check if error, exception and shutdown handlers are registered
94: */
95: public function isRegistered(): bool
96: {
97: return $this->IsRegistered;
98: }
99:
100: /**
101: * Check if the running script is terminating
102: */
103: public function isShuttingDown(): bool
104: {
105: return $this->IsShuttingDown;
106: }
107:
108: /**
109: * Check if the running script is terminating after a fatal error, uncaught
110: * exception or exit signal
111: */
112: public function isShuttingDownOnError(): bool
113: {
114: return $this->IsShuttingDownOnFatalError
115: || $this->IsShuttingDownOnUncaughtException
116: || $this->IsShuttingDownOnExitSignal;
117: }
118:
119: /**
120: * Get the exit status of the running script if it is terminating
121: *
122: * @throws LogicException if the running script is not terminating.
123: */
124: public function getExitStatus(): int
125: {
126: if (!$this->IsShuttingDown) {
127: throw new LogicException('Script is not terminating');
128: }
129: return $this->ExitStatus;
130: }
131:
132: /**
133: * Deregister previously registered error and exception handlers
134: *
135: * @return $this
136: */
137: public function deregister(): self
138: {
139: if ($this->IsRegistered) {
140: restore_error_handler();
141: restore_exception_handler();
142:
143: $this->IsRegistered = false;
144: }
145:
146: $this->unloadFacades();
147:
148: return $this;
149: }
150:
151: /**
152: * Silence errors in a file or directory
153: *
154: * @return $this
155: */
156: public function silencePath(string $path, int $levels = \E_DEPRECATED | \E_USER_DEPRECATED): self
157: {
158: if (file_exists($path)) {
159: $path = File::realpath($path);
160: $this->silencePattern(
161: '@^' . Regex::quote($path, '@') . (is_dir($path) ? '/' : '$') . '@D',
162: $levels,
163: );
164: }
165: return $this;
166: }
167:
168: /**
169: * Silence errors in paths that match a regular expression
170: *
171: * @return $this
172: */
173: public function silencePattern(string $pattern, int $levels = \E_DEPRECATED | \E_USER_DEPRECATED): self
174: {
175: $entry = [$pattern, $levels];
176: if (!in_array($entry, $this->Silenced, true)) {
177: $this->Silenced[] = $entry;
178: }
179: return $this;
180: }
181:
182: /**
183: * @internal
184: */
185: public function handleShutdown(): void
186: {
187: // Shutdown functions can't be deregistered, so do nothing if this
188: // instance has been deregistered
189: if (!$this->IsRegistered) {
190: return;
191: }
192:
193: $this->IsShuttingDown = true;
194:
195: $error = error_get_last();
196: if ($error && ($error['type'] & self::FATAL_ERRORS)) {
197: $this->IsShuttingDownOnFatalError = true;
198: $this->ExitStatus = 255;
199: $this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
200: }
201: }
202:
203: /**
204: * @internal
205: */
206: public function handleError(int $level, string $message, string $file, int $line): bool
207: {
208: // Leave errors that would otherwise be silenced alone
209: if (!($level & error_reporting())) {
210: return false;
211: }
212:
213: // Ignore explicitly silenced errors
214: foreach ($this->Silenced as [$pattern, $levels]) {
215: if (
216: ($levels & $level)
217: && Regex::match($pattern, $file)
218: ) {
219: return true;
220: }
221: }
222:
223: // Convert the error to an exception
224: $exception = new ErrorException($message, 0, $level, $file, $line);
225:
226: if ($this->IsShuttingDown) {
227: $this->handleException($exception);
228: return true;
229: }
230:
231: throw $exception;
232: }
233:
234: /**
235: * @internal
236: */
237: public function handleException(Throwable $exception): void
238: {
239: if ($this->IsShuttingDown) {
240: Console::exception($exception);
241: return;
242: }
243:
244: $this->IsShuttingDown = true;
245: $this->IsShuttingDownOnUncaughtException = true;
246: if ($exception instanceof Exception) {
247: $exitStatus = $exception->getExitStatus();
248: }
249: $this->ExitStatus = $exitStatus ??= self::DEFAULT_EXIT_STATUS;
250: Console::exception($exception);
251: exit($exitStatus);
252: }
253:
254: /**
255: * Report the exit status of the running script before it terminates on
256: * SIGTERM, SIGINT or SIGHUP
257: *
258: * @throws LogicException if the instance is not registered to handle errors
259: * and exceptions.
260: */
261: public function handleExitSignal(int $exitStatus): void
262: {
263: if (!$this->IsRegistered) {
264: throw new LogicException(sprintf('%s is not registered', static::class));
265: }
266:
267: if ($this->IsShuttingDown) {
268: return;
269: }
270:
271: $this->IsShuttingDown = true;
272: $this->IsShuttingDownOnExitSignal = true;
273: $this->ExitStatus = $exitStatus;
274: }
275: }
276: