Passed
Push — master ( 471128...356857 )
by Alexander
03:11
created

Handler::renderExceptionWithDebug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php 
2
3
/**
4
 * Lenevor Framework
5
 *
6
 * LICENSE
7
 *
8
 * This source file is subject to the new BSD license that is bundled
9
 * with this package in the file license.md.
10
 * It is also available through the world-wide-web at this URL:
11
 * https://lenevor.com/license
12
 * If you did not receive a copy of the license and are unable to
13
 * obtain it through the world-wide-web, please send an email
14
 * to [email protected] so we can send you a copy immediately.
15
 *
16
 * @package     Lenevor
17
 * @subpackage  Base
18
 * @link        https://lenevor.com
19
 * @copyright   Copyright (c) 2019 - 2023 Alexander Campo <[email protected]>
20
 * @license     https://opensource.org/licenses/BSD-3-Clause New BSD license or see https://lenevor.com/license or see /license.md
21
 */
22
23
namespace Syscodes\Components\Core\Exceptions;
24
25
use Exception;
26
use Throwable;
27
use Psr\Log\LoggerInterface;
28
use Syscodes\Components\Http\Response;
29
use Syscodes\Components\Routing\Router;
30
use Syscodes\Components\Http\RedirectResponse;
31
use Syscodes\Components\Debug\ExceptionHandler;
32
use Syscodes\Components\Contracts\Container\Container;
33
use Syscodes\Components\Contracts\Core\ExceptionRender;
34
use Syscodes\Components\Core\Http\Exceptions\HttpException;
35
use Syscodes\Components\Http\Exceptions\HttpResponseException;
36
use Syscodes\Components\Debug\FatalExceptions\FlattenException;
37
use Syscodes\Components\Core\Http\Exceptions\NotFoundHttpException;
38
use Syscodes\Components\Database\Erostrine\Exceptions\ModelNotFoundException;
39
use Syscodes\Components\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
40
41
/**
42
 * The system's main exception class is loaded for activate the render method of debugging.
43
 */
44
class Handler implements ExceptionHandlerContract
45
{
46
    /**
47
     * The container implementation.
48
     * 
49
     * @var \Syscodes\Contracts\Container\Container $container
0 ignored issues
show
Bug introduced by
The type Syscodes\Contracts\Container\Container was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
50
     */
51
    protected $container;
52
53
    /**
54
     * A list of the exception types that should not be reported.
55
     * 
56
     * @var array $dontReport
57
     */
58
    protected $dontReport = [];
59
60
    /**
61
     * A list of the Core exception types that should not be reported.
62
     * 
63
     * @var array $coreDontReport
64
     */
65
    protected $coreDontReport = [
66
        HttpException::class,
67
        HttpResponseException::class,
68
        ModelNotFoundException::class,
69
    ];
70
71
    /**
72
     * The callbacks that should be used during reporting.
73
     * 
74
     * @var array $reportCallbacks
75
     */
76
    protected $reportCallbacks = [];
77
78
    /**
79
     * The callbacks that should be used during rendering.
80
     * 
81
     * @var array $renderCallbacks
82
     */
83
    protected $renderCallbacks = [];
84
85
    /**
86
     * Constructor. Create a new exception handler instance.
87
     * 
88
     * @param  \Syscodes\Components\Contracts\Container\Container  $container
89
     * 
90
     * @return void
91
     */
92
    public function __construct(Container $container)
93
    {
94
        $this->container = $container;
0 ignored issues
show
Documentation Bug introduced by
It seems like $container of type Syscodes\Components\Contracts\Container\Container is incompatible with the declared type Syscodes\Contracts\Container\Container of property $container.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
95
96
        $this->register();
97
    }
98
99
    /**
100
     * Register the exception handling with callbacks for the application.
101
     * 
102
     * @return void
103
     */
104
    public function register() {}
105
106
    /**
107
     * Register a reportable callback.
108
     * 
109
     * @param  \callable  $callback
0 ignored issues
show
Bug introduced by
The type callable was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
110
     * 
111
     * @return static
112
     */
113
    public function reportable(callable $callback): static
114
    {
115
        $this->reportCallbacks[] = $callback;
116
117
        return $this;
118
    }
119
120
    /**
121
     * Register a renderable callback.
122
     * 
123
     * @param  \callable  $callback
124
     * 
125
     * @return static
126
     */
127
    public function renderable(callable $callback): static
128
    {
129
        $this->renderCallbacks[] = $callback;
130
131
        return $this;
132
    }
133
    
134
    /**
135
     * Report or log an exception.
136
     * 
137
     * @param  \Throwable  $e
138
     * 
139
     * @return mixed
140
     * 
141
     * @throws \Exception
142
     */
143
    public function report(Throwable $e)
144
    {
145
        if ($this->shouldntReport($e)) {
146
            return;
147
        }
148
149
        if (method_exists($e, 'report')) {
150
            return $e->report($e);
151
        }
152
        
153
        foreach ($this->reportCallbacks as $reportCallback) {
154
            if ($reportCallback($e) === false) {
155
                return;
156
            }
157
        }
158
159
        try {
160
            $logger = $this->container->make(LoggerInterface::class);
161
        } catch (Exception $e) {
162
            throw $e;
163
        }
164
        
165
        $logger->error($e->getMessage());
166
    }
167
168
    /**
169
     * Determine if the exception should be reported.
170
     * 
171
     * @param  \Throwable  $e
172
     * 
173
     * @return bool
174
     */
175
    public function shouldReport(Throwable $e): bool
176
    {
177
        return ! $this->shouldntReport($e);
178
    }
179
180
    /**
181
     * Determine if the exception is in the "do not report" list.
182
     * 
183
     * @param  \Throwable  $e
184
     * 
185
     * @return bool
186
     */
187
    public function shouldntReport(Throwable $e): bool
188
    {
189
        $dontReport = array_merge($this->dontReport, $this->coreDontReport);
190
191
        foreach ($dontReport as $type) {
192
            if ($e instanceof $type) {
193
                return true;
194
            }
195
        }
196
197
        return false;
198
    }
199
200
    /**
201
     * Render an exception into an HTTP response.
202
     * 
203
     * @param  \Syscodes\Components\Http\Request  $request
204
     * @param  \Throwable  $e
205
     * 
206
     * @return \Syscodes\Components\Http\Response
207
     */
208
    public function render($request, Throwable $e)
209
    {
210
        if (method_exists($e, 'render') && $response = $e->render($request)) {
211
            return Router::toResponse($request, $response);
0 ignored issues
show
Bug introduced by
The method toResponse() does not exist on Syscodes\Components\Routing\Router. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

211
            return Router::/** @scrutinizer ignore-call */ toResponse($request, $response);
Loading history...
212
        }
213
        
214
        $e = $this->prepareException($e);
215
        
216
        foreach ($this->renderCallbacks as $renderCallback) {
217
            $response = $renderCallback($e, $request);
218
            
219
            if ( ! is_null($response)) {
220
                return $response;
221
            }
222
        }
223
224
        foreach ($this->renderCallbacks as $renderCallback) {
225
            $response = $renderCallback($e, $request);
226
227
            if ( ! is_null($response)) {
228
                return $response;
229
            }
230
        }
231
232
        if ($e instanceof HttpResponseException) {
233
            $e->getResponse();
234
        }
235
236
        return $this->prepareResponse($request, $e);
237
    }
238
239
    /**
240
     * Prepare exception for rendering.
241
     * 
242
     * @param  \Throwable  $e
243
     * 
244
     * @return \Throwable
245
     */
246
    protected function prepareException(Throwable $e): Throwable
247
    {
248
        if ($e instanceof ModelNotFoundException) {
249
            $e = new NotFoundHttpException($e->getMessage(), $e);
250
        }
251
252
        return $e;
253
    }
254
     
255
    /**
256
     * Prepare a response for the given exception.
257
     * 
258
     * @param  \Syscodes\Components\Http\Request  $request
259
     * @param  \Throwable  $e
260
     * 
261
     * @return \Syscodes\Components\Http\Response
262
     * 
263
     * @uses   \Syscodes\Components\Core\Http\Exceptions\HttpException
264
     */
265
    protected function prepareResponse($request, Throwable $e)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

265
    protected function prepareResponse(/** @scrutinizer ignore-unused */ $request, Throwable $e)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
266
    {
267
        if ( ! $this->isHttpException($e) && config('app.debug')) {
268
            return $this->toSyscodesResponse($this->convertExceptionToResponse($e), $e);
269
        }
270
271
        // When the debug is not active, the HTTP 500 code view is throw
272
        if ( ! $this->isHttpException($e)) {
273
            $e = new HttpException(500, $e->getMessage());
274
        }
275
276
        return $this->toSyscodesResponse($this->renderHttpException($e), $e);
277
    }
278
279
    /**
280
     * Render the given HttpException.
281
     * 
282
     * @param  \Syscodes\Components\Core\Http\Exceptions\HttpException  $e
283
     * 
284
     * @return \Syscodes\Components\Http\Response
285
     */
286
    protected function renderHttpException(HttpException $e)
287
    {
288
        $this->registerViewErrorPaths();
289
290
        if (view()->viewExists($view = $this->getHttpExceptionView($e))) {
0 ignored issues
show
Bug introduced by
The method viewExists() does not exist on Syscodes\Components\View\View. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

290
        if (view()->/** @scrutinizer ignore-call */ viewExists($view = $this->getHttpExceptionView($e))) {
Loading history...
291
            return response()->view(
0 ignored issues
show
Bug introduced by
The method view() does not exist on Syscodes\Components\Http\Response. It seems like you code against a sub-type of Syscodes\Components\Http\Response such as Syscodes\Components\Http\RedirectResponse. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

291
            return response()->/** @scrutinizer ignore-call */ view(
Loading history...
292
                $view, 
293
                ['exception' => $e],
294
                $e->getStatusCode(),
295
                $e->getHeaders()
296
            );
297
        }
298
299
        return $this->convertExceptionToResponse($e);
300
    }
301
302
    /**
303
     * Register the error view paths.
304
     * 
305
     * @return void
306
     */
307
    protected function registerViewErrorPaths(): void
308
    {
309
        (new RegisterErrorViewPaths)();
310
    }
311
312
    /**
313
     * Get the view used to render HTTP exceptions.
314
     * 
315
     * @param  \Syscodes\Components\Core\Http\Exceptions\HttpException  $e
316
     * 
317
     * @return string
318
     */
319
    protected function getHttpExceptionView(HttpException $e): string
320
    {
321
        return "errors::{$e->getStatusCode()}";
322
    }
323
324
    /**
325
     * Create a response for the given exception.
326
     * 
327
     * @param  \Throwable  $e
328
     * 
329
     * @return \Syscodes\Components\Http\Response
330
     */
331
    protected function convertExceptionToResponse(Throwable $e)
332
    {
333
        return Response::render(
334
            $this->renderExceptionContent($e),
335
            $this->isHttpException($e) ? $e->getStatusCode() : 500,
0 ignored issues
show
Bug introduced by
The method getStatusCode() does not exist on Throwable. It seems like you code against a sub-type of Throwable such as Syscodes\Components\Core...xceptions\HttpException. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

335
            $this->isHttpException($e) ? $e->/** @scrutinizer ignore-call */ getStatusCode() : 500,
Loading history...
336
            $this->isHttpException($e) ? $e->getHeaders() : []
0 ignored issues
show
Bug introduced by
The method getHeaders() does not exist on Throwable. It seems like you code against a sub-type of Throwable such as Syscodes\Components\Core...xceptions\HttpException. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

336
            $this->isHttpException($e) ? $e->/** @scrutinizer ignore-call */ getHeaders() : []
Loading history...
337
        );
338
    }
339
340
    /**
341
     * Gets the response content for the given exception.
342
     * 
343
     * @param  \Throwable  $e
344
     * 
345
     * @return string
346
     */
347
    protected function renderExceptionContent(Throwable $e): string
348
    {
349
        try {
350
            return config('app.debug') && app()->has(ExceptionRender::class)
351
                        ? $this->renderExceptionWithDebug($e) 
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->renderExceptionWithDebug($e) targeting Syscodes\Components\Core...derExceptionWithDebug() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
352
                        : $this->renderExceptionWithFlatDesignDebug($e, config('app.debug'));
0 ignored issues
show
Bug introduced by
It seems like config('app.debug') can also be of type Syscodes\Components\Config\Configure; however, parameter $debug of Syscodes\Components\Core...onWithFlatDesignDebug() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

352
                        : $this->renderExceptionWithFlatDesignDebug($e, /** @scrutinizer ignore-type */ config('app.debug'));
Loading history...
353
        } catch (Exception $e) {
354
            $this->renderExceptionWithFlatDesignDebug($e, config('app.debug'));
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
355
        }
356
    }
357
358
    /**
359
     * Render an exception to a string of debug.
360
     * 
361
     * @param  \Throwable  $e
362
     * 
363
     * @return void
364
     * 
365
     * @uses   \Syscodes\Components\Contracts\Core\ExceptionRender
366
     */
367
    protected function renderExceptionWithDebug(Throwable $e)
368
    {
369
        return app(ExceptionRender::class)->render($e);
0 ignored issues
show
Bug introduced by
The method render() does not exist on Syscodes\Components\Contracts\Core\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

369
        return app(ExceptionRender::class)->/** @scrutinizer ignore-call */ render($e);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
370
    }
371
372
    /**
373
     * Render an exception to a string using Flat Design Debug.
374
     * 
375
     * @param  \Throwable  $e
376
     * @param  bool  $debug
377
     * 
378
     * @return string
379
     */
380
    protected function renderExceptionWithFlatDesignDebug(Throwable $e, $debug)
381
    {
382
        return (new ExceptionHandler($debug))->getHtmlResponse(
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Syscodes\Comp...tenException::make($e)) returns the type void which is incompatible with the documented return type string.
Loading history...
Bug introduced by
Are you sure the usage of new Syscodes\Components\...tenException::make($e)) targeting Syscodes\Components\Debu...dler::getHtmlResponse() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
383
            FlattenException::make($e)
0 ignored issues
show
Bug introduced by
Are you sure the usage of Syscodes\Components\Debu...ttenException::make($e) targeting Syscodes\Components\Debu...lattenException::make() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Syscodes\Components\Debu...ttenException::make($e) of type void is incompatible with the type Exception|Syscodes\Compo...ptions\FlattenException expected by parameter $exception of Syscodes\Components\Debu...dler::getHtmlResponse(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

383
            /** @scrutinizer ignore-type */ FlattenException::make($e)
Loading history...
384
        );
385
    }
386
387
    /**
388
     * Map the given exception into an Syscodes response.
389
     * 
390
     * @param  \Syscodes\Components\Http\Response  $response
391
     * @param  \Exception  $e 
392
     * 
393
     * @return \Syscodes\Components\Http\Response
394
     */
395
    protected function toSyscodesResponse($response, Throwable $e)
396
    {
397
        if ($response instanceof RedirectResponse) {
398
            $response = new RedirectResponse(
399
                $response->getTargetUrl(), $response->status(), $response->headers->all()
400
            );
401
        } else {
402
            $response = new Response(
403
                $response->content(), $response->status(), $response->headers->all()
404
            );
405
        }
406
407
        return $response->withException($e);
408
    }
409
410
    /**
411
     * Render an exception to the console.
412
     * 
413
     * @param  \Syscodes\Components\Contracts\Console\Output\ConsoleOutput  $output
414
     * @param  \Throwable  $e
415
     * 
416
     * @return void
417
     */
418
    public function renderForConsole($output, Throwable $e)
419
    {
420
        $message = sprintf(
421
            $output->write("<error>".getClass($e, true)."</>")." %s in file %s on line %d\n\n%s\n",
422
            //getClass($e, true),
423
            $e->getMessage(),            
424
            $e->getFile(),
425
            $e->getLine(),
426
            $e->getTraceAsString()
427
        );
428
429
        echo $message;
430
    }
431
432
    /**
433
     * Determine if the given exception is an HTTP exception.
434
     * 
435
     * @param  \Throwable  $e
436
     * 
437
     * @return bool
438
     */
439
    protected function isHttpException(Throwable $e): bool
440
    {
441
        return $e instanceof HttpException;
442
    }
443
}