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

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

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

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

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

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

1490
        $name = $this->formatRouteName(/** @scrutinizer ignore-type */ $options['as'] ?? $options['name'] ?? $routeKey);
Loading history...
1491
1492
        // Ne remplacez aucun 'from' existant afin que les routes découvertes automatiquement
1493
        // n'écrase pas les paramètres app/Config/Routes.
1494
        // les routes manuelement définies doivent toujours être la "source de vérité".
1495
        // cela ne fonctionne que parce que les routes découvertes sont ajoutées juste avant
1496
        // pour tenter de router la requête.
1497 66
        $routeKeyExists = isset($this->routes[$verb][$routeKey]);
1498
        if ((isset($this->routesNames[$verb][$name]) || $routeKeyExists) && ! $overwrite) {
1499 14
            return;
1500
        }
1501
1502
        $this->routes[$verb][$routeKey] = [
1503
            'name'    => $name,
1504
            'handler' => $to,
1505
            'from'    => $from,
1506 66
        ];
1507 66
        $this->routesOptions[$verb][$routeKey] = $options;
1508 66
        $this->routesNames[$verb][$name]       = $routeKey;
1509
1510
        // C'est une redirection ?
1511
        if (isset($options['redirect']) && is_numeric($options['redirect'])) {
1512 4
            $this->routes['*'][$routeKey]['redirect'] = $options['redirect'];
1513
        }
1514
    }
1515
1516
    /**
1517
     * Compare le nom d'hôte transmis avec le nom d'hôte actuel sur cette demande de page.
1518
     *
1519
     * @param list<string>|string $hostname Nom d'hôte dans les options d'itinéraire
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Router\list was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
1520
     */
1521
    private function checkHostname(array|string $hostname): bool
1522
    {
1523
        // Les appels CLI ne peuvent pas être sur le nom d'hôte.
1524
        if (! isset($this->httpHost)) {
1525
            return false;
1526
        }
1527
1528
        // hostname multiples
1529
        if (is_array($hostname)) {
0 ignored issues
show
introduced by
The condition is_array($hostname) is always true.
Loading history...
1530 2
            $hostnameLower = array_map('strtolower', $hostname);
1531
1532 2
            return in_array(strtolower($this->httpHost), $hostnameLower, true);
0 ignored issues
show
Bug introduced by
It seems like $this->httpHost can also be of type null; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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