Passed
Push — main ( 7ca792...577f5e )
by Dimitri
03:34
created

Dispatcher::bootApp()   B

Complexity

Conditions 11
Paths 1

Size

Total Lines 55
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 32
nc 1
nop 0
dl 0
loc 55
rs 7.3166
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Router;
13
14
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
15
use BlitzPHP\Controllers\ApplicationController;
16
use BlitzPHP\Controllers\RestController;
17
use BlitzPHP\Core\App;
18
use BlitzPHP\Debug\Timer;
19
use BlitzPHP\Exceptions\FrameworkException;
20
use BlitzPHP\Exceptions\PageNotFoundException;
21
use BlitzPHP\Exceptions\RedirectException;
22
use BlitzPHP\Exceptions\ValidationException;
23
use BlitzPHP\Http\Middleware;
24
use BlitzPHP\Http\Response;
25
use BlitzPHP\Http\ServerRequest;
26
use BlitzPHP\Http\Uri;
27
use BlitzPHP\Loader\Services;
28
use BlitzPHP\Traits\SingletonTrait;
29
use BlitzPHP\Utilities\Helpers;
30
use BlitzPHP\View\View;
31
use Closure;
32
use InvalidArgumentException;
33
use Psr\Http\Message\ResponseInterface;
34
use Psr\Http\Message\ServerRequestInterface;
35
use stdClass;
36
37
class Dispatcher
38
{
39
    use SingletonTrait;
40
41
    /**
42
     * Heure de démarrage de l'application.
43
     *
44
     * @var mixed
45
     */
46
    protected $startTime;
47
48
    /**
49
     * Durée totale d'exécution de l'application
50
     *
51
     * @var float
52
     */
53
    protected $totalTime;
54
55
    /**
56
     * Main application configuration
57
     *
58
     * @var stdClass
59
     */
60
    protected $config;
61
62
    /**
63
     * instance Timer.
64
     *
65
     * @var Timer
66
     */
67
    protected $timer;
68
69
    /**
70
     * requête courrante.
71
     *
72
     * @var ServerRequest
73
     */
74
    protected $request;
75
76
    /**
77
     * Reponse courrante.
78
     *
79
     * @var Response
80
     */
81
    protected $response;
82
83
    /**
84
     * Router à utiliser.
85
     *
86
     * @var Router
87
     */
88
    protected $router;
89
90
    /**
91
     * @var Middleware
92
     */
93
    private $middleware;
94
95
    /**
96
     * Contrôleur à utiliser.
97
     *
98
     * @var Closure|string
99
     */
100
    protected $controller;
101
102
    /**
103
     * Méthode du ontrôleur à exécuter.
104
     *
105
     * @var string
106
     */
107
    protected $method;
108
109
    /**
110
     * Gestionnaire de sortie à utiliser.
111
     *
112
     * @var string
113
     */
114
    protected $output;
115
116
    /**
117
     * Délai d'expiration du cache
118
     *
119
     * @var int
120
     */
121
    protected static $cacheTTL = 0;
122
123
    /**
124
     * Chemin de requête à utiliser.
125
     *
126
     * @var string
127
     */
128
    protected $path;
129
130
    /**
131
     * L'instance Response doit-elle "faire semblant"
132
     * pour éviter de définir des en-têtes/cookies/etc
133
     *
134
     * @var bool
135
     */
136
    protected $useSafeOutput = false;
137
138
    /**
139
     * Constructor.
140
     */
141
    private function __construct()
142
    {
143
        $this->startTime = microtime(true);
144
145
        $this->config = (object) config('app');
146
    }
147
148
    public static function init(bool $returnResponse = false)
149
    {
150
        return self::instance()->run(null, $returnResponse);
151
    }
152
153
    /**
154
     * Retourne la methode invoquee
155
     */
156
    public static function getMethod(): ?string
157
    {
158
        $method = self::instance()->method;
159
        if (empty($method)) {
160
            $method = Services::routes()->getDefaultMethod();
161
        }
162
163
        return $method;
164
    }
165
166
    /**
167
     * Retourne le contrôleur utilisé
168
     *
169
     * @return Closure|string
170
     */
171
    public static function getController(bool $fullName = true)
172
    {
173
        $routes = Services::routes();
174
175
        $controller = self::instance()->controller;
176
        if (empty($controller)) {
177
            $controller = $routes->getDefaultController();
178
        }
179
180
        if (! $fullName && is_string($controller)) {
181
            $controller = str_replace($routes->getDefaultNamespace(), '', $controller);
182
        }
183
184
        return $controller;
185
    }
186
187
    /**
188
     * Lancez l'application !
189
     *
190
     * C'est "la boucle" si vous voulez. Le principal point d'entrée dans le script
191
     * qui obtient les instances de classe requises, déclenche les filtres,
192
     * essaie d'acheminer la réponse, charge le contrôleur et généralement
193
     * fait fonctionner toutes les pièces ensemble.
194
     *
195
     * @return bool|mixed|ResponseInterface|ServerRequestInterface
196
     *
197
     * @throws Exception
198
     * @throws RedirectException
199
     */
200
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
201
    {
202
        $this->startBenchmark();
203
204
        $this->getRequestObject();
205
        $this->getResponseObject();
206
207
        $this->initMiddlewareQueue();
208
209
        $this->forceSecureAccess();
210
211
        /**
212
         * Init event manager
213
         */
214
        $events_file = CONFIG_PATH . 'events.php';
215
        if (file_exists($events_file)) {
216
            require_once $events_file;
217
        }
218
219
        Services::event()->trigger('pre_system');
220
221
        // Recherche une page en cache. L'exécution s'arrêtera
222
        // si la page a été mise en cache.
223
        $response = $this->displayCache();
224
        if ($response instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$response is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
225
            if ($returnResponse) {
226
                return $response;
227
            }
228
229
            return $this->emitResponse($response);
0 ignored issues
show
Unused Code introduced by
The call to BlitzPHP\Router\Dispatcher::emitResponse() has too many arguments starting with $response. ( Ignorable by Annotation )

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

229
            return $this->/** @scrutinizer ignore-call */ emitResponse($response);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
Are you sure the usage of $this->emitResponse($response) targeting BlitzPHP\Router\Dispatcher::emitResponse() 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...
230
        }
231
232
        try {
233
            return $this->handleRequest($routes, $returnResponse);
234
        } catch (RedirectException $e) {
235
            Services::logger()->info('REDIRECTED ROUTE at ' . $e->getMessage());
236
237
            // Si la route est une route de "redirection", elle lance
238
            // l'exception avec le $to comme message
239
            // $this->response->redirect(base_url($e->getMessage()), 'auto', $e->getCode());
240
            $this->response = $this->response->withHeader('Location', base_url($e->getMessage()), 'auto', $e->getCode());
241
242
            $this->sendResponse();
243
244
            $this->callExit(EXIT_SUCCESS);
245
246
            return;
247
        } catch (PageNotFoundException $e) {
248
            $this->display404errors($e);
249
        }
250
    }
251
252
    /**
253
     * Définissez notre instance Response sur le mode "faire semblant" afin que des choses comme
254
     * les cookies et les en-têtes ne sont pas réellement envoyés, permettant à PHP 7.2+ de
255
     * ne pas se plaindre lorsque la fonction ini_set() est utilisée.
256
     */
257
    public function useSafeOutput(bool $safe = true): self
258
    {
259
        $this->useSafeOutput = $safe;
260
261
        return $this;
262
    }
263
264
    /**
265
     * Handles the main request logic and fires the controller.
266
     *
267
     * @return mixed|RequestInterface|ResponseInterface
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Router\RequestInterface 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...
268
     *
269
     * @throws PageNotFoundException
270
     * @throws RedirectException
271
     */
272
    protected function handleRequest(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
273
    {
274
        if (empty($routes)) {
275
            $routes_file = CONFIG_PATH . 'routes.php';
276
277
            if (file_exists($routes_file)) {
278
                require_once $routes_file;
279
            }
280
        }
281
        if (empty($routes) || ! ($routes instanceof RouteCollection)) {
282
            $routes = Services::routes();
283
        }
284
285
        /**
286
         * Route middlewares
287
         */
288
        $routeMiddlewares = (array) $this->dispatchRoutes($routes);
289
290
        // The bootstrapping in a middleware
291
        $this->middleware->append($this->bootApp());
292
293
        /**
294
         * Ajouter des middlewares de routes
295
         */
296
        foreach ($routeMiddlewares as $middleware) {
297
            $this->middleware->prepend($middleware);
298
        }
299
300
        // Enregistrer notre URI actuel en tant qu'URI précédent dans la session
301
        // pour une utilisation plus sûre et plus précise avec la fonction d'assistance `previous_url()`.
302
        $this->storePreviousURL(current_url(true));
303
304
        /**
305
         * Emission de la reponse
306
         */
307
        $this->gatherOutput($this->middleware->handle($this->request));
308
309
        if (! $returnResponse) {
310
            $this->sendResponse();
311
        }
312
313
        // Y a-t-il un événement post-système ?
314
        Services::event()->trigger('post_system');
315
316
        return $this->response;
317
    }
318
319
    /**
320
     * Démarrer le benchmark
321
     *
322
     * La minuterie est utilisée pour afficher l'exécution totale du script à la fois dans la
323
     * barre d'outils de débogage, et éventuellement sur la page affichée.
324
     */
325
    protected function startBenchmark()
326
    {
327
        if ($this->startTime === null) {
328
            $this->startTime = microtime(true);
329
        }
330
331
        $this->timer = Services::timer();
332
        $this->timer->start('total_execution', $this->startTime);
0 ignored issues
show
Bug introduced by
It seems like $this->startTime can also be of type string; however, parameter $time of BlitzPHP\Debug\Timer::start() does only seem to accept double|null, 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

332
        $this->timer->start('total_execution', /** @scrutinizer ignore-type */ $this->startTime);
Loading history...
333
        $this->timer->start('bootstrap');
334
    }
335
336
    /**
337
     * Définit un objet Request à utiliser pour cette requête.
338
     * Utilisé lors de l'exécution de certains tests.
339
     */
340
    public function setRequest(ServerRequestInterface $request): self
341
    {
342
        $this->request = $request;
0 ignored issues
show
Documentation Bug introduced by
$request is of type Psr\Http\Message\ServerRequestInterface, but the property $request was declared to be of type BlitzPHP\Http\ServerRequest. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
343
344
        return $this;
345
    }
346
347
    /**
348
     * Obtenez notre objet Request et définissez le protocole du serveur en fonction des informations fournies
349
     * par le serveur.
350
     */
351
    protected function getRequestObject()
352
    {
353
        if ($this->request instanceof ServerRequestInterface) {
0 ignored issues
show
introduced by
$this->request is always a sub-type of Psr\Http\Message\ServerRequestInterface.
Loading history...
354
            return;
355
        }
356
357
        if (is_cli() && ! on_test()) {
358
            // @codeCoverageIgnoreStart
359
            // $this->request = Services::clirequest($this->config);
360
            // @codeCoverageIgnoreEnd
361
        }
362
363
        $version = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
364
        if (! is_numeric($version)) {
365
            $version = substr($version, strpos($version, '/') + 1);
366
        }
367
368
        // Assurez-vous que la version est au bon format
369
        $version = number_format((float) $version, 1);
370
371
        $this->request = Services::request()->withProtocolVersion($version);
372
    }
373
374
    /**
375
     * Obtenez notre objet Response et définissez des valeurs par défaut, notamment
376
     * la version du protocole HTTP et une réponse réussie par défaut.
377
     */
378
    protected function getResponseObject()
379
    {
380
        // Supposons le succès jusqu'à preuve du contraire.
381
        $this->response = Services::response()->withStatus(200);
382
383
        if (! is_cli() || on_test()) {
384
        }
385
386
        $this->response = $this->response->withProtocolVersion($this->request->getProtocolVersion());
387
    }
388
389
    /**
390
     * Forcer l'accès au site sécurisé ? Si la valeur de configuration 'forceGlobalSecureRequests'
391
     * est vrai, imposera que toutes les demandes adressées à ce site soient effectuées via
392
     * HTTPS. Redirigera également l'utilisateur vers la page actuelle avec HTTPS
393
     * comme défini l'en-tête HTTP Strict Transport Security pour ces navigateurs
394
     * qui le supportent.
395
     *
396
     * @param int $duration Combien de temps la sécurité stricte des transports
397
     *                      doit être appliqué pour cette URL.
398
     */
399
    protected function forceSecureAccess($duration = 31536000)
400
    {
401
        if ($this->config->force_global_secure_requests !== true) {
402
            return;
403
        }
404
405
        force_https($duration, $this->request, $this->response);
406
    }
407
408
    /**
409
     * Détermine si une réponse a été mise en cache pour l'URI donné.
410
     *
411
     * @return bool|ResponseInterface
412
     *
413
     * @throws FrameworkException
414
     */
415
    public function displayCache()
416
    {
417
        if ($cachedResponse = Services::cache()->read($this->generateCacheName())) {
418
            $cachedResponse = unserialize($cachedResponse);
419
            if (! is_array($cachedResponse) || ! isset($cachedResponse['output']) || ! isset($cachedResponse['headers'])) {
420
                throw new FrameworkException('Error unserializing page cache');
421
            }
422
423
            $headers = $cachedResponse['headers'];
424
            $output  = $cachedResponse['output'];
425
426
            // Effacer tous les en-têtes par défaut
427
            foreach (array_keys($this->response->getHeaders()) as $key) {
428
                $this->response = $this->response->withoutHeader($key);
429
            }
430
431
            // Définir les en-têtes mis en cache
432
            foreach ($headers as $name => $value) {
433
                $this->response = $this->response->withHeader($name, $value);
434
            }
435
436
            $output = $this->displayPerformanceMetrics($output);
437
438
            return $this->response->withBody(to_stream($output));
439
        }
440
441
        return false;
442
    }
443
444
    /**
445
     * Indique à l'application que la sortie finale doit être mise en cache.
446
     */
447
    public static function cache(int $time)
448
    {
449
        static::$cacheTTL = $time;
450
    }
451
452
    /**
453
     * Met en cache la réponse complète de la requête actuelle. Pour utiliser
454
     * la mise en cache pleine page pour des performances très élevées.
455
     *
456
     * @return mixed
457
     */
458
    protected function cachePage()
459
    {
460
        $headers = [];
461
462
        foreach (array_keys($this->response->getHeaders()) as $header) {
463
            $headers[$header] = $this->response->getHeaderLine($header);
464
        }
465
466
        return Services::cache()->write(
467
            $this->generateCacheName(),
468
            serialize(['headers' => $headers, 'output' => $this->output]),
469
            static::$cacheTTL
470
        );
471
    }
472
473
    /**
474
     * Renvoie un tableau avec nos statistiques de performances de base collectées.
475
     */
476
    public function getPerformanceStats(): array
477
    {
478
        return [
479
            'startTime' => $this->startTime,
480
            'totalTime' => $this->totalTime,
481
        ];
482
    }
483
484
    /**
485
     * Génère le nom du cache à utiliser pour notre mise en cache pleine page.
486
     */
487
    protected function generateCacheName(): string
488
    {
489
        $uri = $this->request->getUri();
490
491
        $name = Uri::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath());
492
493
        return md5($name);
494
    }
495
496
    /**
497
     * Remplace les balises memory_usage et elapsed_time.
498
     */
499
    public function displayPerformanceMetrics(string $output): string
500
    {
501
        $this->totalTime = $this->timer->getElapsedTime('total_execution');
502
503
        return str_replace('{elapsed_time}', (string) $this->totalTime, $output);
504
    }
505
506
    /**
507
     * Fonctionne avec le routeur pour
508
     * faire correspondre une route à l'URI actuel. Si la route est une
509
     * "route de redirection", gérera également la redirection.
510
     *
511
     * @param RouteCollectionInterface|null $routes Une interface de collecte à utiliser à la place
512
     *                                              du fichier de configuration.
513
     *
514
     * @return string|string[]|null
515
     *
516
     * @throws RedirectException
517
     */
518
    protected function dispatchRoutes(RouteCollectionInterface $routes)
519
    {
520
        $this->router = Services::router($routes, $this->request, false);
521
522
        $path = $this->determinePath();
523
524
        $this->timer->stop('bootstrap');
525
        $this->timer->start('routing');
526
527
        ob_start();
528
        $this->controller = $this->router->handle($path);
529
        $this->method     = $this->router->methodName();
530
531
        // Si un segment {locale} correspondait dans la route finale,
532
        // alors nous devons définir les paramètres régionaux corrects sur notre requête.
533
        if ($this->router->hasLocale()) {
534
            $this->request = $this->request->withLocale($this->router->getLocale());
535
        }
536
537
        $this->timer->stop('routing');
538
539
        return $this->router->getMiddlewares();
540
    }
541
542
    /**
543
     * Détermine le chemin à utiliser pour que nous essayions d'acheminer vers, en fonction
544
     * de l'entrée de l'utilisateur (setPath), ou le chemin CLI/IncomingRequest.
545
     */
546
    protected function determinePath(): string
547
    {
548
        if (! empty($this->path)) {
549
            return $this->path;
550
        }
551
552
        $path = method_exists($this->request, 'getPath')
553
            ? $this->request->getPath()
554
            : $this->request->getUri()->getPath();
555
556
        return preg_replace('#^' . App::getUri()->getPath() . '#i', '', $path);
557
    }
558
559
    /**
560
     * Permet de définir le chemin de la requête depuis l'extérieur de la classe,
561
     * au lieu de compter sur CLIRequest ou IncomingRequest pour le chemin.
562
     *
563
     * Ceci est principalement utilisé par la console.
564
     */
565
    public function setPath(string $path): self
566
    {
567
        $this->path = $path;
568
569
        return $this;
570
    }
571
572
    /**
573
     * Maintenant que tout a été configuré, cette méthode tente d'exécuter le
574
     * méthode du contrôleur et lancez le script. S'il n'en est pas capable, le fera
575
     * afficher l'erreur Page introuvable appropriée.
576
     */
577
    protected function startController(ServerRequest $request, Response $response)
578
    {
579
        // Aucun contrôleur spécifié - nous ne savons pas quoi faire maintenant.
580
        if (empty($this->controller)) {
581
            throw PageNotFoundException::emptyController();
582
        }
583
584
        $this->timer->start('controller');
585
        $this->timer->start('controller_constructor');
586
587
        // Est-il acheminé vers une Closure ?
588
        if (is_object($this->controller) && (get_class($this->controller) === 'Closure')) {
589
            $controller = $this->controller;
590
591
            $sendParameters = [];
592
593
            foreach ($this->router->params() as $parameter) {
594
                $sendParameters[] = $parameter;
595
            }
596
            array_push($sendParameters, $request, $response);
597
598
            return Services::injector()->call($controller, $sendParameters);
599
        }
600
601
        // Essayez de charger automatiquement la classe
602
        if (! class_exists($this->controller, true) || $this->method[0] === '_') {
0 ignored issues
show
Bug introduced by
It seems like $this->controller can also be of type Closure; however, parameter $class of class_exists() does only seem to accept string, 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

602
        if (! class_exists(/** @scrutinizer ignore-type */ $this->controller, true) || $this->method[0] === '_') {
Loading history...
603
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
0 ignored issues
show
Bug introduced by
It seems like $this->controller can also be of type Closure; however, parameter $controller of BlitzPHP\Exceptions\Page...n::controllerNotFound() does only seem to accept string, 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

603
            throw PageNotFoundException::controllerNotFound(/** @scrutinizer ignore-type */ $this->controller, $this->method);
Loading history...
604
        }
605
606
        return null;
607
    }
608
609
    /**
610
     * Instancie la classe contrôleur.
611
     *
612
     * @return \BlitzPHP\Controllers\BaseController|mixed
613
     */
614
    private function createController(ServerRequestInterface $request, ResponseInterface $response)
615
    {
616
        /**
617
         * @var \BlitzPHP\Controllers\BaseController
618
         */
619
        $class = Services::injector()->get($this->controller);
0 ignored issues
show
Bug introduced by
It seems like $this->controller can also be of type Closure; however, parameter $name of BlitzPHP\Loader\Injector::get() does only seem to accept string, 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

619
        $class = Services::injector()->get(/** @scrutinizer ignore-type */ $this->controller);
Loading history...
620
621
        if (method_exists($class, 'initialize')) {
622
            $class->initialize($request, $response, Services::logger());
623
        }
624
625
        $this->timer->stop('controller_constructor');
626
627
        return $class;
628
    }
629
630
    /**
631
     * Exécute le contrôleur, permettant aux méthodes _remap de fonctionner.
632
     *
633
     * @param mixed $class
634
     *
635
     * @return mixed
636
     */
637
    protected function runController($class)
638
    {
639
        // S'il s'agit d'une demande de console, utilisez les segments d'entrée comme paramètres
640
        $params = defined('KLINGED') ? $this->request->getSegments() : $this->router->params();
0 ignored issues
show
Bug introduced by
The method getSegments() does not exist on BlitzPHP\Http\ServerRequest. 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

640
        $params = defined('KLINGED') ? $this->request->/** @scrutinizer ignore-call */ getSegments() : $this->router->params();
Loading history...
641
        $method = $this->method;
642
643
        if (method_exists($class, '_remap')) {
644
            $params = [$method, $params];
645
            $method = '_remap';
646
        }
647
648
        $output = Services::injector()->call([$class, $method], (array) $params);
649
650
        $this->timer->stop('controller');
651
652
        if ($output instanceof View) {
653
            $output = $this->response->withBody(to_stream($output->get()));
654
        }
655
656
        return $output;
657
    }
658
659
    /**
660
     * Affiche une page d'erreur 404 introuvable. S'il est défini, essaiera de
661
     * appelez le contrôleur/méthode 404Override qui a été défini dans la configuration de routage.
662
     */
663
    protected function display404errors(PageNotFoundException $e)
664
    {
665
        // Is there a 404 Override available?
666
        if ($override = $this->router->get404Override()) {
667
            if ($override instanceof Closure) {
0 ignored issues
show
introduced by
$override is never a sub-type of Closure.
Loading history...
668
                echo $override($e->getMessage());
669
            } elseif (is_array($override)) {
0 ignored issues
show
introduced by
The condition is_array($override) is always true.
Loading history...
670
                $this->timer->start('controller');
671
                $this->timer->start('controller_constructor');
672
673
                $this->controller = $override[0];
674
                $this->method     = $override[1];
675
676
                $controller = $this->createController($this->request, $this->response);
677
                $this->runController($controller);
678
            }
679
680
            unset($override);
681
682
            $this->emitResponse();
683
684
            return;
685
        }
686
687
        // Affiche l'erreur 404
688
        $this->response = $this->response->withStatus($e->getCode());
689
690
        if (! on_test()) {
691
            // @codeCoverageIgnoreStart
692
            if (ob_get_level() > 0) {
693
                ob_end_flush();
694
            }
695
        // @codeCoverageIgnoreEnd
696
        }
697
        // Lors des tests, l'un est pour phpunit, l'autre pour le cas de test.
698
        elseif (ob_get_level() > 2) {
699
            ob_end_flush(); // @codeCoverageIgnore
700
        }
701
702
        throw PageNotFoundException::pageNotFound(! on_prod() || is_cli() ? $e->getMessage() : '');
703
    }
704
705
    /**
706
     * Rassemble la sortie du script à partir du tampon, remplace certaines balises d'exécutions
707
     * d'horodatage dans la sortie et affiche la barre d'outils de débogage, si nécessaire.
708
     *
709
     * @param mixed|null $returned
710
     */
711
    protected function gatherOutput($returned = null)
712
    {
713
        $this->output = ob_get_contents();
714
        // Si la mise en mémoire tampon n'est pas nulle.
715
        // Nettoyer (effacer) le tampon de sortie et désactiver le tampon de sortie
716
        if (ob_get_length()) {
717
            ob_end_clean();
718
        }
719
720
        // Si le contrôleur a renvoyé un objet de réponse,
721
        // nous devons en saisir le corps pour qu'il puisse
722
        // être ajouté à tout ce qui aurait déjà pu être ajouté avant de faire le écho.
723
        // Nous devons également enregistrer l'instance localement
724
        // afin que tout changement de code d'état, etc., ait lieu.
725
        if ($returned instanceof ResponseInterface) {
726
            $this->response = $returned;
0 ignored issues
show
Documentation Bug introduced by
$returned is of type Psr\Http\Message\ResponseInterface, but the property $response was declared to be of type BlitzPHP\Http\Response. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
727
            $returned       = $returned->getBody()->getContents();
728
        }
729
730
        if (is_string($returned)) {
731
            $this->output .= $returned;
732
        }
733
734
        // Mettez-le en cache sans remplacer les mesures de performances
735
        // afin que nous puissions avoir des mises à jour de vitesse en direct en cours de route.
736
        if (static::$cacheTTL > 0) {
737
            $this->cachePage();
738
        }
739
740
        $this->output = $this->displayPerformanceMetrics($this->output);
741
742
        $this->response = $this->response->withBody(to_stream($this->output));
743
    }
744
745
    /**
746
     * Si nous avons un objet de session à utiliser, stockez l'URI actuel
747
     * comme l'URI précédent. Ceci est appelé juste avant d'envoyer la
748
     * réponse au client, et le rendra disponible à la prochaine demande.
749
     *
750
     * Cela permet au fournisseur une détection plus sûre et plus fiable de la fonction previous_url().
751
     *
752
     * @param \BlitzPHP\Http\URI|string $uri
753
     */
754
    public function storePreviousURL($uri)
755
    {
756
        // Ignorer les requêtes CLI
757
        if (is_cli() && ! on_test()) {
758
            return; // @codeCoverageIgnore
759
        }
760
761
        // Ignorer les requêtes AJAX
762
        if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
0 ignored issues
show
Bug introduced by
The method isAJAX() does not exist on BlitzPHP\Http\ServerRequest. 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

762
        if (method_exists($this->request, 'isAJAX') && $this->request->/** @scrutinizer ignore-call */ isAJAX()) {
Loading history...
763
            return;
764
        }
765
766
        // Ignorer les reponses non-HTML
767
        if (strpos($this->response->getHeaderLine('Content-Type'), 'text/html') === false) {
768
            return;
769
        }
770
771
        // Ceci est principalement nécessaire lors des tests ...
772
        if (is_string($uri)) {
773
            $uri = Services::uri($uri, false);
774
        }
775
776
        if (isset($_SESSION)) {
777
            $_SESSION['_blitz_previous_url'] = Uri::createURIString(
778
                $uri->getScheme(),
779
                $uri->getAuthority(),
780
                $uri->getPath(),
781
                $uri->getQuery(),
782
                $uri->getFragment()
783
            );
784
        }
785
    }
786
787
    /**
788
     * Renvoie la sortie de cette requête au client.
789
     * C'est ce qu'il attendait !
790
     */
791
    protected function sendResponse()
792
    {
793
        $this->totalTime = $this->timer->getElapsedTime('total_execution');
794
        Services::emitter()->emit(
795
            Services::toolbar()->prepare($this->getPerformanceStats(), $this->request, $this->response)
796
        );
797
    }
798
799
    protected function emitResponse()
800
    {
801
        $this->gatherOutput();
802
        $this->sendResponse();
803
    }
804
805
    /**
806
     * Construit une reponse adequate en fonction du retour du controleur
807
     */
808
    protected function formatResponse(ResponseInterface $response, $returned): ResponseInterface
809
    {
810
        if ($returned instanceof ResponseInterface) {
811
            return $returned;
812
        } 
813
        
814
        if (is_object($returned)) {
815
            if (method_exists($returned, 'toArray')) {
816
                $returned = $returned->toArray();
817
            } else if (method_exists($returned, 'toJSON')) {
818
                $returned = $returned->toJSON();
819
            } else {
820
                $returned = (array) $returned;
821
            }
822
        }
823
824
        if (is_array($returned)) {
825
            $returned = Helpers::collect($returned);
826
            $response = $response->withHeader('Content-Type', 'application/json');
827
        }
828
            
829
        try {
830
            $response = $response->withBody(to_stream($returned));
831
        }
832
        catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
833
        }
834
    
835
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface.
Loading history...
836
    }
837
838
    /**
839
     * Quitte l'application en définissant le code de sortie pour les applications basées sur CLI
840
     * qui pourrait regarder.
841
     *
842
     * Fabriqué dans une méthode distincte afin qu'il puisse être simulé pendant les tests
843
     * sans réellement arrêter l'exécution du script.
844
     */
845
    protected function callExit(int $code)
846
    {
847
        exit($code); // @codeCoverageIgnore
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
848
    }
849
850
    /**
851
     * Initialise le gestionnaire de middleware
852
     */
853
    protected function initMiddlewareQueue(): void
854
    {
855
        $this->middleware = Services::injector()->make(Middleware::class, ['response' => $this->response]);
856
        
857
        $middlewaresFile = CONFIG_PATH . 'middlewares.php';
858
        if (file_exists($middlewaresFile) && ! in_array($middlewaresFile, get_included_files(), true)) {
859
            $middleware = require $middlewaresFile;
860
            if (is_callable($middleware)) {
861
                $middlewareQueue = $middleware($this->middleware, $this->request);
862
                if ($middlewareQueue instanceof Middleware) {
863
                    $this->middleware = $middlewareQueue;
864
                }
865
            }
866
        }
867
868
        $this->middleware->prepend($this->spoofRequestMethod());
869
    }
870
871
    /**
872
     * Modifie l'objet de requête pour utiliser une méthode différente
873
     * si une variable POST appelée _method est trouvée.
874
     */
875
    private function spoofRequestMethod(): callable
876
    {
877
        return static function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
878
            $post = $request->getParsedBody();
879
880
            // Ne fonctionne qu'avec les formulaires POST
881
            if (strtoupper($request->getMethod()) === 'POST' && ! empty($post['_method'])) {
882
                // Accepte seulement PUT, PATCH, DELETE
883
                if (in_array(strtoupper($post['_method']), ['PUT', 'PATCH', 'DELETE'], true)) {
884
                    $request = $request->withMethod($post['_method']);
885
                }
886
            }
887
888
            return $next($request, $response);
889
        };
890
    }
891
892
    private function bootApp(): callable
893
    {
894
        $_this = $this;
895
896
        return static function (ServerRequestInterface $request, ResponseInterface $response, callable $next) use($_this): ResponseInterface {
897
            try {
898
                $returned = $_this->startController($request, $response);
899
900
                // Closure controller has run in startController().
901
                if (! is_callable($_this->controller)) {
902
                    $controller = $_this->createController($request, $response);
903
904
                    if (! method_exists($controller, '_remap') && ! is_callable([$controller, $_this->method], false)) {
905
                        throw PageNotFoundException::methodNotFound($_this->method);
906
                    }
907
908
                    // Is there a "post_controller_constructor" event?
909
                    Services::event()->trigger('post_controller_constructor');
910
911
                    $returned = $_this->runController($controller);
912
                } else {
913
                    $_this->timer->stop('controller_constructor');
914
                    $_this->timer->stop('controller');
915
                }
916
917
                Services::event()->trigger('post_system');
918
919
                return $_this->formatResponse($response, $returned);
920
            }
921
            catch (ValidationException $e) {
922
                $code   = $e->getCode();
923
                $errors = $e->getErrors();
924
                if (empty($errors)) {
925
                    $errors = [$e->getMessage()];
926
                }
927
928
                if (is_string($_this->controller)) {
929
                    if (strtoupper($request->getMethod()) === 'POST') {
930
                        if (is_subclass_of($_this->controller, ApplicationController::class)) {
931
                            return Services::redirection()->back()->withInput()->withErrors($errors)->withStatus($code);
932
                        }
933
                        else if (is_subclass_of($_this->controller, RestController::class)) {
934
                            return $_this->formatResponse($response->withStatus($code), [
935
                                'success' => false,
936
                                'code'    => $code,
937
                                'errors'  => $errors,
938
                            ]);
939
                        }
940
                    }
941
                }
942
                else if (strtoupper($request->getMethod()) === 'POST') {
943
                    return Services::redirection()->back()->withInput()->withErrors($errors)->withStatus($code);
944
                }
945
946
                throw $e;
947
            }
948
        };
949
    }
950
}
951