Passed
Pull Request — main (#27)
by Dimitri
05:06
created

RouteCollection::loadRoutes()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 32
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 32.244

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 7
nop 1
dl 0
loc 32
ccs 1
cts 10
cp 0.1
crap 32.244
rs 9.2222
c 0
b 0
f 0
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 71
        $this->httpHost = env('HTTP_HOST');
225
226
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
227 71
        $this->defaultNamespace        = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
228 71
        $this->defaultController       = $routing->default_controller ?: $this->defaultController;
229 71
        $this->defaultMethod           = $routing->default_method ?: $this->defaultMethod;
230 71
        $this->translateURIDashes      = $routing->translate_uri_dashes ?: $this->translateURIDashes;
231 71
        $this->override404             = $routing->fallback ?: $this->override404;
232 71
        $this->autoRoute               = $routing->auto_route ?: $this->autoRoute;
233 71
        $this->routeFiles              = $routing->route_files ?: $this->routeFiles;
234 71
        $this->prioritize              = $routing->prioritize ?: $this->prioritize;
235 71
        $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 71
            $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 20
        $this->defaultNamespace = esc(strip_tags($value));
332 20
        $this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';
333
334 20
        return $this;
335
    }
336
337
    /**
338
     * {@inheritDoc}
339
     */
340
    public function setDefaultController(string $value): self
341
    {
342 8
        $this->defaultController = esc(strip_tags($value));
343
344 8
        return $this;
345
    }
346
347
    /**
348
     * {@inheritDoc}
349
     */
350
    public function setDefaultMethod(string $value): self
351
    {
352 4
        $this->defaultMethod = esc(strip_tags($value));
353
354 4
        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 52
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
419
420 52
        $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 52
                continue;
426
            }
427
428
            include_once $file;
429
        }
430
431 52
        $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 24
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
453
    }
454
455
    /**
456
     * {@inheritDoc}
457
     */
458
    public function getDefaultMethod(): string
459
    {
460 24
        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 18
        return $this->translateURIDashes;
489
    }
490
491
    /**
492
     * {@inheritDoc}
493
     */
494
    public function shouldAutoRoute(): bool
495
    {
496 18
        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 54
        $this->discoverRoutes();
572
573 54
        $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 54
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
578
579
            foreach ($collection as $routeKey => $r) {
580 54
                $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 54
        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 28
        $options = $this->loadRoutesOptions($verb);
607
608 28
        return $from ? $options[$from] ?? [] : $options;
609
    }
610
611
    /**
612
     * {@inheritDoc}
613
     */
614
    public function getHTTPVerb(): string
615
    {
616 52
        return $this->HTTPVerb;
617
    }
618
619
    /**
620
     * {@inheritDoc}
621
     */
622
    public function setHTTPVerb(string $verb): self
623
    {
624 68
        $this->HTTPVerb = strtoupper($verb);
625
626 68
        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 22
        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 40
        $this->create(Method::GET, $from, $to, $options);
1052
1053 40
        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 20
        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 20
        $options = $this->loadRoutesOptions($verb);
1278
1279 20
        $middlewares = $options[$search]['middlewares'] ?? ($options[$search]['middleware'] ?? []);
1280
1281 20
        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 28
        $verb ??= $this->getHTTPVerb();
1354
1355 28
        $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 28
        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 64
        $overwrite = false;
1381 64
        $prefix    = $this->group === null ? '' : $this->group . '/';
1382
1383 64
        $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 64
            $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 64
        $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 6
                return;
1437
            }
1438
1439 8
            $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 64
        $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 64
            $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 60
                $namespace = $options['namespace'] ?? $this->defaultNamespace;
1483 60
                $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 64
            $to = '\\' . ltrim($to, '\\');
1488
        }
1489
1490 64
        $name = $this->formatRouteName($options['as'] ?? $options['name'] ?? $routeKey);
0 ignored issues
show
Bug introduced by
It seems like $options['as'] ?? $options['name'] ?? $routeKey can also be of type array; however, parameter $name of BlitzPHP\Router\RouteCollection::formatRouteName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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 64
        $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 64
        ];
1507 64
        $this->routesOptions[$verb][$routeKey] = $options;
1508 64
        $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 string $hostname Nom d'hôte dans les options d'itinéraire
1520
     */
1521
    private function checkHostname(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 8
        return strtolower($this->httpHost) === strtolower($hostname);
0 ignored issues
show
Bug introduced by
It seems like $this->httpHost can also be of type null; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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