Passed
Push — master ( f4c27e...d54548 )
by Alexander
03:11
created

Handler::renderable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
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\Support\Arr;
29
use Syscodes\Components\Http\Response;
30
use Syscodes\Components\Routing\Router;
31
use Syscodes\Components\Http\RedirectResponse;
32
use Syscodes\Components\Debug\ExceptionHandler;
33
use Syscodes\Components\Contracts\Container\Container;
34
use Syscodes\Components\Contracts\Core\ExceptionRender;
35
use Syscodes\Components\Core\Http\Exceptions\HttpException;
36
use Syscodes\Components\Http\Exceptions\HttpResponseException;
37
use Syscodes\Components\Debug\FatalExceptions\FlattenException;
38
use Syscodes\Components\Auth\Exceptions\AuthenticationException;
39
use Syscodes\Components\Session\Exceptions\TokenMismatchException;
40
use Syscodes\Components\Core\Http\Exceptions\NotFoundHttpException;
41
use Syscodes\Components\Database\Erostrine\Exceptions\ModelNotFoundException;
42
use Syscodes\Components\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
43
44
/**
45
 * The system's main exception class is loaded for activate the render method of debugging.
46
 */
47
class Handler implements ExceptionHandlerContract
48
{
49
    /**
50
     * The container implementation.
51
     * 
52
     * @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...
53
     */
54
    protected $container;
55
56
    /**
57
     * A list of the exception types that should not be reported.
58
     * 
59
     * @var array $dontReport
60
     */
61
    protected $dontReport = [];
62
63
    /**
64
     * A list of the Core exception types that should not be reported.
65
     * 
66
     * @var array $coreDontReport
67
     */
68
    protected $coreDontReport = [
69
        AuthenticationException::class,
70
        HttpException::class,
71
        HttpResponseException::class,
72
        ModelNotFoundException::class,
73
        TokenMismatchException::class,
74
    ];
75
76
    /**
77
     * The callbacks that should be used during reporting.
78
     * 
79
     * @var array $reportCallbacks
80
     */
81
    protected $reportCallbacks = [];
82
83
    /**
84
     * The callbacks that should be used during rendering.
85
     * 
86
     * @var array $renderCallbacks
87
     */
88
    protected $renderCallbacks = [];
89
90
    /**
91
     * Constructor. Create a new exception handler instance.
92
     * 
93
     * @param  \Syscodes\Components\Contracts\Container\Container  $container
94
     * 
95
     * @return void
96
     */
97
    public function __construct(Container $container)
98
    {
99
        $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...
100
101
        $this->register();
102
    }
103
104
    /**
105
     * Register the exception handling with callbacks for the application.
106
     * 
107
     * @return void
108
     */
109
    public function register() {}
110
111
    /**
112
     * Register a reportable callback.
113
     * 
114
     * @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...
115
     * 
116
     * @return static
117
     */
118
    public function reportable(callable $callback): static
119
    {
120
        $this->reportCallbacks[] = $callback;
121
122
        return $this;
123
    }
124
125
    /**
126
     * Register a renderable callback.
127
     * 
128
     * @param  \callable  $callback
129
     * 
130
     * @return static
131
     */
132
    public function renderable(callable $callback): static
133
    {
134
        $this->renderCallbacks[] = $callback;
135
136
        return $this;
137
    }
138
    
139
    /**
140
     * Report or log an exception.
141
     * 
142
     * @param  \Throwable  $e
143
     * 
144
     * @return mixed
145
     * 
146
     * @throws \Exception
147
     */
148
    public function report(Throwable $e)
149
    {
150
        if ($this->shouldntReport($e)) {
151
            return;
152
        }
153
154
        if (method_exists($e, 'report')) {
155
            return $e->report($e);
156
        }
157
        
158
        foreach ($this->reportCallbacks as $reportCallback) {
159
            if ($reportCallback($e) === false) {
160
                return;
161
            }
162
        }
163
164
        try {
165
            $logger = $this->container->make(LoggerInterface::class);
166
        } catch (Exception $e) {
167
            throw $e;
168
        }
169
        
170
        $logger->error($e->getMessage());
171
    }
172
173
    /**
174
     * Determine if the exception should be reported.
175
     * 
176
     * @param  \Throwable  $e
177
     * 
178
     * @return bool
179
     */
180
    public function shouldReport(Throwable $e): bool
181
    {
182
        return ! $this->shouldntReport($e);
183
    }
184
185
    /**
186
     * Determine if the exception is in the "do not report" list.
187
     * 
188
     * @param  \Throwable  $e
189
     * 
190
     * @return bool
191
     */
192
    public function shouldntReport(Throwable $e): bool
193
    {
194
        $dontReport = array_merge($this->dontReport, $this->coreDontReport);
195
        
196
        return ! is_null(Arr::first($dontReport, fn ($type) => $e instanceof $type));
197
    }
198
199
    /**
200
     * Render an exception into an HTTP response.
201
     * 
202
     * @param  \Syscodes\Components\Http\Request  $request
203
     * @param  \Throwable  $e
204
     * 
205
     * @return \Syscodes\Components\Http\Response
206
     */
207
    public function render($request, Throwable $e)
208
    {
209
        if (method_exists($e, 'render') && $response = $e->render($request)) {
210
            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

210
            return Router::/** @scrutinizer ignore-call */ toResponse($request, $response);
Loading history...
211
        }
212
        
213
        $e = $this->prepareException($e);
214
        
215
        foreach ($this->renderCallbacks as $renderCallback) {
216
            $response = $renderCallback($e, $request);
217
            
218
            if ( ! is_null($response)) {
219
                return $response;
220
            }
221
        }
222
223
        foreach ($this->renderCallbacks as $renderCallback) {
224
            $response = $renderCallback($e, $request);
225
226
            if ( ! is_null($response)) {
227
                return $response;
228
            }
229
        }
230
231
        if ($e instanceof HttpResponseException) {
232
            $e->getResponse();
233
        }
234
235
        return $this->prepareResponse($request, $e);
236
    }
237
238
    /**
239
     * Prepare exception for rendering.
240
     * 
241
     * @param  \Throwable  $e
242
     * 
243
     * @return \Throwable
244
     */
245
    protected function prepareException(Throwable $e): Throwable
246
    {
247
        return match (true) {            
248
            $e instanceof ModelNotFoundException => new NotFoundHttpException($e->getMessage(), $e),
249
            $e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e),
250
            default => $e,
251
        };
252
    }
253
     
254
    /**
255
     * Prepare a response for the given exception.
256
     * 
257
     * @param  \Syscodes\Components\Http\Request  $request
258
     * @param  \Throwable  $e
259
     * 
260
     * @return \Syscodes\Components\Http\Response
261
     * 
262
     * @uses   \Syscodes\Components\Core\Http\Exceptions\HttpException
263
     */
264
    protected function prepareResponse($request, Throwable $e)
265
    {
266
        if ( ! $this->isHttpException($e) && config('app.debug')) {
267
            return $this->toSyscodesResponse($this->convertExceptionToResponse($e), $e);
268
        }
269
270
        // When the debug is not active, the HTTP 500 code view is throw
271
        if ( ! $this->isHttpException($e)) {
272
            $e = new HttpException(500, $e->getMessage());
273
        }
274
275
        return $this->toSyscodesResponse(
276
                    $this->renderHttpException($e), $e
277
                )->prepare($request);
278
    }
279
280
    /**
281
     * Render the given HttpException.
282
     * 
283
     * @param  \Syscodes\Components\Core\Http\Exceptions\HttpException  $e
284
     * 
285
     * @return \Syscodes\Components\Http\Response
286
     */
287
    protected function renderHttpException(HttpException $e)
288
    {
289
        $this->registerViewErrorPaths();
290
291
        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

291
        if (view()->/** @scrutinizer ignore-call */ viewExists($view = $this->getHttpExceptionView($e))) {
Loading history...
292
            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

292
            return response()->/** @scrutinizer ignore-call */ view(
Loading history...
293
                $view, 
294
                ['exception' => $e],
295
                $e->getStatusCode(),
296
                $e->getHeaders()
297
            );
298
        }
299
300
        return $this->convertExceptionToResponse($e);
301
    }
302
303
    /**
304
     * Register the error view paths.
305
     * 
306
     * @return void
307
     */
308
    protected function registerViewErrorPaths(): void
309
    {
310
        (new RegisterErrorViewPaths)();
311
    }
312
313
    /**
314
     * Get the view used to render HTTP exceptions.
315
     * 
316
     * @param  \Syscodes\Components\Core\Http\Exceptions\HttpException  $e
317
     * 
318
     * @return string
319
     */
320
    protected function getHttpExceptionView(HttpException $e): string
321
    {
322
        return "errors::{$e->getStatusCode()}";
323
    }
324
325
    /**
326
     * Create a response for the given exception.
327
     * 
328
     * @param  \Throwable  $e
329
     * 
330
     * @return \Syscodes\Components\Http\Response
331
     */
332
    protected function convertExceptionToResponse(Throwable $e)
333
    {
334
        return Response::render(
335
            $this->renderExceptionContent($e),
336
            $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

336
            $this->isHttpException($e) ? $e->/** @scrutinizer ignore-call */ getStatusCode() : 500,
Loading history...
337
            $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

337
            $this->isHttpException($e) ? $e->/** @scrutinizer ignore-call */ getHeaders() : []
Loading history...
338
        );
339
    }
340
341
    /**
342
     * Gets the response content for the given exception.
343
     * 
344
     * @param  \Throwable  $e
345
     * 
346
     * @return string
347
     */
348
    protected function renderExceptionContent(Throwable $e): string
349
    {
350
        try {
351
            return config('app.debug') && app()->has(ExceptionRender::class)
352
                        ? $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...
353
                        : $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

353
                        : $this->renderExceptionWithFlatDesignDebug($e, /** @scrutinizer ignore-type */ config('app.debug'));
Loading history...
354
        } catch (Exception $e) {
355
            $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...
356
        }
357
    }
358
359
    /**
360
     * Render an exception to a string of debug.
361
     * 
362
     * @param  \Throwable  $e
363
     * 
364
     * @return void
365
     * 
366
     * @uses   \Syscodes\Components\Contracts\Core\ExceptionRender
367
     */
368
    protected function renderExceptionWithDebug(Throwable $e)
369
    {
370
        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

370
        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...
371
    }
372
373
    /**
374
     * Render an exception to a string using Flat Design Debug.
375
     * 
376
     * @param  \Throwable  $e
377
     * @param  bool  $debug
378
     * 
379
     * @return string
380
     */
381
    protected function renderExceptionWithFlatDesignDebug(Throwable $e, $debug)
382
    {
383
        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...
384
            FlattenException::make($e)
0 ignored issues
show
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

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