Passed
Pull Request — main (#18)
by Dimitri
03:55
created

RouteCollection::presenter()   F

Complexity

Conditions 18
Paths 12288

Size

Total Lines 89
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 18.7184

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 18
eloc 46
c 5
b 1
f 0
nc 12288
nop 2
dl 0
loc 89
ccs 20
cts 23
cp 0.8696
crap 18.7184
rs 0.7

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Container\Services;
15
use BlitzPHP\Contracts\Autoloader\LocatorInterface;
16
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
17
use BlitzPHP\Enums\Method;
18
use BlitzPHP\Exceptions\RouterException;
19
use BlitzPHP\Utilities\String\Text;
20
use Closure;
21
use InvalidArgumentException;
22
use Psr\Http\Message\ResponseInterface;
23
24
class RouteCollection implements RouteCollectionInterface
25
{
26
    /**
27
     * L'espace de noms à ajouter à tous les contrôleurs.
28
     * Par défaut, les espaces de noms globaux (\)
29
     */
30
    protected string $defaultNamespace = '\\';
31
32
    /**
33
     * Le nom du contrôleur par défaut à utiliser
34
     * lorsqu'aucun autre contrôleur n'est spécifié.
35
     *
36
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
37
     */
38
    protected string $defaultController = 'Home';
39
40
    /**
41
     * Le nom de la méthode par défaut à utiliser
42
     * lorsqu'aucune autre méthode n'a été spécifiée.
43
     *
44
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
45
     */
46
    protected string $defaultMethod = 'index';
47
48
    /**
49
     * L'espace réservé utilisé lors du routage des "ressources"
50
     * lorsqu'aucun autre espace réservé n'a été spécifié.
51
     */
52
    protected string $defaultPlaceholder = 'any';
53
54
    /**
55
     * S'il faut convertir les tirets en traits de soulignement dans l'URI.
56
     *
57
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
58
     */
59
    protected bool $translateURIDashes = true;
60
61
    /**
62
     * S'il faut faire correspondre l'URI aux contrôleurs
63
     * lorsqu'il ne correspond pas aux itinéraires définis.
64
     *
65
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
66
     */
67
    protected bool $autoRoute = false;
68
69
    /**
70
     * Un appelable qui sera affiché
71
     * lorsque la route ne peut pas être matchée.
72
     *
73
     * @var (Closure(string): (ResponseInterface|string|void))|string
0 ignored issues
show
Documentation Bug introduced by
The doc comment (Closure(string): (Respo...ce|string|void))|string at position 1 could not be parsed: Expected ')' at position 1, but found 'Closure'.
Loading history...
74
     */
75
    protected $override404;
76
77
    /**
78
     * Tableau de fichiers qui contiendrait les définitions de routes.
79
     */
80
    protected array $routeFiles = [];
81
82
    /**
83
     * Espaces réservés définis pouvant être utilisés.
84
     */
85
    protected array $placeholders = [
86
        'any'      => '.*',
87
        'segment'  => '[^/]+',
88
        'alphanum' => '[a-zA-Z0-9]+',
89
        'num'      => '[0-9]+',
90
        'alpha'    => '[a-zA-Z]+',
91
        'hash'     => '[^/]+',
92
        'slug'     => '[a-z0-9-]+',
93
        'uuid'     => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
94
    ];
95
96
    /**
97
     * Tableau de toutes les routes et leurs mappages.
98
     *
99
     * @example
100
     * ```php
101
     * [
102
     *     verb => [
103
     *         routeName => [
104
     *             'route' => [
105
     *                 routeKey(regex) => handler,
106
     *             ],
107
     *             'redirect' => statusCode,
108
     *         ]
109
     *     ],
110
     * ]
111
     * ```
112
     */
113
    protected array $routes = [
114
        '*'             => [],
115
        Method::OPTIONS => [],
116
        Method::GET     => [],
117
        Method::HEAD    => [],
118
        Method::POST    => [],
119
        Method::PATCH   => [],
120
        Method::PUT     => [],
121
        Method::DELETE  => [],
122
        Method::TRACE   => [],
123
        Method::CONNECT => [],
124
        'CLI'           => [],
125
    ];
126
127
    /**
128
     * Tableau des noms des routes
129
     *
130
     * [
131
     *     verb => [
132
     *         routeName => routeKey(regex)
133
     *     ],
134
     * ]
135
     */
136
    protected array $routesNames = [
137
        '*'             => [],
138
        Method::OPTIONS => [],
139
        Method::GET     => [],
140
        Method::HEAD    => [],
141
        Method::POST    => [],
142
        Method::PATCH   => [],
143
        Method::PUT     => [],
144
        Method::DELETE  => [],
145
        Method::TRACE   => [],
146
        Method::CONNECT => [],
147
        'CLI'           => [],
148
    ];
149
150
    /**
151
     * Tableaux des options des routes.
152
     *
153
     * @example
154
     * ```php
155
     * [
156
     *     verb => [
157
     *         routeKey(regex) => [
158
     *             key => value,
159
     *         ]
160
     *     ],
161
     * ]
162
     * ```
163
     */
164
    protected array $routesOptions = [];
165
166
    /**
167
     * La méthode actuelle par laquelle le script est appelé.
168
     */
169
    protected string $HTTPVerb = '*';
170
171
    /**
172
     * La liste par défaut des méthodes HTTP (et CLI pour l'utilisation de la ligne de commande)
173
     * qui est autorisé si aucune autre méthode n'est fournie.
174
     */
175
    protected array $defaultHTTPMethods = Router::HTTP_METHODS;
176
177
    /**
178
     * Le nom du groupe de route courant
179
     */
180
    protected ?string $group = null;
181
182
    /**
183
     * Le sous domaine courant
184
     */
185
    protected ?string $currentSubdomain = null;
186
187
    /**
188
     * Stocke une copie des options actuelles en cours appliqué lors de la création.
189
     */
190
    protected ?array $currentOptions = null;
191
192
    /**
193
     * Un petit booster de performances.
194
     */
195
    protected bool $didDiscover = false;
196
197
    /**
198
     * Drapeau pour trier les routes par priorité.
199
     */
200
    protected bool $prioritize = false;
201
202
    /**
203
     * Indicateur de détection de priorité de route.
204
     */
205
    protected bool $prioritizeDetected = false;
206
207
    /**
208
     * Drapeau pour limiter ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
209
     */
210
    protected bool $useSupportedLocalesOnly = false;
211
212
    /**
213
     * Le nom d'hôte actuel de $_SERVER['HTTP_HOST']
214
     */
215
    private ?string $httpHost = null;
216
217
    /**
218
     * Constructor
219
     *
220
     * @param LocatorInterface $locator Descripteur du localisateur de fichiers à utiliser.
221
     */
222
    public function __construct(protected LocatorInterface $locator, object $routing)
223
    {
224 71
        $this->httpHost = env('HTTP_HOST');
225
226
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
227 71
        $this->defaultNamespace   = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
228 71
        $this->defaultController  = $routing->default_controller ?: $this->defaultController;
229 71
        $this->defaultMethod      = $routing->default_method ?: $this->defaultMethod;
230 71
        $this->translateURIDashes = $routing->translate_uri_dashes ?: $this->translateURIDashes;
231 71
        $this->override404        = $routing->fallback ?: $this->override404;
232 71
        $this->autoRoute          = $routing->auto_route ?: $this->autoRoute;
233 71
        $this->routeFiles         = $routing->route_files ?: $this->routeFiles;
234 71
        $this->prioritize         = $routing->prioritize ?: $this->prioritize;
235
236
        // Normaliser la chaîne de path dans le tableau routeFiles.
237
        foreach ($this->routeFiles as $routeKey => $routesFile) {
238 71
            $this->routeFiles[$routeKey] = realpath($routesFile) ?: $routesFile;
239
        }
240
    }
241
242
    /**
243
     * Charge le fichier des routes principales et découvre les routes.
244
     *
245
     * Charge une seule fois sauf réinitialisation.
246
     */
247
    public function loadRoutes(string $routesFile = CONFIG_PATH . 'routes.php'): self
248
    {
249
        if ($this->didDiscover) {
250
            return $this;
251
        }
252
253
        // Normaliser la chaîne de chemin dans routesFile
254
        $routesFile = realpath($routesFile) ?: $routesFile;
255
256
        // Incluez le fichier routesFile s'il n'existe pas.
257
        // Ne conserver que pour les fins BC pour l'instant.
258
        $routeFiles = $this->routeFiles;
259
        if (! in_array($routesFile, $routeFiles, true)) {
260
            $routeFiles[] = $routesFile;
261
        }
262
263
        // Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
264
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
265
266
        foreach ($routeFiles as $routesFile) {
267
            if (! is_file($routesFile)) {
268
                logger()->warning(sprintf('Fichier de route introuvable : "%s"', $routesFile));
269
270
                continue;
271
            }
272
273
            require_once $routesFile;
274
        }
275
276
        $this->discoverRoutes();
277
278
        return $this;
279
    }
280
281
    /**
282
     * Réinitialisez les routes, afin qu'un cas de test puisse fournir le
283
     * ceux explicites nécessaires pour cela.
284
     */
285
    public function resetRoutes()
286
    {
287
        $this->routes = $this->routesNames = ['*' => []];
288
289
        foreach ($this->defaultHTTPMethods as $verb) {
290
            $this->routes[$verb]      = [];
291
            $this->routesNames[$verb] = [];
292
        }
293
294
        $this->routesOptions = [];
295
296
        $this->prioritizeDetected = false;
297
        $this->didDiscover        = false;
298
    }
299
300
    /**
301
     * {@inheritDoc}
302
     *
303
     * Utilisez `placeholder` a la place
304
     */
305
    public function addPlaceholder($placeholder, ?string $pattern = null): self
306
    {
307 4
        return $this->placeholder($placeholder, $pattern);
308
    }
309
310
    /**
311
     * Enregistre une nouvelle contrainte auprès du système.
312
     * Les contraintes sont utilisées par les routes en tant qu'espaces réservés pour les expressions régulières afin de définir les parcours plus humains.
313
     */
314
    public function placeholder(array|string $placeholder, ?string $pattern = null): self
315
    {
316
        if (! is_array($placeholder)) {
0 ignored issues
show
introduced by
The condition is_array($placeholder) is always true.
Loading history...
317 6
            $placeholder = [$placeholder => $pattern];
318
        }
319
320 6
        $this->placeholders = array_merge($this->placeholders, $placeholder);
321
322 6
        return $this;
323
    }
324
325
    /**
326
     * {@inheritDoc}
327
     */
328
    public function setDefaultNamespace(string $value): self
329
    {
330 20
        $this->defaultNamespace = esc(strip_tags($value));
331 20
        $this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';
332
333 20
        return $this;
334
    }
335
336
    /**
337
     * {@inheritDoc}
338
     */
339
    public function setDefaultController(string $value): self
340
    {
341 8
        $this->defaultController = esc(strip_tags($value));
342
343 8
        return $this;
344
    }
345
346
    /**
347
     * {@inheritDoc}
348
     */
349
    public function setDefaultMethod(string $value): self
350
    {
351 4
        $this->defaultMethod = esc(strip_tags($value));
352
353 4
        return $this;
354
    }
355
356
    /**
357
     * {@inheritDoc}
358
     */
359
    public function setTranslateURIDashes(bool $value): self
360
    {
361 4
        $this->translateURIDashes = $value;
362
363 4
        return $this;
364
    }
365
366
    /**
367
     * {@inheritDoc}
368
     */
369
    public function setAutoRoute(bool $value): self
370
    {
371 4
        $this->autoRoute = $value;
372
373 4
        return $this;
374
    }
375
376
    /**
377
     * {@inheritDoc}
378
     *
379
     * Utilisez self::fallback()
380
     */
381
    public function set404Override($callable = null): self
382
    {
383
        return $this->fallback($callable);
384
    }
385
386
    /**
387
     * Définit la classe/méthode qui doit être appelée si le routage ne trouver pas une correspondance.
388
     *
389
     * @param callable|string|null $callable
390
     */
391
    public function fallback($callable = null): self
392
    {
393 4
        $this->override404 = $callable;
394
395 4
        return $this;
396
    }
397
398
    /**
399
     * {@inheritDoc}
400
     */
401
    public function get404Override()
402
    {
403 4
        return $this->override404;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->override404 also could return the type mixed which is incompatible with the return type mandated by BlitzPHP\Contracts\Route...rface::get404Override() of Closure|null|string.
Loading history...
404
    }
405
406
    /**
407
     * Tentera de découvrir d'éventuelles routes supplémentaires, soit par
408
     * les espaces de noms PSR4 locaux ou via des packages Composer sélectionnés.
409
     */
410
    protected function discoverRoutes()
411
    {
412
        if ($this->didDiscover) {
413 14
            return;
414
        }
415
416
        // Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
417 52
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
418
419 52
        $files = $this->locator->search('Config/routes.php');
420
421
        foreach ($files as $file) {
422
            // N'incluez plus notre fichier principal...
423
            if (in_array($file, $this->routeFiles, true)) {
424 52
                continue;
425
            }
426
427
            include_once $file;
428
        }
429
430 52
        $this->didDiscover = true;
431
    }
432
433
    /**
434
     * Définit la contrainte par défaut à utiliser dans le système. Typiquement
435
     * à utiliser avec la méthode 'ressource'.
436
     */
437
    public function setDefaultConstraint(string $placeholder): self
438
    {
439
        if (array_key_exists($placeholder, $this->placeholders)) {
440 2
            $this->defaultPlaceholder = $placeholder;
441
        }
442
443 2
        return $this;
444
    }
445
446
    /**
447
     * {@inheritDoc}
448
     */
449
    public function getDefaultController(): string
450
    {
451 22
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
452
    }
453
454
    /**
455
     * {@inheritDoc}
456
     */
457
    public function getDefaultMethod(): string
458
    {
459 22
        return $this->defaultMethod;
460
    }
461
462
    /**
463
     * {@inheritDoc}
464
     */
465
    public function getDefaultNamespace(): string
466
    {
467 4
        return $this->defaultNamespace;
468
    }
469
470
    /**
471
     * Pour `klinge route:list`
472
     *
473
     * @return array<string, string>
474
     *
475
     * @internal
476
     */
477
    public function getPlaceholders(): array
478
    {
479
        return $this->placeholders;
480
    }
481
482
    /**
483
     *{@inheritDoc}
484
     */
485
    public function shouldTranslateURIDashes(): bool
486
    {
487 16
        return $this->translateURIDashes;
488
    }
489
490
    /**
491
     * {@inheritDoc}
492
     */
493
    public function shouldAutoRoute(): bool
494
    {
495 16
        return $this->autoRoute;
496
    }
497
498
    /**
499
     * Activer ou désactiver le tri des routes par priorité
500
     */
501
    public function setPrioritize(bool $enabled = true): self
502
    {
503 2
        $this->prioritize = $enabled;
504
505 2
        return $this;
506
    }
507
508
    /**
509
     * Définissez le drapeau qui limite ou non les routes avec l'espace réservé {locale} à App::$supportedLocales
510
     */
511
    public function useSupportedLocalesOnly(bool $useOnly): self
512
    {
513 2
        $this->useSupportedLocalesOnly = $useOnly;
514
515 2
        return $this;
516
    }
517
518
    /**
519
     * Obtenez le drapeau qui limite ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
520
     */
521
    public function shouldUseSupportedLocalesOnly(): bool
522
    {
523 4
        return $this->useSupportedLocalesOnly;
524
    }
525
526
    /**
527
     * {@inheritDoc}
528
     *
529
     * @internal
530
     */
531
    public function getRegisteredControllers(?string $verb = '*'): array
532
    {
533 4
        $controllers = [];
534
535
        if ($verb === '*') {
536
            foreach ($this->defaultHTTPMethods as $tmpVerb) {
537
                foreach ($this->routes[$tmpVerb] as $route) {
538 2
                    $controller = $this->getControllerName($route['handler']);
539
                    if ($controller !== null) {
540 2
                        $controllers[] = $controller;
541
                    }
542
                }
543
            }
544
        } else {
545 2
            $routes = $this->getRoutes($verb);
546
547
            foreach ($routes as $handler) {
548 2
                $controller = $this->getControllerName($handler);
549
                if ($controller !== null) {
550 2
                    $controllers[] = $controller;
551
                }
552
            }
553
        }
554
555 4
        return array_unique($controllers);
556
    }
557
558
    /**
559
     * {@inheritDoc}
560
     */
561
    public function getRoutes(?string $verb = null, bool $includeWildcard = true): array
562
    {
563
        if ($verb === null || $verb === '') {
564 32
            $verb = $this->getHTTPVerb();
565
        }
566
567
        // Puisqu'il s'agit du point d'entrée du routeur,
568
        // prenez un moment pour faire toute découverte de route
569
        // que nous pourrions avoir besoin de faire.
570 52
        $this->discoverRoutes();
571
572 52
        $routes = [];
573
        if (isset($this->routes[$verb])) {
574
            // Conserve les itinéraires du verbe actuel au début afin qu'ils soient
575
            // mis en correspondance avant l'un des itinéraires génériques "add".
576 52
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
577
578
            foreach ($collection as $routeKey => $r) {
579 52
                $routes[$routeKey] = $r['handler'];
580
            }
581
        }
582
583
        // tri des routes par priorité
584
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
585 2
            $order = [];
586
587
            foreach ($routes as $key => $value) {
588 2
                $key                    = $key === '/' ? $key : ltrim($key, '/ ');
589 2
                $priority               = $this->getRoutesOptions($key, $verb)['priority'] ?? 0;
590 2
                $order[$priority][$key] = $value;
591
            }
592
593 2
            ksort($order);
594 2
            $routes = array_merge(...$order);
595
        }
596
597 52
        return $routes;
598
    }
599
600
    /**
601
     * Renvoie une ou toutes les options d'itinéraire
602
     */
603
    public function getRoutesOptions(?string $from = null, ?string $verb = null): array
604
    {
605 26
        $options = $this->loadRoutesOptions($verb);
606
607 26
        return $from ? $options[$from] ?? [] : $options;
608
    }
609
610
    /**
611
     * {@inheritDoc}
612
     */
613
    public function getHTTPVerb(): string
614
    {
615 50
        return $this->HTTPVerb;
616
    }
617
618
    /**
619
     * {@inheritDoc}
620
     */
621
    public function setHTTPVerb(string $verb): self
622
    {
623 66
        $this->HTTPVerb = strtoupper($verb);
624
625 66
        return $this;
626
    }
627
628
    /**
629
     * Une méthode de raccourci pour ajouter un certain nombre d'itinéraires en une seule fois.
630
     * Il ne permet pas de définir des options sur l'itinéraire, ou de
631
     * définir la méthode utilisée.
632
     */
633
    public function map(array $routes = [], ?array $options = null): self
634
    {
635
        foreach ($routes as $from => $to) {
636 12
            $this->add($from, $to, $options);
637
        }
638
639 12
        return $this;
640
    }
641
642
    /**
643
     * {@inheritDoc}
644
     */
645
    public function add(string $from, $to, ?array $options = null): self
646
    {
647 48
        $this->create('*', $from, $to, $options);
648
649 48
        return $this;
650
    }
651
652
    /**
653
     * Ajoute une redirection temporaire d'une route à une autre.
654
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
655
     *
656
     * @param string $from   Le modèle à comparer
657
     * @param string $to     Soit un nom de route ou un URI vers lequel rediriger
658
     * @param int    $status Le code d'état HTTP qui doit être renvoyé avec cette redirection
659
     */
660
    public function redirect(string $from, string $to, int $status = 302): self
661
    {
662
        // Utilisez le modèle de la route nommée s'il s'agit d'une route nommée.
663
        if (array_key_exists($to, $this->routesNames['*'])) {
664 4
            $routeName  = $to;
665 4
            $routeKey   = $this->routesNames['*'][$routeName];
666 4
            $redirectTo = [$routeKey => $this->routes['*'][$routeKey]['handler']];
667
        } elseif (array_key_exists($to, $this->routesNames[Method::GET])) {
668 2
            $routeName  = $to;
669 2
            $routeKey   = $this->routesNames[Method::GET][$routeName];
670 2
            $redirectTo = [$routeKey => $this->routes[Method::GET][$routeKey]['handler']];
671
        } else {
672
            // La route nommee n'a pas ete trouvée
673 4
            $redirectTo = $to;
674
        }
675
676 4
        $this->create('*', $from, $redirectTo, ['redirect' => $status]);
677
678 4
        return $this;
679
    }
680
681
    /**
682
     * Ajoute une redirection permanente d'une route à une autre.
683
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
684
     */
685
    public function permanentRedirect(string $from, string $to): self
686
    {
687 2
        return $this->redirect($from, $to, 301);
688
    }
689
690
    /**
691
     * @deprecated 0.9 Please use redirect() instead
692
     */
693
    public function addRedirect(string $from, string $to, int $status = 302): self
694
    {
695
        return $this->redirect($from, $to, $status);
696
    }
697
698
    /**
699
     * {@inheritDoc}
700
     *
701
     * @param string $routeKey cle de route ou route nommee
702
     */
703
    public function isRedirect(string $routeKey): bool
704
    {
705 20
        return isset($this->routes['*'][$routeKey]['redirect']);
706
    }
707
708
    /**
709
     * {@inheritDoc}
710
     *
711
     * @param string $routeKey cle de route ou route nommee
712
     */
713
    public function getRedirectCode(string $routeKey): int
714
    {
715 4
        return $this->routes['*'][$routeKey]['redirect'] ?? 0;
716
    }
717
718
    /**
719
     * Regroupez une série de routes sous un seul segment d'URL. C'est pratique
720
     * pour regrouper des éléments dans une zone d'administration, comme :
721
     *
722
     * Example:
723
     *     // Creates route: admin/users
724
     *     $route->group('admin', function() {
725
     *            $route->resource('users');
726
     *     });
727
     *
728
     * @param string         $name      Le nom avec lequel grouper/préfixer les routes.
729
     * @param array|callable ...$params
730
     */
731
    public function group(string $name, ...$params)
732
    {
733 10
        $oldGroup   = $this->group ?: '';
734 10
        $oldOptions = $this->currentOptions;
735
736
        // Pour enregistrer une route, nous allons définir un indicateur afin que notre routeur
737
        // donc il verra le nom du groupe.
738
        // Si le nom du groupe est vide, nous continuons à utiliser le nom du groupe précédemment construit.
739 10
        $this->group = $name ? trim($oldGroup . '/' . $name, '/') : $oldGroup;
740
741 10
        $callback = array_pop($params);
742
743
        if ($params && is_array($params[0])) {
744 10
            $options = array_shift($params);
745
746
            if (isset($options['middlewares']) || isset($options['middleware'])) {
747 8
                $currentMiddlewares     = (array) ($this->currentOptions['middlewares'] ?? []);
748 8
                $options['middlewares'] = array_merge($currentMiddlewares, (array) ($options['middlewares'] ?? $options['middleware']));
749
            }
750
751
            // Fusionner les options autres que les middlewares.
752
            $this->currentOptions = array_merge(
753
                $this->currentOptions ?: [],
754
                $options ?: [],
755 10
            );
756
        }
757
758
        if (is_callable($callback)) {
759 10
            $callback($this);
760
        }
761
762 10
        $this->group          = $oldGroup;
763 10
        $this->currentOptions = $oldOptions;
764
    }
765
766
    /*
767
     * ------------------------------------------------- -------------------
768
     * Routage basé sur les verbes HTTP
769
     * ------------------------------------------------- -------------------
770
     * Le routage fonctionne ici car, comme le fichier de configuration des routes est lu,
771
     * les différentes routes basées sur le verbe HTTP ne seront ajoutées qu'à la mémoire en mémoire
772
     * routes s'il s'agit d'un appel qui doit répondre à ce verbe.
773
     *
774
     * Le tableau d'options est généralement utilisé pour transmettre un 'as' ou var, mais peut
775
     * être étendu à l'avenir. Voir le docblock pour la méthode 'add' ci-dessus pour
776
     * liste actuelle des options disponibles dans le monde.*/
777
778
    /**
779
     * Crée une collection d'itinéraires basés sur HTTP-verb pour un contrôleur.
780
     *
781
     * Options possibles :
782
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
783
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
784
     * 'websafe' - - '1' si seuls les verbes HTTP GET et POST sont pris en charge
785
     *
786
     * Exemple:
787
     *
788
     *      $route->resource('photos');
789
     *
790
     *      // Genère les routes suivantes:
791
     *      HTTP Verb | Path        | Action        | Used for...
792
     *      ----------+-------------+---------------+-----------------
793
     *      GET         /photos             index           un tableau d'objets photo
794
     *      GET         /photos/new         new             un objet photo vide, avec des propriétés par défaut
795
     *      GET         /photos/{id}/edit   edit            un objet photo spécifique, propriétés modifiables
796
     *      GET         /photos/{id}        show            un objet photo spécifique, toutes les propriétés
797
     *      POST        /photos             create          un nouvel objet photo, à ajouter à la ressource
798
     *      DELETE      /photos/{id}        delete          supprime l'objet photo spécifié
799
     *      PUT/PATCH   /photos/{id}        update          propriétés de remplacement pour la photo existante
800
     *
801
     *  Si l'option 'websafe' est présente, les chemins suivants sont également disponibles :
802
     *
803
     *      POST		/photos/{id}/delete delete
804
     *      POST        /photos/{id}        update
805
     *
806
     * @param string $name    Le nom de la ressource/du contrôleur vers lequel router.
807
     * @param array  $options Une liste des façons possibles de personnaliser le routage.
808
     */
809
    public function resource(string $name, array $options = []): self
810
    {
811
        // Afin de permettre la personnalisation de la route, le
812
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
813
        // pour stocker les valeurs.
814 6
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
815
816
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
817
        // valeur de $name avec le nom du nouveau contrôleur.
818
        if (isset($options['controller'])) {
819 6
            $newName = ucfirst(esc(strip_tags($options['controller'])));
0 ignored issues
show
Bug introduced by
It seems like esc(strip_tags($options['controller'])) can also be of type array; however, parameter $string of ucfirst() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

819
            $newName = ucfirst(/** @scrutinizer ignore-type */ esc(strip_tags($options['controller'])));
Loading history...
820 6
            unset($options['controller']);
821
        }
822
823 6
        $newName = Text::convertTo($newName, 'pascalcase');
824
825
        // Afin de permettre la personnalisation des valeurs d'identifiant autorisées
826
        // nous avons besoin d'un endroit pour les stocker.
827 6
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
828
829
        // On s'assure de capturer les références arrière
830 6
        $id = '(' . trim($id, '()') . ')';
831
832 6
        $methods = isset($options['only']) ? (is_string($options['only']) ? explode(',', $options['only']) : $options['only']) : ['index', 'show', 'create', 'update', 'delete', 'new', 'edit'];
833
834
        if (isset($options['except'])) {
835 2
            $options['except'] = is_array($options['except']) ? $options['except'] : explode(',', $options['except']);
836
837
            foreach ($methods as $i => $method) {
838
                if (in_array($method, $options['except'], true)) {
839 2
                    unset($methods[$i]);
840
                }
841
            }
842
        }
843
844 6
        $routeName = $name;
845
        if (isset($options['as']) || isset($options['name'])) {
846 6
            $routeName = trim($options['as'] ?? $options['name'], ' .');
847
            unset($options['name'], $options['as']);
848
        }
849
850
        if (in_array('index', $methods, true)) {
851
            $this->get($name, $newName . '::index', $options + [
852
                'as' => $routeName . '.index',
853 6
            ]);
854
        }
855
        if (in_array('new', $methods, true)) {
856
            $this->get($name . '/new', $newName . '::new', $options + [
857
                'as' => $routeName . '.new',
858 6
            ]);
859
        }
860
        if (in_array('edit', $methods, true)) {
861
            $this->get($name . '/' . $id . '/edit', $newName . '::edit/$1', $options + [
862
                'as' => $routeName . '.edit',
863 6
            ]);
864
        }
865
        if (in_array('show', $methods, true)) {
866
            $this->get($name . '/' . $id, $newName . '::show/$1', $options + [
867
                'as' => $routeName . '.show',
868 6
            ]);
869
        }
870
        if (in_array('create', $methods, true)) {
871
            $this->post($name, $newName . '::create', $options + [
872
                'as' => $routeName . '.create',
873 6
            ]);
874
        }
875
        if (in_array('update', $methods, true)) {
876
            $this->match(['put', 'patch'], $name . '/' . $id, $newName . '::update/$1', $options + [
877
                'as' => $routeName . '.update',
878 6
            ]);
879
        }
880
        if (in_array('delete', $methods, true)) {
881
            $this->delete($name . '/' . $id, $newName . '::delete/$1', $options + [
882
                'as' => $routeName . '.delete',
883 6
            ]);
884
        }
885
886
        // Websafe ? la suppression doit être vérifiée avant la mise à jour en raison du nom de la méthode
887
        if (isset($options['websafe'])) {
888
            if (in_array('delete', $methods, true)) {
889
                $this->post($name . '/' . $id . '/delete', $newName . '::delete/$1', $options + [
890
                    'as' => $routeName . '.delete',
891 2
                ]);
892
            }
893
            if (in_array('update', $methods, true)) {
894
                $this->post($name . '/' . $id, $newName . '::update/$1', $options + [
895
                    'as' => $routeName . '.update',
896 2
                ]);
897
            }
898
        }
899
900 6
        return $this;
901
    }
902
903
    /**
904
     * Crée une collection de routes basées sur les verbes HTTP pour un contrôleur de présentateur.
905
     *
906
     * Options possibles :
907
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
908
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
909
     *
910
     * Example:
911
     *
912
     *      $route->presenter('photos');
913
     *
914
     *      // Génère les routes suivantes
915
     *      HTTP Verb | Path        | Action        | Used for...
916
     *      ----------+-------------+---------------+-----------------
917
     *      GET         /photos             index           affiche le tableau des tous les objets photo
918
     *      GET         /photos/show/{id}   show            affiche un objet photo spécifique, toutes les propriétés
919
     *      GET         /photos/new         new             affiche un formulaire pour un objet photo vide, avec les propriétés par défaut
920
     *      POST        /photos/create      create          traitement du formulaire pour une nouvelle photo
921
     *      GET         /photos/edit/{id}   edit            affiche un formulaire d'édition pour un objet photo spécifique, propriétés modifiables
922
     *      POST        /photos/update/{id} update          traitement des données du formulaire d'édition
923
     *      GET         /photos/remove/{id} remove          affiche un formulaire pour confirmer la suppression d'un objet photo spécifique
924
     *      POST        /photos/delete/{id} delete          suppression de l'objet photo spécifié
925
     *
926
     * @param string $name    Le nom du contrôleur vers lequel router.
927
     * @param array  $options Une liste des façons possibles de personnaliser le routage.
928
     */
929
    public function presenter(string $name, array $options = []): self
930
    {
931
        // Afin de permettre la personnalisation de la route, le
932
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
933
        // pour stocker les valeurs.
934 4
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
935
936
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
937
        // valeur de $name avec le nom du nouveau contrôleur.
938
        if (isset($options['controller'])) {
939 4
            $newName = ucfirst(esc(strip_tags($options['controller'])));
0 ignored issues
show
Bug introduced by
It seems like esc(strip_tags($options['controller'])) can also be of type array; however, parameter $string of ucfirst() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

939
            $newName = ucfirst(/** @scrutinizer ignore-type */ esc(strip_tags($options['controller'])));
Loading history...
940
            unset($options['controller']);
941
        }
942
943 4
        $newName = Text::convertTo($newName, 'pascalcase');
944
945
        // Afin de permettre la personnalisation des valeurs d'identifiant autorisées
946
        // nous avons besoin d'un endroit pour les stocker.
947 4
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
948
949
        // On s'assure de capturer les références arrière
950 4
        $id = '(' . trim($id, '()') . ')';
951
952 4
        $methods = isset($options['only']) ? (is_string($options['only']) ? explode(',', $options['only']) : $options['only']) : ['index', 'show', 'new', 'create', 'edit', 'update', 'remove', 'delete'];
953
954
        if (isset($options['except'])) {
955 4
            $options['except'] = is_array($options['except']) ? $options['except'] : explode(',', $options['except']);
956
957
            foreach ($methods as $i => $method) {
958
                if (in_array($method, $options['except'], true)) {
959
                    unset($methods[$i]);
960
                }
961
            }
962
        }
963
964 4
        $routeName = $name;
965
        if (isset($options['as']) || isset($options['name'])) {
966 4
            $routeName = trim($options['as'] ?? $options['name'], ' .');
967
            unset($options['name'], $options['as']);
968
        }
969
970
        if (in_array('index', $methods, true)) {
971
            $this->get($name, $newName . '::index', $options + [
972
                'as' => $routeName . '.index',
973 4
            ]);
974
        }
975
        if (in_array('new', $methods, true)) {
976
            $this->get($name . '/new', $newName . '::new', $options + [
977
                'as' => $routeName . '.new',
978 4
            ]);
979
        }
980
        if (in_array('edit', $methods, true)) {
981
            $this->get($name . '/edit/' . $id, $newName . '::edit/$1', $options + [
982
                'as' => $routeName . '.edit',
983 4
            ]);
984
        }
985
        if (in_array('update', $methods, true)) {
986
            $this->post($name . '/update/' . $id, $newName . '::update/$1', $options + [
987
                'as' => $routeName . '.update',
988 4
            ]);
989
        }
990
        if (in_array('remove', $methods, true)) {
991
            $this->get($name . '/remove/' . $id, $newName . '::remove/$1', $options + [
992
                'as' => $routeName . '.remove',
993 4
            ]);
994
        }
995
        if (in_array('delete', $methods, true)) {
996
            $this->post($name . '/delete/' . $id, $newName . '::delete/$1', $options + [
997
                'as' => $routeName . '.delete',
998 4
            ]);
999
        }
1000
        if (in_array('create', $methods, true)) {
1001
            $this->post($name . '/create', $newName . '::create', $options + [
1002
                'as' => $routeName . '.create',
1003 4
            ]);
1004
            $this->post($name, $newName . '::create', $options + [
1005
                'as' => $routeName . '.store',
1006 4
            ]);
1007
        }
1008
        if (in_array('show', $methods, true)) {
1009
            $this->get($name . '/show/' . $id, $newName . '::show/$1', $options + [
1010
                'as' => $routeName . '.view',
1011 4
            ]);
1012
            $this->get($name . '/' . $id, $newName . '::show/$1', $options + [
1013
                'as' => $routeName . '.show',
1014 4
            ]);
1015
        }
1016
1017 4
        return $this;
1018
    }
1019
1020
    /**
1021
     * Spécifie une seule route à faire correspondre pour plusieurs verbes HTTP.
1022
     *
1023
     * Exemple:
1024
     *  $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
1025
     *
1026
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1027
     */
1028
    public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): self
1029
    {
1030
        if ($from === '' || empty($to)) {
1031 14
            throw new InvalidArgumentException('Vous devez fournir les paramètres : $from, $to.');
1032
        }
1033
1034
        foreach ($verbs as $verb) {
1035 14
            $verb = strtolower($verb);
1036
1037 14
            $this->{$verb}($from, $to, $options);
1038
        }
1039
1040 14
        return $this;
1041
    }
1042
1043
    /**
1044
     * Spécifie une route qui n'est disponible que pour les requêtes GET.
1045
     *
1046
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1047
     */
1048
    public function get(string $from, $to, ?array $options = null): self
1049
    {
1050 40
        $this->create(Method::GET, $from, $to, $options);
1051
1052 40
        return $this;
1053
    }
1054
1055
    /**
1056
     * Spécifie une route qui n'est disponible que pour les requêtes POST.
1057
     *
1058
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1059
     */
1060
    public function post(string $from, $to, ?array $options = null): self
1061
    {
1062 22
        $this->create(Method::POST, $from, $to, $options);
1063
1064 22
        return $this;
1065
    }
1066
1067
    /**
1068
     * Spécifie une route qui n'est disponible que pour les requêtes PUT.
1069
     *
1070
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1071
     */
1072
    public function put(string $from, $to, ?array $options = null): self
1073
    {
1074 14
        $this->create(Method::PUT, $from, $to, $options);
1075
1076 14
        return $this;
1077
    }
1078
1079
    /**
1080
     * Spécifie une route qui n'est disponible que pour les requêtes DELETE.
1081
     *
1082
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1083
     */
1084
    public function delete(string $from, $to, ?array $options = null): self
1085
    {
1086 8
        $this->create(Method::DELETE, $from, $to, $options);
1087
1088 8
        return $this;
1089
    }
1090
1091
    /**
1092
     * Spécifie une route qui n'est disponible que pour les requêtes HEAD.
1093
     *
1094
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1095
     */
1096
    public function head(string $from, $to, ?array $options = null): self
1097
    {
1098 2
        $this->create(Method::HEAD, $from, $to, $options);
1099
1100 2
        return $this;
1101
    }
1102
1103
    /**
1104
     * Spécifie une route qui n'est disponible que pour les requêtes PATCH.
1105
     *
1106
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1107
     */
1108
    public function patch(string $from, $to, ?array $options = null): self
1109
    {
1110 8
        $this->create(Method::PATCH, $from, $to, $options);
1111
1112 8
        return $this;
1113
    }
1114
1115
    /**
1116
     * Spécifie une route qui n'est disponible que pour les requêtes OPTIONS.
1117
     *
1118
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1119
     */
1120
    public function options(string $from, $to, ?array $options = null): self
1121
    {
1122 2
        $this->create(Method::OPTIONS, $from, $to, $options);
1123
1124 2
        return $this;
1125
    }
1126
1127
    /**
1128
     * Spécifie une route qui n'est disponible que pour les requêtes GET et POST.
1129
     *
1130
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1131
     */
1132
    public function form(string $from, $to, ?array $options = null): self
1133
    {
1134 4
        return $this->match([Method::GET, Method::POST], $from, $to, $options);
1135
    }
1136
1137
    /**
1138
     * Spécifie une route qui n'est disponible que pour les requêtes de ligne de commande.
1139
     *
1140
     * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to
0 ignored issues
show
Documentation Bug introduced by
The doc comment array|(Closure(mixed...)...ce|string|void))|string at position 3 could not be parsed: Expected ')' at position 3, but found 'Closure'.
Loading history...
1141
     */
1142
    public function cli(string $from, $to, ?array $options = null): self
1143
    {
1144 2
        $this->create('CLI', $from, $to, $options);
1145
1146 2
        return $this;
1147
    }
1148
1149
    /**
1150
     * Spécifie une route qui n'affichera qu'une vue.
1151
     * Ne fonctionne que pour les requêtes GET.
1152
     */
1153
    public function view(string $from, string $view, array $options = []): self
1154
    {
1155
        $to = static fn (...$data) => Services::viewer()
1156
            ->setData(['segments' => $data], 'raw')
1157
            ->display($view)
1158
            ->options($options)
1159
            ->render();
1160
1161 6
        $routeOptions = array_merge($options, ['view' => $view]);
1162
1163 6
        $this->create(Method::GET, $from, $to, $routeOptions);
1164
1165 6
        return $this;
1166
    }
1167
1168
    /**
1169
     * Limite les itinéraires à un ENVIRONNEMENT spécifié ou ils ne fonctionneront pas.
1170
     */
1171
    public function environment(string $env, Closure $callback): self
1172
    {
1173
        if (environment($env)) {
1174 2
            $callback($this);
1175
        }
1176
1177 2
        return $this;
1178
    }
1179
1180
    /**
1181
     * {@inheritDoc}
1182
     */
1183
    public function reverseRoute(string $search, ...$params)
1184
    {
1185
        if ($search === '') {
1186 2
            return false;
1187
        }
1188
1189 10
        $queries = [];
1190
1191
        if (is_array($last = array_pop($params))) {
1192 2
            $queries = $last;
1193
        } elseif (null !== $last) {
1194 8
            $params[] = $last;
1195
        }
1196
1197 10
        $name = $this->formatRouteName($search);
1198
1199
        // Les routes nommées ont une priorité plus élevée.
1200
        foreach ($this->routesNames as $verb => $collection) {
1201
            if (array_key_exists($name, $collection)) {
1202 10
                $routeKey = $collection[$name];
1203
1204 10
                $from = $this->routes[$verb][$routeKey]['from'];
1205
1206 10
                return $this->buildReverseRoute($from, $params, $queries);
1207
            }
1208
        }
1209
1210
        // Ajoutez l'espace de noms par défaut si nécessaire.
1211 6
        $namespace = trim($this->defaultNamespace, '\\') . '\\';
1212
        if (
1213
            ! str_starts_with($search, '\\')
1214
            && ! str_starts_with($search, $namespace)
1215
        ) {
1216 6
            $search = $namespace . $search;
1217
        }
1218
1219
        // Si ce n'est pas une route nommée, alors bouclez
1220
        // toutes les routes pour trouver une correspondance.
1221
        foreach ($this->routes as $collection) {
1222
            foreach ($collection as $route) {
1223 6
                $to   = $route['handler'];
1224 6
                $from = $route['from'];
1225
1226
                // on ignore les closures
1227
                if (! is_string($to)) {
1228 2
                    continue;
1229
                }
1230
1231
                // Perd toute barre oblique d'espace de noms au début des chaînes
1232
                // pour assurer une correspondance plus cohérente.$to     = ltrim($to, '\\');
1233 6
                $to     = ltrim($to, '\\');
1234 6
                $search = ltrim($search, '\\');
1235
1236
                // S'il y a une chance de correspondance, alors ce sera
1237
                // soit avec $search au début de la chaîne $to.
1238
                if (! str_starts_with($to, $search)) {
1239 6
                    continue;
1240
                }
1241
1242
                // Assurez-vous que le nombre de $params donné ici
1243
                // correspond au nombre de back-references dans la route
1244
                if (substr_count($to, '$') !== count($params)) {
1245 2
                    continue;
1246
                }
1247
1248 4
                return $this->buildReverseRoute($from, $params, $queries);
1249
            }
1250
        }
1251
1252
        // Si nous sommes toujours là, alors nous n'avons pas trouvé de correspondance.
1253 6
        return false;
1254
    }
1255
1256
    /**
1257
     * Vérifie une route (en utilisant le "from") pour voir si elle est filtrée ou non.
1258
     */
1259
    public function isFiltered(string $search, ?string $verb = null): bool
1260
    {
1261 18
        return $this->getFiltersForRoute($search, $verb) !== [];
1262
    }
1263
1264
    /**
1265
     * Renvoie les filtres qui doivent être appliqués pour un seul itinéraire, ainsi que
1266
     * avec tous les paramètres qu'il pourrait avoir. Les paramètres sont trouvés en divisant
1267
     * le nom du paramètre entre deux points pour séparer le nom du filtre de la liste des paramètres,
1268
     * et le fractionnement du résultat sur des virgules. Alors:
1269
     *
1270
     *    'role:admin,manager'
1271
     *
1272
     * a un filtre de "rôle", avec des paramètres de ['admin', 'manager'].
1273
     */
1274
    public function getFiltersForRoute(string $search, ?string $verb = null): array
1275
    {
1276 18
        $options = $this->loadRoutesOptions($verb);
1277
1278 18
        $middlewares = $options[$search]['middlewares'] ?? ($options[$search]['middleware'] ?? []);
1279
1280 18
        return (array) $middlewares;
1281
    }
1282
1283
    /**
1284
     * Construit une route inverse
1285
     *
1286
     * @param array $params Un ou plusieurs paramètres à transmettre à la route.
1287
     *                      Le dernier paramètre vous permet de définir la locale.
1288
     */
1289
    protected function buildReverseRoute(string $from, array $params, array $queries = []): string
1290
    {
1291 10
        $locale = null;
1292
1293
        // Retrouvez l'ensemble de nos rétro-références dans le parcours d'origine.
1294 10
        preg_match_all('/\(([^)]+)\)/', $from, $matches);
1295
1296
        if (empty($matches[0])) {
1297
            if (str_contains($from, '{locale}')) {
1298 6
                $locale = $params[0] ?? null;
1299
            }
1300
1301 10
            $from = '/' . ltrim($this->replaceLocale($from, $locale), '/');
1302
1303
            if ($queries !== []) {
1304 2
                $from .= '?' . http_build_query($queries);
1305
            }
1306
1307 10
            return $from;
1308
        }
1309
1310
        // Les paramètres régionaux sont passés ?
1311 8
        $placeholderCount = count($matches[0]);
1312
        if (count($params) > $placeholderCount) {
1313 8
            $locale = $params[$placeholderCount];
1314
        }
1315
1316
        // Construisez notre chaîne résultante, en insérant les $params aux endroits appropriés.
1317
        foreach ($matches[0] as $index => $placeholder) {
1318
            if (! isset($params[$index])) {
1319
                throw new InvalidArgumentException(
1320
                    'Argument manquant pour "' . $placeholder . '" dans la route "' . $from . '".'
1321 8
                );
1322
            }
1323
1324
            // Supprimez `(:` et `)` lorsque $placeholder est un espace réservé.
1325 8
            $placeholderName = substr($placeholder, 2, -1);
1326
            // ou peut-être que $placeholder n'est pas un espace réservé, mais une regex.
1327 8
            $pattern = $this->placeholders[$placeholderName] ?? $placeholder;
1328
1329
            if (! preg_match('#^' . $pattern . '$#u', (string) $params[$index])) {
1330 4
                throw RouterException::invalidParameterType();
1331
            }
1332
1333
            // Assurez-vous que le paramètre que nous insérons correspond au type de paramètre attendu.
1334 8
            $pos  = strpos($from, $placeholder);
1335 8
            $from = substr_replace($from, $params[$index], $pos, strlen($placeholder));
1336
        }
1337
1338 8
        $from = '/' . ltrim($this->replaceLocale($from, $locale), '/');
0 ignored issues
show
Bug introduced by
It seems like $from can also be of type array; however, parameter $route of BlitzPHP\Router\RouteCollection::replaceLocale() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1338
        $from = '/' . ltrim($this->replaceLocale(/** @scrutinizer ignore-type */ $from, $locale), '/');
Loading history...
1339
1340
        if ($queries !== []) {
1341 2
            $from .= '?' . http_build_query($queries);
1342
        }
1343
1344 8
        return $from;
1345
    }
1346
1347
    /**
1348
     * Charger les options d'itinéraires en fonction du verbe
1349
     */
1350
    protected function loadRoutesOptions(?string $verb = null): array
1351
    {
1352 26
        $verb ??= $this->getHTTPVerb();
1353
1354 26
        $options = $this->routesOptions[$verb] ?? [];
1355
1356
        if (isset($this->routesOptions['*'])) {
1357
            foreach ($this->routesOptions['*'] as $key => $val) {
1358
                if (isset($options[$key])) {
1359 4
                    $extraOptions  = array_diff_key($val, $options[$key]);
1360 4
                    $options[$key] = array_merge($options[$key], $extraOptions);
1361
                } else {
1362 14
                    $options[$key] = $val;
1363
                }
1364
            }
1365
        }
1366
1367 26
        return $options;
1368
    }
1369
1370
    /**
1371
     * Fait le gros du travail de création d'un itinéraire réel. Vous devez spécifier
1372
     * la ou les méthodes de demande pour lesquelles cette route fonctionnera. Ils peuvent être séparés
1373
     * par un caractère pipe "|" s'il y en a plusieurs.
1374
     *
1375
     * @param array|Closure|string $to
1376
     */
1377
    protected function create(string $verb, string $from, $to, ?array $options = null)
1378
    {
1379 64
        $overwrite = false;
1380 64
        $prefix    = $this->group === null ? '' : $this->group . '/';
1381
1382 64
        $from = esc(strip_tags(rtrim($prefix, '/') . '/' . ltrim($from, '/')));
1383
1384
        // Alors que nous voulons ajouter une route dans un groupe de '/',
1385
        // ça ne marche pas avec la correspondance, alors supprimez-les...
1386
        if ($from !== '/') {
1387 64
            $from = trim($from, '/');
0 ignored issues
show
Bug introduced by
It seems like $from can also be of type array; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1387
            $from = trim(/** @scrutinizer ignore-type */ $from, '/');
Loading history...
1388
        }
1389
1390
        if (is_string($to) && ! str_contains($to, '::') && class_exists($to) && method_exists($to, '__invoke')) {
1391 64
            $to = [$to, '__invoke'];
1392
        }
1393
1394
        // Lors de la redirection vers une route nommée, $to est un tableau tel que `['zombies' => '\Zombies::index']`.
1395
        if (is_array($to) && isset($to[0])) {
1396 2
            $to = $this->processArrayCallableSyntax($from, $to);
1397
        }
1398
1399 64
        $options = array_merge($this->currentOptions ?? [], $options ?? []);
1400
1401
        if (isset($options['middleware'])) {
1402 6
            $options['middleware'] = (array) $options['middleware'];
1403
1404
            if (! isset($options['middlewares'])) {
1405 2
                $options['middlewares'] = $options['middleware'];
1406
            } else {
1407 6
                $options['middlewares'] = array_merge($options['middlewares'], $options['middleware']);
1408
            }
1409
1410 6
            unset($options['middleware']);
1411
        }
1412
1413
        if (isset($options['middlewares'])) {
1414 8
            $options['middlewares'] = array_unique($options['middlewares']);
1415
        }
1416
1417
        if (is_string($to) && isset($options['controller'])) {
1418 2
            $to = str_replace($options['controller'] . '::', '', $to);
1419 2
            $to = str_replace($this->defaultNamespace, '', $options['controller']) . '::' . $to;
1420
        }
1421
1422
        // Détection de priorité de routage
1423
        if (isset($options['priority'])) {
1424 4
            $options['priority'] = abs((int) $options['priority']);
1425
1426
            if ($options['priority'] > 0) {
1427 4
                $this->prioritizeDetected = true;
1428
            }
1429
        }
1430
1431
        // Limitation du nom d'hôte ?
1432
        if (! empty($options['hostname'])) {
1433
            // @todo déterminer s'il existe un moyen de mettre les hôtes sur liste blanche ?
1434
            if (! $this->checkHostname($options['hostname'])) {
1435 6
                return;
1436
            }
1437
1438 8
            $overwrite = true;
1439
        }
1440
1441
        // Limitation du nom sous-domaine ?
1442
        elseif (! empty($options['subdomain'])) {
1443
            // Si nous ne correspondons pas au sous-domaine actuel, alors
1444
            // nous n'avons pas besoin d'ajouter la route.
1445
            if (! $this->checkSubdomains($options['subdomain'])) {
1446 6
                return;
1447
            }
1448
1449 8
            $overwrite = true;
1450
        }
1451
1452
        // Sommes-nous en train de compenser les liaisons ?
1453
        // Si oui, occupez-vous d'eux ici en un
1454
        // abattre en plein vol.
1455
        if (isset($options['offset']) && is_string($to)) {
1456
            // Récupère une chaîne constante avec laquelle travailler.
1457 2
            $to = preg_replace('/(\$\d+)/', '$X', $to);
1458
1459 2
            for ($i = (int) $options['offset'] + 1; $i < (int) $options['offset'] + 7; $i++) {
1460
                $to = preg_replace_callback(
1461
                    '/\$X/',
1462
                    static fn ($m) => '$' . $i,
1463
                    $to,
1464
                    1
1465 2
                );
1466
            }
1467
        }
1468
1469 64
        $routeKey = $from;
1470
1471
        // Remplacez nos espaces réservés de regex par la chose réelle
1472
        // pour que le routeur n'ait pas besoin de savoir quoi que ce soit.
1473
        foreach (($this->placeholders + ($options['where'] ?? [])) as $tag => $pattern) {
1474 64
            $routeKey = str_ireplace(':' . $tag, $pattern, $routeKey);
1475
        }
1476
1477
        // S'il s'agit d'une redirection, aucun traitement
1478
        if (! isset($options['redirect']) && is_string($to)) {
1479
            // Si aucun espace de noms n'est trouvé, ajouter l'espace de noms par défaut
1480
            if (! str_contains($to, '\\') || strpos($to, '\\') > 0) {
1481 60
                $namespace = $options['namespace'] ?? $this->defaultNamespace;
1482 60
                $to        = trim($namespace, '\\') . '\\' . $to;
1483
            }
1484
            // Assurez-vous toujours que nous échappons à notre espace de noms afin de ne pas pointer vers
1485
            // \BlitzPHP\Routes\Controller::method.
1486 64
            $to = '\\' . ltrim($to, '\\');
1487
        }
1488
1489 64
        $name = $this->formatRouteName($options['as'] ?? $options['name'] ?? $routeKey);
0 ignored issues
show
Bug introduced by
It seems like $options['as'] ?? $options['name'] ?? $routeKey can also be of type array; however, parameter $name of BlitzPHP\Router\RouteCollection::formatRouteName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1489
        $name = $this->formatRouteName(/** @scrutinizer ignore-type */ $options['as'] ?? $options['name'] ?? $routeKey);
Loading history...
1490
1491
        // Ne remplacez aucun 'from' existant afin que les routes découvertes automatiquement
1492
        // n'écrase pas les paramètres app/Config/Routes.
1493
        // les routes manuelement définies doivent toujours être la "source de vérité".
1494
        // cela ne fonctionne que parce que les routes découvertes sont ajoutées juste avant
1495
        // pour tenter de router la requête.
1496 64
        $routeKeyExists = isset($this->routes[$verb][$routeKey]);
1497
        if ((isset($this->routesNames[$verb][$name]) || $routeKeyExists) && ! $overwrite) {
1498 14
            return;
1499
        }
1500
1501
        $this->routes[$verb][$routeKey] = [
1502
            'name'    => $name,
1503
            'handler' => $to,
1504
            'from'    => $from,
1505 64
        ];
1506 64
        $this->routesOptions[$verb][$routeKey] = $options;
1507 64
        $this->routesNames[$verb][$name]       = $routeKey;
1508
1509
        // C'est une redirection ?
1510
        if (isset($options['redirect']) && is_numeric($options['redirect'])) {
1511 4
            $this->routes['*'][$routeKey]['redirect'] = $options['redirect'];
1512
        }
1513
    }
1514
1515
    /**
1516
     * Compare le nom d'hôte transmis avec le nom d'hôte actuel sur cette demande de page.
1517
     *
1518
     * @param string $hostname Nom d'hôte dans les options d'itinéraire
1519
     */
1520
    private function checkHostname(string $hostname): bool
1521
    {
1522
        // Les appels CLI ne peuvent pas être sur le nom d'hôte.
1523
        if (! isset($this->httpHost)) {
1524
            return false;
1525
        }
1526
1527 8
        return strtolower($this->httpHost) === strtolower($hostname);
0 ignored issues
show
Bug introduced by
It seems like $this->httpHost can also be of type null; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1527
        return strtolower(/** @scrutinizer ignore-type */ $this->httpHost) === strtolower($hostname);
Loading history...
1528
    }
1529
1530
    /**
1531
     * Compare le ou les sous-domaines transmis avec le sous-domaine actuel
1532
     * sur cette page demande.
1533
     *
1534
     * @param mixed $subdomains
1535
     */
1536
    private function checkSubdomains($subdomains): bool
1537
    {
1538
        // Les appels CLI ne peuvent pas être sur le sous-domaine.
1539
        if (! isset($this->httpHost)) {
1540
            return false;
1541
        }
1542
1543
        if ($this->currentSubdomain === null) {
1544 8
            $this->currentSubdomain = $this->determineCurrentSubdomain();
1545
        }
1546
1547
        if (! is_array($subdomains)) {
1548 8
            $subdomains = [$subdomains];
1549
        }
1550
1551
        // Les routes peuvent être limitées à n'importe quel sous-domaine. Dans ce cas, cependant,
1552
        // il nécessite la présence d'un sous-domaine.
1553
        if (! empty($this->currentSubdomain) && in_array('*', $subdomains, true)) {
1554 4
            return true;
1555
        }
1556
1557 8
        return in_array($this->currentSubdomain, $subdomains, true);
1558
    }
1559
1560
    /**
1561
     * Examine le HTTP_HOST pour obtenir une meilleure correspondance pour le sous-domaine. Ce
1562
     * ne sera pas parfait, mais devrait répondre à nos besoins.
1563
     *
1564
     * Ce n'est surtout pas parfait puisqu'il est possible d'enregistrer un domaine
1565
     * avec un point (.) dans le cadre du nom de domaine.
1566
     *
1567
     * @return mixed
1568
     */
1569
    private function determineCurrentSubdomain()
1570
    {
1571
        // Nous devons nous assurer qu'un schéma existe
1572
        // sur l'URL sinon parse_url sera mal interprété
1573
        // 'hôte' comme 'chemin'.
1574 8
        $url = $this->httpHost;
1575
        if (! str_starts_with($url, 'http')) {
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type null; however, parameter $haystack of str_starts_with() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1575
        if (! str_starts_with(/** @scrutinizer ignore-type */ $url, 'http')) {
Loading history...
1576 8
            $url = 'http://' . $url;
1577
        }
1578
1579 8
        $parsedUrl = parse_url($url);
1580
1581 8
        $host = explode('.', $parsedUrl['host']);
1582
1583
        if ($host[0] === 'www') {
1584 2
            unset($host[0]);
1585
        }
1586
1587
        // Débarrassez-vous de tous les domaines, qui seront les derniers
1588 8
        unset($host[count($host) - 1]);
1589
1590
        // Compte pour les domaines .co.uk, .co.nz, etc.
1591
        if (end($host) === 'co') {
1592 8
            $host = array_slice($host, 0, -1);
1593
        }
1594
1595
        // S'il ne nous reste qu'une partie, alors nous n'avons pas de sous-domaine.
1596
        if (count($host) === 1) {
1597
            // Définissez-le sur false pour ne pas revenir ici.
1598 4
            return false;
1599
        }
1600
1601 8
        return array_shift($host);
1602
    }
1603
1604
    /**
1605
     * Formate le nom des routes
1606
     */
1607
    private function formatRouteName(string $name): string
1608
    {
1609 64
        $name = trim($name, '/');
1610
1611 64
        return str_replace(['/', '\\', '_', '.', ' '], '.', $name);
1612
    }
1613
1614
    /**
1615
     * @param (Closure(mixed...): (ResponseInterface|string|void))|string $handler
0 ignored issues
show
Documentation Bug introduced by
The doc comment (Closure(mixed...): (Res...ce|string|void))|string at position 1 could not be parsed: Expected ')' at position 1, but found 'Closure'.
Loading history...
1616
     */
1617
    private function getControllerName(Closure|string $handler): ?string
1618
    {
1619
        if (! is_string($handler)) {
1620 2
            return null;
1621
        }
1622
1623 2
        [$controller] = explode('::', $handler, 2);
1624
1625 2
        return $controller;
1626
    }
1627
1628
    /**
1629
     * Renvoie la chaîne de paramètres de méthode comme `/$1/$2` pour les espaces réservés
1630
     */
1631
    private function getMethodParams(string $from): string
1632
    {
1633 2
        preg_match_all('/\(.+?\)/', $from, $matches);
1634 2
        $count = is_countable($matches[0]) ? count($matches[0]) : 0;
1635
1636 2
        $params = '';
1637
1638 2
        for ($i = 1; $i <= $count; $i++) {
1639 2
            $params .= '/$' . $i;
1640
        }
1641
1642 2
        return $params;
1643
    }
1644
1645
    private function processArrayCallableSyntax(string $from, array $to): string
1646
    {
1647
        // [classname, method]
1648
        // eg, [Home::class, 'index']
1649
        if (is_callable($to, true, $callableName)) {
1650
            // Si la route a des espaces réservés, ajoutez des paramètres automatiquement.
1651 2
            $params = $this->getMethodParams($from);
1652
1653
            if (str_contains($callableName, '\\') && $callableName[0] !== '\\') {
1654 2
                $callableName = '\\' . $callableName;
1655
            }
1656
1657 2
            return $callableName . $params;
1658
        }
1659
1660
        // [[classname, method], params]
1661
        // eg, [[Home::class, 'index'], '$1/$2']
1662
        if (
1663
            isset($to[0], $to[1])
1664
            && is_callable($to[0], true, $callableName)
1665
            && is_string($to[1])
1666
        ) {
1667 2
            $to = '\\' . $callableName . '/' . $to[1];
1668
        }
1669
1670 2
        return $to;
1671
    }
1672
1673
    /**
1674
     * Remplace la balise {locale} par la locale.
1675
     */
1676
    private function replaceLocale(string $route, ?string $locale = null): string
1677
    {
1678
        if (! str_contains($route, '{locale}')) {
1679 10
            return $route;
1680
        }
1681
1682
        // Vérifier les paramètres régionaux non valides
1683
        if ($locale !== null && ! in_array($locale, config('app.supported_locales'), true)) {
0 ignored issues
show
Bug introduced by
config('app.supported_locales') of type BlitzPHP\Config\Config|null is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

1683
        if ($locale !== null && ! in_array($locale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
1684
            $locale = null;
1685
        }
1686
1687
        if ($locale === null) {
1688 6
            $locale = Services::request()->getLocale();
1689
        }
1690
1691 6
        return strtr($route, ['{locale}' => $locale]);
1692
    }
1693
}
1694