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

aplus-framework / debug / 15768832213

20 Jun 2025 12:53AM UTC coverage: 97.059% (-0.4%) from 97.449%
15768832213

push

github

natanfelles
Use ExceptionHandler::getLog method

6 of 6 new or added lines in 1 file covered. (100.0%)

17 existing lines in 1 file now uncovered.

594 of 612 relevant lines covered (97.06%)

3.2 hits per line

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

87.94
/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);
18✔
64
        if ($logger) {
18✔
65
            $this->logger = $logger;
3✔
66
        }
67
        if ($language) {
18✔
68
            $this->setLanguage($language);
1✔
69
        }
70
    }
71

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

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

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

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

104
    /**
105
     * @return Language
106
     */
107
    public function getLanguage() : Language
108
    {
109
        if (!isset($this->language)) {
8✔
110
            $this->setLanguage();
8✔
111
        }
112
        return $this->language;
8✔
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;
1✔
127
        return $this;
1✔
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;
8✔
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()) {
9✔
UNCOV
193
            \ob_end_clean();
×
194
        }
195
        $this->log((string) $exception);
9✔
196
        if ($this->isCli()) {
9✔
197
            $this->cliError($exception);
1✔
198
            return;
1✔
199
        }
200
        \http_response_code(500);
8✔
201
        if (!\headers_sent()) {
8✔
202
            $this->sendHeaders();
8✔
203
        }
204
        if ($this->isJson() || $this->acceptJson()) {
8✔
205
            $this->sendJson($exception);
4✔
206
            return;
4✔
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');
1✔
220
    }
221

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

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

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

260
    public function getJsonFlags() : int
261
    {
262
        return $this->jsonFlags;
5✔
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';
8✔
274
        if ($this->isJson() || $this->acceptJson()) {
8✔
275
            $contentType = 'application/json';
4✔
276
        }
277
        \header('Content-Type: ' . $contentType . '; charset=UTF-8');
8✔
278
    }
279

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

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

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

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

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

376
    public function setSearchEngines(SearchEngines $searchEngines) : static
377
    {
378
        $this->searchEngines = $searchEngines;
2✔
379
        return $this;
2✔
380
    }
381
}
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