1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | /* |
||
6 | * This file is part of the Micro framework package. |
||
7 | * |
||
8 | * (c) Stanislau Komar <[email protected]> |
||
9 | * |
||
10 | * For the full copyright and license information, please view the LICENSE |
||
11 | * file that was distributed with this source code. |
||
12 | */ |
||
13 | |||
14 | namespace Micro\Plugin\Http\Exception; |
||
15 | |||
16 | use Symfony\Component\HttpFoundation\Response; |
||
17 | |||
18 | /** |
||
19 | * @author Stanislau Komar <[email protected]> |
||
20 | */ |
||
21 | class FlattenException |
||
22 | { |
||
23 | private string $message; |
||
24 | private string|int $code; |
||
25 | private ?self $previous = null; |
||
26 | private array $trace; |
||
27 | private string $traceAsString; |
||
28 | private string $class; |
||
29 | private int $statusCode; |
||
30 | private string $statusText; |
||
31 | /** |
||
32 | * @var array<string, string> |
||
33 | */ |
||
34 | private array $headers; |
||
35 | private string $file; |
||
36 | private int $line; |
||
37 | private ?string $asString = null; |
||
38 | |||
39 | 1 | public static function create(\Exception $exception, array $headers = []): static |
|
40 | { |
||
41 | 1 | return static::createFromThrowable($exception, $headers); |
|
42 | } |
||
43 | |||
44 | 22 | public static function createFromThrowable(\Throwable $exception, array $headers = []): static |
|
45 | { |
||
46 | 22 | $e = new static(); |
|
47 | |||
48 | 22 | $realException = $exception; |
|
49 | 22 | if ($exception instanceof HttpInternalServerException) { |
|
50 | 12 | if (null !== $exception->getPrevious()) { |
|
51 | 12 | $realException = $exception->getPrevious(); |
|
52 | } |
||
53 | } |
||
54 | |||
55 | 22 | $statusCode = $exception->getCode(); |
|
56 | |||
57 | 22 | $e->setMessage($realException->getMessage()); |
|
58 | 22 | $e->setCode($statusCode); |
|
59 | |||
60 | 22 | if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) { |
|
61 | 18 | $statusText = Response::$statusTexts[$statusCode]; |
|
62 | } else { |
||
63 | 10 | $statusText = 'Whoops, looks like something went wrong.'; |
|
64 | } |
||
65 | |||
66 | 22 | $e->setStatusText($statusText); |
|
67 | 22 | $e->setStatusCode($statusCode); |
|
68 | 22 | $e->setHeaders($headers); |
|
69 | 22 | $e->setTraceFromThrowable($realException); |
|
70 | 22 | $e->setClass(get_debug_type($realException)); |
|
71 | 22 | $e->setFile($realException->getFile()); |
|
72 | 22 | $e->setLine($realException->getLine()); |
|
73 | |||
74 | 22 | $previous = $realException->getPrevious(); |
|
75 | |||
76 | 22 | if ($previous instanceof \Throwable) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
77 | 8 | $e->setPrevious(static::createFromThrowable($previous)); |
|
78 | } |
||
79 | |||
80 | 22 | return $e; |
|
81 | } |
||
82 | |||
83 | 18 | public function toArray(): array |
|
84 | { |
||
85 | 18 | $exceptions = []; |
|
86 | 18 | foreach (array_merge([$this], $this->getAllPrevious()) as $exception) { |
|
87 | 18 | $exceptions[] = [ |
|
88 | 18 | 'message' => $exception->getMessage(), |
|
89 | 18 | 'class' => $exception->getClass(), |
|
90 | 18 | 'trace' => $exception->getTrace(), |
|
91 | 18 | ]; |
|
92 | } |
||
93 | |||
94 | 18 | return $exceptions; |
|
95 | } |
||
96 | |||
97 | 1 | public function getStatusCode(): int |
|
98 | { |
||
99 | 1 | return $this->statusCode; |
|
100 | } |
||
101 | |||
102 | /** |
||
103 | * @return $this |
||
104 | */ |
||
105 | 22 | public function setStatusCode(int $code): static |
|
106 | { |
||
107 | 22 | $this->statusCode = $code; |
|
108 | |||
109 | 22 | return $this; |
|
110 | } |
||
111 | |||
112 | 1 | public function getHeaders(): array |
|
113 | { |
||
114 | 1 | return $this->headers; |
|
115 | } |
||
116 | |||
117 | /** |
||
118 | * @return $this |
||
119 | */ |
||
120 | 22 | public function setHeaders(array $headers): static |
|
121 | { |
||
122 | 22 | $this->headers = $headers; |
|
123 | |||
124 | 22 | return $this; |
|
125 | } |
||
126 | |||
127 | 19 | public function getClass(): string |
|
128 | { |
||
129 | 19 | return $this->class; |
|
130 | } |
||
131 | |||
132 | /** |
||
133 | * @return $this |
||
134 | */ |
||
135 | 22 | public function setClass(string $class): static |
|
136 | { |
||
137 | 22 | $this->class = str_contains($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class; |
|
138 | |||
139 | 22 | return $this; |
|
140 | } |
||
141 | |||
142 | 1 | public function getFile(): string |
|
143 | { |
||
144 | 1 | return $this->file; |
|
145 | } |
||
146 | |||
147 | /** |
||
148 | * @return $this |
||
149 | */ |
||
150 | 22 | public function setFile(string $file): static |
|
151 | { |
||
152 | 22 | $this->file = $file; |
|
153 | |||
154 | 22 | return $this; |
|
155 | } |
||
156 | |||
157 | 1 | public function getLine(): int |
|
158 | { |
||
159 | 1 | return $this->line; |
|
160 | } |
||
161 | |||
162 | /** |
||
163 | * @return $this |
||
164 | */ |
||
165 | 22 | public function setLine(int $line): static |
|
166 | { |
||
167 | 22 | $this->line = $line; |
|
168 | |||
169 | 22 | return $this; |
|
170 | } |
||
171 | |||
172 | 11 | public function getStatusText(): string |
|
173 | { |
||
174 | 11 | return $this->statusText; |
|
175 | } |
||
176 | |||
177 | /** |
||
178 | * @return $this |
||
179 | */ |
||
180 | 22 | public function setStatusText(string $statusText): static |
|
181 | { |
||
182 | 22 | $this->statusText = $statusText; |
|
183 | |||
184 | 22 | return $this; |
|
185 | } |
||
186 | |||
187 | 20 | public function getMessage(): string |
|
188 | { |
||
189 | 20 | return $this->message; |
|
190 | } |
||
191 | |||
192 | /** |
||
193 | * @return $this |
||
194 | */ |
||
195 | 23 | public function setMessage(string $message): static |
|
196 | { |
||
197 | 23 | if (str_contains($message, "@anonymous\0")) { |
|
198 | 1 | $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { |
|
199 | 1 | return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; |
|
200 | 1 | }, $message); |
|
201 | } |
||
202 | |||
203 | 23 | $this->message = $message; |
|
204 | |||
205 | 23 | return $this; |
|
206 | } |
||
207 | |||
208 | /** |
||
209 | * @return int|string int most of the time (might be a string with PDOException) |
||
210 | */ |
||
211 | 10 | public function getCode(): int|string |
|
212 | { |
||
213 | 10 | return $this->code; |
|
214 | } |
||
215 | |||
216 | /** |
||
217 | * @return $this |
||
218 | */ |
||
219 | 22 | public function setCode(int|string $code): static |
|
220 | { |
||
221 | 22 | $this->code = $code; |
|
222 | |||
223 | 22 | return $this; |
|
224 | } |
||
225 | |||
226 | 19 | public function getPrevious(): ?self |
|
227 | { |
||
228 | 19 | return $this->previous; |
|
229 | } |
||
230 | |||
231 | /** |
||
232 | * @return $this |
||
233 | */ |
||
234 | 8 | public function setPrevious(?self $previous): static |
|
235 | { |
||
236 | 8 | $this->previous = $previous; |
|
237 | |||
238 | 8 | return $this; |
|
239 | } |
||
240 | |||
241 | /** |
||
242 | * @return self[] |
||
243 | */ |
||
244 | 19 | public function getAllPrevious(): array |
|
245 | { |
||
246 | 19 | $exceptions = []; |
|
247 | 19 | $e = $this; |
|
248 | 19 | while ($e = $e->getPrevious()) { |
|
249 | 7 | $exceptions[] = $e; |
|
250 | } |
||
251 | |||
252 | 19 | return $exceptions; |
|
253 | } |
||
254 | |||
255 | 23 | public function getTrace(): array |
|
256 | { |
||
257 | 23 | return $this->trace; |
|
258 | } |
||
259 | |||
260 | /** |
||
261 | * @return $this |
||
262 | */ |
||
263 | 22 | public function setTraceFromThrowable(\Throwable $throwable): static |
|
264 | { |
||
265 | 22 | $this->traceAsString = $throwable->getTraceAsString(); |
|
266 | |||
267 | 22 | return $this->setTrace($throwable->getTrace(), $throwable->getFile(), $throwable->getLine()); |
|
268 | } |
||
269 | |||
270 | /** |
||
271 | * @return $this |
||
272 | */ |
||
273 | 27 | public function setTrace(array $trace, ?string $file, ?int $line): static |
|
274 | { |
||
275 | 27 | $this->trace = []; |
|
276 | 27 | $this->trace[] = [ |
|
277 | 27 | 'namespace' => '', |
|
278 | 27 | 'short_class' => '', |
|
279 | 27 | 'class' => '', |
|
280 | 27 | 'type' => '', |
|
281 | 27 | 'function' => '', |
|
282 | 27 | 'file' => $file, |
|
283 | 27 | 'line' => $line, |
|
284 | 27 | 'args' => [], |
|
285 | 27 | ]; |
|
286 | 27 | foreach ($trace as $entry) { |
|
287 | 27 | $class = ''; |
|
288 | 27 | $namespace = ''; |
|
289 | 27 | if (isset($entry['class'])) { |
|
290 | 27 | $parts = explode('\\', $entry['class']); |
|
291 | 27 | $class = array_pop($parts); |
|
292 | 27 | $namespace = implode('\\', $parts); |
|
293 | } |
||
294 | |||
295 | 27 | $this->trace[] = [ |
|
296 | 27 | 'namespace' => $namespace, |
|
297 | 27 | 'short_class' => $class, |
|
298 | 27 | 'class' => $entry['class'] ?? '', |
|
299 | 27 | 'type' => $entry['type'] ?? '', |
|
300 | 27 | 'function' => $entry['function'] ?? null, |
|
301 | 27 | 'file' => $entry['file'] ?? null, |
|
302 | 27 | 'line' => $entry['line'] ?? null, |
|
303 | 27 | 'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : [], |
|
304 | 27 | ]; |
|
305 | } |
||
306 | |||
307 | 27 | return $this; |
|
308 | } |
||
309 | |||
310 | 27 | private function flattenArgs(array $args, int $level = 0, int &$count = 0): array |
|
311 | { |
||
312 | 27 | $result = []; |
|
313 | 27 | foreach ($args as $key => $value) { |
|
314 | 27 | if (++$count > 1e4) { |
|
315 | 1 | return ['array', '*SKIPPED over 10000 entries*']; |
|
316 | } |
||
317 | 27 | if ($value instanceof \__PHP_Incomplete_Class) { |
|
318 | $result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)]; |
||
319 | 27 | } elseif (\is_object($value)) { |
|
320 | 24 | $result[$key] = ['object', get_debug_type($value)]; |
|
321 | 27 | } elseif (\is_array($value)) { |
|
322 | 26 | if ($level > 10) { |
|
323 | 1 | $result[$key] = ['array', '*DEEP NESTED ARRAY*']; |
|
324 | } else { |
||
325 | 26 | $result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)]; |
|
326 | } |
||
327 | 27 | } elseif (null === $value) { |
|
328 | 24 | $result[$key] = ['null', null]; |
|
329 | 27 | } elseif (\is_bool($value)) { |
|
330 | 24 | $result[$key] = ['boolean', $value]; |
|
331 | 27 | } elseif (\is_int($value)) { |
|
332 | 24 | $result[$key] = ['integer', $value]; |
|
333 | 27 | } elseif (\is_float($value)) { |
|
334 | 2 | $result[$key] = ['float', $value]; |
|
335 | 25 | } elseif (\is_resource($value)) { |
|
336 | 1 | $result[$key] = ['resource', get_resource_type($value)]; |
|
337 | } else { |
||
338 | 24 | $result[$key] = ['string', (string) $value]; |
|
339 | } |
||
340 | } |
||
341 | |||
342 | 27 | return $result; |
|
343 | } |
||
344 | |||
345 | private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value): string |
||
346 | { |
||
347 | $array = new \ArrayObject($value); |
||
348 | |||
349 | return $array['__PHP_Incomplete_Class_Name']; |
||
350 | } |
||
351 | |||
352 | 1 | public function getTraceAsString(): string |
|
353 | { |
||
354 | 1 | return $this->traceAsString; |
|
355 | } |
||
356 | |||
357 | /** |
||
358 | * @return $this |
||
359 | */ |
||
360 | 1 | public function setAsString(?string $asString): static |
|
361 | { |
||
362 | 1 | $this->asString = $asString; |
|
363 | |||
364 | 1 | return $this; |
|
365 | } |
||
366 | |||
367 | 1 | public function getAsString(): string |
|
368 | { |
||
369 | 1 | if (null !== $this->asString) { |
|
370 | 1 | return $this->asString; |
|
371 | } |
||
372 | |||
373 | 1 | $message = ''; |
|
374 | 1 | $next = false; |
|
375 | |||
376 | 1 | foreach (array_reverse(array_merge([$this], $this->getAllPrevious())) as $exception) { |
|
377 | 1 | if ($next) { |
|
378 | 1 | $message .= 'Next '; |
|
379 | } else { |
||
380 | 1 | $next = true; |
|
381 | } |
||
382 | 1 | $message .= $exception->getClass(); |
|
383 | |||
384 | 1 | if ('' != $exception->getMessage()) { |
|
385 | 1 | $message .= ': '.$exception->getMessage(); |
|
386 | } |
||
387 | |||
388 | 1 | $message .= ' in '.$exception->getFile().':'.$exception->getLine(). |
|
389 | 1 | "\nStack trace:\n".$exception->getTraceAsString()."\n\n"; |
|
390 | } |
||
391 | |||
392 | 1 | return rtrim($message); |
|
393 | } |
||
394 | } |
||
395 |