Passed
Pull Request — main (#8)
by
unknown
15:18
created

RouteCollection::presenter()   F

Complexity

Conditions 18
Paths 12288

Size

Total Lines 89
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 18.7184

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Router;
13
14
use BlitzPHP\Container\Services;
15
use BlitzPHP\Contracts\Autoloader\LocatorInterface;
16
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
17
use BlitzPHP\Enums\Method;
18
use BlitzPHP\Exceptions\RouterException;
19
use BlitzPHP\Utilities\String\Text;
20
use Closure;
21
use InvalidArgumentException;
22
use Psr\Http\Message\ResponseInterface;
23
24
class RouteCollection implements RouteCollectionInterface
25
{
26
    /**
27
     * L'espace de noms à ajouter à tous les contrôleurs.
28
     * Par défaut, les espaces de noms globaux (\)
29
     */
30
    protected string $defaultNamespace = '\\';
31
32
    /**
33
     * Le nom du contrôleur par défaut à utiliser
34
     * lorsqu'aucun autre contrôleur n'est spécifié.
35
     *
36
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
37
     */
38
    protected string $defaultController = 'Home';
39
40
    /**
41
     * Le nom de la méthode par défaut à utiliser
42
     * lorsqu'aucune autre méthode n'a été spécifiée.
43
     *
44
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
45
     */
46
    protected string $defaultMethod = 'index';
47
48
    /**
49
     * L'espace réservé utilisé lors du routage des "ressources"
50
     * lorsqu'aucun autre espace réservé n'a été spécifié.
51
     */
52
    protected string $defaultPlaceholder = 'any';
53
54
    /**
55
     * S'il faut convertir les tirets en traits de soulignement dans l'URI.
56
     *
57
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
58
     */
59
    protected bool $translateURIDashes = true;
60
61
    /**
62
     * S'il faut faire correspondre l'URI aux contrôleurs
63
     * lorsqu'il ne correspond pas aux itinéraires définis.
64
     *
65
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
66
     */
67
    protected bool $autoRoute = false;
68
69
    /**
70
     * Un appelable qui sera affiché
71
     * lorsque la route ne peut pas être matchée.
72
     *
73
     * @var (Closure(string): (ResponseInterface|string|void))|string
0 ignored issues
show
Documentation Bug introduced by
The doc comment (Closure(string): (Respo...ce|string|void))|string at position 1 could not be parsed: Expected ')' at position 1, but found 'Closure'.
Loading history...
74
     */
75
    protected $override404;
76
77
    /**
78
     * Tableau de fichiers qui contiendrait les définitions de routes.
79
     */
80
    protected array $routeFiles = [];
81
82
    /**
83
     * Espaces réservés définis pouvant être utilisés.
84
     */
85
    protected array $placeholders = [
86
        'any'      => '.*',
87
        'segment'  => '[^/]+',
88
        'alphanum' => '[a-zA-Z0-9]+',
89
        'num'      => '[0-9]+',
90
        'alpha'    => '[a-zA-Z]+',
91
        'hash'     => '[^/]+',
92
        'slug'     => '[a-z0-9-]+',
93
        'uuid'     => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
94
    ];
95
96
    /**
97
     * Tableau de toutes les routes et leurs mappages.
98
     *
99
     * @example
100
     * ```php
101
     * [
102
     *     verb => [
103
     *         routeName => [
104
     *             'route' => [
105
     *                 routeKey(regex) => handler,
106
     *             ],
107
     *             'redirect' => statusCode,
108
     *         ]
109
     *     ],
110
     * ]
111
     * ```
112
     */
113
    protected array $routes = [
114
        '*'             => [],
115
        Method::OPTIONS => [],
116
        Method::GET     => [],
117
        Method::HEAD    => [],
118
        Method::POST    => [],
119
        Method::PATCH   => [],
120
        Method::PUT     => [],
121
        Method::DELETE  => [],
122
        Method::TRACE   => [],
123
        Method::CONNECT => [],
124
        'CLI'           => [],
125
    ];
126
127
    /**
128
     * Tableau des noms des routes
129
     *
130
     * [
131
     *     verb => [
132
     *         routeName => routeKey(regex)
133
     *     ],
134
     * ]
135
     */
136
    protected array $routesNames = [
137
        '*'             => [],
138
        Method::OPTIONS => [],
139
        Method::GET     => [],
140
        Method::HEAD    => [],
141
        Method::POST    => [],
142
        Method::PATCH   => [],
143
        Method::PUT     => [],
144
        Method::DELETE  => [],
145
        Method::TRACE   => [],
146
        Method::CONNECT => [],
147
        'CLI'           => [],
148
    ];
149
150
    /**
151
     * Tableaux des options des routes.
152
     *
153
     * @example
154
     * ```php
155
     * [
156
     *     verb => [
157
     *         routeKey(regex) => [
158
     *             key => value,
159
     *         ]
160
     *     ],
161
     * ]
162
     * ```
163
     */
164
    protected array $routesOptions = [];
165
166
    /**
167
     * La méthode actuelle par laquelle le script est appelé.
168
     */
169
    protected string $HTTPVerb = '*';
170
171
    /**
172
     * La liste par défaut des méthodes HTTP (et CLI pour l'utilisation de la ligne de commande)
173
     * qui est autorisé si aucune autre méthode n'est fournie.
174
     */
175
    protected array $defaultHTTPMethods = Router::HTTP_METHODS;
176
177
    /**
178
     * Le nom du groupe de route courant
179
     */
180
    protected ?string $group = null;
181
182
    /**
183
     * Le sous domaine courant
184
     */
185
    protected ?string $currentSubdomain = null;
186
187
    /**
188
     * Stocke une copie des options actuelles en cours appliqué lors de la création.
189
     */
190
    protected ?array $currentOptions = null;
191
192
    /**
193
     * Un petit booster de performances.
194
     */
195
    protected bool $didDiscover = false;
196
197
    /**
198
     * Drapeau pour trier les routes par priorité.
199
     */
200
    protected bool $prioritize = false;
201
202
    /**
203
     * Indicateur de détection de priorité de route.
204
     */
205
    protected bool $prioritizeDetected = false;
206
207
    /**
208
     * Drapeau pour limiter ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
209
     */
210
    protected bool $useSupportedLocalesOnly = false;
211
212
    /**
213
     * Le nom d'hôte actuel de $_SERVER['HTTP_HOST']
214
     */
215
    private ?string $httpHost = null;
216
217
    /**
218
     * Constructor
219
     *
220
     * @param LocatorInterface $locator Descripteur du localisateur de fichiers à utiliser.
221
     */
222
    public function __construct(protected LocatorInterface $locator, object $routing)
223
    {
224 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