Test Failed
Branch main (64d39c)
by Dimitri
02:51
created

RouteCollection::__construct()   B

Complexity

Conditions 11
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 11
nc 2
nop 2
dl 0
loc 17
rs 7.3166
c 0
b 0
f 0

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
        $this->httpHost = Services::request()->getEnv('HTTP_HOST');
230
231
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
232
        $this->defaultNamespace   = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
233
        $this->defaultController  = $routing->default_controller ?: $this->defaultController;
234
        $this->defaultMethod      = $routing->default_method ?: $this->defaultMethod;
235
        $this->translateURIDashes = $routing->translate_uri_dashes ?: $this->translateURIDashes;
236
        $this->override404        = $routing->fallback ?: $this->override404;
237
        $this->autoRoute          = $routing->auto_route ?: $this->autoRoute;
238
        $this->routeFiles         = $routing->route_files ?: $this->routeFiles;
239
        $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
            $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
        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
            $placeholder = [$placeholder => $pattern];
325
        }
326
327
        $this->placeholders = array_merge($this->placeholders, $placeholder);
328
329
        return $this;
330
    }
331
332
    /**
333
     * {@inheritDoc}
334
     */
335
    public function setDefaultNamespace(string $value): self
336
    {
337
        $this->defaultNamespace = esc(strip_tags($value));
338
        $this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';
339
340
        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
        $this->translateURIDashes = $value;
369
370
        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
            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
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
425
426
        $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
                continue;
432
            }
433
434
            include_once $file;
435
        }
436
437
        $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
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
459
    }
460
461
    /**
462
     * {@inheritDoc}
463
     */
464
    public function getDefaultMethod(): string
465
    {
466
        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
        return $this->translateURIDashes;
488
    }
489
490
    /**
491
     * {@inheritDoc}
492
     */
493
    public function shouldAutoRoute(): bool
494
    {
495
        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
        $this->prioritize = $enabled;
504
505
        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
        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
            $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
        $this->discoverRoutes();
569
570
        $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
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
575
576
            foreach ($collection as $routeKey => $r) {
577
                $routes[$routeKey] = $r['handler'];
578
            }
579
        }
580
581
        // tri des routes par priorité
582
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
583
            $order = [];
584
585
            foreach ($routes as $key => $value) {
586
                $key                    = $key === '/' ? $key : ltrim($key, '/ ');
587
                $priority               = $this->getRoutesOptions($key, $verb)['priority'] ?? 0;
588
                $order[$priority][$key] = $value;
589
            }
590
591
            ksort($order);
592
            $routes = array_merge(...$order);
593
        }
594
595
        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
        $options = $this->loadRoutesOptions($verb);
604
605
        return $from ? $options[$from] ?? [] : $options;
606
    }
607
608
    /**
609
     * {@inheritDoc}
610
     */
611
    public function getHTTPVerb(): string
612
    {
613
        return $this->HTTPVerb;
614
    }
615
616
    /**
617
     * {@inheritDoc}
618
     */
619
    public function setHTTPVerb(string $verb): self
620
    {
621
        $this->HTTPVerb = strtolower($verb);
622
623
        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
            $this->add($from, $to, $options);
635
        }
636
637
        return $this;
638
    }
639
640
    /**
641
     * {@inheritDoc}
642
     */
643
    public function add(string $from, $to, ?array $options = null): self
644
    {
645
        $this->create('*', $from, $to, $options);
646
647
        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
        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
        $oldGroup   = $this->group ?: '';
736
        $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
        $this->group = implode('/', array_unique(explode('/', $name ? (rtrim($oldGroup, '/') . '/' . ltrim($name, '/')) : $oldGroup)));
742
743
        $callback = array_pop($params);
744
745
        if ($params && is_array($params[0])) {
746
            $this->currentOptions = array_shift($params);
747
            $options              = array_shift($params);
748
749
            if (isset($options['middlewares']) || isset($options['middleware'])) {
750
                $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
            );
759
        }
760
761
        if (is_callable($callback)) {
762
            $callback($this);
763
        }
764
765
        $this->group          = $oldGroup;
766
        $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
        $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
            $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
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
828
829
        // On s'assure de capturer les références arrière
830
        $id = '(' . trim($id, '()') . ')';
831
832
        $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
            $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
        $routeName = $name;
845
        if (isset($options['as']) || isset($options['name'])) {
846
            $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
            ]);
854
        }
855
        if (in_array('new', $methods, true)) {
856
            $this->get($name . '/new', $newName . '::new', $options + [
857
                'as' => $routeName . '.new',
858
            ]);
859
        }
860
        if (in_array('edit', $methods, true)) {
861
            $this->get($name . '/' . $id . '/edit', $newName . '::edit/$1', $options + [
862
                'as' => $routeName . '.edit',
863
            ]);
864
        }
865
        if (in_array('show', $methods, true)) {
866
            $this->get($name . '/' . $id, $newName . '::show/$1', $options + [
867
                'as' => $routeName . '.show',
868
            ]);
869
        }
870
        if (in_array('create', $methods, true)) {
871
            $this->post($name, $newName . '::create', $options + [
872
                'as' => $routeName . '.create',
873
            ]);
874
        }
875
        if (in_array('update', $methods, true)) {
876
            $this->match(['put', 'patch'], $name . '/' . $id, $newName . '::update/$1', $options + [
877
                'as' => $routeName . '.update',
878
            ]);
879
        }
880
        if (in_array('delete', $methods, true)) {
881
            $this->delete($name . '/' . $id, $newName . '::delete/$1', $options + [
882
                'as' => $routeName . '.delete',
883
            ]);
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
        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
            throw new InvalidArgumentException('Vous devez fournir les paramètres : $from, $to.');
1029
        }
1030
1031
        foreach ($verbs as $verb) {
1032
            $verb = strtolower($verb);
1033
1034
            $this->{$verb}($from, $to, $options);
1035
        }
1036
1037
        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
        $this->create('get', $from, $to, $options);
1048
1049
        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
        $this->create('post', $from, $to, $options);
1060
1061
        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
        $this->create('put', $from, $to, $options);
1072
1073
        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
        $this->create('delete', $from, $to, $options);
1084
1085
        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
        $this->create('patch', $from, $to, $options);
1108
1109
        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
        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
        $options = $this->loadRoutesOptions($verb);
1264
1265
        $middlewares = $options[$search]['middlewares'] ?? ($options[$search]['middleware'] ?? []);
1266
1267
        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
        $verb = strtolower($verb ?: $this->getHTTPVerb());
1332
1333
        $options = $this->routesOptions[$verb] ?? [];
1334
1335
        if (isset($this->routesOptions['*'])) {
1336
            foreach ($this->routesOptions['*'] as $key => $val) {
1337
                if (isset($options[$key])) {
1338
                    $extraOptions  = array_diff_key($val, $options[$key]);
1339
                    $options[$key] = array_merge($options[$key], $extraOptions);
1340
                } else {
1341
                    $options[$key] = $val;
1342
                }
1343
            }
1344
        }
1345
1346
        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
        $overwrite = false;
1359
        $prefix    = $this->group === null ? '' : $this->group . '/';
1360
1361
        $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
            $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
            $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
            $to = $this->processArrayCallableSyntax($from, $to);
1376
        }
1377
1378
        $options = array_merge($this->currentOptions ?? [], $options ?? []);
1379
1380
        if (isset($options['middleware'])) {
1381
            if (! isset($options['middlewares'])) {
1382
                $options['middlewares'] = (array) $options['middleware'];
1383
            }
1384
            unset($options['middleware']);
1385
        }
1386
1387
        if (is_string($to) && isset($options['controller'])) {
1388
            $to = str_replace($options['controller'] . '::', '', $to);
1389
            $to = str_replace($this->defaultNamespace, '', $options['controller']) . '::' . $to;
1390
        }
1391
1392
        // Détection de priorité de routage
1393
        if (isset($options['priority'])) {
1394
            $options['priority'] = abs((int) $options['priority']);
1395
1396
            if ($options['priority'] > 0) {
1397
                $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
        $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
            $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
                $namespace = $options['namespace'] ?? $this->defaultNamespace;
1452
                $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
            $to = '\\' . ltrim($to, '\\');
1457
        }
1458
1459
        $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
        $routeKeyExists = isset($this->routes[$verb][$routeKey]);
1467
        if ((isset($this->routesNames[$verb][$name]) || $routeKeyExists) && ! $overwrite) {
1468
            return;
1469
        }
1470
1471
        $this->routes[$verb][$routeKey] = [
1472
            'name'    => $name,
1473
            'handler' => $to,
1474
            'from'    => $from,
1475
        ];
1476
        $this->routesOptions[$verb][$routeKey] = $options;
1477
        $this->routesNames[$verb][$name]       = $routeKey;
1478
1479
        // C'est une redirection ?
1480
        if (isset($options['redirect']) && is_numeric($options['redirect'])) {
1481
            $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
        $name = trim($name, '/');
1580
1581
        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