Passed
Push — main ( 05025c...7b6df8 )
by Dimitri
03:11
created

RouteCollection::reverseRoute()   C

Complexity

Conditions 13
Paths 76

Size

Total Lines 71
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 13

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 13
eloc 32
c 4
b 2
f 0
nc 76
nop 2
dl 0
loc 71
ccs 19
cts 19
cp 1
crap 13
rs 6.6166

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 69
        $this->httpHost = env('HTTP_HOST');
225
226
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
227 69
        $this->defaultNamespace   = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
228 69
        $this->defaultController  = $routing->default_controller ?: $this->defaultController;
229 69
        $this->defaultMethod      = $routing->default_method ?: $this->defaultMethod;
230 69
        $this->translateURIDashes = $routing->translate_uri_dashes ?: $this->translateURIDashes;
231 69
        $this->override404        = $routing->fallback ?: $this->override404;
232 69
        $this->autoRoute          = $routing->auto_route ?: $this->autoRoute;
233 69
        $this->routeFiles         = $routing->route_files ?: $this->routeFiles;
234 69
        $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 69
            $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
        if (false !== $realpath = realpath($routesFile)) {
255
            $routesFile = $realpath;
256
        }
257
258
        // Incluez le fichier routesFile s'il n'existe pas.
259
        // Ne conserver que pour les fins BC pour l'instant.
260
        $routeFiles = $this->routeFiles;
261
        if (! in_array($routesFile, $routeFiles, true)) {
262
            $routeFiles[] = $routesFile;
263
        }
264
265
        // Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
266
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
267
268
        foreach ($routeFiles as $routesFile) {
269
            if (! is_file($routesFile)) {
270
                logger()->warning(sprintf('Fichier de route introuvable : "%s"', $routesFile));
271
272
                continue;
273
            }
274
275
            require_once $routesFile;
276
        }
277
278
        $this->discoverRoutes();
279
280
        return $this;
281
    }
282
283
    /**
284
     * Réinitialisez les routes, afin qu'un cas de test puisse fournir le
285
     * ceux explicites nécessaires pour cela.
286
     */
287
    public function resetRoutes()
288
    {
289
        $this->routes = $this->routesNames = ['*' => []];
290
291
        foreach ($this->defaultHTTPMethods as $verb) {
292
            $this->routes[$verb]      = [];
293
            $this->routesNames[$verb] = [];
294
        }
295
296
        $this->routesOptions = [];
297
298
        $this->prioritizeDetected = false;
299
        $this->didDiscover        = false;
300
    }
301
302
    /**
303
     * {@inheritDoc}
304
     *
305
     * Utilisez `placeholder` a la place
306
     */
307
    public function addPlaceholder($placeholder, ?string $pattern = null): self
308
    {
309 4
        return $this->placeholder($placeholder, $pattern);
310
    }
311
312
    /**
313
     * Enregistre une nouvelle contrainte auprès du système.
314
     * 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.
315
     */
316
    public function placeholder(array|string $placeholder, ?string $pattern = null): self
317
    {
318
        if (! is_array($placeholder)) {
0 ignored issues
show
introduced by
The condition is_array($placeholder) is always true.
Loading history...
319 6
            $placeholder = [$placeholder => $pattern];
320
        }
321
322 6
        $this->placeholders = array_merge($this->placeholders, $placeholder);
323
324 6
        return $this;
325
    }
326
327
    /**
328
     * {@inheritDoc}
329
     */
330
    public function setDefaultNamespace(string $value): self
331
    {
332 20
        $this->defaultNamespace = esc(strip_tags($value));
333 20
        $this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';
334
335 20
        return $this;
336
    }
337
338
    /**
339
     * {@inheritDoc}
340
     */
341
    public function setDefaultController(string $value): self
342
    {
343 6
        $this->defaultController = esc(strip_tags($value));
344
345 6
        return $this;
346
    }
347
348
    /**
349
     * {@inheritDoc}
350
     */
351
    public function setDefaultMethod(string $value): self
352
    {
353 4
        $this->defaultMethod = esc(strip_tags($value));
354
355 4
        return $this;
356
    }
357
358
    /**
359
     * {@inheritDoc}
360
     */
361
    public function setTranslateURIDashes(bool $value): self
362
    {
363 4
        $this->translateURIDashes = $value;
364
365 4
        return $this;
366
    }
367
368
    /**
369
     * {@inheritDoc}
370
     */
371
    public function setAutoRoute(bool $value): self
372
    {
373 4
        $this->autoRoute = $value;
374
375 4
        return $this;
376
    }
377
378
    /**
379
     * {@inheritDoc}
380
     *
381
     * Utilisez self::fallback()
382
     */
383
    public function set404Override($callable = null): self
384
    {
385
        return $this->fallback($callable);
386
    }
387
388
    /**
389
     * Définit la classe/méthode qui doit être appelée si le routage ne trouver pas une correspondance.
390
     *
391
     * @param callable|string|null $callable
392
     */
393
    public function fallback($callable = null): self
394
    {
395 4
        $this->override404 = $callable;
396
397 4
        return $this;
398
    }
399
400
    /**
401
     * {@inheritDoc}
402
     */
403
    public function get404Override()
404
    {
405 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...
406
    }
407
408
    /**
409
     * Tentera de découvrir d'éventuelles routes supplémentaires, soit par
410
     * les espaces de noms PSR4 locaux ou via des packages Composer sélectionnés.
411
     */
412
    protected function discoverRoutes()
413
    {
414
        if ($this->didDiscover) {
415 14
            return;
416
        }
417
418
        // Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
419 52
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
420
421 52
        $files = $this->locator->search('Config/routes.php');
422
423
        foreach ($files as $file) {
424
            // N'incluez plus notre fichier principal...
425
            if (in_array($file, $this->routeFiles, true)) {
426 52
                continue;
427
            }
428
429
            include_once $file;
430
        }
431
432 52
        $this->didDiscover = true;
433
    }
434
435
    /**
436
     * Définit la contrainte par défaut à utiliser dans le système. Typiquement
437
     * à utiliser avec la méthode 'ressource'.
438
     */
439
    public function setDefaultConstraint(string $placeholder): self
440
    {
441
        if (array_key_exists($placeholder, $this->placeholders)) {
442 2
            $this->defaultPlaceholder = $placeholder;
443
        }
444
445 2
        return $this;
446
    }
447
448
    /**
449
     * {@inheritDoc}
450
     */
451
    public function getDefaultController(): string
452
    {
453 20
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
454
    }
455
456
    /**
457
     * {@inheritDoc}
458
     */
459
    public function getDefaultMethod(): string
460
    {
461 20
        return $this->defaultMethod;
462
    }
463
464
    /**
465
     * {@inheritDoc}
466
     */
467
    public function getDefaultNamespace(): string
468
    {
469 4
        return $this->defaultNamespace;
470
    }
471
472
    /**
473
     * Pour `klinge route:list`
474
     *
475
     * @return array<string, string>
476
     *
477
     * @internal
478
     */
479
    public function getPlaceholders(): array
480
    {
481
        return $this->placeholders;
482
    }
483
484
    /**
485
     *{@inheritDoc}
486
     */
487
    public function shouldTranslateURIDashes(): bool
488
    {
489 16
        return $this->translateURIDashes;
490
    }
491
492
    /**
493
     * {@inheritDoc}
494
     */
495
    public function shouldAutoRoute(): bool
496
    {
497 16
        return $this->autoRoute;
498
    }
499
500
    /**
501
     * Activer ou désactiver le tri des routes par priorité
502
     */
503
    public function setPrioritize(bool $enabled = true): self
504
    {
505 2
        $this->prioritize = $enabled;
506
507 2
        return $this;
508
    }
509
510
    /**
511
     * Définissez le drapeau qui limite ou non les routes avec l'espace réservé {locale} à App::$supportedLocales
512
     */
513
    public function useSupportedLocalesOnly(bool $useOnly): self
514
    {
515 2
        $this->useSupportedLocalesOnly = $useOnly;
516
517 2
        return $this;
518
    }
519
520
    /**
521
     * Obtenez le drapeau qui limite ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
522
     */
523
    public function shouldUseSupportedLocalesOnly(): bool
524
    {
525 4
        return $this->useSupportedLocalesOnly;
526
    }
527
528
    /**
529
     * {@inheritDoc}
530
     *
531
     * @internal
532
     */
533
    public function getRegisteredControllers(?string $verb = '*'): array
534
    {
535 4
        $controllers = [];
536
537
        if ($verb === '*') {
538
            foreach ($this->defaultHTTPMethods as $tmpVerb) {
539
                foreach ($this->routes[$tmpVerb] as $route) {
540 2
                    $controller = $this->getControllerName($route['handler']);
541
                    if ($controller !== null) {
542 2
                        $controllers[] = $controller;
543
                    }
544
                }
545
            }
546
        } else {
547 2
            $routes = $this->getRoutes($verb);
548
549
            foreach ($routes as $handler) {
550 2
                $controller = $this->getControllerName($handler);
551
                if ($controller !== null) {
552 2
                    $controllers[] = $controller;
553
                }
554
            }
555
        }
556
557 4
        return array_unique($controllers);
558
    }
559
560
    /**
561
     * {@inheritDoc}
562
     */
563
    public function getRoutes(?string $verb = null, bool $includeWildcard = true): array
564
    {
565
        if (empty($verb)) {
566 32
            $verb = $this->getHTTPVerb();
567
        }
568
569
        // Puisqu'il s'agit du point d'entrée du routeur,
570
        // prenez un moment pour faire toute découverte de route
571
        // que nous pourrions avoir besoin de faire.
572 52
        $this->discoverRoutes();
573
574 52
        $routes = [];
575
        if (isset($this->routes[$verb])) {
576
            // Conserve les itinéraires du verbe actuel au début afin qu'ils soient
577
            // mis en correspondance avant l'un des itinéraires génériques "add".
578 52
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
579
580
            foreach ($collection as $routeKey => $r) {
581 52
                $routes[$routeKey] = $r['handler'];
582
            }
583
        }
584
585
        // tri des routes par priorité
586
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
587 2
            $order = [];
588
589
            foreach ($routes as $key => $value) {
590 2
                $key                    = $key === '/' ? $key : ltrim($key, '/ ');
591 2
                $priority               = $this->getRoutesOptions($key, $verb)['priority'] ?? 0;
592 2
                $order[$priority][$key] = $value;
593
            }
594
595 2
            ksort($order);
596 2
            $routes = array_merge(...$order);
597
        }
598
599 52
        return $routes;
600
    }
601
602
    /**
603
     * Renvoie une ou toutes les options d'itinéraire
604
     */
605
    public function getRoutesOptions(?string $from = null, ?string $verb = null): array
606
    {
607 26
        $options = $this->loadRoutesOptions($verb);
608
609 26
        return $from ? $options[$from] ?? [] : $options;
610
    }
611
612
    /**
613
     * {@inheritDoc}
614
     */
615
    public function getHTTPVerb(): string
616
    {
617 50
        return $this->HTTPVerb;
618
    }
619
620
    /**
621
     * {@inheritDoc}
622
     */
623
    public function setHTTPVerb(string $verb): self
624
    {
625 66
        $this->HTTPVerb = strtoupper($verb);
626
627 66
        return $this;
628
    }
629
630
    /**
631
     * Une méthode de raccourci pour ajouter un certain nombre d'itinéraires en une seule fois.
632
     * Il ne permet pas de définir des options sur l'itinéraire, ou de
633
     * définir la méthode utilisée.
634
     */
635
    public function map(array $routes = [], ?array $options = null): self
636
    {
637
        foreach ($routes as $from => $to) {
638 12
            $this->add($from, $to, $options);
639
        }
640
641 12
        return $this;
642
    }
643
644
    /**
645
     * {@inheritDoc}
646
     */
647
    public function add(string $from, $to, ?array $options = null): self
648
    {
649 48
        $this->create('*', $from, $to, $options);
650
651 48
        return $this;
652
    }
653
654
    /**
655
     * Ajoute une redirection temporaire d'une route à une autre.
656
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
657
     *
658
     * @param string $from   Le modèle à comparer
659
     * @param string $to     Soit un nom de route ou un URI vers lequel rediriger
660
     * @param int    $status Le code d'état HTTP qui doit être renvoyé avec cette redirection
661
     */
662
    public function redirect(string $from, string $to, int $status = 302): self
663
    {
664
        // Utilisez le modèle de la route nommée s'il s'agit d'une route nommée.
665
        if (array_key_exists($to, $this->routesNames['*'])) {
666 4
            $routeName  = $to;
667 4
            $routeKey   = $this->routesNames['*'][$routeName];
668 4
            $redirectTo = [$routeKey => $this->routes['*'][$routeKey]['handler']];
669
        } elseif (array_key_exists($to, $this->routesNames[Method::GET])) {
670 2
            $routeName  = $to;
671 2
            $routeKey   = $this->routesNames[Method::GET][$routeName];
672 2
            $redirectTo = [$routeKey => $this->routes[Method::GET][$routeKey]['handler']];
673
        } else {
674
            // La route nommee n'a pas ete trouvée
675 4
            $redirectTo = $to;
676
        }
677
678 4
        $this->create('*', $from, $redirectTo, ['redirect' => $status]);
679
680 4
        return $this;
681
    }
682
683
    /**
684
     * Ajoute une redirection permanente d'une route à une autre.
685
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
686
     */
687
    public function permanentRedirect(string $from, string $to): self
688
    {
689 2
        return $this->redirect($from, $to, 301);
690
    }
691
692
    /**
693
     * @deprecated 0.9 Please use redirect() instead
694
     */
695
    public function addRedirect(string $from, string $to, int $status = 302): self
696
    {
697
        return $this->redirect($from, $to, $status);
698
    }
699
700
    /**
701
     * {@inheritDoc}
702
     *
703
     * @param string $routeKey cle de route ou route nommee
704
     */
705
    public function isRedirect(string $routeKey): bool
706
    {
707 20
        return isset($this->routes['*'][$routeKey]['redirect']);
708
    }
709
710
    /**
711
     * {@inheritDoc}
712
     *
713
     * @param string $routeKey cle de route ou route nommee
714
     */
715
    public function getRedirectCode(string $routeKey): int
716
    {
717
        if (isset($this->routes['*'][$routeKey]['redirect'])) {
718 4
            return $this->routes['*'][$routeKey]['redirect'];
719
        }
720
721 4
        return 0;
722
    }
723
724
    /**
725
     * Regroupez une série de routes sous un seul segment d'URL. C'est pratique
726
     * pour regrouper des éléments dans une zone d'administration, comme :
727
     *
728
     * Example:
729
     *     // Creates route: admin/users
730
     *     $route->group('admin', function() {
731
     *            $route->resource('users');
732
     *     });
733
     *
734
     * @param string         $name      Le nom avec lequel grouper/préfixer les routes.
735
     * @param array|callable ...$params
736
     */
737
    public function group(string $name, ...$params)
738
    {
739 10
        $oldGroup   = $this->group ?: '';
740 10
        $oldOptions = $this->currentOptions;
741
742
        // Pour enregistrer une route, nous allons définir un indicateur afin que notre routeur
743
        // donc il verra le nom du groupe.
744
        // Si le nom du groupe est vide, nous continuons à utiliser le nom du groupe précédemment construit.
745 10
        $this->group = $name ? trim($oldGroup . '/' . $name, '/') : $oldGroup;
746
747 10
        $callback = array_pop($params);
748
749
        if ($params && is_array($params[0])) {
750 10
            $options = array_shift($params);
751
752
            if (isset($options['middlewares']) || isset($options['middleware'])) {
753 8
                $currentMiddlewares     = (array) ($this->currentOptions['middlewares'] ?? []);
754 8
                $options['middlewares'] = array_merge($currentMiddlewares, (array) ($options['middlewares'] ?? $options['middleware']));
755
            }
756
757
            // Fusionner les options autres que les middlewares.
758
            $this->currentOptions = array_merge(
759
                $this->currentOptions ?: [],
760
                $options ?: [],
761 10
            );
762
        }
763
764
        if (is_callable($callback)) {
765 10
            $callback($this);
766
        }
767
768 10
        $this->group          = $oldGroup;
769 10
        $this->currentOptions = $oldOptions;
770
    }
771
772
    /*
773
     * ------------------------------------------------- -------------------
774
     * Routage basé sur les verbes HTTP
775
     * ------------------------------------------------- -------------------
776
     * Le routage fonctionne ici car, comme le fichier de configuration des routes est lu,
777
     * les différentes routes basées sur le verbe HTTP ne seront ajoutées qu'à la mémoire en mémoire
778
     * routes s'il s'agit d'un appel qui doit répondre à ce verbe.
779
     *
780
     * Le tableau d'options est généralement utilisé pour transmettre un 'as' ou var, mais peut
781
     * être étendu à l'avenir. Voir le docblock pour la méthode 'add' ci-dessus pour
782
     * liste actuelle des options disponibles dans le monde.*/
783
784
    /**
785
     * Crée une collection d'itinéraires basés sur HTTP-verb pour un contrôleur.
786
     *
787
     * Options possibles :
788
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
789
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
790
     * 'websafe' - - '1' si seuls les verbes HTTP GET et POST sont pris en charge
791
     *
792
     * Exemple:
793
     *
794
     *      $route->resource('photos');
795
     *
796
     *      // Genère les routes suivantes:
797
     *      HTTP Verb | Path        | Action        | Used for...
798
     *      ----------+-------------+---------------+-----------------
799
     *      GET         /photos             index           un tableau d'objets photo
800
     *      GET         /photos/new         new             un objet photo vide, avec des propriétés par défaut
801
     *      GET         /photos/{id}/edit   edit            un objet photo spécifique, propriétés modifiables
802
     *      GET         /photos/{id}        show            un objet photo spécifique, toutes les propriétés
803
     *      POST        /photos             create          un nouvel objet photo, à ajouter à la ressource
804
     *      DELETE      /photos/{id}        delete          supprime l'objet photo spécifié
805
     *      PUT/PATCH   /photos/{id}        update          propriétés de remplacement pour la photo existante
806
     *
807
     *  Si l'option 'websafe' est présente, les chemins suivants sont également disponibles :
808
     *
809
     *      POST		/photos/{id}/delete delete
810
     *      POST        /photos/{id}        update
811
     *
812
     * @param string $name    Le nom de la ressource/du contrôleur vers lequel router.
813
     * @param array  $options Une liste des façons possibles de personnaliser le routage.
814
     */
815
    public function resource(string $name, array $options = []): self
816
    {
817
        // Afin de permettre la personnalisation de la route, le
818
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
819
        // pour stocker les valeurs.
820 6
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
821
822
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
823
        // valeur de $name avec le nom du nouveau contrôleur.
824
        if (isset($options['controller'])) {
825 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

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

945
            $newName = ucfirst(/** @scrutinizer ignore-type */ esc(strip_tags($options['controller'])));
Loading history...
946
            unset($options['controller']);
947
        }
948
949 4
        $newName = Text::convertTo($newName, 'pascalcase');
950
951
        // Afin de permettre la personnalisation des valeurs d'identifiant autorisées
952
        // nous avons besoin d'un endroit pour les stocker.
953 4
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
954
955
        // On s'assure de capturer les références arrière
956 4
        $id = '(' . trim($id, '()') . ')';
957
958 4
        $methods = isset($options['only']) ? (is_string($options['only']) ? explode(',', $options['only']) : $options['only']) : ['index', 'show', 'new', 'create', 'edit', 'update', 'remove', 'delete'];
959
960
        if (isset($options['except'])) {
961 4
            $options['except'] = is_array($options['except']) ? $options['except'] : explode(',', $options['except']);
962
963
            foreach ($methods as $i => $method) {
964
                if (in_array($method, $options['except'], true)) {
965
                    unset($methods[$i]);
966
                }
967
            }
968
        }
969
970 4
        $routeName = $name;
971
        if (isset($options['as']) || isset($options['name'])) {
972 4
            $routeName = trim($options['as'] ?? $options['name'], ' .');
973
            unset($options['name'], $options['as']);
974
        }
975
976
        if (in_array('index', $methods, true)) {
977
            $this->get($name, $newName . '::index', $options + [
978
                'as' => $routeName . '.index',
979 4
            ]);
980
        }
981
        if (in_array('new', $methods, true)) {
982
            $this->get($name . '/new', $newName . '::new', $options + [
983
                'as' => $routeName . '.new',
984 4
            ]);
985
        }
986
        if (in_array('edit', $methods, true)) {
987
            $this->get($name . '/edit/' . $id, $newName . '::edit/$1', $options + [
988
                'as' => $routeName . '.edit',
989 4
            ]);
990
        }
991
        if (in_array('update', $methods, true)) {
992
            $this->post($name . '/update/' . $id, $newName . '::update/$1', $options + [
993
                'as' => $routeName . '.update',
994 4
            ]);
995
        }
996
        if (in_array('remove', $methods, true)) {
997
            $this->get($name . '/remove/' . $id, $newName . '::remove/$1', $options + [
998
                'as' => $routeName . '.remove',
999 4
            ]);
1000
        }
1001
        if (in_array('delete', $methods, true)) {
1002
            $this->post($name . '/delete/' . $id, $newName . '::delete/$1', $options + [
1003
                'as' => $routeName . '.delete',
1004 4
            ]);
1005
        }
1006
        if (in_array('create', $methods, true)) {
1007
            $this->post($name . '/create', $newName . '::create', $options + [
1008
                'as' => $routeName . '.create',
1009 4
            ]);
1010
            $this->post($name, $newName . '::create', $options + [
1011
                'as' => $routeName . '.store',
1012 4
            ]);
1013
        }
1014
        if (in_array('show', $methods, true)) {
1015
            $this->get($name . '/show/' . $id, $newName . '::show/$1', $options + [
1016
                'as' => $routeName . '.view',
1017 4
            ]);
1018
            $this->get($name . '/' . $id, $newName . '::show/$1', $options + [
1019
                'as' => $routeName . '.show',
1020 4
            ]);
1021
        }
1022
1023 4
        return $this;
1024
    }
1025
1026
    /**
1027
     * Spécifie une seule route à faire correspondre pour plusieurs verbes HTTP.
1028
     *
1029
     * Exemple:
1030
     *  $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
1031
     *
1032
     * @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...
1033
     */
1034
    public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): self
1035
    {
1036
        if (empty($from) || empty($to)) {
1037 14
            throw new InvalidArgumentException('Vous devez fournir les paramètres : $from, $to.');
1038
        }
1039
1040
        foreach ($verbs as $verb) {
1041 14
            $verb = strtolower($verb);
1042
1043 14
            $this->{$verb}($from, $to, $options);
1044
        }
1045
1046 14
        return $this;
1047
    }
1048
1049
    /**
1050
     * Spécifie une route qui n'est disponible que pour les requêtes GET.
1051
     *
1052
     * @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...
1053
     */
1054
    public function get(string $from, $to, ?array $options = null): self
1055
    {
1056 40
        $this->create(Method::GET, $from, $to, $options);
1057
1058 40
        return $this;
1059
    }
1060
1061
    /**
1062
     * Spécifie une route qui n'est disponible que pour les requêtes POST.
1063
     *
1064
     * @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...
1065
     */
1066
    public function post(string $from, $to, ?array $options = null): self
1067
    {
1068 22
        $this->create(Method::POST, $from, $to, $options);
1069
1070 22
        return $this;
1071
    }
1072
1073
    /**
1074
     * Spécifie une route qui n'est disponible que pour les requêtes PUT.
1075
     *
1076
     * @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...
1077
     */
1078
    public function put(string $from, $to, ?array $options = null): self
1079
    {
1080 14
        $this->create(Method::PUT, $from, $to, $options);
1081
1082 14
        return $this;
1083
    }
1084
1085
    /**
1086
     * Spécifie une route qui n'est disponible que pour les requêtes DELETE.
1087
     *
1088
     * @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...
1089
     */
1090
    public function delete(string $from, $to, ?array $options = null): self
1091
    {
1092 8
        $this->create(Method::DELETE, $from, $to, $options);
1093
1094 8
        return $this;
1095
    }
1096
1097
    /**
1098
     * Spécifie une route qui n'est disponible que pour les requêtes HEAD.
1099
     *
1100
     * @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...
1101
     */
1102
    public function head(string $from, $to, ?array $options = null): self
1103
    {
1104 2
        $this->create(Method::HEAD, $from, $to, $options);
1105
1106 2
        return $this;
1107
    }
1108
1109
    /**
1110
     * Spécifie une route qui n'est disponible que pour les requêtes PATCH.
1111
     *
1112
     * @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...
1113
     */
1114
    public function patch(string $from, $to, ?array $options = null): self
1115
    {
1116 8
        $this->create(Method::PATCH, $from, $to, $options);
1117
1118 8
        return $this;
1119
    }
1120
1121
    /**
1122
     * Spécifie une route qui n'est disponible que pour les requêtes OPTIONS.
1123
     *
1124
     * @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...
1125
     */
1126
    public function options(string $from, $to, ?array $options = null): self
1127
    {
1128 2
        $this->create(Method::OPTIONS, $from, $to, $options);
1129
1130 2
        return $this;
1131
    }
1132
1133
    /**
1134
     * Spécifie une route qui n'est disponible que pour les requêtes GET et POST.
1135
     *
1136
     * @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...
1137
     */
1138
    public function form(string $from, $to, ?array $options = null): self
1139
    {
1140 4
        return $this->match([Method::GET, Method::POST], $from, $to, $options);
1141
    }
1142
1143
    /**
1144
     * Spécifie une route qui n'est disponible que pour les requêtes de ligne de commande.
1145
     *
1146
     * @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...
1147
     */
1148
    public function cli(string $from, $to, ?array $options = null): self
1149
    {
1150 2
        $this->create('CLI', $from, $to, $options);
1151
1152 2
        return $this;
1153
    }
1154
1155
    /**
1156
     * Spécifie une route qui n'affichera qu'une vue.
1157
     * Ne fonctionne que pour les requêtes GET.
1158
     */
1159
    public function view(string $from, string $view, array $options = []): self
1160
    {
1161
        $to = static fn (...$data) => Services::viewer()
0 ignored issues
show
Deprecated Code introduced by
The function BlitzPHP\View\View::setOptions() has been deprecated. ( Ignorable by Annotation )

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

1161
        $to = static fn (...$data) => /** @scrutinizer ignore-deprecated */ Services::viewer()
Loading history...
1162
            ->setData(['segments' => $data] + $options, 'raw')
1163
            ->display($view)
1164
            ->setOptions($options)
1165
            ->render();
1166
1167 6
        $routeOptions = array_merge($options, ['view' => $view]);
1168
1169 6
        $this->create(Method::GET, $from, $to, $routeOptions);
1170
1171 6
        return $this;
1172
    }
1173
1174
    /**
1175
     * Limite les itinéraires à un ENVIRONNEMENT spécifié ou ils ne fonctionneront pas.
1176
     */
1177
    public function environment(string $env, Closure $callback): self
1178
    {
1179
        if (environment($env)) {
1180 2
            $callback($this);
1181
        }
1182
1183 2
        return $this;
1184
    }
1185
1186
    /**
1187
     * {@inheritDoc}
1188
     */
1189
    public function reverseRoute(string $search, ...$params)
1190
    {
1191
        if ($search === '') {
1192 2
            return false;
1193
        }
1194
1195 10
        $queries = [];
1196
1197
        if (is_array($last = array_pop($params))) {
1198 2
            $queries = $last;
1199
        } elseif (null !== $last) {
1200 8
            $params[] = $last;
1201
        }
1202
1203 10
        $name = $this->formatRouteName($search);
1204
1205
        // Les routes nommées ont une priorité plus élevée.
1206
        foreach ($this->routesNames as $verb => $collection) {
1207
            if (array_key_exists($name, $collection)) {
1208 10
                $routeKey = $collection[$name];
1209
1210 10
                $from = $this->routes[$verb][$routeKey]['from'];
1211
1212 10
                return $this->buildReverseRoute($from, $params, $queries);
1213
            }
1214
        }
1215
1216
        // Ajoutez l'espace de noms par défaut si nécessaire.
1217 6
        $namespace = trim($this->defaultNamespace, '\\') . '\\';
1218
        if (
1219
            substr($search, 0, 1) !== '\\'
1220
            && substr($search, 0, strlen($namespace)) !== $namespace
1221
        ) {
1222 6
            $search = $namespace . $search;
1223
        }
1224
1225
        // Si ce n'est pas une route nommée, alors bouclez
1226
        // toutes les routes pour trouver une correspondance.
1227
        foreach ($this->routes as $collection) {
1228
            foreach ($collection as $route) {
1229 6
                $to   = $route['handler'];
1230 6
                $from = $route['from'];
1231
1232
                // on ignore les closures
1233
                if (! is_string($to)) {
1234 2
                    continue;
1235
                }
1236
1237
                // Perd toute barre oblique d'espace de noms au début des chaînes
1238
                // pour assurer une correspondance plus cohérente.$to     = ltrim($to, '\\');
1239 6
                $to     = ltrim($to, '\\');
1240 6
                $search = ltrim($search, '\\');
1241
1242
                // S'il y a une chance de correspondance, alors ce sera
1243
                // soit avec $search au début de la chaîne $to.
1244
                if (! str_starts_with($to, $search)) {
1245 6
                    continue;
1246
                }
1247
1248
                // Assurez-vous que le nombre de $params donné ici
1249
                // correspond au nombre de back-references dans la route
1250
                if (substr_count($to, '$') !== count($params)) {
1251 2
                    continue;
1252
                }
1253
1254 4
                return $this->buildReverseRoute($from, $params, $queries);
1255
            }
1256
        }
1257
1258
        // Si nous sommes toujours là, alors nous n'avons pas trouvé de correspondance.
1259 6
        return false;
1260
    }
1261
1262
    /**
1263
     * Vérifie une route (en utilisant le "from") pour voir si elle est filtrée ou non.
1264
     */
1265
    public function isFiltered(string $search, ?string $verb = null): bool
1266
    {
1267 18
        return $this->getFiltersForRoute($search, $verb) !== [];
1268
    }
1269
1270
    /**
1271
     * Renvoie les filtres qui doivent être appliqués pour un seul itinéraire, ainsi que
1272
     * avec tous les paramètres qu'il pourrait avoir. Les paramètres sont trouvés en divisant
1273
     * le nom du paramètre entre deux points pour séparer le nom du filtre de la liste des paramètres,
1274
     * et le fractionnement du résultat sur des virgules. Alors:
1275
     *
1276
     *    'role:admin,manager'
1277
     *
1278
     * a un filtre de "rôle", avec des paramètres de ['admin', 'manager'].
1279
     */
1280
    public function getFiltersForRoute(string $search, ?string $verb = null): array
1281
    {
1282 18
        $options = $this->loadRoutesOptions($verb);
1283
1284 18
        $middlewares = $options[$search]['middlewares'] ?? ($options[$search]['middleware'] ?? []);
1285
1286 18
        return (array) $middlewares;
1287
    }
1288
1289
    /**
1290
     * Construit une route inverse
1291
     *
1292
     * @param array $params Un ou plusieurs paramètres à transmettre à la route.
1293
     *                      Le dernier paramètre vous permet de définir la locale.
1294
     */
1295
    protected function buildReverseRoute(string $from, array $params, array $queries = []): string
1296
    {
1297 10
        $locale = null;
1298
1299
        // Retrouvez l'ensemble de nos rétro-références dans le parcours d'origine.
1300 10
        preg_match_all('/\(([^)]+)\)/', $from, $matches);
1301
1302
        if (empty($matches[0])) {
1303
            if (str_contains($from, '{locale}')) {
1304 6
                $locale = $params[0] ?? null;
1305
            }
1306
1307 10
            $from = '/' . ltrim($this->replaceLocale($from, $locale), '/');
1308
1309
            if ($queries !== []) {
1310 2
                $from .= '?' . http_build_query($queries);
1311
            }
1312
1313 10
            return $from;
1314
        }
1315
1316
        // Les paramètres régionaux sont passés ?
1317 8
        $placeholderCount = count($matches[0]);
1318
        if (count($params) > $placeholderCount) {
1319 8
            $locale = $params[$placeholderCount];
1320
        }
1321
1322
        // Construisez notre chaîne résultante, en insérant les $params aux endroits appropriés.
1323
        foreach ($matches[0] as $index => $placeholder) {
1324
            if (! isset($params[$index])) {
1325
                throw new InvalidArgumentException(
1326
                    'Argument manquant pour "' . $placeholder . '" dans la route "' . $from . '".'
1327 8
                );
1328
            }
1329
1330
            // Supprimez `(:` et `)` lorsque $placeholder est un espace réservé.
1331 8
            $placeholderName = substr($placeholder, 2, -1);
1332
            // ou peut-être que $placeholder n'est pas un espace réservé, mais une regex.
1333 8
            $pattern = $this->placeholders[$placeholderName] ?? $placeholder;
1334
1335
            if (! preg_match('#^' . $pattern . '$#u', (string) $params[$index])) {
1336 4
                throw RouterException::invalidParameterType();
1337
            }
1338
1339
            // Assurez-vous que le paramètre que nous insérons correspond au type de paramètre attendu.
1340 8
            $pos  = strpos($from, $placeholder);
1341 8
            $from = substr_replace($from, $params[$index], $pos, strlen($placeholder));
1342
        }
1343
1344 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

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

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

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

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

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

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