ExceptionInspector   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 222
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 74
c 2
b 0
f 0
dl 0
loc 222
rs 9.6
wmc 35

16 Methods

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