Passed
Push — main ( 1fe2cd...c1deb1 )
by Dimitri
04:10
created

Dispatcher::formatResponse()   B

Complexity

Conditions 9
Paths 22

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 3
Bugs 2 Features 0
Metric Value
cc 9
eloc 20
c 3
b 2
f 0
nc 22
nop 2
dl 0
loc 33
ccs 0
cts 10
cp 0
crap 90
rs 8.0555
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\Debug\Timer;
22
use BlitzPHP\Enums\Method;
23
use BlitzPHP\Exceptions\PageNotFoundException;
24
use BlitzPHP\Exceptions\RedirectException;
25
use BlitzPHP\Exceptions\ValidationException;
26
use BlitzPHP\Http\MiddlewareQueue;
27
use BlitzPHP\Http\MiddlewareRunner;
28
use BlitzPHP\Http\Request;
29
use BlitzPHP\Http\Response;
30
use BlitzPHP\Http\Uri;
31
use BlitzPHP\Utilities\Helpers;
32
use BlitzPHP\Utilities\String\Text;
33
use BlitzPHP\Validation\ErrorBag;
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 Request
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 MiddlewareQueue
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
        /**
250
         * Ajouter des middlewares de routes
251
         */
252
        foreach ($routeMiddlewares as $middleware) {
253
            $this->middleware->append($middleware);
254
        }
255
256
        $this->middleware->append($this->bootApp());
257
258
        // Enregistrer notre URI actuel en tant qu'URI précédent dans la session
259
        // pour une utilisation plus sûre et plus précise avec la fonction d'assistance `previous_url()`.
260
        $this->storePreviousURL(current_url(true));
261
262
        return (new MiddlewareRunner())->run($this->middleware, $this->request);
263
    }
264
265
    /**
266
     * Démarrer le benchmark
267
     *
268
     * La minuterie est utilisée pour afficher l'exécution totale du script à la fois dans la
269
     * barre d'outils de débogage, et éventuellement sur la page affichée.
270
     */
271
    protected function startBenchmark()
272
    {
273
        if ($this->startTime === null) {
274
            $this->startTime = microtime(true);
275
        }
276
277
        $this->timer = Services::timer();
278
        $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

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

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

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