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

RouteCollection::view()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1.0156

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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