Passed
Push — main ( 6a6271...1ab0b5 )
by Filipe
01:10
created

ExceptionInspector::statusCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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\Exception;
13
14
use Throwable;
15
16
/**
17
 * ExceptionInspector
18
 *
19
 * @package Slick\ErrorHandler\Exception
20
 */
21
class ExceptionInspector
22
{
23
    private string $firstAppFile = '';
24
    private int $lineOfFirstAppFile = 0;
25
26
    /**
27
     * @var array<string, array{statusCode: int, help: string}>
28
     */
29
    private array $errorsDb = [];
30
31
    private int $statusCode = 500;
32
33
    private ?string $help = null;
34
35
    /**
36
     * @var array<object{code: int, description: string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{code: int, description: string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
37
     */
38
    private array $httpCodes = [];
39
40
    public function __construct(private readonly Throwable $throwable, private readonly string $applicationRoot = '')
41
    {
42
        $this->readFirstAppFile();
43
        $errorsDbFile = dirname(__DIR__, 2) . '/config/errors_db.php';
44
        $httpCodesFile = dirname(__DIR__, 2) . '/config/http_codes.json';
45
        $this->errorsDb = include $errorsDbFile;
46
        $this->loadDb();
47
        $this->httpCodes = json_decode(file_get_contents($httpCodesFile));
48
    }
49
50
    /**
51
     * Returns the name of the exception.
52
     *
53
     * @return string The name of the exception.
54
     */
55
    public function exceptionName(): string
56
    {
57
        return $this->parseName($this->throwable);
58
    }
59
60
    /**
61
     * Get the path of exceptions thrown in reverse order.
62
     *
63
     * @return array<string> The reversed array of exception paths.
64
     */
65
    public function exceptionsPath(): array
66
    {
67
        return array_reverse($this->allNames($this->throwable));
68
    }
69
70
    /**
71
     * Retrieve a selected code snippet surrounding the line where the throwable occurred.
72
     *
73
     * @param int $around The number of lines to include before and after the line of the throwable (default: 4)
74
     * @return string The selected code snippet as a string
75
     */
76
    public function code(int $around = 4): string
77
    {
78
        return $this->codeSnippet(
79
            $this->throwable->getFile(),
80
            $this->throwable->getLine(),
81
            $around
82
        );
83
    }
84
85
    public function line(): int
86
    {
87
        return $this->throwable->getLine();
88
    }
89
90
    /**
91
     * Retrieve a selected code snippet surrounding the line where the first stack trace
92
     * file which class name starts with the provided namespace
93
     *
94
     * @param int $around The number of lines to include before and after the line of the
95
     *                    throwable (default: 4)
96
     * @return string The selected code snippet as a string
97
     */
98
    public function codeOfFirstAppFile(int $around = 4): string
99
    {
100
        return $this->codeSnippet($this->firstAppFile, $this->lineOfFirstAppFile, $around);
101
    }
102
103
    public function firstAppFile(): string
104
    {
105
        return $this->firstAppFile;
106
    }
107
108
    public function lineOfFirstAppFile(): int
109
    {
110
        return $this->lineOfFirstAppFile;
111
    }
112
113
    /**
114
     * Retrieve a selected code snippet surrounding the line from provided file
115
     *
116
     * @param string $file The file to retrieve the code from
117
     * @param int $line The line where the error occurs
118
     * @param int $around The number of lines to include before and after the line of the throwable (default: 4)
119
     * @return string The selected code snippet as a string
120
     */
121
    private function codeSnippet(string $file, int $line = 1, int $around = 4): string
122
    {
123
        $code = "";
124
        $lines = file($file);
125
126
        if (is_array($lines)) {
0 ignored issues
show
introduced by
The condition is_array($lines) is always true.
Loading history...
127
            $start = ($line - 1) - $around;
128
            $selected = array_slice($lines, $start, (2*$around) + 1);
129
            $code = implode("", $selected);
130
        }
131
132
        return $code;
133
    }
134
135
    /**
136
     * Parse the name of the throwable.
137
     *
138
     * @param Throwable $throwable The throwable object
139
     * @return string The parsed name of the throwable
140
     */
141
    private function parseName(Throwable $throwable): string
142
    {
143
        $parts = explode('\\', get_class($throwable));
144
        return trim(end($parts), '\\');
145
    }
146
147
    /**
148
     * Retrieve all names of the throwables in the chain.
149
     *
150
     * @param Throwable $throwable The current throwable
151
     * @return array<string> An array of names of throwables
152
     */
153
    private function allNames(Throwable $throwable): array
154
    {
155
        $names = [$this->parseName($throwable)];
156
        if ($throwable->getPrevious() instanceof Throwable) {
0 ignored issues
show
introduced by
$throwable->getPrevious() is always a sub-type of Throwable.
Loading history...
157
            $names = array_merge($names, $this->allNames($throwable->getPrevious()));
158
        }
159
        return $names;
160
    }
161
162
    public function statusCode(): int
163
    {
164
        return $this->statusCode;
165
    }
166
167
    private function readFirstAppFile(): void
168
    {
169
        $file = $this->throwable->getFile();
170
        $line = $this->throwable->getLine();
171
172
        /** @var array{function: string, line: int, file: string, class: class-string} $traceEntry */
173
        foreach ($this->throwable->getTrace() as $traceEntry) {
174
            if (!array_key_exists('file', $traceEntry) || !is_string($traceEntry['file'])) {
175
                continue;
176
            }
177
178
            if (str_starts_with($traceEntry['file'], $this->applicationRoot)) {
179
                $file = $traceEntry['file'];
180
                $line = $traceEntry['line'];
181
                break;
182
            }
183
        }
184
185
        $this->firstAppFile = $file;
186
        $this->lineOfFirstAppFile = $line;
187
    }
188
189
    private function loadDb(): void
190
    {
191
        foreach ($this->errorsDb as $className => $data) {
192
            if (!is_a($this->throwable, $className)) {
193
                continue;
194
            }
195
196
            $this->statusCode = $data["statusCode"];
197
            $this->help = $data["help"];
198
            break;
199
        }
200
    }
201
202
    public function help(): ?string
203
    {
204
        if (!$this->help) {
205
            return null;
206
        }
207
208
        $search = [
209
            '%path' => array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : ''
210
        ];
211
        return str_replace(array_keys($search), array_values($search), $this->help);
212
    }
213
214
    public function httpError(?int $statusCode = null): string
215
    {
216
        $statusCode = $statusCode ?? $this->statusCode();
217
        foreach ($this->httpCodes as $httpCode) {
218
            if ($httpCode->code === $statusCode) {
219
                return $httpCode->description;
220
            }
221
        }
222
        return '';
223
    }
224
}
225