RouteCollection::presenter()   F
last analyzed

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\Contracts\Autoloader\LocatorInterface;
15
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
16
use BlitzPHP\Enums\Method;
17
use BlitzPHP\Exceptions\RouterException;
18
use BlitzPHP\Utilities\String\Text;
19
use Closure;
20
use InvalidArgumentException;
21
use Psr\Http\Message\ResponseInterface;
22
23
class RouteCollection implements RouteCollectionInterface
24
{
25
    /**
26
     * L'espace de noms à ajouter à tous les contrôleurs.
27
     * Par défaut, les espaces de noms globaux (\)
28
     */
29
    protected string $defaultNamespace = '\\';
30
31
    /**
32
     * Le nom du contrôleur par défaut à utiliser
33
     * lorsqu'aucun autre contrôleur n'est spécifié.
34
     *
35
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
36
     */
37
    protected string $defaultController = 'Home';
38
39
    /**
40
     * Le nom de la méthode par défaut à utiliser
41
     * lorsqu'aucune autre méthode n'a été spécifiée.
42
     *
43
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
44
     */
45
    protected string $defaultMethod = 'index';
46
47
    /**
48
     * L'espace réservé utilisé lors du routage des "ressources"
49
     * lorsqu'aucun autre espace réservé n'a été spécifié.
50
     */
51
    protected string $defaultPlaceholder = 'any';
52
53
    /**
54
     * S'il faut convertir les tirets en traits de soulignement dans l'URI.
55
     *
56
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
57
     */
58
    protected bool $translateURIDashes = true;
59
60
    /**
61
     * S'il faut faire correspondre l'URI aux contrôleurs
62
     * lorsqu'il ne correspond pas aux itinéraires définis.
63
     *
64
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
65
     */
66
    protected bool $autoRoute = false;
67
68
    /**
69
     * Un appelable qui sera affiché
70
     * lorsque la route ne peut pas être matchée.
71
     *
72
     * @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...
73
     */
74
    protected $override404;
75
76
    /**
77
     * Tableau de fichiers qui contiendrait les définitions de routes.
78
     */
79
    protected array $routeFiles = [];
80
81
    /**
82
     * Espaces réservés définis pouvant être utilisés.
83
     */
84
    protected array $placeholders = [
85
        'any'      => '.*',
86
        'segment'  => '[^/]+',
87
        'alphanum' => '[a-zA-Z0-9]+',
88
        'num'      => '[0-9]+',
89
        'alpha'    => '[a-zA-Z]+',
90
        'hash'     => '[^/]+',
91
        'slug'     => '[a-z0-9-]+',
92
        'uuid'     => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
93
    ];
94
95
    /**
96
     * Tableau de toutes les routes et leurs mappages.
97
     *
98
     * @example
99
     * ```php
100
     * [
101
     *     verb => [
102
     *         routeName => [
103
     *             'route' => [
104
     *                 routeKey(regex) => handler,
105
     *             ],
106
     *             'redirect' => statusCode,
107
     *         ]
108
     *     ],
109
     * ]
110
     * ```
111
     */
112
    protected array $routes = [
113
        '*'             => [],
114
        Method::OPTIONS => [],
115
        Method::GET     => [],
116
        Method::HEAD    => [],
117
        Method::POST    => [],
118
        Method::PATCH   => [],
119
        Method::PUT     => [],
120
        Method::DELETE  => [],
121
        Method::TRACE   => [],
122
        Method::CONNECT => [],
123
        'CLI'           => [],
124
    ];
125
126
    /**
127
     * Tableau des noms des routes
128
     *
129
     * [
130
     *     verb => [
131
     *         routeName => routeKey(regex)
132
     *     ],
133
     * ]
134
     */
135
    protected array $routesNames = [
136
        '*'             => [],
137
        Method::OPTIONS => [],
138
        Method::GET     => [],
139
        Method::HEAD    => [],
140
        Method::POST    => [],
141
        Method::PATCH   => [],
142
        Method::PUT     => [],
143
        Method::DELETE  => [],
144
        Method::TRACE   => [],
145
        Method::CONNECT => [],
146
        'CLI'           => [],
147
    ];
148
149
    /**
150
     * Tableaux des options des routes.
151
     *
152
     * @example
153
     * ```php
154
     * [
155
     *     verb => [
156
     *         routeKey(regex) => [
157
     *             key => value,
158
     *         ]
159
     *     ],
160
     * ]
161
     * ```
162
     */
163
    protected array $routesOptions = [];
164
165
    /**
166
     * La méthode actuelle par laquelle le script est appelé.
167
     */
168
    protected string $HTTPVerb = '*';
169
170
    /**
171
     * La liste par défaut des méthodes HTTP (et CLI pour l'utilisation de la ligne de commande)
172
     * qui est autorisé si aucune autre méthode n'est fournie.
173
     */
174
    protected array $defaultHTTPMethods = Router::HTTP_METHODS;
175
176
    /**
177
     * Le nom du groupe de route courant
178
     */
179
    protected ?string $group = null;
180
181
    /**
182
     * Le sous domaine courant
183
     */
184
    protected ?string $currentSubdomain = null;
185
186
    /**
187
     * Stocke une copie des options actuelles en cours appliqué lors de la création.
188
     */
189
    protected ?array $currentOptions = null;
190
191
    /**
192
     * Un petit booster de performances.
193
     */
194
    protected bool $didDiscover = false;
195
196
    /**
197
     * Drapeau pour trier les routes par priorité.
198
     */
199
    protected bool $prioritize = false;
200
201
    /**
202
     * Indicateur de détection de priorité de route.
203
     */
204
    protected bool $prioritizeDetected = false;
205
206
    /**
207
     * Drapeau pour limiter ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
208
     */
209
    protected bool $useSupportedLocalesOnly = false;
210
211
    /**
212
     * Le nom d'hôte actuel de $_SERVER['HTTP_HOST']
213
     */
214
    private ?string $httpHost = null;
215
216
    /**
217
     * Constructor
218
     *
219
     * @param LocatorInterface $locator Descripteur du localisateur de fichiers à utiliser.
220
     */
221
    public function __construct(protected LocatorInterface $locator, object $routing)
222
    {
223 73
        $this->httpHost = env('HTTP_HOST');
224
225
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
226 73
        $this->defaultNamespace        = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
227 73
        $this->defaultController       = $routing->default_controller ?: $this->defaultController;
228 73
        $this->defaultMethod           = $routing->default_method ?: $this->defaultMethod;
229 73
        $this->translateURIDashes      = $routing->translate_uri_dashes ?: $this->translateURIDashes;
230 73
        $this->override404             = $routing->fallback ?: $this->override404;
231 73
        $this->autoRoute               = $routing->auto_route ?: $this->autoRoute;
232 73
        $this->routeFiles              = $routing->route_files ?: $this->routeFiles;
233 73
        $this->prioritize              = $routing->prioritize ?: $this->prioritize;
234 73
        $this->useSupportedLocalesOnly = $routing->use_supported_locales_only ?: $this->useSupportedLocalesOnly;
235
236
        // Normaliser la chaîne de path dans le tableau routeFiles.
237
        foreach ($this->routeFiles as $routeKey => $routesFile) {
238 73
            $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 2
            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 22
        $this->defaultNamespace = esc(strip_tags($value));
331 22
        $this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';
332
333 22
        return $this;
334
    }
335
336
    /**
337
     * {@inheritDoc}
338
     */
339
    public function setDefaultController(string $value): self
340
    {
341 10
        $this->defaultController = esc(strip_tags($value));
342
343 10
        return $this;
344
    }
345
346
    /**
347
     * {@inheritDoc}
348
     */
349
    public function setDefaultMethod(string $value): self
350
    {
351 6
        $this->defaultMethod = esc(strip_tags($value));
352
353 6
        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 16
            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 54
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
418
419 54
        $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 54
                continue;
425
            }
426
427
            include_once $file;
428
        }
429
430 54
        $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 26
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
452
    }
453
454
    /**
455
     * {@inheritDoc}
456
     */
457
    public function getDefaultMethod(): string
458
    {
459 26
        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 20
        return $this->translateURIDashes;
488
    }
489
490
    /**
491
     * {@inheritDoc}
492
     */
493
    public function shouldAutoRoute(): bool
494
    {
495 20
        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 56
        $this->discoverRoutes();
571
572 56
        $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 56
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
577
578
            foreach ($collection as $routeKey => $r) {
579 56
                $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 56
        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 30
        $options = $this->loadRoutesOptions($verb);
606
607 30
        return $from ? $options[$from] ?? [] : $options;
608
    }
609
610
    /**
611
     * {@inheritDoc}
612
     */
613
    public function getHTTPVerb(): string
614
    {
615 54
        return $this->HTTPVerb;
616
    }
617
618
    /**
619
     * {@inheritDoc}
620
     */
621
    public function setHTTPVerb(string $verb): self
622
    {
623 70
        $this->HTTPVerb = strtoupper($verb);
624
625 70
        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 24
        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 !== '' && $name !== '0' ? 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 42
        $this->create(Method::GET, $from, $to, $options);
1051
1052 42
        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) => service('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 22
        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 22
        $options = $this->loadRoutesOptions($verb);
1277
1278 22
        $middlewares = $options[$search]['middlewares'] ?? ($options[$search]['middleware'] ?? []);
1279
1280 22
        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 30
        $verb ??= $this->getHTTPVerb();
1353
1354 30
        $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 30
        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 66
        $overwrite = false;
1380 66
        $prefix    = $this->group === null ? '' : $this->group . '/';
1381
1382 66
        $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 66
            $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 66
        $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 8
                return;
1436
            }
1437
1438 10
            $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 66
        $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 66
            $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 62
                $namespace = $options['namespace'] ?? $this->defaultNamespace;
1482 62
                $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 66
            $to = '\\' . ltrim($to, '\\');
1487
        }
1488
1489 66
        $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 66
        $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 66
        ];
1506 66
        $this->routesOptions[$verb][$routeKey] = $options;
1507 66
        $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 list<string>|string $hostname Nom d'hôte dans les options d'itinéraire
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Router\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1519
     */
1520
    private function checkHostname(array|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
        // hostname multiples
1528
        if (is_array($hostname)) {
0 ignored issues
show
introduced by
The condition is_array($hostname) is always true.
Loading history...
1529 2
            $hostnameLower = array_map('strtolower', $hostname);
1530
1531 2
            return in_array(strtolower($this->httpHost), $hostnameLower, true);
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

1531
            return in_array(strtolower(/** @scrutinizer ignore-type */ $this->httpHost), $hostnameLower, true);
Loading history...
1532
        }
1533
1534 10
        return strtolower($this->httpHost) === strtolower($hostname);
1535
    }
1536
1537
    /**
1538
     * Compare le ou les sous-domaines transmis avec le sous-domaine actuel
1539
     * sur cette page demande.
1540
     *
1541
     * @param mixed $subdomains
1542
     */
1543
    private function checkSubdomains($subdomains): bool
1544
    {
1545
        // Les appels CLI ne peuvent pas être sur le sous-domaine.
1546
        if (! isset($this->httpHost)) {
1547
            return false;
1548
        }
1549
1550
        if ($this->currentSubdomain === null) {
1551 8
            $this->currentSubdomain = $this->determineCurrentSubdomain();
1552
        }
1553
1554
        if (! is_array($subdomains)) {
1555 8
            $subdomains = [$subdomains];
1556
        }
1557
1558
        // Les routes peuvent être limitées à n'importe quel sous-domaine. Dans ce cas, cependant,
1559
        // il nécessite la présence d'un sous-domaine.
1560
        if (! empty($this->currentSubdomain) && in_array('*', $subdomains, true)) {
1561 4
            return true;
1562
        }
1563
1564 8
        return in_array($this->currentSubdomain, $subdomains, true);
1565
    }
1566
1567
    /**
1568
     * Examine le HTTP_HOST pour obtenir une meilleure correspondance pour le sous-domaine. Ce
1569
     * ne sera pas parfait, mais devrait répondre à nos besoins.
1570
     *
1571
     * Ce n'est surtout pas parfait puisqu'il est possible d'enregistrer un domaine
1572
     * avec un point (.) dans le cadre du nom de domaine.
1573
     *
1574
     * @return mixed
1575
     */
1576
    private function determineCurrentSubdomain()
1577
    {
1578
        // Nous devons nous assurer qu'un schéma existe
1579
        // sur l'URL sinon parse_url sera mal interprété
1580
        // 'hôte' comme 'chemin'.
1581 8
        $url = $this->httpHost;
1582
        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

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

1690
        if ($locale !== null && ! in_array($locale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
1691
            $locale = null;
1692
        }
1693
1694
        if ($locale === null) {
1695 6
            $locale = service('request')->getLocale();
1696
        }
1697
1698 6
        return strtr($route, ['{locale}' => $locale]);
1699
    }
1700
}
1701