Passed
Push — main ( c0f50c...cd5116 )
by Dimitri
04:24
created

Dispatcher::formatValidationResponse()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
c 2
b 0
f 0
nc 6
nop 3
dl 0
loc 20
ccs 0
cts 5
cp 0
crap 30
rs 9.6111
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\PageNotFoundException;
26
use BlitzPHP\Exceptions\RedirectException;
27
use BlitzPHP\Exceptions\ValidationException;
28
use BlitzPHP\Http\Middleware;
29
use BlitzPHP\Http\Request;
30
use BlitzPHP\Http\Response;
31
use BlitzPHP\Http\Uri;
32
use BlitzPHP\Utilities\Helpers;
33
use Closure;
34
use Exception;
35
use InvalidArgumentException;
36
use Psr\Http\Message\ResponseInterface;
37
use Psr\Http\Message\ServerRequestInterface;
38
use stdClass;
39
use Throwable;
40
41
/**
42
 * Cette classe est la porte d'entree du framework. Elle analyse la requete,
43
 * recherche la route correspondante et invoque le bon controleurm puis renvoie la reponse.
44
 */
45
class Dispatcher
46
{
47
    /**
48
     * Heure de démarrage de l'application.
49
     *
50
     * @var mixed
51
     */
52
    protected $startTime;
53
54
    /**
55
     * Durée totale d'exécution de l'application
56
     *
57
     * @var float
58
     */
59
    protected $totalTime;
60
61
    /**
62
     * Main application configuration
63
     *
64
     * @var stdClass
65
     */
66
    protected $config;
67
68
    /**
69
     * instance Timer.
70
     *
71
     * @var Timer
72
     */
73
    protected $timer;
74
75
    /**
76
     * requête courrante.
77
     *
78
     * @var Request
79
     */
80
    protected $request;
81
82
    /**
83
     * Reponse courrante.
84
     *
85
     * @var Response
86
     */
87
    protected $response;
88
89
    /**
90
     * Router à utiliser.
91
     *
92
     * @var Router
93
     */
94
    protected $router;
95
96
    /**
97
     * @var Middleware
98
     */
99
    private $middleware;
100
101
    /**
102
     * Contrôleur à utiliser.
103
     *
104
     * @var (Closure(mixed...): ResponseInterface|string)|string
0 ignored issues
show
Documentation Bug introduced by
The doc comment (Closure(mixed...): Resp...nterface|string)|string at position 1 could not be parsed: Expected ')' at position 1, but found 'Closure'.
Loading history...
105
     */
106
    protected $controller;
107
108
    /**
109
     * Méthode du ontrôleur à exécuter.
110
     *
111
     * @var string
112
     */
113
    protected $method;
114
115
    /**
116
     * Gestionnaire de sortie à utiliser.
117
     *
118
     * @var string
119
     */
120
    protected $output;
121
122
    /**
123
     * Chemin de requête à utiliser.
124
     *
125
     * @var string
126
     *
127
     * @deprecated No longer used.
128
     */
129
    protected $path;
130
131
    /**
132
     * Niveau de mise en mémoire tampon de sortie de l'application
133
     */
134
    protected int $bufferLevel = 0;
135
136
    /**
137
     * Mise en cache des pages Web
138
     */
139
    protected ResponseCache $pageCache;
140
141
    /**
142
     * Constructeur.
143
     */
144
    public function __construct(protected EventManagerInterface $event, protected Container $container)
145
    {
146
        $this->startTime = microtime(true);
147
        $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...
148
149
        $this->pageCache = Services::responsecache();
150
    }
151
152
    /**
153
     * Retourne la methode invoquee
154
     */
155
    public static function getMethod(): ?string
156
    {
157
        $method = Services::singleton(self::class)->method;
158
        if (empty($method)) {
159
            $method = Services::routes()->getDefaultMethod();
160
        }
161
162
        return $method;
163
    }
164
165
    /**
166
     * Retourne le contrôleur utilisé
167
     *
168
     * @return Closure|string
169
     */
170
    public static function getController(bool $fullName = true)
171
    {
172
        $routes = Services::routes();
173
174
        $controller = Services::singleton(self::class)->controller;
175
        if (empty($controller)) {
176
            $controller = $routes->getDefaultController();
177
        }
178
179
        if (! $fullName && is_string($controller)) {
180
            $controller = str_replace($routes->getDefaultNamespace(), '', $controller);
181
        }
182
183
        return $controller;
184
    }
185
186
    /**
187
     * Lancez l'application !
188
     *
189
     * C'est "la boucle" si vous voulez. Le principal point d'entrée dans le script
190
     * qui obtient les instances de classe requises, déclenche les filtres,
191
     * essaie d'acheminer la réponse, charge le contrôleur et généralement
192
     * fait fonctionner toutes les pièces ensemble.
193
     *
194
     * @return bool|mixed|ResponseInterface|ServerRequestInterface
195
     *
196
     * @throws Exception
197
     * @throws RedirectException
198
     */
199
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
200
    {
201
        $this->pageCache->setTtl(0);
202
        $this->bufferLevel = ob_get_level();
203
204
        $this->startBenchmark();
205
206
        $this->getRequestObject();
207
        $this->getResponseObject();
208
209
        $this->event->trigger('pre_system');
210
211
        $this->timer->stop('bootstrap');
212
213
        $this->initMiddlewareQueue();
214
215
        try {
216
            $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

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

280
        $this->timer->start('total_execution', /** @scrutinizer ignore-type */ $this->startTime);
Loading history...
281
        $this->timer->start('bootstrap');
282
    }
283
284
    /**
285
     * Définit un objet Request à utiliser pour cette requête.
286
     * Utilisé lors de l'exécution de certains tests.
287
     */
288
    public function setRequest(ServerRequestInterface $request): self
289
    {
290
        $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\Request. 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...
291
292
        return $this;
293
    }
294
295
    /**
296
     * Obtenez notre objet Request et définissez le protocole du serveur en fonction des informations fournies
297
     * par le serveur.
298
     */
299
    protected function getRequestObject()
300
    {
301
        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...
302
            return;
303
        }
304
305
        if (is_cli() && ! on_test()) {
306
            // @codeCoverageIgnoreStart
307
            // $this->request = Services::clirequest($this->config);
308
            // @codeCoverageIgnoreEnd
309
        }
310
311
        $version = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
312
        if (! is_numeric($version)) {
313
            $version = substr($version, strpos($version, '/') + 1);
314
        }
315
316
        // Assurez-vous que la version est au bon format
317
        $version = number_format((float) $version, 1);
318
319
        $this->request = Services::request()->withProtocolVersion($version);
320
    }
321
322
    /**
323
     * Obtenez notre objet Response et définissez des valeurs par défaut, notamment
324
     * la version du protocole HTTP et une réponse réussie par défaut.
325
     */
326
    protected function getResponseObject()
327
    {
328
        // Supposons le succès jusqu'à preuve du contraire.
329
        $this->response = Services::response()->withStatus(200);
330
331
        if (! is_cli() || on_test()) {
332
        }
333
334
        $this->response = $this->response->withProtocolVersion($this->request->getProtocolVersion());
335
    }
336
337
    /**
338
     * Renvoie un tableau avec nos statistiques de performances de base collectées.
339
     */
340
    public function getPerformanceStats(): array
341
    {
342
        // Après le filtre, la barre d'outils de débogage nécessite 'total_execution'.
343
        $this->totalTime = $this->timer->getElapsedTime('total_execution');
344
345
        return [
346
            'startTime' => $this->startTime,
347
            'totalTime' => $this->totalTime,
348
        ];
349
    }
350
351
    /**
352
     * Fonctionne avec le routeur pour
353
     * faire correspondre une route à l'URI actuel. Si la route est une
354
     * "route de redirection", gérera également la redirection.
355
     *
356
     * @param RouteCollectionInterface|null $routes Une interface de collecte à utiliser à la place
357
     *                                              du fichier de configuration.
358
     *
359
     * @return string[]
360
     *
361
     * @throws RedirectException
362
     */
363
    protected function dispatchRoutes(?RouteCollectionInterface $routes = null): array
364
    {
365
        $this->timer->start('routing');
366
367
        if ($routes === null) {
368
            $routes = Services::routes()->loadRoutes();
369
        }
370
371
        // $routes est defini dans app/Config/routes.php
372
        $this->router = Services::router($routes, $this->request, false);
373
374
        $this->outputBufferingStart();
375
376
        $this->controller = $this->router->handle($this->request->getPath());
377
        $this->method     = $this->router->methodName();
378
379
        // Si un segment {locale} correspondait dans la route finale,
380
        // alors nous devons définir les paramètres régionaux corrects sur notre requête.
381
        if ($this->router->hasLocale()) {
382
            $this->request = $this->request->withLocale($this->router->getLocale());
383
        }
384
385
        $this->timer->stop('routing');
386
387
        return $this->router->getMiddlewares();
388
    }
389
390
    /**
391
     * Détermine le chemin à utiliser pour que nous essayions d'acheminer vers, en fonction
392
     * de l'entrée de l'utilisateur (setPath), ou le chemin CLI/IncomingRequest.
393
     *
394
     * @deprecated 0.10.0
395
     */
396
    protected function determinePath(): string
397
    {
398
        $path = $this->request->getPath();
399
400
        return preg_replace('#^' . App::getUri()->getPath() . '#i', '', $path);
401
    }
402
403
    /**
404
     * Maintenant que tout a été configuré, cette méthode tente d'exécuter le
405
     * méthode du contrôleur et lancez le script. S'il n'en est pas capable, le fera
406
     * afficher l'erreur Page introuvable appropriée.
407
     */
408
    protected function startController()
409
    {
410
        $this->timer->start('controller');
411
        $this->timer->start('controller_constructor');
412
413
        // Aucun contrôleur spécifié - nous ne savons pas quoi faire maintenant.
414
        if (empty($this->controller)) {
415
            throw PageNotFoundException::emptyController();
416
        }
417
418
        // Est-il acheminé vers une Closure ?
419
        if (is_object($this->controller) && (get_class($this->controller) === 'Closure')) {
420
            if (empty($returned = $this->container->call($this->controller, $this->router->params()))) {
0 ignored issues
show
Bug introduced by
$this->controller of type object is incompatible with the type array|callable|string expected by parameter $callable of BlitzPHP\Container\Container::call(). ( Ignorable by Annotation )

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

420
            if (empty($returned = $this->container->call(/** @scrutinizer ignore-type */ $this->controller, $this->router->params()))) {
Loading history...
421
                $returned = $this->outputBufferingEnd();
422
            }
423
424
            return $returned;
425
        }
426
427
        // Essayez de charger automatiquement la classe
428
        if (! class_exists($this->controller, true) || ($this->method[0] === '_' && $this->method !== '__invoke')) {
429
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
430
        }
431
432
        return null;
433
    }
434
435
    /**
436
     * Instancie la classe contrôleur.
437
     *
438
     * @return \BlitzPHP\Controllers\BaseController|mixed
439
     */
440
    private function createController(ServerRequestInterface $request, ResponseInterface $response)
441
    {
442
        /**
443
         * @var \BlitzPHP\Controllers\BaseController
444
         */
445
        $class = $this->container->get($this->controller);
446
447
        if (method_exists($class, 'initialize')) {
448
            $class->initialize($request, $response, Services::logger());
449
        }
450
451
        $this->timer->stop('controller_constructor');
452
453
        return $class;
454
    }
455
456
    /**
457
     * Exécute le contrôleur, permettant aux méthodes _remap de fonctionner.
458
     *
459
     * @param mixed $class
460
     *
461
     * @return mixed
462
     */
463
    protected function runController($class)
464
    {
465
        $params = $this->router->params();
466
        $method = $this->method;
467
468
        if (method_exists($class, '_remap')) {
469
            $params = [$method, $params];
470
            $method = '_remap';
471
        }
472
473
        if (empty($output = $this->container->call([$class, $method], $params))) {
474
            $output = $this->outputBufferingEnd();
475
        }
476
477
        $this->timer->stop('controller');
478
479
        return $output;
480
    }
481
482
    /**
483
     * Affiche une page d'erreur 404 introuvable. S'il est défini, essaiera de
484
     * appelez le contrôleur/méthode 404Override qui a été défini dans la configuration de routage.
485
     */
486
    protected function display404errors(PageNotFoundException $e)
487
    {
488
        // Existe-t-il une dérogation 404 disponible ?
489
        if ($override = $this->router->get404Override()) {
490
            $returned = null;
491
492
            if ($override instanceof Closure) {
0 ignored issues
show
introduced by
$override is never a sub-type of Closure.
Loading history...
493
                echo $override($e->getMessage());
494
            } elseif (is_array($override)) {
0 ignored issues
show
introduced by
The condition is_array($override) is always true.
Loading history...
495
                $this->timer->start('controller');
496
                $this->timer->start('controller_constructor');
497
498
                $this->controller = $override[0];
499
                $this->method     = $override[1];
500
501
                $controller = $this->createController($this->request, $this->response);
502
                $returned   = $this->runController($controller);
503
            }
504
505
            unset($override);
506
507
            $this->gatherOutput($returned);
508
509
            return $this->response;
510
        }
511
512
        // Affiche l'erreur 404
513
        $this->response = $this->response->withStatus($e->getCode());
514
515
        $this->outputBufferingEnd();
516
517
        throw PageNotFoundException::pageNotFound(! on_prod() || is_cli() ? $e->getMessage() : '');
518
    }
519
520
    /**
521
     * Rassemble la sortie du script à partir du tampon, remplace certaines balises d'exécutions
522
     * d'horodatage dans la sortie et affiche la barre d'outils de débogage, si nécessaire.
523
     *
524
     * @param mixed|null $returned
525
     */
526
    protected function gatherOutput($returned = null)
527
    {
528
        $this->output = $this->outputBufferingEnd();
529
530
        // Si le contrôleur a renvoyé un objet de réponse,
531
        // nous devons en saisir le corps pour qu'il puisse
532
        // être ajouté à tout ce qui aurait déjà pu être ajouté avant de faire le écho.
533
        // Nous devons également enregistrer l'instance localement
534
        // afin que tout changement de code d'état, etc., ait lieu.
535
        if ($returned instanceof ResponseInterface) {
536
            $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...
537
            $returned       = $returned->getBody()->getContents();
538
        }
539
540
        if (is_string($returned)) {
541
            $this->output .= $returned;
542
        }
543
544
        $this->response = $this->response->withBody(to_stream($this->output));
545
    }
546
547
    /**
548
     * Si nous avons un objet de session à utiliser, stockez l'URI actuel
549
     * comme l'URI précédent. Ceci est appelé juste avant d'envoyer la
550
     * réponse au client, et le rendra disponible à la prochaine demande.
551
     *
552
     * Cela permet au fournisseur une détection plus sûre et plus fiable de la fonction previous_url().
553
     *
554
     * @param \BlitzPHP\Http\Uri|string $uri
555
     */
556
    public function storePreviousURL($uri)
557
    {
558
        // Ignorer les requêtes CLI
559
        if (is_cli() && ! on_test()) {
560
            return; // @codeCoverageIgnore
561
        }
562
563
        // Ignorer les requêtes AJAX
564
        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\Request. 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

564
        if (method_exists($this->request, 'isAJAX') && $this->request->/** @scrutinizer ignore-call */ isAJAX()) {
Loading history...
565
            return;
566
        }
567
568
        // Ignorer les reponses non-HTML
569
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
570
            return;
571
        }
572
573
        // Ceci est principalement nécessaire lors des tests ...
574
        if (is_string($uri)) {
575
            $uri = Services::uri($uri, false);
576
        }
577
578
        Services::session()->setPreviousUrl(Uri::createURIString(
579
            $uri->getScheme(),
580
            $uri->getAuthority(),
581
            $uri->getPath(),
582
            $uri->getQuery(),
583
            $uri->getFragment()
584
        ));
585
    }
586
587
    /**
588
     * Renvoie la sortie de cette requête au client.
589
     * C'est ce qu'il attendait !
590
     */
591
    protected function sendResponse()
592
    {
593
        if (! ($this->request->expectsJson() || $this->request->isJson())) {
594
            $this->response = Services::toolbar()->prepare(
595
                $this->getPerformanceStats(),
596
                $this->request,
597
                $this->response
598
            );
599
        }
600
601
        Services::emitter()->emit($this->response);
602
    }
603
604
    protected function emitResponse()
605
    {
606
        $this->gatherOutput();
607
        $this->sendResponse();
608
    }
609
610
    /**
611
     * Construit une reponse adequate en fonction du retour du controleur
612
     *
613
     * @param mixed $returned
614
     */
615
    protected function formatResponse(ResponseInterface $response, $returned): ResponseInterface
616
    {
617
        if ($returned instanceof ResponseInterface) {
618
            return $returned;
619
        }
620
621
        if ($returned instanceof Responsable) {
622
            return $returned->toResponse($this->request);
623
        }
624
625
        if (is_object($returned)) {
626
            if (method_exists($returned, '__toString')) {
627
                $returned = $returned->__toString();
628
            } elseif (method_exists($returned, 'toArray')) {
629
                $returned = $returned->toArray();
630
            } elseif (method_exists($returned, 'toJSON')) {
631
                $returned = $returned->toJSON();
632
            } else {
633
                $returned = (array) $returned;
634
            }
635
        }
636
637
        if (is_array($returned)) {
638
            $returned = Helpers::collect($returned);
639
            $response = $response->withHeader('Content-Type', 'application/json');
640
        }
641
642
        try {
643
            $response = $response->withBody(to_stream($returned));
644
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
645
        }
646
647
        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...
648
    }
649
650
    /**
651
     * Initialise le gestionnaire de middleware
652
     */
653
    protected function initMiddlewareQueue(): void
654
    {
655
        $this->middleware = new Middleware($this->response, $this->request->getPath());
656
657
        $this->middleware->append($this->spoofRequestMethod());
658
        $this->middleware->register($this->request);
659
    }
660
661
    protected function outputBufferingStart(): void
662
    {
663
        $this->bufferLevel = ob_get_level();
664
665
        ob_start();
666
    }
667
668
    protected function outputBufferingEnd(): string
669
    {
670
        $buffer = '';
671
672
        while (ob_get_level() > $this->bufferLevel) {
673
            $buffer .= ob_get_contents();
674
            ob_end_clean();
675
        }
676
677
        return $buffer;
678
    }
679
680
    /**
681
     * Modifie l'objet de requête pour utiliser une méthode différente
682
     * si une variable POST appelée _method est trouvée.
683
     */
684
    private function spoofRequestMethod(): callable
685
    {
686
        return static function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
687
            $post = $request->getParsedBody();
688
689
            // Ne fonctionne qu'avec les formulaires POST
690
            if ($request->getMethod() === 'POST' && ! empty($post['_method'])) {
691
                // Accepte seulement PUT, PATCH, DELETE
692
                if (in_array($post['_method'], ['PUT', 'PATCH', 'DELETE'], true)) {
693
                    $request = $request->withMethod($post['_method']);
694
                }
695
            }
696
697
            return $next($request, $response);
698
        };
699
    }
700
701
    private function bootApp(): callable
702
    {
703
        return function (ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface {
704
            Services::set(Request::class, $request);
705
            Services::set(Response::class, $response);
706
707
            try {
708
                $returned = $this->startController();
709
710
                // Les controleur sous forme de Closure sont executes dans startController().
711
                if (! is_callable($this->controller)) {
712
                    $controller = $this->createController($request, $response);
713
714
                    if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
715
                        throw PageNotFoundException::methodNotFound($this->method);
716
                    }
717
718
                    // Y'a t-il un evenement "post_controller_constructor"
719
                    $this->event->trigger('post_controller_constructor');
720
721
                    $returned = $this->runController($controller);
722
                } else {
723
                    $this->timer->stop('controller_constructor');
724
                    $this->timer->stop('controller');
725
                }
726
727
                $this->event->trigger('post_system');
728
729
                return $this->formatResponse($response, $returned);
730
            } catch (ValidationException $e) {
731
                return $this->formatValidationResponse($e, $request, $response);
732
            }
733
        };
734
    }
735
736
    /**
737
     * Formattage des erreurs de validation
738
     */
739
    private function formatValidationResponse(ValidationException $e, ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
740
    {
741
        $code = $e->getCode();
742
        if (empty($errors = $e->getErrors())) {
743
            $errors = [$e->getMessage()];
744
        }
745
746
		if (in_array($request->getMethod(), ['OPTIONS', 'HEAD'], true)) {
747
			throw $e;
748
		}
749
750
		if ($this->request->isJson() || $this->request->expectsJson()) {
751
			return $this->formatResponse($response->withStatus($code), [
752
				'success' => false,
753
				'code'    => $code,
754
				'errors'  => $errors,
755
			]);
756
		}
757
758
		return Services::redirection()->back()->withInput()->withErrors($errors)->withStatus($code);
759
    }
760
}
761