Passed
Push — main ( 4cd58e...3399b6 )
by Dimitri
03:02
created

Dispatcher::runController()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 8
nop 1
dl 0
loc 20
rs 9.9332
c 0
b 0
f 0
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
    protected static int $cacheTTL = 0;
120
121
    /**
122
     * Chemin de requête à utiliser.
123
     *
124
     * @var string
125
     */
126
    protected $path;
127
    
128
    /**
129
    * Indique s'il faut renvoyer l'objet Response ou envoyer la réponse.
130
    */
131
   protected bool $returnResponse = false;
132
133
    /**
134
     * Constructor.
135
     */
136
    private function __construct()
137
    {
138
        $this->startTime = microtime(true);
139
        $this->config    = (object) config('app');
140
    }
141
142
    public static function init(bool $returnResponse = false)
143
    {
144
        return self::instance()->run(null, $returnResponse);
145
    }
146
147
    /**
148
     * Retourne la methode invoquee
149
     */
150
    public static function getMethod(): ?string
151
    {
152
        $method = self::instance()->method;
153
        if (empty($method)) {
154
            $method = Services::routes()->getDefaultMethod();
155
        }
156
157
        return $method;
158
    }
159
160
    /**
161
     * Retourne le contrôleur utilisé
162
     *
163
     * @return Closure|string
164
     */
165
    public static function getController(bool $fullName = true)
166
    {
167
        $routes = Services::routes();
168
169
        $controller = self::instance()->controller;
170
        if (empty($controller)) {
171
            $controller = $routes->getDefaultController();
172
        }
173
174
        if (! $fullName && is_string($controller)) {
175
            $controller = str_replace($routes->getDefaultNamespace(), '', $controller);
176
        }
177
178
        return $controller;
179
    }
180
181
    /**
182
     * Lancez l'application !
183
     *
184
     * C'est "la boucle" si vous voulez. Le principal point d'entrée dans le script
185
     * qui obtient les instances de classe requises, déclenche les filtres,
186
     * essaie d'acheminer la réponse, charge le contrôleur et généralement
187
     * fait fonctionner toutes les pièces ensemble.
188
     *
189
     * @return bool|mixed|ResponseInterface|ServerRequestInterface
190
     *
191
     * @throws Exception
192
     * @throws RedirectException
193
     */
194
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
195
    {
196
        $this->returnResponse = $returnResponse;
197
198
        $this->startBenchmark();
199
200
        $this->getRequestObject();
201
        $this->getResponseObject();
202
203
        $this->initMiddlewareQueue();
204
205
        $this->forceSecureAccess();
206
207
        /**
208
         * Init event manager
209
         */
210
        $events_file = CONFIG_PATH . 'events.php';
211
        if (file_exists($events_file)) {
212
            require_once $events_file;
213
        }
214
215
        Services::event()->trigger('pre_system');
216
217
        // Recherche une page en cache. L'exécution s'arrêtera
218
        // si la page a été mise en cache.
219
        $response = $this->displayCache();
220
        if ($response instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$response is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
221
            if ($this->returnResponse) {
222
                return $response;
223
            }
224
225
            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

225
            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...
226
        }
227
228
        try {
229
            return $this->handleRequest($routes);
230
        } catch (RedirectException $e) {
231
            Services::logger()->info('REDIRECTED ROUTE at ' . $e->getMessage());
232
233
            // Si la route est une route de "redirection", elle lance
234
            // l'exception avec le $to comme message
235
            // $this->response->redirect(base_url($e->getMessage()), 'auto', $e->getCode());
236
            $this->response = $this->response->withHeader('Location', base_url($e->getMessage()), 'auto', $e->getCode());
237
238
            $this->sendResponse();
239
240
            $this->callExit(EXIT_SUCCESS);
241
242
            return;
243
        } catch (PageNotFoundException $e) {
244
            $return = $this->display404errors($e);
245
246
            if ($return instanceof ResponseInterface) {
247
                return $return;
248
            }
249
        }
250
    }
251
252
    /**
253
     * Gère la logique de requête principale et déclenche le contrôleur.
254
     *
255
     * @return mixed|ResponseInterface
256
     *
257
     * @throws PageNotFoundException
258
     * @throws RedirectException
259
     */
260
    protected function handleRequest(?RouteCollectionInterface $routes = null)
261
    {
262
        $routeMiddlewares = (array) $this->dispatchRoutes($routes);
263
264
        // Le bootstrap dans un middleware
265
        $this->middleware->append($this->bootApp());
266
267
        /**
268
         * Ajouter des middlewares de routes
269
         */
270
        foreach ($routeMiddlewares as $middleware) {
271
            $this->middleware->prepend($middleware);
272
        }
273
274
        // Enregistrer notre URI actuel en tant qu'URI précédent dans la session
275
        // pour une utilisation plus sûre et plus précise avec la fonction d'assistance `previous_url()`.
276
        $this->storePreviousURL(current_url(true));
277
278
        /**
279
         * Emission de la reponse
280
         */
281
        $this->gatherOutput($this->middleware->handle($this->request));
282
283
        if (! $this->returnResponse) {
284
            $this->sendResponse();
285
        }
286
287
        // Y a-t-il un événement post-système ?
288
        Services::event()->trigger('post_system');
289
290
        return $this->response;
291
    }
292
293
    /**
294
     * Démarrer le benchmark
295
     *
296
     * La minuterie est utilisée pour afficher l'exécution totale du script à la fois dans la
297
     * barre d'outils de débogage, et éventuellement sur la page affichée.
298
     */
299
    protected function startBenchmark()
300
    {
301
        if ($this->startTime === null) {
302
            $this->startTime = microtime(true);
303
        }
304
305
        $this->timer = Services::timer();
306
        $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

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

580
        if (! class_exists(/** @scrutinizer ignore-type */ $this->controller, true) || ($this->method[0] === '_' && $this->method !== '__invoke')) {
Loading history...
581
            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

581
            throw PageNotFoundException::controllerNotFound(/** @scrutinizer ignore-type */ $this->controller, $this->method);
Loading history...
582
        }
583
584
        return null;
585
    }
586
587
    /**
588
     * Instancie la classe contrôleur.
589
     *
590
     * @return \BlitzPHP\Controllers\BaseController|mixed
591
     */
592
    private function createController(ServerRequestInterface $request, ResponseInterface $response)
593
    {
594
        /**
595
         * @var \BlitzPHP\Controllers\BaseController
596
         */
597
        $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

597
        $class = Services::injector()->get(/** @scrutinizer ignore-type */ $this->controller);
Loading history...
598
599
        if (method_exists($class, 'initialize')) {
600
            $class->initialize($request, $response, Services::logger());
601
        }
602
603
        $this->timer->stop('controller_constructor');
604
605
        return $class;
606
    }
607
608
    /**
609
     * Exécute le contrôleur, permettant aux méthodes _remap de fonctionner.
610
     *
611
     * @param mixed $class
612
     *
613
     * @return mixed
614
     */
615
    protected function runController($class)
616
    {
617
        // S'il s'agit d'une demande de console, utilisez les segments d'entrée comme paramètres
618
        $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

618
        $params = defined('KLINGED') ? $this->request->/** @scrutinizer ignore-call */ getSegments() : $this->router->params();
Loading history...
619
        $method = $this->method;
620
621
        if (method_exists($class, '_remap')) {
622
            $params = [$method, $params];
623
            $method = '_remap';
624
        }
625
626
        $output = Services::injector()->call([$class, $method], (array) $params);
627
628
        $this->timer->stop('controller');
629
630
        if ($output instanceof View) {
631
            $output = $this->response->withBody(to_stream($output->get()));
632
        }
633
634
        return $output;
635
    }
636
637
    /**
638
     * Affiche une page d'erreur 404 introuvable. S'il est défini, essaiera de
639
     * appelez le contrôleur/méthode 404Override qui a été défini dans la configuration de routage.
640
     */
641
    protected function display404errors(PageNotFoundException $e)
642
    {
643
        // Existe-t-il une dérogation 404 disponible ?
644
        if ($override = $this->router->get404Override()) {
645
            $returned = null;
646
            
647
            if ($override instanceof Closure) {
0 ignored issues
show
introduced by
$override is never a sub-type of Closure.
Loading history...
648
                echo $override($e->getMessage());
649
            } elseif (is_array($override)) {
0 ignored issues
show
introduced by
The condition is_array($override) is always true.
Loading history...
650
                $this->timer->start('controller');
651
                $this->timer->start('controller_constructor');
652
653
                $this->controller = $override[0];
654
                $this->method     = $override[1];
655
656
                $controller = $this->createController($this->request, $this->response);
657
                $returned   = $this->runController($controller);
658
            }
659
660
            unset($override);
661
662
            $this->gatherOutput($returned);
663
            if ($this->returnResponse) {
664
                return $this->response;
665
            }
666
            $this->emitResponse();
667
668
            return $returned;
669
        }
670
671
        // Affiche l'erreur 404
672
        $this->response = $this->response->withStatus($e->getCode());
673
674
        if (! on_test()) {
675
            // @codeCoverageIgnoreStart
676
            if (ob_get_level() > 0) {
677
                ob_end_flush();
678
            }
679
            // @codeCoverageIgnoreEnd
680
        }
681
        // Lors des tests, l'un est pour phpunit, l'autre pour le cas de test.
682
        elseif (ob_get_level() > 2) {
683
            ob_end_flush(); // @codeCoverageIgnore
684
        }
685
686
        throw PageNotFoundException::pageNotFound(! on_prod() || is_cli() ? $e->getMessage() : '');
687
    }
688
689
    /**
690
     * Rassemble la sortie du script à partir du tampon, remplace certaines balises d'exécutions
691
     * d'horodatage dans la sortie et affiche la barre d'outils de débogage, si nécessaire.
692
     *
693
     * @param mixed|null $returned
694
     */
695
    protected function gatherOutput($returned = null)
696
    {
697
        $this->output = ob_get_contents();
698
        // Si la mise en mémoire tampon n'est pas nulle.
699
        // Nettoyer (effacer) le tampon de sortie et désactiver le tampon de sortie
700
        if (ob_get_length()) {
701
            ob_end_clean();
702
        }
703
704
        // Si le contrôleur a renvoyé un objet de réponse,
705
        // nous devons en saisir le corps pour qu'il puisse
706
        // être ajouté à tout ce qui aurait déjà pu être ajouté avant de faire le écho.
707
        // Nous devons également enregistrer l'instance localement
708
        // afin que tout changement de code d'état, etc., ait lieu.
709
        if ($returned instanceof ResponseInterface) {
710
            $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...
711
            $returned       = $returned->getBody()->getContents();
712
        }
713
714
        if (is_string($returned)) {
715
            $this->output .= $returned;
716
        }
717
718
        // Mettez-le en cache sans remplacer les mesures de performances
719
        // afin que nous puissions avoir des mises à jour de vitesse en direct en cours de route.
720
        if (static::$cacheTTL > 0) {
721
            $this->cachePage();
722
        }
723
724
        $this->output = $this->displayPerformanceMetrics($this->output);
725
726
        $this->response = $this->response->withBody(to_stream($this->output));
727
    }
728
729
    /**
730
     * Si nous avons un objet de session à utiliser, stockez l'URI actuel
731
     * comme l'URI précédent. Ceci est appelé juste avant d'envoyer la
732
     * réponse au client, et le rendra disponible à la prochaine demande.
733
     *
734
     * Cela permet au fournisseur une détection plus sûre et plus fiable de la fonction previous_url().
735
     *
736
     * @param \BlitzPHP\Http\URI|string $uri
737
     */
738
    public function storePreviousURL($uri)
739
    {
740
        // Ignorer les requêtes CLI
741
        if (is_cli() && ! on_test()) {
742
            return; // @codeCoverageIgnore
743
        }
744
745
        // Ignorer les requêtes AJAX
746
        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

746
        if (method_exists($this->request, 'isAJAX') && $this->request->/** @scrutinizer ignore-call */ isAJAX()) {
Loading history...
747
            return;
748
        }
749
750
        // Ignorer les reponses non-HTML
751
        if (strpos($this->response->getHeaderLine('Content-Type'), 'text/html') === false) {
752
            return;
753
        }
754
755
        // Ceci est principalement nécessaire lors des tests ...
756
        if (is_string($uri)) {
757
            $uri = Services::uri($uri, false);
758
        }
759
760
        if (isset($_SESSION)) {
761
            $_SESSION['_blitz_previous_url'] = Uri::createURIString(
762
                $uri->getScheme(),
763
                $uri->getAuthority(),
764
                $uri->getPath(),
765
                $uri->getQuery(),
766
                $uri->getFragment()
767
            );
768
        }
769
    }
770
771
    /**
772
     * Renvoie la sortie de cette requête au client.
773
     * C'est ce qu'il attendait !
774
     */
775
    protected function sendResponse()
776
    {
777
        $this->totalTime = $this->timer->getElapsedTime('total_execution');
778
        Services::emitter()->emit(
779
            Services::toolbar()->prepare($this->getPerformanceStats(), $this->request, $this->response)
780
        );
781
    }
782
783
    protected function emitResponse()
784
    {
785
        $this->gatherOutput();
786
        $this->sendResponse();
787
    }
788
789
    /**
790
     * Construit une reponse adequate en fonction du retour du controleur
791
     *
792
     * @param mixed $returned
793
     */
794
    protected function formatResponse(ResponseInterface $response, $returned): ResponseInterface
795
    {
796
        if ($returned instanceof ResponseInterface) {
797
            return $returned;
798
        }
799
800
        if (is_object($returned)) {
801
            if (method_exists($returned, '__toString')) {
802
                $returned = $returned->__toString();
803
            } elseif (method_exists($returned, 'toArray')) {
804
                $returned = $returned->toArray();
805
            } elseif (method_exists($returned, 'toJSON')) {
806
                $returned = $returned->toJSON();
807
            } else {
808
                $returned = (array) $returned;
809
            }
810
        }
811
812
        if (is_array($returned)) {
813
            $returned = Helpers::collect($returned);
814
            $response = $response->withHeader('Content-Type', 'application/json');
815
        }
816
817
        try {
818
            $response = $response->withBody(to_stream($returned));
819
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
820
        }
821
822
        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...
823
    }
824
825
    /**
826
     * Quitte l'application en définissant le code de sortie pour les applications basées sur CLI
827
     * qui pourrait regarder.
828
     *
829
     * Fabriqué dans une méthode distincte afin qu'il puisse être simulé pendant les tests
830
     * sans réellement arrêter l'exécution du script.
831
     */
832
    protected function callExit(int $code)
833
    {
834
        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...
835
    }
836
837
    /**
838
     * Initialise le gestionnaire de middleware
839
     */
840
    protected function initMiddlewareQueue(): void
841
    {
842
        $this->middleware = Services::injector()->make(Middleware::class, [
843
            'response' => $this->response,
844
            'path'     => $this->determinePath(),
845
        ]);
846
847
        $middlewaresFile = CONFIG_PATH . 'middlewares.php';
848
        if (file_exists($middlewaresFile) && ! in_array($middlewaresFile, get_included_files(), true)) {
849
            $middleware = require $middlewaresFile;
850
            if (is_callable($middleware)) {
851
                $middleware($this->middleware, $this->request);
852
            }
853
        }
854
855
        $this->middleware->prepend($this->spoofRequestMethod());
856
    }
857
858
    /**
859
     * Modifie l'objet de requête pour utiliser une méthode différente
860
     * si une variable POST appelée _method est trouvée.
861
     */
862
    private function spoofRequestMethod(): callable
863
    {
864
        return static function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
865
            $post = $request->getParsedBody();
866
867
            // Ne fonctionne qu'avec les formulaires POST
868
            if (strtoupper($request->getMethod()) === 'POST' && ! empty($post['_method'])) {
869
                // Accepte seulement PUT, PATCH, DELETE
870
                if (in_array(strtoupper($post['_method']), ['PUT', 'PATCH', 'DELETE'], true)) {
871
                    $request = $request->withMethod($post['_method']);
872
                }
873
            }
874
875
            return $next($request, $response);
876
        };
877
    }
878
879
    private function bootApp(): callable
880
    {
881
        $_this = $this;
882
883
        return static function (ServerRequestInterface $request, ResponseInterface $response, callable $next) use ($_this): ResponseInterface {
884
            try {
885
                $returned = $_this->startController($request, $response);
886
887
                // Closure controller has run in startController().
888
                if (! is_callable($_this->controller)) {
889
                    $controller = $_this->createController($request, $response);
890
891
                    if (! method_exists($controller, '_remap') && ! is_callable([$controller, $_this->method], false)) {
892
                        throw PageNotFoundException::methodNotFound($_this->method);
893
                    }
894
895
                    // Is there a "post_controller_constructor" event?
896
                    Services::event()->trigger('post_controller_constructor');
897
898
                    $returned = $_this->runController($controller);
899
                } else {
900
                    $_this->timer->stop('controller_constructor');
901
                    $_this->timer->stop('controller');
902
                }
903
904
                Services::event()->trigger('post_system');
905
906
                return $_this->formatResponse($response, $returned);
907
            } catch (ValidationException $e) {
908
                $code   = $e->getCode();
909
                $errors = $e->getErrors();
910
                if (empty($errors)) {
911
                    $errors = [$e->getMessage()];
912
                }
913
914
                if (is_string($_this->controller)) {
915
                    if (strtoupper($request->getMethod()) === 'POST') {
916
                        if (is_subclass_of($_this->controller, ApplicationController::class)) {
917
                            return Services::redirection()->back()->withInput()->withErrors($errors)->withStatus($code);
918
                        }
919
                        if (is_subclass_of($_this->controller, RestController::class)) {
920
                            return $_this->formatResponse($response->withStatus($code), [
921
                                'success' => false,
922
                                'code'    => $code,
923
                                'errors'  => $errors,
924
                            ]);
925
                        }
926
                    }
927
                } elseif (strtoupper($request->getMethod()) === 'POST') {
928
                    return Services::redirection()->back()->withInput()->withErrors($errors)->withStatus($code);
929
                }
930
931
                throw $e;
932
            }
933
        };
934
    }
935
}
936