Test Failed
Push — main ( 88b8ff...27a594 )
by Dimitri
03:42
created

RouteCollection::reverseRoute()   B

Complexity

Conditions 11
Paths 26

Size

Total Lines 63
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 11.7975

Importance

Changes 3
Bugs 2 Features 0
Metric Value
cc 11
eloc 27
c 3
b 2
f 0
nc 26
nop 2
dl 0
loc 63
ccs 13
cts 16
cp 0.8125
crap 11.7975
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Router;
13
14
use BlitzPHP\Container\Services;
15
use BlitzPHP\Contracts\Autoloader\LocatorInterface;
16
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
17
use BlitzPHP\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 13
        $this->httpHost = env('HTTP_HOST');
230
231
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
232 13
        $this->defaultNamespace   = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
233 13
        $this->defaultController  = $routing->default_controller ?: $this->defaultController;
234 13
        $this->defaultMethod      = $routing->default_method ?: $this->defaultMethod;
235 13
        $this->translateURIDashes = $routing->translate_uri_dashes ?: $this->translateURIDashes;
236 13
        $this->override404        = $routing->fallback ?: $this->override404;
237 13
        $this->autoRoute          = $routing->auto_route ?: $this->autoRoute;
238 13
        $this->routeFiles         = $routing->route_files ?: $this->routeFiles;
239 13
        $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 13
            $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 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
        $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 4
        $this->defaultController = esc(strip_tags($value));
349
350 4
        return $this;
351
    }
352
353
    /**
354
     * {@inheritDoc}
355
     */
356
    public function setDefaultMethod(string $value): self
357
    {
358 2
        $this->defaultMethod = esc(strip_tags($value));
359
360 2
        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 2
            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 10
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
425
426 10
        $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 10
                continue;
432
            }
433
434
            include_once $file;
435
        }
436
437 10
        $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 4
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
459
    }
460
461
    /**
462
     * {@inheritDoc}
463
     */
464
    public function getDefaultMethod(): string
465
    {
466 2
        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 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 10
        $this->discoverRoutes();
569
570 10
        $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 10
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
575
576
            foreach ($collection as $routeKey => $r) {
577 10
                $routes[$routeKey] = $r['handler'];
578
            }
579
        }
580
581
        // tri des routes par priorité
582
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
583 10
            $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 10
        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 4
        $options = $this->loadRoutesOptions($verb);
604
605 4
        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 10
        $this->HTTPVerb = strtolower($verb);
622
623 10
        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 10
        $this->create('*', $from, $to, $options);
646
647 10
        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->routesNames['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 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 = $name ? trim($oldGroup . '/' . $name, '/') : $oldGroup;
742
743 2
        $callback = array_pop($params);
744
745
        if ($params && is_array($params[0])) {
746 2
            $options = array_shift($params);
747
748
            if (isset($options['middlewares']) || isset($options['middleware'])) {
749 2
                $currentMiddlewares     = (array) ($this->currentOptions['middlewares'] ?? []);
750 2
                $options['middlewares'] = array_merge($currentMiddlewares, (array) ($options['middlewares'] ?? $options['middleware']));
751
            }
752
753
            // Fusionner les options autres que les middlewares.
754
            $this->currentOptions = array_merge(
755
                $this->currentOptions ?: [],
756
                $options ?: [],
757 2
            );
758
        }
759
760
        if (is_callable($callback)) {
761 2
            $callback($this);
762
        }
763
764 2
        $this->group          = $oldGroup;
765 2
        $this->currentOptions = $oldOptions;
766
    }
767
768
    /*
769
     * ------------------------------------------------- -------------------
770
     * Routage basé sur les verbes HTTP
771
     * ------------------------------------------------- -------------------
772
     * Le routage fonctionne ici car, comme le fichier de configuration des routes est lu,
773
     * les différentes routes basées sur le verbe HTTP ne seront ajoutées qu'à la mémoire en mémoire
774
     * routes s'il s'agit d'un appel qui doit répondre à ce verbe.
775
     *
776
     * Le tableau d'options est généralement utilisé pour transmettre un 'as' ou var, mais peut
777
     * être étendu à l'avenir. Voir le docblock pour la méthode 'add' ci-dessus pour
778
     * liste actuelle des options disponibles dans le monde.*/
779
780
    /**
781
     * Crée une collection d'itinéraires basés sur HTTP-verb pour un contrôleur.
782
     *
783
     * Options possibles :
784
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
785
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
786
     * 'websafe' - - '1' si seuls les verbes HTTP GET et POST sont pris en charge
787
     *
788
     * Exemple:
789
     *
790
     *      $route->resource('photos');
791
     *
792
     *      // Genère les routes suivantes:
793
     *      HTTP Verb | Path        | Action        | Used for...
794
     *      ----------+-------------+---------------+-----------------
795
     *      GET         /photos             index           un tableau d'objets photo
796
     *      GET         /photos/new         new             un objet photo vide, avec des propriétés par défaut
797
     *      GET         /photos/{id}/edit   edit            un objet photo spécifique, propriétés modifiables
798
     *      GET         /photos/{id}        show            un objet photo spécifique, toutes les propriétés
799
     *      POST        /photos             create          un nouvel objet photo, à ajouter à la ressource
800
     *      DELETE      /photos/{id}        delete          supprime l'objet photo spécifié
801
     *      PUT/PATCH   /photos/{id}        update          propriétés de remplacement pour la photo existante
802
     *
803
     *  Si l'option 'websafe' est présente, les chemins suivants sont également disponibles :
804
     *
805
     *      POST		/photos/{id}/delete delete
806
     *      POST        /photos/{id}        update
807
     *
808
     * @param string $name    Le nom de la ressource/du contrôleur vers lequel router.
809
     * @param array  $options Une liste des façons possibles de personnaliser le routage.
810
     */
811
    public function resource(string $name, array $options = []): self
812
    {
813
        // Afin de permettre la personnalisation de la route, le
814
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
815
        // pour stocker les valeurs.
816 2
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
817
818
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
819
        // valeur de $name avec le nom du nouveau contrôleur.
820
        if (isset($options['controller'])) {
821 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

821
            $newName = ucfirst(/** @scrutinizer ignore-type */ esc(strip_tags($options['controller'])));
Loading history...
822 2
            unset($options['controller']);
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 . '.delete',
891
                ]);
892
            }
893
            if (in_array('update', $methods, true)) {
894
                $this->post($name . '/' . $id, $newName . '::update/$1', $options + [
895
                    'as' => $routeName . '.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  $options Une liste des façons possibles de personnaliser le routage.
928
     */
929
    public function presenter(string $name, array $options = []): self
930
    {
931
        // Afin de permettre la personnalisation de la route, le
932
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
933
        // pour stocker les valeurs.
934 2
        $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 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

939
            $newName = ucfirst(/** @scrutinizer ignore-type */ esc(strip_tags($options['controller'])));
Loading history...
940
            unset($options['controller']);
941
        }
942
943
        // Afin de permettre la personnalisation des valeurs d'identifiant autorisées
944
        // nous avons besoin d'un endroit pour les stocker.
945 2
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
946
947
        // On s'assure de capturer les références arrière
948 2
        $id = '(' . trim($id, '()') . ')';
949
950 2
        $methods = isset($options['only']) ? (is_string($options['only']) ? explode(',', $options['only']) : $options['only']) : ['index', 'show', 'new', 'create', 'edit', 'update', 'remove', 'delete'];
951
952
        if (isset($options['except'])) {
953 2
            $options['except'] = is_array($options['except']) ? $options['except'] : explode(',', $options['except']);
954
955
            foreach ($methods as $i => $method) {
956
                if (in_array($method, $options['except'], true)) {
957
                    unset($methods[$i]);
958
                }
959
            }
960
        }
961
962 2
        $routeName = $name;
963
        if (isset($options['as']) || isset($options['name'])) {
964 2
            $routeName = trim($options['as'] ?? $options['name'], ' .');
965
            unset($options['name'], $options['as']);
966
        }
967
968
        if (in_array('index', $methods, true)) {
969
            $this->get($name, $newName . '::index', $options + [
970
                'as' => $routeName . '.index',
971 2
            ]);
972
        }
973
        if (in_array('new', $methods, true)) {
974
            $this->get($name . '/new', $newName . '::new', $options + [
975
                'as' => $routeName . '.new',
976 2
            ]);
977
        }
978
        if (in_array('edit', $methods, true)) {
979
            $this->get($name . '/edit/' . $id, $newName . '::edit/$1', $options + [
980
                'as' => $routeName . '.edit',
981 2
            ]);
982
        }
983
        if (in_array('update', $methods, true)) {
984
            $this->post($name . '/update/' . $id, $newName . '::update/$1', $options + [
985
                'as' => $routeName . '.update',
986 2
            ]);
987
        }
988
        if (in_array('remove', $methods, true)) {
989
            $this->get($name . '/remove/' . $id, $newName . '::remove/$1', $options + [
990
                'as' => $routeName . '.remove',
991 2
            ]);
992
        }
993
        if (in_array('delete', $methods, true)) {
994
            $this->post($name . '/delete/' . $id, $newName . '::delete/$1', $options + [
995
                'as' => $routeName . '.delete',
996 2
            ]);
997
        }
998
        if (in_array('create', $methods, true)) {
999
            $this->post($name . '/create', $newName . '::create', $options + [
1000
                'as' => $routeName . '.create',
1001 2
            ]);
1002
            $this->post($name, $newName . '::create', $options + [
1003
                'as' => $routeName . '.store',
1004 2
            ]);
1005
        }
1006
        if (in_array('show', $methods, true)) {
1007
            $this->get($name . '/show/' . $id, $newName . '::show/$1', $options + [
1008
                'as' => $routeName . '.view',
1009 2
            ]);
1010
            $this->get($name . '/' . $id, $newName . '::show/$1', $options + [
1011
                'as' => $routeName . '.show',
1012 2
            ]);
1013
        }
1014
1015 2
        return $this;
1016
    }
1017
1018
    /**
1019
     * Spécifie une seule route à faire correspondre pour plusieurs verbes HTTP.
1020
     *
1021
     * Exemple:
1022
     *  $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
1023
     *
1024
     * @param array|Closure|string $to
1025
     */
1026
    public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): self
1027
    {
1028
        if (empty($from) || empty($to)) {
1029 4
            throw new InvalidArgumentException('Vous devez fournir les paramètres : $from, $to.');
1030
        }
1031
1032
        foreach ($verbs as $verb) {
1033 4
            $verb = strtolower($verb);
1034
1035 4
            $this->{$verb}($from, $to, $options);
1036
        }
1037
1038 4
        return $this;
1039
    }
1040
1041
    /**
1042
     * Spécifie une route qui n'est disponible que pour les requêtes GET.
1043
     *
1044
     * @param array|Closure|string $to
1045
     */
1046
    public function get(string $from, $to, ?array $options = null): self
1047
    {
1048 8
        $this->create('get', $from, $to, $options);
1049
1050 8
        return $this;
1051
    }
1052
1053
    /**
1054
     * Spécifie une route qui n'est disponible que pour les requêtes POST.
1055
     *
1056
     * @param array|Closure|string $to
1057
     */
1058
    public function post(string $from, $to, ?array $options = null): self
1059
    {
1060 4
        $this->create('post', $from, $to, $options);
1061
1062 4
        return $this;
1063
    }
1064
1065
    /**
1066
     * Spécifie une route qui n'est disponible que pour les requêtes PUT.
1067
     *
1068
     * @param array|Closure|string $to
1069
     */
1070
    public function put(string $from, $to, ?array $options = null): self
1071
    {
1072 4
        $this->create('put', $from, $to, $options);
1073
1074 4
        return $this;
1075
    }
1076
1077
    /**
1078
     * Spécifie une route qui n'est disponible que pour les requêtes DELETE.
1079
     *
1080
     * @param array|Closure|string $to
1081
     */
1082
    public function delete(string $from, $to, ?array $options = null): self
1083
    {
1084 2
        $this->create('delete', $from, $to, $options);
1085
1086 2
        return $this;
1087
    }
1088
1089
    /**
1090
     * Spécifie une route qui n'est disponible que pour les requêtes HEAD.
1091
     *
1092
     * @param array|Closure|string $to
1093
     */
1094
    public function head(string $from, $to, ?array $options = null): self
1095
    {
1096
        $this->create('head', $from, $to, $options);
1097
1098
        return $this;
1099
    }
1100
1101
    /**
1102
     * Spécifie une route qui n'est disponible que pour les requêtes PATCH.
1103
     *
1104
     * @param array|Closure|string $to
1105
     */
1106
    public function patch(string $from, $to, ?array $options = null): self
1107
    {
1108 2
        $this->create('patch', $from, $to, $options);
1109
1110 2
        return $this;
1111
    }
1112
1113
    /**
1114
     * Spécifie une route qui n'est disponible que pour les requêtes OPTIONS.
1115
     *
1116
     * @param array|Closure|string $to
1117
     */
1118
    public function options(string $from, $to, ?array $options = null): self
1119
    {
1120
        $this->create('options', $from, $to, $options);
1121
1122
        return $this;
1123
    }
1124
1125
    /**
1126
     * Spécifie une route qui n'est disponible que pour les requêtes GET et POST.
1127
     *
1128
     * @param array|Closure|string $to
1129
     */
1130
    public function form(string $from, $to, ?array $options = null): self
1131
    {
1132
        return $this->match(['GET', 'POST'], $from, $to, $options);
1133
    }
1134
1135
    /**
1136
     * Spécifie une route qui n'est disponible que pour les requêtes de ligne de commande.
1137
     *
1138
     * @param array|Closure|string $to
1139
     */
1140
    public function cli(string $from, $to, ?array $options = null): self
1141
    {
1142
        $this->create('cli', $from, $to, $options);
1143
1144
        return $this;
1145
    }
1146
1147
    /**
1148
     * Spécifie une route qui n'affichera qu'une vue.
1149
     * Ne fonctionne que pour les requêtes GET.
1150
     */
1151
    public function view(string $from, string $view, array $options = []): self
1152
    {
1153
        $to = static fn (...$data) => Services::viewer()
0 ignored issues
show
Deprecated Code introduced by
The function BlitzPHP\View\View::setOptions() has been deprecated. ( Ignorable by Annotation )

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

1153
        $to = static fn (...$data) => /** @scrutinizer ignore-deprecated */ Services::viewer()
Loading history...
1154
            ->setData(['segments' => $data] + $options, 'raw')
1155
            ->display($view)
1156
            ->setOptions($options)
1157
            ->render();
1158
1159 2
        $routeOptions = array_merge($options, ['view' => $view]);
1160
1161 2
        $this->create('get', $from, $to, $routeOptions);
1162
1163 2
        return $this;
1164
    }
1165
1166
    /**
1167
     * Limite les itinéraires à un ENVIRONNEMENT spécifié ou ils ne fonctionneront pas.
1168
     */
1169
    public function environment(string $env, Closure $callback): self
1170
    {
1171
        if (environment($env)) {
1172
            $callback($this);
1173
        }
1174
1175
        return $this;
1176
    }
1177
1178
    /**
1179
     * {@inheritDoc}
1180
     */
1181
    public function reverseRoute(string $search, ...$params)
1182
    {
1183
        if ($search === '') {
1184
            return false;
1185
        }
1186
1187 4
        $name = $this->formatRouteName($search);
1188
1189
        // Les routes nommées ont une priorité plus élevée.
1190
        foreach ($this->routesNames as $verb => $collection) {
1191
            if (array_key_exists($name, $collection)) {
1192 4
                $routeKey = $collection[$name];
1193
1194 4
                $from = $this->routes[$verb][$routeKey]['from'];
1195
1196 4
                return $this->buildReverseRoute($from, $params);
1197
            }
1198
        }
1199
1200
        // Ajoutez l'espace de noms par défaut si nécessaire.
1201 4
        $namespace = trim($this->defaultNamespace, '\\') . '\\';
1202
        if (
1203
            substr($search, 0, 1) !== '\\'
1204
            && substr($search, 0, strlen($namespace)) !== $namespace
1205
        ) {
1206 4
            $search = $namespace . $search;
1207
        }
1208
1209
        // Si ce n'est pas une route nommée, alors bouclez
1210
        // toutes les routes pour trouver une correspondance.
1211
        foreach ($this->routes as $collection) {
1212
            foreach ($collection as $route) {
1213 4
                $to   = $route['handler'];
1214 4
                $from = $route['from'];
1215
1216
                // on ignore les closures
1217
                if (! is_string($to)) {
1218
                    continue;
1219
                }
1220
1221
                // Perd toute barre oblique d'espace de noms au début des chaînes
1222
                // pour assurer une correspondance plus cohérente.$to     = ltrim($to, '\\');
1223 4
                $to     = ltrim($to, '\\');
1224 4
                $search = ltrim($search, '\\');
1225
1226
                // S'il y a une chance de correspondance, alors ce sera
1227
                // soit avec $search au début de la chaîne $to.
1228
                if (! str_starts_with($to, $search)) {
1229 4
                    continue;
1230
                }
1231
1232
                // Assurez-vous que le nombre de $params donné ici
1233
                // correspond au nombre de back-references dans la route
1234
                if (substr_count($to, '$') !== count($params)) {
1235
                    continue;
1236
                }
1237
1238 2
                return $this->buildReverseRoute($from, $params);
1239
            }
1240
        }
1241
1242
        // Si nous sommes toujours là, alors nous n'avons pas trouvé de correspondance.
1243 4
        return false;
1244
    }
1245
1246
    /**
1247
     * Vérifie une route (en utilisant le "from") pour voir si elle est filtrée ou non.
1248
     */
1249
    public function isFiltered(string $search, ?string $verb = null): bool
1250
    {
1251
        return $this->getFiltersForRoute($search, $verb) !== [];
1252
    }
1253
1254
    /**
1255
     * Renvoie les filtres qui doivent être appliqués pour un seul itinéraire, ainsi que
1256
     * avec tous les paramètres qu'il pourrait avoir. Les paramètres sont trouvés en divisant
1257
     * le nom du paramètre entre deux points pour séparer le nom du filtre de la liste des paramètres,
1258
     * et le fractionnement du résultat sur des virgules. Alors:
1259
     *
1260
     *    'role:admin,manager'
1261
     *
1262
     * a un filtre de "rôle", avec des paramètres de ['admin', 'manager'].
1263
     */
1264
    public function getFiltersForRoute(string $search, ?string $verb = null): array
1265
    {
1266
        $options = $this->loadRoutesOptions($verb);
1267
1268
        $middlewares = $options[$search]['middlewares'] ?? ($options[$search]['middleware'] ?? []);
1269
1270
        return (array) $middlewares;
1271
    }
1272
1273
    /**
1274
     * Construit une route inverse
1275
     *
1276
     * @param array $params Un ou plusieurs paramètres à transmettre à la route.
1277
     *                      Le dernier paramètre vous permet de définir la locale.
1278
     */
1279
    protected function buildReverseRoute(string $from, array $params): string
1280
    {
1281 4
        $locale = null;
1282
1283
        // Retrouvez l'ensemble de nos rétro-références dans le parcours d'origine.
1284 4
        preg_match_all('/\(([^)]+)\)/', $from, $matches);
1285
1286
        if (empty($matches[0])) {
1287
            if (str_contains($from, '{locale}')) {
1288 4
                $locale = $params[0] ?? null;
1289
            }
1290
1291 4
            $from = $this->replaceLocale($from, $locale);
1292
1293 4
            return '/' . ltrim($from, '/');
1294
        }
1295
1296
        // Les paramètres régionaux sont passés ?
1297 4
        $placeholderCount = count($matches[0]);
1298
        if (count($params) > $placeholderCount) {
1299 4
            $locale = $params[$placeholderCount];
1300
        }
1301
1302
        // Construisez notre chaîne résultante, en insérant les $params aux endroits appropriés.
1303
        foreach ($matches[0] as $index => $placeholder) {
1304
            if (! isset($params[$index])) {
1305
                throw new InvalidArgumentException(
1306
                    'Argument manquant pour "' . $placeholder . '" dans la route "' . $from . '".'
1307 4
                );
1308
            }
1309
1310
            // Supprimez `(:` et `)` lorsque $placeholder est un espace réservé.
1311 4
            $placeholderName = substr($placeholder, 2, -1);
1312
            // ou peut-être que $placeholder n'est pas un espace réservé, mais une regex.
1313 4
            $pattern = $this->placeholders[$placeholderName] ?? $placeholder;
1314
1315
            if (! preg_match('#^' . $pattern . '$#u', (string) $params[$index])) {
1316 2
                throw RouterException::invalidParameterType();
1317
            }
1318
1319
            // Assurez-vous que le paramètre que nous insérons correspond au type de paramètre attendu.
1320 4
            $pos  = strpos($from, $placeholder);
1321 4
            $from = substr_replace($from, $params[$index], $pos, strlen($placeholder));
1322
        }
1323
1324 4
        $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

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

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

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

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

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

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

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