FlattenException::setTrace()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 27
nc 5
nop 3
dl 0
loc 35
ccs 30
cts 30
cp 1
crap 4
rs 9.488
c 0
b 0
f 0
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
$previous is always a sub-type of Throwable.
Loading history...
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