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

aplus-framework / debug / 14545675875

01 Mar 2025 12:54AM UTC coverage: 97.297%. Remained the same
14545675875

push

github

natanfelles
Add since tags

540 of 555 relevant lines covered (97.3%)

2.74 hits per line

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

89.15
/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\Logger;
17
use InvalidArgumentException;
18
use RuntimeException;
19
use Throwable;
20

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

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

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

80
    public function getEnvironment() : string
81
    {
82
        return $this->environment;
9✔
83
    }
84

85
    /**
86
     * @return Logger|null
87
     */
88
    public function getLogger() : ?Logger
89
    {
90
        return $this->logger;
10✔
91
    }
92

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

100
    /**
101
     * @return Language
102
     */
103
    public function getLanguage() : Language
104
    {
105
        if (!isset($this->language)) {
8✔
106
            $this->setLanguage();
8✔
107
        }
108
        return $this->language;
8✔
109
    }
110

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

126
    /**
127
     * Tells if the log id is being shown.
128
     *
129
     * @since 4.4
130
     *
131
     * @return bool
132
     */
133
    public function isShowingLogId() : bool
134
    {
135
        return $this->showLogId;
3✔
136
    }
137

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

149
    public function setDevelopmentView(string $file) : static
150
    {
151
        $this->developmentView = $this->validateView($file);
1✔
152
        return $this;
1✔
153
    }
154

155
    public function getDevelopmentView() : string
156
    {
157
        return $this->developmentView;
3✔
158
    }
159

160
    public function setProductionView(string $file) : static
161
    {
162
        $this->productionView = $this->validateView($file);
1✔
163
        return $this;
1✔
164
    }
165

166
    public function getProductionView() : string
167
    {
168
        return $this->productionView;
3✔
169
    }
170

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

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

213
    protected function isCli() : bool
214
    {
215
        return \PHP_SAPI === 'cli' || \defined('STDIN');
1✔
216
    }
217

218
    protected function isJson() : bool
219
    {
220
        return isset($_SERVER['HTTP_CONTENT_TYPE'])
8✔
221
            && \str_starts_with($_SERVER['HTTP_CONTENT_TYPE'], 'application/json');
8✔
222
    }
223

224
    protected function acceptJson() : bool
225
    {
226
        return isset($_SERVER['HTTP_ACCEPT'])
6✔
227
            && \str_contains($_SERVER['HTTP_ACCEPT'], 'application/json');
6✔
228
    }
229

230
    protected function sendJson(Throwable $exception) : void
231
    {
232
        $data = $this->getEnvironment() === static::DEVELOPMENT
4✔
233
            ? [
2✔
234
                'exception' => $exception::class,
2✔
235
                'message' => $exception->getMessage(),
2✔
236
                'file' => $exception->getFile(),
2✔
237
                'line' => $exception->getLine(),
2✔
238
                'trace' => $exception->getTrace(),
2✔
239
            ]
2✔
240
            : [
2✔
241
                'message' => $this->getLanguage()->render('debug', 'exceptionDescription'),
2✔
242
            ];
2✔
243
        echo \json_encode([
4✔
244
            'status' => [
4✔
245
                'code' => 500,
4✔
246
                'reason' => 'Internal Server Error',
4✔
247
            ],
4✔
248
            'data' => $data,
4✔
249
        ], \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
4✔
250
    }
251

252
    protected function sendHeaders() : void
253
    {
254
        $contentType = 'text/html';
8✔
255
        if ($this->isJson() || $this->acceptJson()) {
8✔
256
            $contentType = 'application/json';
4✔
257
        }
258
        \header('Content-Type: ' . $contentType . '; charset=UTF-8');
8✔
259
    }
260

261
    protected function cliError(Throwable $exception) : void
262
    {
263
        $language = $this->getLanguage();
1✔
264
        $message = $language->render('debug', 'exception')
1✔
265
            . ': ' . $exception::class . \PHP_EOL;
1✔
266
        $message .= $language->render('debug', 'message')
1✔
267
            . ': ' . $exception->getMessage() . \PHP_EOL;
1✔
268
        $message .= $language->render('debug', 'file')
1✔
269
            . ': ' . $exception->getFile() . \PHP_EOL;
1✔
270
        $message .= $language->render('debug', 'line')
1✔
271
            . ': ' . $exception->getLine() . \PHP_EOL;
1✔
272
        $message .= $language->render('debug', 'trace')
1✔
273
            . ': ' . $exception->getTraceAsString();
1✔
274
        CLI::error($message, $this->testing ? null : 1);
1✔
275
    }
276

277
    protected function log(string $message) : void
278
    {
279
        $this->getLogger()?->logCritical($message);
9✔
280
    }
281

282
    /**
283
     * Error handler.
284
     *
285
     * @param int $errno The level of the error raised
286
     * @param string $errstr The error message
287
     * @param string|null $errfile The filename that the error was raised in
288
     * @param int|null $errline The line number where the error was raised
289
     *
290
     * @see http://php.net/manual/en/function.set-error-handler.php
291
     *
292
     * @throws ErrorException if the error is included in the error_reporting
293
     *
294
     * @return bool
295
     */
296
    public function errorHandler(
297
        int $errno,
298
        string $errstr,
299
        ?string $errfile = null,
300
        ?int $errline = null
301
    ) : bool {
302
        if (!(\error_reporting() & $errno)) {
3✔
303
            return true;
3✔
304
        }
305
        $type = match ($errno) {
3✔
306
            \E_ERROR => 'Error',
×
307
            \E_WARNING => 'Warning',
×
308
            \E_PARSE => 'Parse',
×
309
            \E_NOTICE => 'Notice',
×
310
            \E_CORE_ERROR => 'Core Error',
×
311
            \E_CORE_WARNING => 'Core Warning',
×
312
            \E_COMPILE_ERROR => 'Compile Error',
×
313
            \E_COMPILE_WARNING => 'Compile Warning',
×
314
            \E_USER_ERROR => 'User Error',
×
315
            \E_USER_WARNING => 'User Warning',
1✔
316
            \E_USER_NOTICE => 'User Notice',
1✔
317
            \E_RECOVERABLE_ERROR => 'Recoverable Error',
×
318
            \E_DEPRECATED => 'Deprecated',
×
319
            \E_USER_DEPRECATED => 'User Deprecated',
1✔
320
            \E_ALL => 'All',
×
321
            default => '',
×
322
        };
3✔
323
        throw new ErrorException(
3✔
324
            ($type ? $type . ': ' : '') . $errstr,
3✔
325
            0,
3✔
326
            $errno,
3✔
327
            $errfile,
3✔
328
            $errline
3✔
329
        );
3✔
330
    }
331

332
    public function getSearchEngines() : SearchEngines
333
    {
334
        if (!isset($this->searchEngines)) {
2✔
335
            $this->setSearchEngines(new SearchEngines());
2✔
336
        }
337
        return $this->searchEngines;
2✔
338
    }
339

340
    public function setSearchEngines(SearchEngines $searchEngines) : static
341
    {
342
        $this->searchEngines = $searchEngines;
2✔
343
        return $this;
2✔
344
    }
345
}
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