Passed
Push — main ( c1749a...759bea )
by Dimitri
12:54
created

Dispatcher::formatValidationResponse()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 1
b 0
f 0
nc 12
nop 3
dl 0
loc 25
ccs 0
cts 6
cp 0
crap 56
rs 8.8333
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\Cache\ResponseCache;
15
use BlitzPHP\Container\Container;
16
use BlitzPHP\Container\Services;
17
use BlitzPHP\Contracts\Event\EventManagerInterface;
18
use BlitzPHP\Contracts\Http\ResponsableInterface;
19
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
20
use BlitzPHP\Contracts\Support\Responsable;
21
use BlitzPHP\Controllers\BaseController;
22
use BlitzPHP\Controllers\RestController;
23
use BlitzPHP\Core\App;
24
use BlitzPHP\Debug\Timer;
25
use BlitzPHP\Exceptions\FrameworkException;
26
use BlitzPHP\Exceptions\PageNotFoundException;
27
use BlitzPHP\Exceptions\RedirectException;
28
use BlitzPHP\Exceptions\ValidationException;
29
use BlitzPHP\Http\Middleware;
30
use BlitzPHP\Http\Response;
31
use BlitzPHP\Http\ServerRequest;
32
use BlitzPHP\Http\Uri;
33
use BlitzPHP\Utilities\Helpers;
34
use BlitzPHP\View\View;
35
use Closure;
36
use Exception;
37
use InvalidArgumentException;
38
use Psr\Http\Message\ResponseInterface;
39
use Psr\Http\Message\ServerRequestInterface;
40
use stdClass;
41
use Throwable;
42
43
/**
44
 * Cette classe est la porte d'entree du framework. Elle analyse la requete,
45
 * recherche la route correspondante et invoque le bon controleurm puis renvoie la reponse.
46
 */
47
class Dispatcher
48
{
49
    /**
50
     * Heure de démarrage de l'application.
51
     *
52
     * @var mixed
53
     */
54
    protected $startTime;
55
56
    /**
57
     * Durée totale d'exécution de l'application
58
     *
59
     * @var float
60
     */
61
    protected $totalTime;
62
63
    /**
64
     * Main application configuration
65
     *
66
     * @var stdClass
67
     */
68
    protected $config;
69
70
    /**
71
     * instance Timer.
72
     *
73
     * @var Timer
74
     */
75
    protected $timer;
76
77
    /**
78
     * requête courrante.
79
     *
80
     * @var ServerRequest
81
     */
82
    protected $request;
83
84
    /**
85
     * Reponse courrante.
86
     *
87
     * @var Response
88
     */
89
    protected $response;
90
91
    /**
92
     * Router à utiliser.
93
     *
94
     * @var Router
95
     */
96
    protected $router;
97
98
    /**
99
     * @var Middleware
100
     */
101
    private $middleware;
102
103
    /**
104
     * Contrôleur à utiliser.
105
     *
106
     * @var Closure|string
107
     */
108
    protected $controller;
109
110
    /**
111
     * Méthode du ontrôleur à exécuter.
112
     *
113
     * @var string
114
     */
115
    protected $method;
116
117
    /**
118
     * Gestionnaire de sortie à utiliser.
119
     *
120
     * @var string
121
     */
122
    protected $output;
123
124
    /**
125
     * Chemin de requête à utiliser.
126
     *
127
     * @var string
128
     *
129
     * @deprecated No longer used.
130
     */
131
    protected $path;
132
133
    /**
134
     * Niveau de mise en mémoire tampon de sortie de l'application
135
     */
136
    protected int $bufferLevel = 0;
137
138
    /**
139
     * Mise en cache des pages Web
140
     */
141
    protected ResponseCache $pageCache;
142
143
    /**
144
     * Constructeur.
145
     */
146
    public function __construct(protected EventManagerInterface $event, protected Container $container)
147
    {
148
        $this->startTime = microtime(true);
149
        $this->config    = (object) config('app');
0 ignored issues
show
Documentation Bug introduced by
It seems like (object)config('app') of type BlitzPHP\Config\Config is incompatible with the declared type stdClass of property $config.

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...
150
151
        $this->pageCache = Services::responsecache();
152
    }
153
154
    /**
155
     * Retourne la methode invoquee
156
     */
157
    public static function getMethod(): ?string
158
    {
159
        $method = Services::singleton(self::class)->method;
160
        if (empty($method)) {
161
            $method = Services::routes()->getDefaultMethod();
162
        }
163
164
        return $method;
165
    }
166
167
    /**
168
     * Retourne le contrôleur utilisé
169
     *
170
     * @return Closure|string
171
     */
172
    public static function getController(bool $fullName = true)
173
    {
174
        $routes = Services::routes();
175
176
        $controller = Services::singleton(self::class)->controller;
177
        if (empty($controller)) {
178
            $controller = $routes->getDefaultController();
179
        }
180
181
        if (! $fullName && is_string($controller)) {
182
            $controller = str_replace($routes->getDefaultNamespace(), '', $controller);
183
        }
184
185
        return $controller;
186
    }
187
188
    /**
189
     * Lancez l'application !
190
     *
191
     * C'est "la boucle" si vous voulez. Le principal point d'entrée dans le script
192
     * qui obtient les instances de classe requises, déclenche les filtres,
193
     * essaie d'acheminer la réponse, charge le contrôleur et généralement
194
     * fait fonctionner toutes les pièces ensemble.
195
     *
196
     * @return bool|mixed|ResponseInterface|ServerRequestInterface
197
     *
198
     * @throws Exception
199
     * @throws RedirectException
200
     */
201
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
202
    {
203
        $this->pageCache->setTtl(0);
204
        $this->bufferLevel = ob_get_level();
205
206
        $this->startBenchmark();
207
208
        $this->getRequestObject();
209
        $this->getResponseObject();
210
211
        $this->initMiddlewareQueue();
212
213
        try {
214
            $this->response = $this->handleRequest($routes, config('cache'));
0 ignored issues
show
Bug introduced by
It seems like config('cache') can also be of type BlitzPHP\Config\Config; however, parameter $cacheConfig of BlitzPHP\Router\Dispatcher::handleRequest() does only seem to accept array|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

214
            $this->response = $this->handleRequest($routes, /** @scrutinizer ignore-type */ config('cache'));
Loading history...
215
        } catch (RedirectException|ResponsableInterface $e) {
216
            $this->outputBufferingEnd();
217
            if ($e instanceof RedirectException) {
218
                $e = new RedirectException($e->getMessage(), $e->getCode(), $e);
219
            }
220
221
            $this->response = $e->getResponse();
222
        } catch (PageNotFoundException $e) {
223
            $this->response = $this->display404errors($e);
224
        } catch (Throwable $e) {
225
            $this->outputBufferingEnd();
226
227
            throw $e;
228
        }
229
230
        if ($returnResponse) {
231
            return $this->response;
232
        }
233
234
        $this->sendResponse();
235
    }
236
237
    /**
238
     * Gère la logique de requête principale et déclenche le contrôleur.
239
     *
240
     * @throws PageNotFoundException
241
     * @throws RedirectException
242
     */
243
    protected function handleRequest(?RouteCollectionInterface $routes = null, ?array $cacheConfig = null): ResponseInterface
244
    {
245
        $this->forceSecureAccess();
246
247
        $this->event->trigger('pre_system');
248
249
        // Recherchez une page en cache.
250
        // L'exécution s'arrêtera si la page a été mise en cache.
251
        if (($response = $this->displayCache($cacheConfig)) instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$response = $this->displayCache($cacheConfig) is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
252
            return $response;
253
        }
254
255
        $routeMiddlewares = (array) $this->dispatchRoutes($routes);
256
257
        // Le bootstrap dans un middleware
258
        $this->middleware->alias('blitz', $this->bootApp());
259
260
        /**
261
         * Ajouter des middlewares de routes
262
         */
263
        foreach ($routeMiddlewares as $middleware) {
264
            $this->middleware->append($middleware);
265
        }
266
267
        $this->middleware->append('blitz');
268
269
        // Enregistrer notre URI actuel en tant qu'URI précédent dans la session
270
        // pour une utilisation plus sûre et plus précise avec la fonction d'assistance `previous_url()`.
271
        $this->storePreviousURL(current_url(true));
272
273
        /**
274
         * Emission de la reponse
275
         */
276
        $this->gatherOutput($this->middleware->handle($this->request));
277
278
        // Y a-t-il un événement post-système ?
279
        $this->event->trigger('post_system');
280
281
        return $this->response;
282
    }
283
284
    /**
285
     * Démarrer le benchmark
286
     *
287
     * La minuterie est utilisée pour afficher l'exécution totale du script à la fois dans la
288
     * barre d'outils de débogage, et éventuellement sur la page affichée.
289
     */
290
    protected function startBenchmark()
291
    {
292
        if ($this->startTime === null) {
293
            $this->startTime = microtime(true);
294
        }
295
296
        $this->timer = Services::timer();
297
        $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

297
        $this->timer->start('total_execution', /** @scrutinizer ignore-type */ $this->startTime);
Loading history...
298
        $this->timer->start('bootstrap');
299
    }
300
301
    /**
302
     * Définit un objet Request à utiliser pour cette requête.
303
     * Utilisé lors de l'exécution de certains tests.
304
     */
305
    public function setRequest(ServerRequestInterface $request): self
306
    {
307
        $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...
308
309
        return $this;
310
    }
311
312
    /**
313
     * Obtenez notre objet Request et définissez le protocole du serveur en fonction des informations fournies
314
     * par le serveur.
315
     */
316
    protected function getRequestObject()
317
    {
318
        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...
319
            return;
320
        }
321
322
        if (is_cli() && ! on_test()) {
323
            // @codeCoverageIgnoreStart
324
            // $this->request = Services::clirequest($this->config);
325
            // @codeCoverageIgnoreEnd
326
        }
327
328
        $version = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
329
        if (! is_numeric($version)) {
330
            $version = substr($version, strpos($version, '/') + 1);
331
        }
332
333
        // Assurez-vous que la version est au bon format
334
        $version = number_format((float) $version, 1);
335
336
        $this->request = Services::request()->withProtocolVersion($version);
337
    }
338
339
    /**
340
     * Obtenez notre objet Response et définissez des valeurs par défaut, notamment
341
     * la version du protocole HTTP et une réponse réussie par défaut.
342
     */
343
    protected function getResponseObject()
344
    {
345
        // Supposons le succès jusqu'à preuve du contraire.
346
        $this->response = Services::response()->withStatus(200);
347
348
        if (! is_cli() || on_test()) {
349
        }
350
351
        $this->response = $this->response->withProtocolVersion($this->request->getProtocolVersion());
352
    }
353
354
    /**
355
     * Forcer l'accès au site sécurisé ? Si la valeur de configuration 'forceGlobalSecureRequests'
356
     * est vrai, imposera que toutes les demandes adressées à ce site soient effectuées via
357
     * HTTPS. Redirigera également l'utilisateur vers la page actuelle avec HTTPS
358
     * comme défini l'en-tête HTTP Strict Transport Security pour ces navigateurs
359
     * qui le supportent.
360
     *
361
     * @param int $duration Combien de temps la sécurité stricte des transports
362
     *                      doit être appliqué pour cette URL.
363
     */
364
    protected function forceSecureAccess($duration = 31536000)
365
    {
366
        if ($this->config->force_global_secure_requests !== true) {
367
            return;
368
        }
369
370
        force_https($duration, $this->request, $this->response);
371
    }
372
373
    /**
374
     * Détermine si une réponse a été mise en cache pour l'URI donné.
375
     *
376
     * @return bool|ResponseInterface
377
     *
378
     * @throws FrameworkException
379
     */
380
    public function displayCache(?array $config = null)
381
    {
382
        if ($cachedResponse = $this->pageCache->get($this->request, $this->response)) {
383
            $this->response = $cachedResponse;
384
385
            $this->totalTime = $this->timer->getElapsedTime('total_execution');
386
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
387
388
            return $this->response->withBody(to_stream($output));
389
        }
390
391
        return false;
392
    }
393
394
    /**
395
     * Renvoie un tableau avec nos statistiques de performances de base collectées.
396
     */
397
    public function getPerformanceStats(): array
398
    {
399
        return [
400
            'startTime' => $this->startTime,
401
            'totalTime' => $this->totalTime,
402
        ];
403
    }
404
405
    /**
406
     * Remplace les balises memory_usage et elapsed_time.
407
     */
408
    public function displayPerformanceMetrics(string $output): string
409
    {
410
        $this->totalTime = $this->timer->getElapsedTime('total_execution');
411
412
        return str_replace('{elapsed_time}', (string) $this->totalTime, $output);
413
    }
414
415
    /**
416
     * Fonctionne avec le routeur pour
417
     * faire correspondre une route à l'URI actuel. Si la route est une
418
     * "route de redirection", gérera également la redirection.
419
     *
420
     * @param RouteCollectionInterface|null $routes Une interface de collecte à utiliser à la place
421
     *                                              du fichier de configuration.
422
     *
423
     * @return string[]
424
     *
425
     * @throws RedirectException
426
     */
427
    protected function dispatchRoutes(?RouteCollectionInterface $routes = null): array
428
    {
429
        if ($routes === null) {
430
            $routes = Services::routes()->loadRoutes();
431
        }
432
433
        $this->router = Services::router($routes, $this->request, false);
434
435
        $path = $this->determinePath();
436
437
        $this->timer->stop('bootstrap');
438
        $this->timer->start('routing');
439
440
        $this->outputBufferingStart();
441
442
        $this->controller = $this->router->handle($path ?: '/');
443
        $this->method     = $this->router->methodName();
444
445
        // Si un segment {locale} correspondait dans la route finale,
446
        // alors nous devons définir les paramètres régionaux corrects sur notre requête.
447
        if ($this->router->hasLocale()) {
448
            $this->request = $this->request->withLocale($this->router->getLocale());
449
        }
450
451
        $this->timer->stop('routing');
452
453
        return $this->router->getMiddlewares();
454
    }
455
456
    /**
457
     * Détermine le chemin à utiliser pour que nous essayions d'acheminer vers, en fonction
458
     * de l'entrée de l'utilisateur (setPath), ou le chemin CLI/IncomingRequest.
459
     */
460
    protected function determinePath(): string
461
    {
462
        if (! empty($this->path)) {
0 ignored issues
show
Deprecated Code introduced by
The property BlitzPHP\Router\Dispatcher::$path has been deprecated: No longer used. ( Ignorable by Annotation )

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

462
        if (! empty(/** @scrutinizer ignore-deprecated */ $this->path)) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
463
            return $this->path;
0 ignored issues
show
Deprecated Code introduced by
The property BlitzPHP\Router\Dispatcher::$path has been deprecated: No longer used. ( Ignorable by Annotation )

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

463
            return /** @scrutinizer ignore-deprecated */ $this->path;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
464
        }
465
466
        $path = method_exists($this->request, 'getPath')
467
            ? $this->request->getPath()
468
            : $this->request->getUri()->getPath();
469
470
        return $this->path = preg_replace('#^' . App::getUri()->getPath() . '#i', '', $path);
0 ignored issues
show
Deprecated Code introduced by
The property BlitzPHP\Router\Dispatcher::$path has been deprecated: No longer used. ( Ignorable by Annotation )

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

470
        return /** @scrutinizer ignore-deprecated */ $this->path = preg_replace('#^' . App::getUri()->getPath() . '#i', '', $path);

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
471
    }
472
473
    /**
474
     * Maintenant que tout a été configuré, cette méthode tente d'exécuter le
475
     * méthode du contrôleur et lancez le script. S'il n'en est pas capable, le fera
476
     * afficher l'erreur Page introuvable appropriée.
477
     */
478
    protected function startController(ServerRequest $request, Response $response)
479
    {
480
        // Aucun contrôleur spécifié - nous ne savons pas quoi faire maintenant.
481
        if (empty($this->controller)) {
482
            throw PageNotFoundException::emptyController();
483
        }
484
485
        $this->timer->start('controller');
486
        $this->timer->start('controller_constructor');
487
488
        // Est-il acheminé vers une Closure ?
489
        if (is_object($this->controller) && (get_class($this->controller) === 'Closure')) {
490
            $controller = $this->controller;
491
492
            $sendParameters = [];
493
494
            foreach ($this->router->params() as $parameter) {
495
                $sendParameters[] = $parameter;
496
            }
497
            array_push($sendParameters, $request, $response);
498
499
            return $this->container->call($controller, $sendParameters);
500
        }
501
502
        // Essayez de charger automatiquement la classe
503
        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

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

504
            throw PageNotFoundException::controllerNotFound(/** @scrutinizer ignore-type */ $this->controller, $this->method);
Loading history...
505
        }
506
507
        return null;
508
    }
509
510
    /**
511
     * Instancie la classe contrôleur.
512
     *
513
     * @return \BlitzPHP\Controllers\BaseController|mixed
514
     */
515
    private function createController(ServerRequestInterface $request, ResponseInterface $response)
516
    {
517
        /**
518
         * @var \BlitzPHP\Controllers\BaseController
519
         */
520
        $class = $this->container->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\Container\Container::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

520
        $class = $this->container->get(/** @scrutinizer ignore-type */ $this->controller);
Loading history...
521
522
        if (method_exists($class, 'initialize')) {
523
            $class->initialize($request, $response, Services::logger());
524
        }
525
526
        $this->timer->stop('controller_constructor');
527
528
        return $class;
529
    }
530
531
    /**
532
     * Exécute le contrôleur, permettant aux méthodes _remap de fonctionner.
533
     *
534
     * @param mixed $class
535
     *
536
     * @return mixed
537
     */
538
    protected function runController($class)
539
    {
540
        // S'il s'agit d'une demande de console, utilisez les segments d'entrée comme paramètres
541
        $params = defined('KLINGED') ? [/* $this->request->getSegments() */] : $this->router->params();
542
        $method = $this->method;
543
544
        if (method_exists($class, '_remap')) {
545
            $params = [$method, $params];
546
            $method = '_remap';
547
        }
548
549
        $output = $this->container->call([$class, $method], (array) $params);
550
551
        $this->timer->stop('controller');
552
553
        if ($output instanceof View) {
554
            $output = $this->response->withBody(to_stream($output->get()));
555
        }
556
557
        return $output;
558
    }
559
560
    /**
561
     * Affiche une page d'erreur 404 introuvable. S'il est défini, essaiera de
562
     * appelez le contrôleur/méthode 404Override qui a été défini dans la configuration de routage.
563
     */
564
    protected function display404errors(PageNotFoundException $e)
565
    {
566
        // Existe-t-il une dérogation 404 disponible ?
567
        if ($override = $this->router->get404Override()) {
568
            $returned = null;
569
570
            if ($override instanceof Closure) {
0 ignored issues
show
introduced by
$override is never a sub-type of Closure.
Loading history...
571
                echo $override($e->getMessage());
572
            } elseif (is_array($override)) {
0 ignored issues
show
introduced by
The condition is_array($override) is always true.
Loading history...
573
                $this->timer->start('controller');
574
                $this->timer->start('controller_constructor');
575
576
                $this->controller = $override[0];
577
                $this->method     = $override[1];
578
579
                $controller = $this->createController($this->request, $this->response);
580
                $returned   = $this->runController($controller);
581
            }
582
583
            unset($override);
584
585
            $this->gatherOutput($returned);
586
587
            return $this->response;
588
        }
589
590
        // Affiche l'erreur 404
591
        $this->response = $this->response->withStatus($e->getCode());
592
593
        $this->outputBufferingEnd();
594
595
        throw PageNotFoundException::pageNotFound(! on_prod() || is_cli() ? $e->getMessage() : '');
596
    }
597
598
    /**
599
     * Rassemble la sortie du script à partir du tampon, remplace certaines balises d'exécutions
600
     * d'horodatage dans la sortie et affiche la barre d'outils de débogage, si nécessaire.
601
     *
602
     * @param mixed|null $returned
603
     */
604
    protected function gatherOutput($returned = null)
605
    {
606
        $this->output = $this->outputBufferingEnd();
607
608
        // Si le contrôleur a renvoyé un objet de réponse,
609
        // nous devons en saisir le corps pour qu'il puisse
610
        // être ajouté à tout ce qui aurait déjà pu être ajouté avant de faire le écho.
611
        // Nous devons également enregistrer l'instance localement
612
        // afin que tout changement de code d'état, etc., ait lieu.
613
        if ($returned instanceof ResponseInterface) {
614
            $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...
615
            $returned       = $returned->getBody()->getContents();
616
        }
617
618
        if (is_string($returned)) {
619
            $this->output .= $returned;
620
        }
621
622
        $this->response = $this->response->withBody(to_stream($this->output));
623
    }
624
625
    /**
626
     * Si nous avons un objet de session à utiliser, stockez l'URI actuel
627
     * comme l'URI précédent. Ceci est appelé juste avant d'envoyer la
628
     * réponse au client, et le rendra disponible à la prochaine demande.
629
     *
630
     * Cela permet au fournisseur une détection plus sûre et plus fiable de la fonction previous_url().
631
     *
632
     * @param \BlitzPHP\Http\Uri|string $uri
633
     */
634
    public function storePreviousURL($uri)
635
    {
636
        // Ignorer les requêtes CLI
637
        if (is_cli() && ! on_test()) {
638
            return; // @codeCoverageIgnore
639
        }
640
641
        // Ignorer les requêtes AJAX
642
        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

642
        if (method_exists($this->request, 'isAJAX') && $this->request->/** @scrutinizer ignore-call */ isAJAX()) {
Loading history...
643
            return;
644
        }
645
646
        // Ignorer les reponses non-HTML
647
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
648
            return;
649
        }
650
651
        // Ceci est principalement nécessaire lors des tests ...
652
        if (is_string($uri)) {
653
            $uri = Services::uri($uri, false);
654
        }
655
656
        Services::session()->setPreviousUrl(Uri::createURIString(
657
            $uri->getScheme(),
658
            $uri->getAuthority(),
659
            $uri->getPath(),
660
            $uri->getQuery(),
661
            $uri->getFragment()
662
        ));
663
    }
664
665
    /**
666
     * Renvoie la sortie de cette requête au client.
667
     * C'est ce qu'il attendait !
668
     */
669
    protected function sendResponse()
670
    {
671
        $this->totalTime = $this->timer->getElapsedTime('total_execution');
672
        Services::emitter()->emit(
673
            Services::toolbar()->prepare($this->getPerformanceStats(), $this->request, $this->response)
674
        );
675
    }
676
677
    protected function emitResponse()
678
    {
679
        $this->gatherOutput();
680
        $this->sendResponse();
681
    }
682
683
    /**
684
     * Construit une reponse adequate en fonction du retour du controleur
685
     *
686
     * @param mixed $returned
687
     */
688
    protected function formatResponse(ResponseInterface $response, $returned): ResponseInterface
689
    {
690
        if ($returned instanceof ResponseInterface) {
691
            return $returned;
692
        }
693
694
        if ($returned instanceof Responsable) {
695
            return $returned->toResponse($this->request);
696
        }
697
698
        if (is_object($returned)) {
699
            if (method_exists($returned, '__toString')) {
700
                $returned = $returned->__toString();
701
            } elseif (method_exists($returned, 'toArray')) {
702
                $returned = $returned->toArray();
703
            } elseif (method_exists($returned, 'toJSON')) {
704
                $returned = $returned->toJSON();
705
            } else {
706
                $returned = (array) $returned;
707
            }
708
        }
709
710
        if (is_array($returned)) {
711
            $returned = Helpers::collect($returned);
712
            $response = $response->withHeader('Content-Type', 'application/json');
713
        }
714
715
        try {
716
            $response = $response->withBody(to_stream($returned));
717
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
718
        }
719
720
        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...
721
    }
722
723
    /**
724
     * Initialise le gestionnaire de middleware
725
     */
726
    protected function initMiddlewareQueue(): void
727
    {
728
        $this->middleware = new Middleware($this->response, $this->determinePath());
729
730
        $this->middleware->append($this->spoofRequestMethod());
731
        $this->middleware->register($this->request);
732
    }
733
734
    protected function outputBufferingStart(): void
735
    {
736
        $this->bufferLevel = ob_get_level();
737
738
        ob_start();
739
    }
740
741
    protected function outputBufferingEnd(): string
742
    {
743
        $buffer = '';
744
745
        while (ob_get_level() > $this->bufferLevel) {
746
            $buffer .= ob_get_contents();
747
            ob_end_clean();
748
        }
749
750
        return $buffer;
751
    }
752
753
    /**
754
     * Modifie l'objet de requête pour utiliser une méthode différente
755
     * si une variable POST appelée _method est trouvée.
756
     */
757
    private function spoofRequestMethod(): callable
758
    {
759
        return static function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
760
            $post = $request->getParsedBody();
761
762
            // Ne fonctionne qu'avec les formulaires POST
763
            if (strtoupper($request->getMethod()) === 'POST' && ! empty($post['_method'])) {
764
                // Accepte seulement PUT, PATCH, DELETE
765
                if (in_array(strtoupper($post['_method']), ['PUT', 'PATCH', 'DELETE'], true)) {
766
                    $request = $request->withMethod($post['_method']);
767
                }
768
            }
769
770
            return $next($request, $response);
771
        };
772
    }
773
774
    private function bootApp(): callable
775
    {
776
        return function (ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface {
777
            try {
778
                $returned = $this->startController($request, $response);
779
780
                // Closure controller has run in startController().
781
                if (! is_callable($this->controller)) {
782
                    $controller = $this->createController($request, $response);
783
784
                    if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
785
                        throw PageNotFoundException::methodNotFound($this->method);
786
                    }
787
788
                    // Y'a t-il un evenement "post_controller_constructor"
789
                    $this->event->trigger('post_controller_constructor');
790
791
                    $returned = $this->runController($controller);
792
                } else {
793
                    $this->timer->stop('controller_constructor');
794
                    $this->timer->stop('controller');
795
                }
796
797
                $this->event->trigger('post_system');
798
799
                return $this->formatResponse($response, $returned);
800
            } catch (ValidationException $e) {
801
                return $this->formatValidationResponse($e, $request, $response);
802
            }
803
        };
804
    }
805
806
    /**
807
     * Formattage des erreurs de validation
808
     */
809
    private function formatValidationResponse(ValidationException $e, ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
810
    {
811
        $code = $e->getCode();
812
        if (empty($errors = $e->getErrors())) {
813
            $errors = [$e->getMessage()];
814
        }
815
816
        if (is_string($this->controller)) {
817
            if (strtoupper($request->getMethod()) === 'POST') {
818
                if (is_subclass_of($this->controller, BaseController::class)) {
819
                    return Services::redirection()->back()->withInput()->withErrors($errors)->withStatus($code);
820
                }
821
                if (is_subclass_of($this->controller, RestController::class)) {
822
                    return $this->formatResponse($response->withStatus($code), [
823
                        'success' => false,
824
                        'code'    => $code,
825
                        'errors'  => $errors,
826
                    ]);
827
                }
828
            }
829
        } elseif (strtoupper($request->getMethod()) === 'POST') {
830
            return Services::redirection()->back()->withInput()->withErrors($errors)->withStatus($code);
831
        }
832
833
        throw $e;
834
    }
835
}
836