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

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

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

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

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

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

1665
            if (! in_array($locale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
1666
                $locale = null;
1667
            }
1668
        }
1669
1670
        if ($locale === null) {
1671 6
            $locale = Services::request()->getLocale();
1672
        }
1673
1674 6
        return strtr($route, ['{locale}' => $locale]);
1675
    }
1676
}
1677