Passed
Branch main (64d39c)
by Dimitri
03:50
created

RouteCollection::group()   B

Complexity

Conditions 10
Paths 6

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 10.0578

Importance

Changes 3
Bugs 3 Features 0
Metric Value
cc 10
eloc 17
c 3
b 3
f 0
nc 6
nop 2
dl 0
loc 34
ccs 11
cts 12
cp 0.9167
crap 10.0578
rs 7.6666

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

1651
            if (! in_array($locale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
1652
                $locale = null;
1653
            }
1654
        }
1655
1656
        if ($locale === null) {
1657
            $locale = Services::request()->getLocale();
1658
        }
1659
1660
        return strtr($route, ['{locale}' => $locale]);
1661
    }
1662
}
1663