Test Failed
Push — main ( 57b19c...1fe2cd )
by Dimitri
15:57
created

RouteCollection::getDefaultNamespace()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 1
cp 0
crap 2
rs 10
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
        $this->httpHost = env('HTTP_HOST');
231
232
        // Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
233
        $this->defaultNamespace   = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
234
        $this->defaultController  = $routing->default_controller ?: $this->defaultController;
235
        $this->defaultMethod      = $routing->default_method ?: $this->defaultMethod;
236
        $this->translateURIDashes = $routing->translate_uri_dashes ?: $this->translateURIDashes;
237
        $this->override404        = $routing->fallback ?: $this->override404;
238
        $this->autoRoute          = $routing->auto_route ?: $this->autoRoute;
239
        $this->routeFiles         = $routing->route_files ?: $this->routeFiles;
240
        $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
            $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
            $placeholder = [$placeholder => $pattern];
326
        }
327
328
        $this->placeholders = array_merge($this->placeholders, $placeholder);
329
330
        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
        $this->defaultController = esc(strip_tags($value));
350
351
        return $this;
352
    }
353
354
    /**
355
     * {@inheritDoc}
356
     */
357
    public function setDefaultMethod(string $value): self
358
    {
359
        $this->defaultMethod = esc(strip_tags($value));
360
361
        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
            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
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
426
427
        $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
                continue;
433
            }
434
435
            include_once $file;
436
        }
437
438
        $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
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
460
    }
461
462
    /**
463
     * {@inheritDoc}
464
     */
465
    public function getDefaultMethod(): string
466
    {
467
        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
            $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
        $this->discoverRoutes();
570
571
        $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
            $collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
576
577
            foreach ($collection as $routeKey => $r) {
578
                $routes[$routeKey] = $r['handler'];
579
            }
580
        }
581
582
        // tri des routes par priorité
583
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
584
            $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
        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
        $options = $this->loadRoutesOptions($verb);
605
606
        return $from ? $options[$from] ?? [] : $options;
607
    }
608
609
    /**
610
     * {@inheritDoc}
611
     */
612
    public function getHTTPVerb(): string
613
    {
614
        return $this->HTTPVerb;
615
    }
616
617
    /**
618
     * {@inheritDoc}
619
     */
620
    public function setHTTPVerb(string $verb): self
621
    {
622
        $this->HTTPVerb = strtolower($verb);
623
624
        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
        $this->create('*', $from, $to, $options);
647
648
        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
        $oldGroup   = $this->group ?: '';
737
        $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
        $this->group = $name ? trim($oldGroup . '/' . $name, '/') : $oldGroup;
743
744
        $callback = array_pop($params);
745
746
        if ($params && is_array($params[0])) {
747
            $options = array_shift($params);
748
749
            if (isset($options['middlewares']) || isset($options['middleware'])) {
750
                $currentMiddlewares     = (array) ($this->currentOptions['middlewares'] ?? []);
751
                $options['middlewares'] = array_merge($currentMiddlewares, (array) ($options['middlewares'] ?? $options['middleware']));
752
            }
753
754
            // Fusionner les options autres que les middlewares.
755
            $this->currentOptions = array_merge(
756
                $this->currentOptions ?: [],
757
                $options ?: [],
758
            );
759
        }
760
761
        if (is_callable($callback)) {
762
            $callback($this);
763
        }
764
765
        $this->group          = $oldGroup;
766
        $this->currentOptions = $oldOptions;
767
    }
768
769
    /*
770
     * ------------------------------------------------- -------------------
771
     * Routage basé sur les verbes HTTP
772
     * ------------------------------------------------- -------------------
773
     * Le routage fonctionne ici car, comme le fichier de configuration des routes est lu,
774
     * les différentes routes basées sur le verbe HTTP ne seront ajoutées qu'à la mémoire en mémoire
775
     * routes s'il s'agit d'un appel qui doit répondre à ce verbe.
776
     *
777
     * Le tableau d'options est généralement utilisé pour transmettre un 'as' ou var, mais peut
778
     * être étendu à l'avenir. Voir le docblock pour la méthode 'add' ci-dessus pour
779
     * liste actuelle des options disponibles dans le monde.*/
780
781
    /**
782
     * Crée une collection d'itinéraires basés sur HTTP-verb pour un contrôleur.
783
     *
784
     * Options possibles :
785
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
786
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
787
     * 'websafe' - - '1' si seuls les verbes HTTP GET et POST sont pris en charge
788
     *
789
     * Exemple:
790
     *
791
     *      $route->resource('photos');
792
     *
793
     *      // Genère les routes suivantes:
794
     *      HTTP Verb | Path        | Action        | Used for...
795
     *      ----------+-------------+---------------+-----------------
796
     *      GET         /photos             index           un tableau d'objets photo
797
     *      GET         /photos/new         new             un objet photo vide, avec des propriétés par défaut
798
     *      GET         /photos/{id}/edit   edit            un objet photo spécifique, propriétés modifiables
799
     *      GET         /photos/{id}        show            un objet photo spécifique, toutes les propriétés
800
     *      POST        /photos             create          un nouvel objet photo, à ajouter à la ressource
801
     *      DELETE      /photos/{id}        delete          supprime l'objet photo spécifié
802
     *      PUT/PATCH   /photos/{id}        update          propriétés de remplacement pour la photo existante
803
     *
804
     *  Si l'option 'websafe' est présente, les chemins suivants sont également disponibles :
805
     *
806
     *      POST		/photos/{id}/delete delete
807
     *      POST        /photos/{id}        update
808
     *
809
     * @param string $name    Le nom de la ressource/du contrôleur vers lequel router.
810
     * @param array  $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
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
818
819
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
820
        // valeur de $name avec le nom du nouveau contrôleur.
821
        if (isset($options['controller'])) {
822
            $newName = ucfirst(esc(strip_tags($options['controller'])));
0 ignored issues
show
Bug introduced by
It seems like esc(strip_tags($options['controller'])) can also be of type array; however, parameter $string of ucfirst() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

822
            $newName = ucfirst(/** @scrutinizer ignore-type */ esc(strip_tags($options['controller'])));
Loading history...
823
            unset($options['controller']);
824
        }
825
826
        $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
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
831
832
        // On s'assure de capturer les références arrière
833
        $id = '(' . trim($id, '()') . ')';
834
835
        $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
            $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
        $routeName = $name;
848
        if (isset($options['as']) || isset($options['name'])) {
849
            $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
            ]);
857
        }
858
        if (in_array('new', $methods, true)) {
859
            $this->get($name . '/new', $newName . '::new', $options + [
860
                'as' => $routeName . '.new',
861
            ]);
862
        }
863
        if (in_array('edit', $methods, true)) {
864
            $this->get($name . '/' . $id . '/edit', $newName . '::edit/$1', $options + [
865
                'as' => $routeName . '.edit',
866
            ]);
867
        }
868
        if (in_array('show', $methods, true)) {
869
            $this->get($name . '/' . $id, $newName . '::show/$1', $options + [
870
                'as' => $routeName . '.show',
871
            ]);
872
        }
873
        if (in_array('create', $methods, true)) {
874
            $this->post($name, $newName . '::create', $options + [
875
                'as' => $routeName . '.create',
876
            ]);
877
        }
878
        if (in_array('update', $methods, true)) {
879
            $this->match(['put', 'patch'], $name . '/' . $id, $newName . '::update/$1', $options + [
880
                'as' => $routeName . '.update',
881
            ]);
882
        }
883
        if (in_array('delete', $methods, true)) {
884
            $this->delete($name . '/' . $id, $newName . '::delete/$1', $options + [
885
                'as' => $routeName . '.delete',
886
            ]);
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
        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
        $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
            $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
        $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
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
951
952
        // On s'assure de capturer les références arrière
953
        $id = '(' . trim($id, '()') . ')';
954
955
        $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
            $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
        $routeName = $name;
968
        if (isset($options['as']) || isset($options['name'])) {
969
            $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
            ]);
977
        }
978
        if (in_array('new', $methods, true)) {
979
            $this->get($name . '/new', $newName . '::new', $options + [
980
                'as' => $routeName . '.new',
981
            ]);
982
        }
983
        if (in_array('edit', $methods, true)) {
984
            $this->get($name . '/edit/' . $id, $newName . '::edit/$1', $options + [
985
                'as' => $routeName . '.edit',
986
            ]);
987
        }
988
        if (in_array('update', $methods, true)) {
989
            $this->post($name . '/update/' . $id, $newName . '::update/$1', $options + [
990
                'as' => $routeName . '.update',
991
            ]);
992
        }
993
        if (in_array('remove', $methods, true)) {
994
            $this->get($name . '/remove/' . $id, $newName . '::remove/$1', $options + [
995
                'as' => $routeName . '.remove',
996
            ]);
997
        }
998
        if (in_array('delete', $methods, true)) {
999
            $this->post($name . '/delete/' . $id, $newName . '::delete/$1', $options + [
1000
                'as' => $routeName . '.delete',
1001
            ]);
1002
        }
1003
        if (in_array('create', $methods, true)) {
1004
            $this->post($name . '/create', $newName . '::create', $options + [
1005
                'as' => $routeName . '.create',
1006
            ]);
1007
            $this->post($name, $newName . '::create', $options + [
1008
                'as' => $routeName . '.store',
1009
            ]);
1010
        }
1011
        if (in_array('show', $methods, true)) {
1012
            $this->get($name . '/show/' . $id, $newName . '::show/$1', $options + [
1013
                'as' => $routeName . '.view',
1014
            ]);
1015
            $this->get($name . '/' . $id, $newName . '::show/$1', $options + [
1016
                'as' => $routeName . '.show',
1017
            ]);
1018
        }
1019
1020
        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
            throw new InvalidArgumentException('Vous devez fournir les paramètres : $from, $to.');
1035
        }
1036
1037
        foreach ($verbs as $verb) {
1038
            $verb = strtolower($verb);
1039
1040
            $this->{$verb}($from, $to, $options);
1041
        }
1042
1043
        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
        $this->create('get', $from, $to, $options);
1054
1055
        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
        $this->create('post', $from, $to, $options);
1066
1067
        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
        $this->create('put', $from, $to, $options);
1078
1079
        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
        $this->create('delete', $from, $to, $options);
1090
1091
        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
        $this->create('patch', $from, $to, $options);
1114
1115
        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
        $routeOptions = array_merge($options, ['view' => $view]);
1165
1166
        $this->create('get', $from, $to, $routeOptions);
1167
1168
        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
        $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
                $routeKey = $collection[$name];
1198
1199
                $from = $this->routes[$verb][$routeKey]['from'];
1200
1201
                return $this->buildReverseRoute($from, $params);
1202
            }
1203
        }
1204
1205
        // Ajoutez l'espace de noms par défaut si nécessaire.
1206
        $namespace = trim($this->defaultNamespace, '\\') . '\\';
1207
        if (
1208
            substr($search, 0, 1) !== '\\'
1209
            && substr($search, 0, strlen($namespace)) !== $namespace
1210
        ) {
1211
            $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
                $to   = $route['handler'];
1219
                $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
                $to     = ltrim($to, '\\');
1229
                $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
                    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
                return $this->buildReverseRoute($from, $params);
1244
            }
1245
        }
1246
1247
        // Si nous sommes toujours là, alors nous n'avons pas trouvé de correspondance.
1248
        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
        $locale = null;
1287
1288
        // Retrouvez l'ensemble de nos rétro-références dans le parcours d'origine.
1289
        preg_match_all('/\(([^)]+)\)/', $from, $matches);
1290
1291
        if (empty($matches[0])) {
1292
            if (str_contains($from, '{locale}')) {
1293
                $locale = $params[0] ?? null;
1294
            }
1295
1296
            $from = $this->replaceLocale($from, $locale);
1297
1298
            return '/' . ltrim($from, '/');
1299
        }
1300
1301
        // Les paramètres régionaux sont passés ?
1302
        $placeholderCount = count($matches[0]);
1303
        if (count($params) > $placeholderCount) {
1304
            $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
                );
1313
            }
1314
1315
            // Supprimez `(:` et `)` lorsque $placeholder est un espace réservé.
1316
            $placeholderName = substr($placeholder, 2, -1);
1317
            // ou peut-être que $placeholder n'est pas un espace réservé, mais une regex.
1318
            $pattern = $this->placeholders[$placeholderName] ?? $placeholder;
1319
1320
            if (! preg_match('#^' . $pattern . '$#u', (string) $params[$index])) {
1321
                throw RouterException::invalidParameterType();
1322
            }
1323
1324
            // Assurez-vous que le paramètre que nous insérons correspond au type de paramètre attendu.
1325
            $pos  = strpos($from, $placeholder);
1326
            $from = substr_replace($from, $params[$index], $pos, strlen($placeholder));
1327
        }
1328
1329
        $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
        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
        $verb = strtolower($verb ?: $this->getHTTPVerb());
1340
1341
        $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
        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
        $overwrite = false;
1367
        $prefix    = $this->group === null ? '' : $this->group . '/';
1368
1369
        $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
            $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
            $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
            $to = $this->processArrayCallableSyntax($from, $to);
1384
        }
1385
1386
        $options = array_merge($this->currentOptions ?? [], $options ?? []);
1387
1388
        if (isset($options['middleware'])) {
1389
            if (! isset($options['middlewares'])) {
1390
                $options['middlewares'] = (array) $options['middleware'];
1391
            }
1392
            unset($options['middleware']);
1393
        }
1394
1395
        if (is_string($to) && isset($options['controller'])) {
1396
            $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
            $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
        $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
            $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
                $namespace = $options['namespace'] ?? $this->defaultNamespace;
1460
                $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
            $to = '\\' . ltrim($to, '\\');
1465
        }
1466
1467
        $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
        $routeKeyExists = isset($this->routes[$verb][$routeKey]);
1475
        if ((isset($this->routesNames[$verb][$name]) || $routeKeyExists) && ! $overwrite) {
1476
            return;
1477
        }
1478
1479
        $this->routes[$verb][$routeKey] = [
1480
            'name'    => $name,
1481
            'handler' => $to,
1482
            'from'    => $from,
1483
        ];
1484
        $this->routesOptions[$verb][$routeKey] = $options;
1485
        $this->routesNames[$verb][$name]       = $routeKey;
1486
1487
        // C'est une redirection ?
1488
        if (isset($options['redirect']) && is_numeric($options['redirect'])) {
1489
            $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
        $name = trim($name, '/');
1588
1589
        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
            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