RouteCollection::isRedirect()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Router;
13
14
use BlitzPHP\Contracts\Autoloader\LocatorInterface;
15
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
16
use BlitzPHP\Enums\Method;
17
use BlitzPHP\Exceptions\RouterException;
18
use BlitzPHP\Utilities\String\Text;
19
use Closure;
20
use InvalidArgumentException;
21
use Psr\Http\Message\ResponseInterface;
22
23
class RouteCollection implements RouteCollectionInterface
24
{
25
    /**
26
     * L'espace de noms à ajouter à tous les contrôleurs.
27
     * Par défaut, les espaces de noms globaux (\)
28
     */
29
    protected string $defaultNamespace = '\\';
30
31
    /**
32
     * Le nom du contrôleur par défaut à utiliser
33
     * lorsqu'aucun autre contrôleur n'est spécifié.
34
     *
35
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
36
     */
37
    protected string $defaultController = 'Home';
38
39
    /**
40
     * Le nom de la méthode par défaut à utiliser
41
     * lorsqu'aucune autre méthode n'a été spécifiée.
42
     *
43
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
44
     */
45
    protected string $defaultMethod = 'index';
46
47
    /**
48
     * L'espace réservé utilisé lors du routage des "ressources"
49
     * lorsqu'aucun autre espace réservé n'a été spécifié.
50
     */
51
    protected string $defaultPlaceholder = 'any';
52
53
    /**
54
     * S'il faut convertir les tirets en traits de soulignement dans l'URI.
55
     *
56
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
57
     */
58
    protected bool $translateURIDashes = true;
59
60
    /**
61
     * S'il faut faire correspondre l'URI aux contrôleurs
62
     * lorsqu'il ne correspond pas aux itinéraires définis.
63
     *
64
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
65
     */
66
    protected bool $autoRoute = false;
67
68
    /**
69
     * Un appelable qui sera affiché
70
     * lorsque la route ne peut pas être matchée.
71
     *
72
     * @var (Closure(string): (ResponseInterface|string|void))|string
0 ignored issues
show
Documentation Bug introduced by
The doc comment (Closure(string): (Respo...ce|string|void))|string at position 1 could not be parsed: Expected ')' at position 1, but found 'Closure'.
Loading history...
73
     */
74
    protected $override404;
75
76
    /**
77
     * Tableau de fichiers qui contiendrait les définitions de routes.
78
     */
79
    protected array $routeFiles = [];
80
81
    /**
82
     * Espaces réservés définis pouvant être utilisés.
83
     */
84
    protected array $placeholders = [
85
        'any'      => '.*',
86
        'segment'  => '[^/]+',
87
        'alphanum' => '[a-zA-Z0-9]+',
88
        'num'      => '[0-9]+',
89
        'alpha'    => '[a-zA-Z]+',
90
        'hash'     => '[^/]+',
91
        'slug'     => '[a-z0-9-]+',
92
        'uuid'     => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
93
    ];
94
95
    /**
96
     * Tableau de toutes les routes et leurs mappages.
97
     *
98
     * @example
99
     * ```php
100
     * [
101
     *     verb => [
102
     *         routeName => [
103
     *             'route' => [
104
     *                 routeKey(regex) => handler,
105
     *             ],
106
     *             'redirect' => statusCode,
107
     *         ]
108
     *     ],
109
     * ]
110
     * ```
111
     */
112
    protected array $routes = [
113
        '*'             => [],
114
        Method::OPTIONS => [],
115
        Method::GET     => [],
116
        Method::HEAD    => [],
117
        Method::POST    => [],
118
        Method::PATCH   => [],
119
        Method::PUT     => [],
120
        Method::DELETE  => [],
121
        Method::TRACE   => [],
122
        Method::CONNECT => [],
123
        'CLI'           => [],
124
    ];
125
126
    /**
127
     * Tableau des noms des routes
128
     *
129
     * [
130
     *     verb => [
131
     *         routeName => routeKey(regex)
132
     *     ],
133
     * ]
134
     */
135
    protected array $routesNames = [
136
        '*'             => [],
137
        Method::OPTIONS => [],
138
        Method::GET     => [],
139
        Method::HEAD    => [],
140
        Method::POST    => [],
141
        Method::PATCH   => [],
142
        Method::PUT     => [],
143
        Method::DELETE  => [],
144
        Method::TRACE   => [],
145
        Method::CONNECT => [],
146
        'CLI'           => [],
147
    ];
148
149
    /**
150
     * Tableaux des options des routes.
151
     *
152
     * @example
153
     * ```php
154
     * [
155
     *     verb => [
156
     *         routeKey(regex) => [
157
     *             key => value,
158
     *         ]
159
     *     ],
160
     * ]
161
     * ```
162
     */
163
    protected array $routesOptions = [];
164
165
    /**
166
     * La méthode actuelle par laquelle le script est appelé.
167
     */
168
    protected string $HTTPVerb = '*';
169
170
    /**
171
     * La liste par défaut des méthodes HTTP (et CLI pour l'utilisation de la ligne de commande)
172
     * qui est autorisé si aucune autre méthode n'est fournie.
173
     */
174
    protected array $defaultHTTPMethods = Router::HTTP_METHODS;
175
176
    /**
177
     * Le nom du groupe de route courant
178
     */
179
    protected ?string $group = null;
180
181
    /**
182
     * Le sous domaine courant
183
     */
184
    protected ?string $currentSubdomain = null;
185
186
    /**
187
     * Stocke une copie des options actuelles en cours appliqué lors de la création.
188
     */
189
    protected ?array $currentOptions = null;
190
191
    /**
192
     * Un petit booster de performances.
193
     */
194
    protected bool $didDiscover = false;
195
196
    /**
197
     * Drapeau pour trier les routes par priorité.
198
     */
199
    protected bool $prioritize = false;
200
201
    /**
202
     * Indicateur de détection de priorité de route.
203
     */
204
    protected bool $prioritizeDetected = false;
205
206
    /**
207
     * Drapeau pour limiter ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
208
     */
209
    protected bool $useSupportedLocalesOnly = false;
210
211
    /**
212
     * Le nom d'hôte actuel de $_SERVER['HTTP_HOST']
213
     */
214
    private ?string $httpHost = null;
215
216
    /**
217
     * Constructor
218
     *
219
     * @param LocatorInterface $locator Descripteur du localisateur de fichiers à utiliser.
220
     */
221
    public function __construct(protected LocatorInterface $locator, object $routing)
222
    {
223 73
        $this->httpHost = env('HTTP_HOST');
224
225
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
226 73
        $this->defaultNamespace        = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
227 73
        $this->defaultController       = $routing->default_controller ?: $this->defaultController;
228 73
        $this->defaultMethod           = $routing->default_method ?: $this->defaultMethod;
229 73
        $this->translateURIDashes      = $routing->translate_uri_dashes ?: $this->translateURIDashes;
230 73
        $this->override404             = $routing->fallback ?: $this->override404;
231 73
        $this->autoRoute               = $routing->auto_route ?: $this->autoRoute;
232 73
        $this->routeFiles              = $routing->route_files ?: $this->routeFiles;
233 73
        $this->prioritize              = $routing->prioritize ?: $this->prioritize;
234 73
        $this->useSupportedLocalesOnly = $routing->use_supported_locales_only ?: $this->useSupportedLocalesOnly;
235
236
        // Normaliser la chaîne de path dans le tableau routeFiles.
237
        foreach ($this->routeFiles as $routeKey => $routesFile) {
238 73
            $this->routeFiles[$routeKey] = realpath($routesFile) ?: $routesFile;
239
        }
240
    }
241
242
    /**
243
     * Charge le fichier des routes principales et découvre les routes.
244
     *
245
     * Charge une seule fois sauf réinitialisation.
246
     */
247
    public function loadRoutes(string $routesFile = CONFIG_PATH . 'routes.php'): self
248
    {
249
        if ($this->didDiscover) {
250 2
            return $this;
251
        }
252
253
        // Normaliser la chaîne de chemin dans routesFile
254
        $routesFile = realpath($routesFile) ?: $routesFile;
255
256
        // Incluez le fichier routesFile s'il n'existe pas.
257
        // Ne conserver que pour les fins BC pour l'instant.
258
        $routeFiles = $this->routeFiles;
259
        if (! in_array($routesFile, $routeFiles, true)) {
260
            $routeFiles[] = $routesFile;
261
        }
262
263
        // Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
264
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
265
266
        foreach ($routeFiles as $routesFile) {
267
            if (! is_file($routesFile)) {
268
                logger()->warning(sprintf('Fichier de route introuvable : "%s"', $routesFile));
269
270
                continue;
271
            }
272
273
            require_once $routesFile;
274
        }
275
276
        $this->discoverRoutes();
277
278
        return $this;
279
    }
280
281
    /**
282
     * Réinitialisez les routes, afin qu'un cas de test puisse fournir le
283
     * ceux explicites nécessaires pour cela.
284
     */
285
    public function resetRoutes()
286
    {
287
        $this->routes = $this->routesNames = ['*' => []];
288
289
        foreach ($this->defaultHTTPMethods as $verb) {
290
            $this->routes[$verb]      = [];
291
            $this->routesNames[$verb] = [];
292
        }
293
294
        $this->routesOptions = [];
295
296
        $this->prioritizeDetected = false;
297
        $this->didDiscover        = false;
298
    }
299
300
    /**
301
     * {@inheritDoc}
302
     *
303
     * Utilisez `placeholder` a la place
304
     */
305
    public function addPlaceholder($placeholder, ?string $pattern = null): self
306
    {
307 4
        return $this->placeholder($placeholder, $pattern);
308
    }
309
310
    /**
311
     * Enregistre une nouvelle contrainte auprès du système.
312
     * Les contraintes sont utilisées par les routes en tant qu'espaces réservés pour les expressions régulières afin de définir les parcours plus humains.
313
     */
314
    public function placeholder(array|string $placeholder, ?string $pattern = null): self
315
    {
316
        if (! is_array($placeholder)) {
0 ignored issues
show
introduced by
The condition is_array($placeholder) is always true.
Loading history...
317 6
            $placeholder = [$placeholder => $pattern];
318
        }
319
320 6
        $this->placeholders = array_merge($this->placeholders, $placeholder);
321
322 6
        return $this;
323
    }
324
325
    /**
326
     * {@inheritDoc}
327
     */
328
    public function setDefaultNamespace(string $value): self
329
    {
330 22
        $this->defaultNamespace = esc(strip_tags($value));
331 22
        $this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';
332
333 22
        return $this;
334
    }
335
336
    /**
337
     * {@inheritDoc}
338
     */
339
    public function setDefaultController(string $value): self
340
    {
341 10
        $this->defaultController = esc(strip_tags($value));
342
343 10
        return $this;
344
    }
345
346
    /**
347
     * {@inheritDoc}
348
     */
349
    public function setDefaultMethod(string $value): self
350
    {
351 6
        $this->defaultMethod = esc(strip_tags($value));
352
353 6
        return $this;
354
    }
355
356
    /**
357
     * {@inheritDoc}
358
     */
359
    public function setTranslateURIDashes(bool $value): self
360
    {
361 4
        $this->translateURIDashes = $value;
362
363 4
        return $this;
364
    }
365
366
    /**
367
     * {@inheritDoc}
368
     */
369
    public function setAutoRoute(bool $value): self
370
    {
371 4
        $this->autoRoute = $value;
372
373 4
        return $this;
374
    }
375
376
    /**
377
     * {@inheritDoc}
378
     *
379
     * Utilisez self::fallback()
380
     */
381
    public function set404Override($callable = null): self
382
    {
383
        return $this->fallback($callable);
384
    }
385
386
    /**
387
     * Définit la classe/méthode qui doit être appelée si le routage ne trouver pas une correspondance.
388
     *
389
     * @param callable|string|null $callable
390
     */
391
    public function fallback($callable = null): self
392
    {
393 4
        $this->override404 = $callable;
394
395 4
        return $this;
396
    }
397
398
    /**
399
     * {@inheritDoc}
400
     */
401
    public function get404Override()
402
    {
403 4
        return $this->override404;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->override404 also could return the type mixed which is incompatible with the return type mandated by BlitzPHP\Contracts\Route...rface::get404Override() of Closure|null|string.
Loading history...
404
    }
405
406
    /**
407
     * Tentera de découvrir d'éventuelles routes supplémentaires, soit par
408
     * les espaces de noms PSR4 locaux ou via des packages Composer sélectionnés.
409
     */
410
    protected function discoverRoutes()
411
    {
412
        if ($this->didDiscover) {
413 16
            return;
414
        }
415
416
        // Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
417 54
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
418
419 54
        $files = $this->locator->search('Config/routes.php');
420
421
        foreach ($files as $file) {
422
            // N'incluez plus notre fichier principal...
423
            if (in_array($file, $this->routeFiles, true)) {
424 54
                continue;
425
            }
426
427
            include_once $file;
428
        }
429
430 54
        $this->didDiscover = true;
431
    }
432
433
    /**
434
     * Définit la contrainte par défaut à utiliser dans le système. Typiquement
435
     * à utiliser avec la méthode 'ressource'.
436
     */
437
    public function setDefaultConstraint(string $placeholder): self
438
    {
439
        if (array_key_exists($placeholder, $this->placeholders)) {
440 2
            $this->defaultPlaceholder = $placeholder;
441
        }
442
443 2
        return $this;
444
    }
445
446
    /**
447
     * {@inheritDoc}
448
     */
449
    public function getDefaultController(): string
450
    {
451 26
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
452
    }
453
454
    /**
455
     * {@inheritDoc}
456
     */
457
    public function getDefaultMethod(): string
458
    {
459 26
        return $this->defaultMethod;
460
    }
461
462
    /**
463
     * {@inheritDoc}
464
     */
465
    public function getDefaultNamespace(): string
466
    {
467 4
        return $this->defaultNamespace;
468
    }
469
470
    /**
471
     * Pour `klinge route:list`
472
     *
473
     * @return array<string, string>
474
     *
475
     * @internal
476
     */
477
    public function getPlaceholders(): array
478
    {
479
        return $this->placeholders;
480
    }
481
482
    /**
483
     *{@inheritDoc}
484
     */
485
    public function shouldTranslateURIDashes(): bool
486
    {
487 20
        return $this->translateURIDashes;
488
    }
489
490
    /**
491
     * {@inheritDoc}
492
     */
493
    public function shouldAutoRoute(): bool
494
    {
495 20
        return $this->autoRoute;
496
    }
497
498
    /**
499
     * Activer ou désactiver le tri des routes par priorité
500
     */
501
    public function setPrioritize(bool $enabled = true): self
502
    {
503 2
        $this->prioritize = $enabled;
504
505 2
        return $this;
506
    }
507
508
    /**
509
     * Définissez le drapeau qui limite ou non les routes avec l'espace réservé {locale} à App::$supportedLocales
510
     */
511
    public function useSupportedLocalesOnly(bool $useOnly): self
512
    {
513 2
        $this->useSupportedLocalesOnly = $useOnly;
514
515 2
        return $this;
516
    }
517
518
    /**
519
     * Obtenez le drapeau qui limite ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
520
     */
521
    public function shouldUseSupportedLocalesOnly(): bool
522
    {
523 4
        return $this->useSupportedLocalesOnly;
524
    }
525
526
    /**
527
     * {@inheritDoc}
528
     *
529
     * @internal
530
     */
531
    public function getRegisteredControllers(?string $verb = '*'): array
532
    {
533 4
        $controllers = [];
534
535
        if ($verb === '*') {
536
            foreach ($this->defaultHTTPMethods as $tmpVerb) {
537
                foreach ($this->routes[$tmpVerb] as $route) {
538 2
                    $controller = $this->getControllerName($route['handler']);
539
                    if ($controller !== null) {
540 2
                        $controllers[] = $controller;
541
                    }
542
                }
543
            }
544
        } else {
545 2
            $routes = $this->getRoutes($verb);
546
547
            foreach ($routes as $handler) {
548 2
                $controller = $this->getControllerName($handler);
549
                if ($controller !== null) {
550 2
                    $controllers[] = $controller;
551
                }
552
            }
553
        }
554
555 4
        return array_unique($controllers);
556
    }
557
558
    /**
559
     * {@inheritDoc}
560
     */
561
    public function getRoutes(?string $verb = null, bool $includeWildcard = true): array
562
    {
563
        if ($verb === null || $verb === '') {
564 32
            $verb = $this->getHTTPVerb();
565
        }
566
567
        // Puisqu'il s'agit du point d'entrée du routeur,
568
        // prenez un moment pour faire toute découverte de route
569
        // que nous pourrions avoir besoin de faire.
570 56
        $this->discoverRoutes();
571
572 56
        $routes = [];
573
        if (isset($this->routes[$verb])) {
574
            // Conserve les itinéraires du verbe actuel au début afin qu'ils soient
575
            // mis en correspondance avant l'un des itinéraires génériques "add".
576 56
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
577
578
            foreach ($collection as $routeKey => $r) {
579 56
                $routes[$routeKey] = $r['handler'];
580
            }
581
        }
582
583
        // tri des routes par priorité
584
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
585 2
            $order = [];
586
587
            foreach ($routes as $key => $value) {
588 2
                $key                    = $key === '/' ? $key : ltrim($key, '/ ');
589 2
                $priority               = $this->getRoutesOptions($key, $verb)['priority'] ?? 0;
590 2
                $order[$priority][$key] = $value;
591
            }
592
593 2
            ksort($order);
594 2
            $routes = array_merge(...$order);
595
        }
596
597 56
        return $routes;
598
    }
599
600
    /**
601
     * Renvoie une ou toutes les options d'itinéraire
602
     */
603
    public function getRoutesOptions(?string $from = null, ?string $verb = null): array
604
    {
605 30
        $options = $this->loadRoutesOptions($verb);
606
607 30
        return $from ? $options[$from] ?? [] : $options;
608
    }
609
610
    /**
611
     * {@inheritDoc}
612
     */
613
    public function getHTTPVerb(): string
614
    {
615 54
        return $this->HTTPVerb;
616
    }
617
618
    /**
619
     * {@inheritDoc}
620
     */
621
    public function setHTTPVerb(string $verb): self
622
    {
623 70
        $this->HTTPVerb = strtoupper($verb);
624
625 70
        return $this;
626
    }
627
628
    /**
629
     * Une méthode de raccourci pour ajouter un certain nombre d'itinéraires en une seule fois.
630
     * Il ne permet pas de définir des options sur l'itinéraire, ou de
631
     * définir la méthode utilisée.
632
     */
633
    public function map(array $routes = [], ?array $options = null): self
634
    {
635
        foreach ($routes as $from => $to) {
636 12
            $this->add($from, $to, $options);
637
        }
638
639 12
        return $this;
640
    }
641
642
    /**
643
     * {@inheritDoc}
644
     */
645
    public function add(string $from, $to, ?array $options = null): self
646
    {
647 48
        $this->create('*', $from, $to, $options);
648
649 48
        return $this;
650
    }
651
652
    /**
653
     * Ajoute une redirection temporaire d'une route à une autre.
654
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
655
     *
656
     * @param string $from   Le modèle à comparer
657
     * @param string $to     Soit un nom de route ou un URI vers lequel rediriger
658
     * @param int    $status Le code d'état HTTP qui doit être renvoyé avec cette redirection
659
     */
660
    public function redirect(string $from, string $to, int $status = 302): self
661
    {
662
        // Utilisez le modèle de la route nommée s'il s'agit d'une route nommée.
663
        if (array_key_exists($to, $this->routesNames['*'])) {
664 4
            $routeName  = $to;
665 4
            $routeKey   = $this->routesNames['*'][$routeName];
666 4
            $redirectTo = [$routeKey => $this->routes['*'][$routeKey]['handler']];
667
        } elseif (array_key_exists($to, $this->routesNames[Method::GET])) {
668 2
            $routeName  = $to;
669 2
            $routeKey   = $this->routesNames[Method::GET][$routeName];
670 2
            $redirectTo = [$routeKey => $this->routes[Method::GET][$routeKey]['handler']];
671
        } else {
672
            // La route nommee n'a pas ete trouvée
673 4
            $redirectTo = $to;
674
        }
675
676 4
        $this->create('*', $from, $redirectTo, ['redirect' => $status]);
677
678 4
        return $this;
679
    }
680
681
    /**
682
     * Ajoute une redirection permanente d'une route à une autre.
683
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
684
     */
685
    public function permanentRedirect(string $from, string $to): self
686
    {
687 2
        return $this->redirect($from, $to, 301);
688
    }
689
690
    /**
691
     * @deprecated 0.9 Please use redirect() instead
692
     */
693
    public function addRedirect(string $from, string $to, int $status = 302): self
694
    {
695
        return $this->redirect($from, $to, $status);
696
    }
697
698
    /**
699
     * {@inheritDoc}
700
     *
701
     * @param string $routeKey cle de route ou route nommee
702
     */
703
    public function isRedirect(string $routeKey): bool
704
    {
705 24
        return isset($this->routes['*'][$routeKey]['redirect']);
706
    }
707
708
    /**
709
     * {@inheritDoc}
710
     *
711
     * @param string $routeKey cle de route ou route nommee
712
     */
713
    public function getRedirectCode(string $routeKey): int
714
    {
715 4
        return $this->routes['*'][$routeKey]['redirect'] ?? 0;
716
    }
717
718
    /**
719
     * Regroupez une série de routes sous un seul segment d'URL. C'est pratique
720
     * pour regrouper des éléments dans une zone d'administration, comme :
721
     *
722
     * Example:
723
     *     // Creates route: admin/users
724
     *     $route->group('admin', function() {
725
     *            $route->resource('users');
726
     *     });
727
     *
728
     * @param string         $name      Le nom avec lequel grouper/préfixer les routes.
729
     * @param array|callable ...$params
730
     */
731
    public function group(string $name, ...$params)
732
    {
733 10
        $oldGroup   = $this->group ?: '';
734 10
        $oldOptions = $this->currentOptions;
735
736
        // Pour enregistrer une route, nous allons définir un indicateur afin que notre routeur
737
        // donc il verra le nom du groupe.
738
        // Si le nom du groupe est vide, nous continuons à utiliser le nom du groupe précédemment construit.
739 10
        $this->group = $name !== '' && $name !== '0' ? trim($oldGroup . '/' . $name, '/') : $oldGroup;
740
741 10
        $callback = array_pop($params);
742
743
        if ($params && is_array($params[0])) {
744 10
            $options = array_shift($params);
745
746
            if (isset($options['middlewares']) || isset($options['middleware'])) {
747 8
                $currentMiddlewares     = (array) ($this->currentOptions['middlewares'] ?? []);
748 8
                $options['middlewares'] = array_merge($currentMiddlewares, (array) ($options['middlewares'] ?? $options['middleware']));
749
            }
750
751
            // Fusionner les options autres que les middlewares.
752
            $this->currentOptions = array_merge(
753
                $this->currentOptions ?: [],
754
                $options ?: [],
755 10
            );
756
        }
757
758
        if (is_callable($callback)) {
759 10
            $callback($this);
760
        }
761
762 10
        $this->group          = $oldGroup;
763 10
        $this->currentOptions = $oldOptions;
764
    }
765
766
    /*
767
     * ------------------------------------------------- -------------------
768
     * Routage basé sur les verbes HTTP
769
     * ------------------------------------------------- -------------------
770
     * Le routage fonctionne ici car, comme le fichier de configuration des routes est lu,
771
     * les différentes routes basées sur le verbe HTTP ne seront ajoutées qu'à la mémoire en mémoire
772
     * routes s'il s'agit d'un appel qui doit répondre à ce verbe.
773
     *
774
     * Le tableau d'options est généralement utilisé pour transmettre un 'as' ou var, mais peut
775
     * être étendu à l'avenir. Voir le docblock pour la méthode 'add' ci-dessus pour
776
     * liste actuelle des options disponibles dans le monde.*/
777
778
    /**
779
     * Crée une collection d'itinéraires basés sur HTTP-verb pour un contrôleur.
780
     *
781
     * Options possibles :
782
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
783
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
784
     * 'websafe' - - '1' si seuls les verbes HTTP GET et POST sont pris en charge
785
     *
786
     * Exemple:
787
     *
788
     *      $route->resource('photos');
789
     *
790
     *      // Genère les routes suivantes:
791
     *      HTTP Verb | Path        | Action        | Used for...
792
     *      ----------+-------------+---------------+-----------------
793
     *      GET         /photos             index           un tableau d'objets photo
794
     *      GET         /photos/new         new             un objet photo vide, avec des propriétés par défaut
795
     *      GET         /photos/{id}/edit   edit            un objet photo spécifique, propriétés modifiables
796
     *      GET         /photos/{id}        show            un objet photo spécifique, toutes les propriétés
797
     *      POST        /photos             create          un nouvel objet photo, à ajouter à la ressource
798
     *      DELETE      /photos/{id}        delete          supprime l'objet photo spécifié
799
     *      PUT/PATCH   /photos/{id}        update          propriétés de remplacement pour la photo existante
800
     *
801
     *  Si l'option 'websafe' est présente, les chemins suivants sont également disponibles :
802
     *
803
     *      POST		/photos/{id}/delete delete
804
     *      POST        /photos/{id}        update
805
     *
806
     * @param string $name    Le nom de la ressource/du contrôleur vers lequel router.
807
     * @param array  $options Une liste des façons possibles de personnaliser le routage.
808
     */
809
    public function resource(string $name, array $options = []): self
810
    {
811
        // Afin de permettre la personnalisation de la route, le
812
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
813
        // pour stocker les valeurs.
814 6
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
815
816
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
817
        // valeur de $name avec le nom du nouveau contrôleur.
818
        if (isset($options['controller'])) {
819 6
            $newName = ucfirst(esc(strip_tags($options['controller'])));
0 ignored issues
show
Bug introduced by
It seems like esc(strip_tags($options['controller'])) can also be of type array; however, parameter $string of ucfirst() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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

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