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

aplus-framework / debug / 15986556140

01 Jul 2025 12:15AM UTC coverage: 97.857% (+0.2%) from 97.667%
15986556140

push

github

natanfelles
Test exception on development with hidden inputs

685 of 700 relevant lines covered (97.86%)

4.09 hits per line

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

91.91
/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
     * @var array<string>
50
     */
51
    protected array $hiddenInputs = [];
52

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

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

88
    public function getEnvironment() : string
89
    {
90
        return $this->environment;
14✔
91
    }
92

93
    /**
94
     * @return Logger|null
95
     */
96
    public function getLogger() : ?Logger
97
    {
98
        return $this->logger;
17✔
99
    }
100

101
    public function setLanguage(?Language $language = null) : static
102
    {
103
        $this->language = $language ?? new Language();
16✔
104
        $this->language->addDirectory(__DIR__ . '/Languages');
16✔
105
        return $this;
16✔
106
    }
107

108
    /**
109
     * @return Language
110
     */
111
    public function getLanguage() : Language
112
    {
113
        if (!isset($this->language)) {
16✔
114
            $this->setLanguage();
16✔
115
        }
116
        return $this->language;
16✔
117
    }
118

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

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

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

157
    public function setDevelopmentView(string $file) : static
158
    {
159
        $this->developmentView = $this->validateView($file);
1✔
160
        return $this;
1✔
161
    }
162

163
    public function getDevelopmentView() : string
164
    {
165
        return $this->developmentView;
4✔
166
    }
167

168
    public function setProductionView(string $file) : static
169
    {
170
        $this->productionView = $this->validateView($file);
1✔
171
        return $this;
1✔
172
    }
173

174
    public function getProductionView() : string
175
    {
176
        return $this->productionView;
3✔
177
    }
178

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

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

221
    protected function isCli() : bool
222
    {
223
        return \PHP_SAPI === 'cli' || \defined('STDIN');
2✔
224
    }
225

226
    protected function isJson() : bool
227
    {
228
        return isset($_SERVER['HTTP_CONTENT_TYPE'])
13✔
229
            && \str_starts_with($_SERVER['HTTP_CONTENT_TYPE'], 'application/json');
13✔
230
    }
231

232
    protected function acceptJson() : bool
233
    {
234
        return isset($_SERVER['HTTP_ACCEPT'])
9✔
235
            && \str_contains($_SERVER['HTTP_ACCEPT'], 'application/json');
9✔
236
    }
237

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

264
    public function getJsonFlags() : int
265
    {
266
        return $this->jsonFlags;
9✔
267
    }
268

269
    public function setJsonFlags(int $flags) : static
270
    {
271
        $this->jsonFlags = $flags;
1✔
272
        return $this;
1✔
273
    }
274

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

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

305
    protected function log(string $message) : void
306
    {
307
        $this->getLogger()?->logCritical($message);
15✔
308
    }
309

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

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

373
    public function getSearchEngines() : SearchEngines
374
    {
375
        if (!isset($this->searchEngines)) {
3✔
376
            $this->setSearchEngines(new SearchEngines());
3✔
377
        }
378
        return $this->searchEngines;
3✔
379
    }
380

381
    public function setSearchEngines(SearchEngines $searchEngines) : static
382
    {
383
        $this->searchEngines = $searchEngines;
3✔
384
        return $this;
3✔
385
    }
386

387
    /**
388
     * @since 4.5
389
     *
390
     * @param string $name
391
     *
392
     * @see https://www.php.net/manual/en/filter.constants.php
393
     */
394
    protected function validateInputName(string $name) : void
395
    {
396
        if (!\in_array($name, [
6✔
397
            '$_COOKIE',
6✔
398
            '$_ENV',
6✔
399
            '$_FILES',
6✔
400
            '$_GET',
6✔
401
            '$_POST',
6✔
402
            '$_SERVER',
6✔
403
        ], true)) {
6✔
404
            throw new InvalidArgumentException('Invalid input name: ' . $name);
2✔
405
        }
406
    }
407

408
    /**
409
     * @since 4.5
410
     *
411
     * @param array<string> $names
412
     *
413
     * @return array<string>
414
     */
415
    protected function makeHiddenInputs(array $names) : array
416
    {
417
        foreach ($names as $name) {
4✔
418
            $this->validateInputName($name);
4✔
419
        }
420
        \sort($names);
4✔
421
        return \array_unique($names);
4✔
422
    }
423

424
    /**
425
     * @since 4.5
426
     *
427
     * @return static
428
     */
429
    protected function reorderHiddenInputs() : static
430
    {
431
        $this->hiddenInputs = $this->makeHiddenInputs($this->hiddenInputs);
1✔
432
        return $this;
1✔
433
    }
434

435
    /**
436
     * @since 4.5
437
     *
438
     * @param string $name
439
     *
440
     * @return bool
441
     */
442
    public function isHiddenInput(string $name) : bool
443
    {
444
        $this->validateInputName($name);
4✔
445
        return \in_array($name, $this->hiddenInputs, true);
4✔
446
    }
447

448
    /**
449
     * @since 4.5
450
     *
451
     * @return array<string>
452
     */
453
    public function getHiddenInputs() : array
454
    {
455
        return $this->hiddenInputs;
2✔
456
    }
457

458
    /**
459
     * @since 4.5
460
     *
461
     * @param string $hiddenInput
462
     * @param string ...$hiddenInputs
463
     *
464
     * @return static
465
     */
466
    public function setHiddenInputs(string $hiddenInput, string ...$hiddenInputs) : static
467
    {
468
        $this->hiddenInputs = $this->makeHiddenInputs([$hiddenInput, ...$hiddenInputs]);
3✔
469
        return $this;
3✔
470
    }
471

472
    /**
473
     * @since 4.5
474
     *
475
     * @param string $hiddenInput
476
     * @param string ...$hiddenInputs
477
     *
478
     * @return static
479
     */
480
    public function addHiddenInputs(string $hiddenInput, string ...$hiddenInputs) : static
481
    {
482
        $hiddenInputs = $this->makeHiddenInputs([$hiddenInput, ...$hiddenInputs]);
1✔
483
        foreach ($hiddenInputs as $hiddenInput) {
1✔
484
            $this->hiddenInputs[] = $hiddenInput;
1✔
485
        }
486
        $this->reorderHiddenInputs();
1✔
487
        return $this;
1✔
488
    }
489

490
    /**
491
     * @since 4.5
492
     *
493
     * @param string $hiddenInput
494
     * @param string ...$hiddenInputs
495
     *
496
     * @return static
497
     */
498
    public function removeHiddenInputs(string $hiddenInput, string ...$hiddenInputs) : static
499
    {
500
        $hiddenInputs = $this->makeHiddenInputs([$hiddenInput, ...$hiddenInputs]);
1✔
501
        foreach ($this->hiddenInputs as $key => $value) {
1✔
502
            if (\in_array($value, $hiddenInputs, true)) {
1✔
503
                unset($this->hiddenInputs[$key]);
1✔
504
            }
505
        }
506
        $this->reorderHiddenInputs();
1✔
507
        return $this;
1✔
508
    }
509
}
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