• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aplus-framework / debug / 15788673943

20 Jun 2025 09:47PM UTC coverage: 97.667% (+0.6%) from 97.059%
15788673943

push

github

natanfelles
Update tr:hover background

628 of 643 relevant lines covered (97.67%)

3.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.14
/src/ExceptionHandler.php
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of Aplus Framework Debug Library.
4
 *
5
 * (c) Natan Felles <natanfelles@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Framework\Debug;
11

12
use ErrorException;
13
use Framework\CLI\CLI;
14
use Framework\Helpers\Isolation;
15
use Framework\Language\Language;
16
use Framework\Log\Log;
17
use Framework\Log\Logger;
18
use InvalidArgumentException;
19
use RuntimeException;
20
use Throwable;
21

22
/**
23
 * Class ExceptionHandler.
24
 *
25
 * @package debug
26
 */
27
class ExceptionHandler
28
{
29
    /**
30
     * Development environment.
31
     */
32
    public const string DEVELOPMENT = 'development';
33
    /**
34
     * Production environment.
35
     */
36
    public const string PRODUCTION = 'production';
37
    protected string $developmentView = __DIR__ . '/Views/exceptions/development.php';
38
    protected string $productionView = __DIR__ . '/Views/exceptions/production.php';
39
    protected ?Logger $logger = null;
40
    protected string $environment = ExceptionHandler::PRODUCTION;
41
    protected Language $language;
42
    protected bool $testing = false;
43
    protected SearchEngines $searchEngines;
44
    protected bool $showLogId = true;
45
    protected int $jsonFlags = \JSON_THROW_ON_ERROR
46
    | \JSON_UNESCAPED_SLASHES
47
    | \JSON_UNESCAPED_UNICODE;
48

49
    /**
50
     * ExceptionHandler constructor.
51
     *
52
     * @param string $environment
53
     * @param Logger|null $logger
54
     * @param Language|null $language
55
     *
56
     * @throws InvalidArgumentException if environment is invalid
57
     */
58
    public function __construct(
59
        string $environment = ExceptionHandler::PRODUCTION,
60
        ?Logger $logger = null,
61
        ?Language $language = null
62
    ) {
63
        $this->setEnvironment($environment);
24✔
64
        if ($logger) {
24✔
65
            $this->logger = $logger;
9✔
66
        }
67
        if ($language) {
24✔
68
            $this->setLanguage($language);
1✔
69
        }
70
    }
71

72
    public function setEnvironment(string $environment) : static
73
    {
74
        if (!\in_array($environment, [
24✔
75
            static::DEVELOPMENT,
24✔
76
            static::PRODUCTION,
24✔
77
        ], true)) {
24✔
78
            throw new InvalidArgumentException('Invalid environment: ' . $environment);
1✔
79
        }
80
        $this->environment = $environment;
24✔
81
        return $this;
24✔
82
    }
83

84
    public function getEnvironment() : string
85
    {
86
        return $this->environment;
13✔
87
    }
88

89
    /**
90
     * @return Logger|null
91
     */
92
    public function getLogger() : ?Logger
93
    {
94
        return $this->logger;
16✔
95
    }
96

97
    public function setLanguage(?Language $language = null) : static
98
    {
99
        $this->language = $language ?? new Language();
15✔
100
        $this->language->addDirectory(__DIR__ . '/Languages');
15✔
101
        return $this;
15✔
102
    }
103

104
    /**
105
     * @return Language
106
     */
107
    public function getLanguage() : Language
108
    {
109
        if (!isset($this->language)) {
15✔
110
            $this->setLanguage();
15✔
111
        }
112
        return $this->language;
15✔
113
    }
114

115
    /**
116
     * Sets whether the log id will be shown in the production view.
117
     *
118
     * @since 4.4
119
     *
120
     * @param bool $showLogId True to show. False to not show.
121
     *
122
     * @return static
123
     */
124
    public function setShowLogId(bool $showLogId = true) : static
125
    {
126
        $this->showLogId = $showLogId;
2✔
127
        return $this;
2✔
128
    }
129

130
    /**
131
     * Tells if the log id is being shown.
132
     *
133
     * @since 4.4
134
     *
135
     * @return bool
136
     */
137
    public function isShowingLogId() : bool
138
    {
139
        return $this->showLogId;
14✔
140
    }
141

142
    protected function validateView(string $file) : string
143
    {
144
        $realpath = \realpath($file);
2✔
145
        if (!$realpath || !\is_file($realpath)) {
2✔
146
            throw new InvalidArgumentException(
2✔
147
                'Invalid exceptions view file: ' . $file
2✔
148
            );
2✔
149
        }
150
        return $realpath;
2✔
151
    }
152

153
    public function setDevelopmentView(string $file) : static
154
    {
155
        $this->developmentView = $this->validateView($file);
1✔
156
        return $this;
1✔
157
    }
158

159
    public function getDevelopmentView() : string
160
    {
161
        return $this->developmentView;
3✔
162
    }
163

164
    public function setProductionView(string $file) : static
165
    {
166
        $this->productionView = $this->validateView($file);
1✔
167
        return $this;
1✔
168
    }
169

170
    public function getProductionView() : string
171
    {
172
        return $this->productionView;
3✔
173
    }
174

175
    public function initialize(bool $handleErrors = true) : void
176
    {
177
        \set_exception_handler([$this, 'exceptionHandler']);
3✔
178
        if ($handleErrors) {
3✔
179
            \set_error_handler([$this, 'errorHandler']);
3✔
180
        }
181
    }
182

183
    /**
184
     * Exception handler.
185
     *
186
     * @param Throwable $exception The Throwable, exception, instance
187
     *
188
     * @throws RuntimeException if view file is not found
189
     */
190
    public function exceptionHandler(Throwable $exception) : void
191
    {
192
        if (\ob_get_length()) {
14✔
193
            \ob_end_clean();
×
194
        }
195
        $this->log((string) $exception);
14✔
196
        if ($this->isCli()) {
14✔
197
            $this->cliError($exception);
2✔
198
            return;
2✔
199
        }
200
        \http_response_code(500);
12✔
201
        if (!\headers_sent()) {
12✔
202
            $this->sendHeaders();
12✔
203
        }
204
        if ($this->isJson() || $this->acceptJson()) {
12✔
205
            $this->sendJson($exception);
8✔
206
            return;
8✔
207
        }
208
        $file = $this->getEnvironment() === static::DEVELOPMENT
4✔
209
            ? $this->getDevelopmentView()
2✔
210
            : $this->getProductionView();
2✔
211
        Isolation::require($file, [
4✔
212
            'handler' => $this,
4✔
213
            'exception' => $exception,
4✔
214
        ]);
4✔
215
    }
216

217
    protected function isCli() : bool
218
    {
219
        return \PHP_SAPI === 'cli' || \defined('STDIN');
2✔
220
    }
221

222
    protected function isJson() : bool
223
    {
224
        return isset($_SERVER['HTTP_CONTENT_TYPE'])
12✔
225
            && \str_starts_with($_SERVER['HTTP_CONTENT_TYPE'], 'application/json');
12✔
226
    }
227

228
    protected function acceptJson() : bool
229
    {
230
        return isset($_SERVER['HTTP_ACCEPT'])
8✔
231
            && \str_contains($_SERVER['HTTP_ACCEPT'], 'application/json');
8✔
232
    }
233

234
    protected function sendJson(Throwable $exception) : void
235
    {
236
        $data = $this->getEnvironment() === static::DEVELOPMENT
8✔
237
            ? [
4✔
238
                'exception' => $exception::class,
4✔
239
                'message' => $exception->getMessage(),
4✔
240
                'file' => $exception->getFile(),
4✔
241
                'line' => $exception->getLine(),
4✔
242
                'trace' => $exception->getTrace(),
4✔
243
            ]
4✔
244
            : [
4✔
245
                'message' => $this->getLanguage()->render('debug', 'exceptionDescription'),
4✔
246
            ];
4✔
247
        $log = $this->getLog();
8✔
248
        if ($log) {
8✔
249
            $data['log_id'] = $log->id;
4✔
250
        }
251
        echo \json_encode([
8✔
252
            'status' => [
8✔
253
                'code' => 500,
8✔
254
                'reason' => $this->getLanguage()->render('debug', 'internalServerError'),
8✔
255
            ],
8✔
256
            'data' => $data,
8✔
257
        ], $this->getJsonFlags());
8✔
258
    }
259

260
    public function getJsonFlags() : int
261
    {
262
        return $this->jsonFlags;
9✔
263
    }
264

265
    public function setJsonFlags(int $flags) : static
266
    {
267
        $this->jsonFlags = $flags;
1✔
268
        return $this;
1✔
269
    }
270

271
    protected function sendHeaders() : void
272
    {
273
        $contentType = 'text/html';
12✔
274
        if ($this->isJson() || $this->acceptJson()) {
12✔
275
            $contentType = 'application/json';
8✔
276
        }
277
        \header('Content-Type: ' . $contentType . '; charset=UTF-8');
12✔
278
        \header('Content-Language: ' . $this->getLanguage()->getCurrentLocale());
12✔
279
    }
280

281
    protected function cliError(Throwable $exception) : void
282
    {
283
        $language = $this->getLanguage();
2✔
284
        $message = $language->render('debug', 'exception')
2✔
285
            . ': ' . $exception::class . \PHP_EOL;
2✔
286
        $message .= $language->render('debug', 'message')
2✔
287
            . ': ' . $exception->getMessage() . \PHP_EOL;
2✔
288
        $message .= $language->render('debug', 'file')
2✔
289
            . ': ' . $exception->getFile() . \PHP_EOL;
2✔
290
        $message .= $language->render('debug', 'line')
2✔
291
            . ': ' . $exception->getLine() . \PHP_EOL;
2✔
292
        $message .= $language->render('debug', 'trace')
2✔
293
            . ': ' . \PHP_EOL . $exception->getTraceAsString();
2✔
294
        $log = $this->getLog();
2✔
295
        if ($log) {
2✔
296
            $message .= \PHP_EOL . $language->render('debug', 'logId') . ': ' . $log->id;
1✔
297
        }
298
        CLI::error($message, $this->testing ? null : 1);
2✔
299
    }
300

301
    protected function log(string $message) : void
302
    {
303
        $this->getLogger()?->logCritical($message);
14✔
304
    }
305

306
    /**
307
     * Get the last log if it is showing log id and logger is set.
308
     *
309
     * @return Log|null
310
     */
311
    public function getLog() : ?Log
312
    {
313
        if ($this->isShowingLogId()) {
13✔
314
            return $this->getLogger()?->getLastLog();
13✔
315
        }
316
        return null;
1✔
317
    }
318

319
    /**
320
     * Error handler.
321
     *
322
     * @param int $errno The level of the error raised
323
     * @param string $errstr The error message
324
     * @param string|null $errfile The filename that the error was raised in
325
     * @param int|null $errline The line number where the error was raised
326
     *
327
     * @see http://php.net/manual/en/function.set-error-handler.php
328
     *
329
     * @throws ErrorException if the error is included in the error_reporting
330
     *
331
     * @return bool
332
     */
333
    public function errorHandler(
334
        int $errno,
335
        string $errstr,
336
        ?string $errfile = null,
337
        ?int $errline = null
338
    ) : bool {
339
        if (!(\error_reporting() & $errno)) {
3✔
340
            return true;
3✔
341
        }
342
        $type = match ($errno) {
3✔
343
            \E_ERROR => 'Error',
×
344
            \E_WARNING => 'Warning',
×
345
            \E_PARSE => 'Parse',
×
346
            \E_NOTICE => 'Notice',
×
347
            \E_CORE_ERROR => 'Core Error',
×
348
            \E_CORE_WARNING => 'Core Warning',
×
349
            \E_COMPILE_ERROR => 'Compile Error',
×
350
            \E_COMPILE_WARNING => 'Compile Warning',
×
351
            \E_USER_ERROR => 'User Error',
×
352
            \E_USER_WARNING => 'User Warning',
1✔
353
            \E_USER_NOTICE => 'User Notice',
1✔
354
            \E_RECOVERABLE_ERROR => 'Recoverable Error',
×
355
            \E_DEPRECATED => 'Deprecated',
×
356
            \E_USER_DEPRECATED => 'User Deprecated',
1✔
357
            \E_ALL => 'All',
×
358
            default => '',
×
359
        };
3✔
360
        throw new ErrorException(
3✔
361
            ($type ? $type . ': ' : '') . $errstr,
3✔
362
            0,
3✔
363
            $errno,
3✔
364
            $errfile,
3✔
365
            $errline
3✔
366
        );
3✔
367
    }
368

369
    public function getSearchEngines() : SearchEngines
370
    {
371
        if (!isset($this->searchEngines)) {
2✔
372
            $this->setSearchEngines(new SearchEngines());
2✔
373
        }
374
        return $this->searchEngines;
2✔
375
    }
376

377
    public function setSearchEngines(SearchEngines $searchEngines) : static
378
    {
379
        $this->searchEngines = $searchEngines;
2✔
380
        return $this;
2✔
381
    }
382
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc