Passed
Push — main ( aec17e...ff79ac )
by Dimitri
04:24
created

Dispatcher::displayCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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

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

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

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