Test Failed
Push — main ( 312655...ea0faa )
by Dimitri
03:17
created

RouteCollection::presenter()   F

Complexity

Conditions 18
Paths 12288

Size

Total Lines 89
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 18.7184

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 18
eloc 46
c 5
b 1
f 0
nc 12288
nop 2
dl 0
loc 89
ccs 20
cts 23
cp 0.8696
crap 18.7184
rs 0.7

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

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

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

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

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

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

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

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

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

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

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