Passed
Push — main ( b49c3d...f366e5 )
by Dimitri
13:14
created

RouteCollection   F

Complexity

Total Complexity 221

Size/Duplication

Total Lines 1617
Duplicated Lines 0 %

Importance

Changes 8
Bugs 2 Features 0
Metric Value
eloc 496
c 8
b 2
f 0
dl 0
loc 1617
rs 2
wmc 221

65 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 17 11
A resetRoutes() 0 13 2
A loadRoutes() 0 29 5
B buildReverseRoute() 0 48 7
A setDefaultMethod() 0 5 1
A patch() 0 5 1
A shouldUseSupportedLocalesOnly() 0 3 1
A permanentRedirect() 0 3 1
A getDefaultMethod() 0 3 1
A getRedirectCode() 0 7 2
A group() 0 22 5
A put() 0 5 1
A post() 0 5 1
A set404Override() 0 3 1
A cli() 0 5 1
F create() 0 119 30
A checkHostname() 0 8 3
A setHTTPVerb() 0 5 1
A shouldAutoRoute() 0 3 1
A getControllerName() 0 9 2
F presenter() 0 86 18
A shouldTranslateURIDashes() 0 3 1
B getRegisteredControllers() 0 25 7
A placeholder() 0 9 2
A isFiltered() 0 3 1
A getDefaultNamespace() 0 3 1
A determineCurrentSubdomain() 0 33 5
A useSupportedLocalesOnly() 0 5 1
A map() 0 7 2
A addRedirect() 0 3 1
A environment() 0 7 2
A setDefaultController() 0 5 1
A view() 0 14 1
F resource() 0 89 20
A getRoutesOptions() 0 5 2
A match() 0 13 4
A get404Override() 0 3 1
A getPlaceholders() 0 3 1
A checkSubdomains() 0 22 6
A getDefaultController() 0 3 1
A getHTTPVerb() 0 3 1
A get() 0 5 1
A redirect() 0 20 3
A delete() 0 5 1
B getRoutes() 0 37 10
A getFiltersForRoute() 0 9 1
A getMethodParams() 0 12 3
A fallback() 0 5 1
A replaceLocale() 0 18 5
A discoverRoutes() 0 21 4
A setPrioritize() 0 5 1
A setDefaultConstraint() 0 7 2
A loadRoutesOptions() 0 18 5
A setAutoRoute() 0 5 1
A head() 0 5 1
A addPlaceholder() 0 3 1
A options() 0 5 1
B processArrayCallableSyntax() 0 26 7
A setDefaultNamespace() 0 6 1
A form() 0 3 1
A add() 0 5 1
A setTranslateURIDashes() 0 5 1
B reverseRoute() 0 57 10
A formatRouteName() 0 5 1
A isRedirect() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like RouteCollection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RouteCollection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Router;
13
14
use BlitzPHP\Autoloader\Locator;
15
use BlitzPHP\Container\Services;
16
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
17
use BlitzPHP\Exceptions\RouterException;
18
use Closure;
19
use InvalidArgumentException;
20
21
class RouteCollection implements RouteCollectionInterface
22
{
23
    /**
24
     * L'espace de noms à ajouter à tous les contrôleurs.
25
     * Par défaut, les espaces de noms globaux (\)
26
     */
27
    protected string $defaultNamespace = '\\';
28
29
    /**
30
     * Le nom du contrôleur par défaut à utiliser
31
     * lorsqu'aucun autre contrôleur n'est spécifié.
32
     *
33
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
34
     */
35
    protected string $defaultController = 'Home';
36
37
    /**
38
     * Le nom de la méthode par défaut à utiliser
39
     * lorsqu'aucune autre méthode n'a été spécifiée.
40
     *
41
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
42
     */
43
    protected string $defaultMethod = 'index';
44
45
    /**
46
     * L'espace réservé utilisé lors du routage des "ressources"
47
     * lorsqu'aucun autre espace réservé n'a été spécifié.
48
     */
49
    protected string $defaultPlaceholder = 'any';
50
51
    /**
52
     * S'il faut convertir les tirets en traits de soulignement dans l'URI.
53
     *
54
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
55
     */
56
    protected bool $translateURIDashes = true;
57
58
    /**
59
     * S'il faut faire correspondre l'URI aux contrôleurs
60
     * lorsqu'il ne correspond pas aux itinéraires définis.
61
     *
62
     * Non utilisé ici. Valeur d'intercommunication pour la classe Routeur.
63
     */
64
    protected bool $autoRoute = false;
65
66
    /**
67
     * Un appelable qui sera affiché
68
     * lorsque la route ne peut pas être matchée.
69
     *
70
     * @var Closure|string
71
     */
72
    protected $override404;
73
74
    /**
75
     * Tableau de fichiers qui contiendrait les définitions de routes.
76
     */
77
    protected array $routeFiles = [];
78
79
    /**
80
     * Espaces réservés définis pouvant être utilisés.
81
     */
82
    protected array $placeholders = [
83
        'any'      => '.*',
84
        'segment'  => '[^/]+',
85
        'alphanum' => '[a-zA-Z0-9]+',
86
        'num'      => '[0-9]+',
87
        'alpha'    => '[a-zA-Z]+',
88
        'hash'     => '[^/]+',
89
        'slug'     => '[a-z0-9-]+',
90
        'uuid'     => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
91
    ];
92
93
    /**
94
     * Tableau de toutes les routes et leurs mappages.
95
     *
96
     * @example
97
     * ```php
98
     * [
99
     *     verb => [
100
     *         routeName => [
101
     *             'route' => [
102
     *                 routeKey(regex) => handler,
103
     *             ],
104
     *             'redirect' => statusCode,
105
     *         ]
106
     *     ],
107
     * ]
108
     * ```
109
     */
110
    protected array $routes = [
111
        '*'       => [],
112
        'options' => [],
113
        'get'     => [],
114
        'head'    => [],
115
        'post'    => [],
116
        'put'     => [],
117
        'delete'  => [],
118
        'trace'   => [],
119
        'connect' => [],
120
        'cli'     => [],
121
    ];
122
123
    /**
124
     * Tableau des noms des routes
125
	 * 
126
     * [
127
     *     verb => [
128
     *         routeName => routeKey(regex)
129
     *     ],
130
     * ]
131
     */
132
    protected array $routesNames = [
133
        '*'       => [],
134
        'options' => [],
135
        'get'     => [],
136
        'head'    => [],
137
        'post'    => [],
138
        'put'     => [],
139
        'delete'  => [],
140
        'trace'   => [],
141
        'connect' => [],
142
        'cli'     => [],
143
    ];
144
145
    /**
146
     * Tableaux des options des routes.
147
     *
148
     * @example
149
     * ```php
150
     * [
151
     *     verb => [
152
     *         routeKey(regex) => [
153
     *             key => value,
154
     *         ]
155
     *     ],
156
     * ]
157
     * ```
158
     */
159
    protected array $routesOptions = [];
160
161
    /**
162
     * La méthode actuelle par laquelle le script est appelé.
163
     */
164
    protected string $HTTPVerb = '*';
165
166
    /**
167
     * La liste par défaut des méthodes HTTP (et CLI pour l'utilisation de la ligne de commande)
168
     * qui est autorisé si aucune autre méthode n'est fournie.
169
     */
170
    protected array $defaultHTTPMethods = [
171
        'options',
172
        'get',
173
        'head',
174
        'post',
175
        'put',
176
        'delete',
177
        'trace',
178
        'connect',
179
        'cli',
180
    ];
181
182
    /**
183
     * Le nom du groupe de route courant
184
     */
185
    protected ?string $group = null;
186
187
    /**
188
     * Le sous domaine courant
189
     */
190
    protected ?string $currentSubdomain = null;
191
192
    /**
193
     * Stocke une copie des options actuelles en cours appliqué lors de la création.
194
     */
195
    protected ?array $currentOptions = null;
196
197
    /**
198
     * Un petit booster de performances.
199
     */
200
    protected bool $didDiscover = false;
201
202
    /**
203
     * Drapeau pour trier les routes par priorité.
204
     */
205
    protected bool $prioritize = false;
206
207
    /**
208
     * Indicateur de détection de priorité de route.
209
     */
210
    protected bool $prioritizeDetected = false;
211
212
    /**
213
     * Drapeau pour limiter ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
214
     */
215
    protected bool $useSupportedLocalesOnly = false;
216
217
    /**
218
     * Le nom d'hôte actuel de $_SERVER['HTTP_HOST']
219
     */
220
    private ?string $httpHost = null;
221
222
    /**
223
     * Constructor
224
	 * 
225
	 * @var Locator $locator Descripteur du localisateur de fichiers à utiliser.
226
     */
227
    public function __construct(protected Locator $locator, object $routing)
228
    {
229
        $this->httpHost = Services::request()->getEnv('HTTP_HOST');
230
231
		// Configuration basée sur le fichier de config. Laissez le fichier routes substituer.
232
        $this->defaultNamespace   = rtrim($routing->default_namespace ?: $this->defaultNamespace, '\\') . '\\';
233
        $this->defaultController  = $routing->default_controller ?: $this->defaultController;
234
        $this->defaultMethod      = $routing->default_method ?: $this->defaultMethod;
235
        $this->translateURIDashes = $routing->translate_uri_dashes ?: $this->translateURIDashes;
236
        $this->override404        = $routing->override404 ?: $this->override404;
237
        $this->autoRoute          = $routing->auto_route ?: $this->autoRoute;
238
        $this->routeFiles         = $routing->route_files ?: $this->routeFiles;
239
        $this->prioritize         = $routing->prioritize ?: $this->prioritize;
240
241
		// Normaliser la chaîne de path dans le tableau routeFiles.
242
        foreach ($this->routeFiles as $routeKey => $routesFile) {
243
            $this->routeFiles[$routeKey] = realpath($routesFile) ?: $routesFile;
244
        }
245
    }
246
247
    /**
248
     * Charge le fichier des routes principales et découvre les routes.
249
     *
250
     * Charge une seule fois sauf réinitialisation.
251
     */
252
    public function loadRoutes(string $routesFile = CONFIG_PATH . 'routes.php'): self
253
    {
254
        if ($this->didDiscover) {
255
            return $this;
256
        }
257
258
        // Incluez le fichier routesFile s'il n'existe pas. 
259
		// Ne conserver que pour les fins BC pour l'instant. 
260
        $routeFiles = $this->routeFiles;
261
        if (! in_array($routesFile, $routeFiles, true)) {
262
            $routeFiles[] = $routesFile;
263
        }
264
		
265
		// Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
266
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
267
268
		foreach ($routeFiles as $routesFile) {
269
            if (! is_file($routesFile)) {
270
				logger()->warning(sprintf('Fichier de route introuvable : "%s"', $routesFile));
0 ignored issues
show
Bug introduced by
Are you sure the usage of logger() is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
271
272
                continue;
273
            }
274
275
            require_once $routesFile;
276
        }
277
278
        $this->discoverRoutes();
279
280
        return $this;
281
    }
282
283
    /**
284
     * Réinitialisez les routes, afin qu'un cas de test puisse fournir le
285
     * ceux explicites nécessaires pour cela.
286
     */
287
    public function resetRoutes()
288
    {
289
        $this->routes = $this->routesNames = ['*' => []];
290
291
        foreach ($this->defaultHTTPMethods as $verb) {
292
			$this->routes[$verb]      = [];
293
			$this->routesNames[$verb] = [];
294
        }
295
296
		$this->routesOptions = [];
297
298
        $this->prioritizeDetected = false;
299
        $this->didDiscover        = false;
300
    }
301
302
    /**
303
     * {@inheritDoc}
304
     * 
305
     * Utilisez `placeholder` a la place
306
     */
307
    public function addPlaceholder($placeholder, ?string $pattern = null): self
308
    {
309
        return $this->placeholder($placeholder, $pattern);
310
    }
311
312
    /**
313
     * Enregistre une nouvelle contrainte auprès du système. 
314
     * 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.
315
     */
316
    public function placeholder(array|string $placeholder, ?string $pattern = null): self
317
    {
318
        if (! is_array($placeholder)) {
0 ignored issues
show
introduced by
The condition is_array($placeholder) is always true.
Loading history...
319
            $placeholder = [$placeholder => $pattern];
320
        }
321
322
        $this->placeholders = array_merge($this->placeholders, $placeholder);
323
324
        return $this;
325
    }
326
327
    /**
328
     * {@inheritDoc}
329
     */
330
    public function setDefaultNamespace(string $value): self
331
    {
332
        $this->defaultNamespace = esc(strip_tags($value));
333
        $this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';
334
335
        return $this;
336
    }
337
338
    /**
339
     * {@inheritDoc}
340
     */
341
    public function setDefaultController(string $value): self
342
    {
343
        $this->defaultController = esc(strip_tags($value));
344
345
        return $this;
346
    }
347
348
    /**
349
     * {@inheritDoc}
350
     */
351
    public function setDefaultMethod(string $value): self
352
    {
353
        $this->defaultMethod = esc(strip_tags($value));
354
355
        return $this;
356
    }
357
358
    /**
359
     * {@inheritDoc}
360
     */
361
    public function setTranslateURIDashes(bool $value): self
362
    {
363
        $this->translateURIDashes = $value;
364
365
        return $this;
366
    }
367
368
    /**
369
     * {@inheritDoc}
370
     */
371
    public function setAutoRoute(bool $value): self
372
    {
373
        $this->autoRoute = $value;
374
375
        return $this;
376
    }
377
378
    /**
379
     * {@inheritDoc}
380
     * 
381
     * Utilisez self::fallback()
382
     */
383
    public function set404Override($callable = null): self
384
    {
385
        return $this->fallback($callable);
386
    }
387
388
    /**
389
     * Définit la classe/méthode qui doit être appelée si le routage ne trouver pas une correspondance.
390
     *
391
     * @param callable|null $callable
392
     */
393
    public function fallback($callable = null): self
394
    {
395
        $this->override404 = $callable;
396
397
        return $this;
398
    }
399
400
    /**
401
     * {@inheritDoc}
402
     */
403
    public function get404Override()
404
    {
405
        return $this->override404;
406
    }
407
408
    /**
409
     * Tentera de découvrir d'éventuelles routes supplémentaires, soit par
410
     * les espaces de noms PSR4 locaux ou via des packages Composer sélectionnés.
411
     */
412
    protected function discoverRoutes()
413
    {
414
        if ($this->didDiscover) {
415
            return;
416
        }
417
418
        // Nous avons besoin de cette variable dans la portée locale pour que les fichiers de route puissent y accéder.
419
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
420
421
        $files    = $this->locator->search('Config/routes.php');
422
        
423
        foreach ($files as $file) {
424
            // N'incluez plus notre fichier principal...
425
            if (in_array($file, $this->routeFiles, true)) {
426
                continue;
427
            }
428
429
            include_once $file;
430
        }
431
432
        $this->didDiscover = true;
433
    }
434
435
    /**
436
     * Définit la contrainte par défaut à utiliser dans le système. Typiquement
437
     * à utiliser avec la méthode 'ressource'.
438
     */
439
    public function setDefaultConstraint(string $placeholder): self
440
    {
441
        if (array_key_exists($placeholder, $this->placeholders)) {
442
            $this->defaultPlaceholder = $placeholder;
443
        }
444
445
        return $this;
446
    }
447
448
    /**
449
     * {@inheritDoc}
450
     */
451
    public function getDefaultController(): string
452
    {
453
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
454
    }
455
456
    /**
457
     * {@inheritDoc}
458
     */
459
    public function getDefaultMethod(): string
460
    {
461
        return $this->defaultMethod;
462
    }
463
464
    /**
465
     * {@inheritDoc}
466
     */
467
    public function getDefaultNamespace(): string
468
    {
469
        return $this->defaultNamespace;
470
    }
471
472
    public function getPlaceholders(): array
473
    {
474
        return $this->placeholders;
475
    }
476
477
    /**
478
     *{@inheritDoc}
479
     */
480
    public function shouldTranslateURIDashes(): bool
481
    {
482
        return $this->translateURIDashes;
483
    }
484
485
    /**
486
     * {@inheritDoc}
487
     */
488
    public function shouldAutoRoute(): bool
489
    {
490
        return $this->autoRoute;
491
    }
492
493
    /**
494
     * Activer ou désactiver le tri des routes par priorité
495
     */
496
    public function setPrioritize(bool $enabled = true): self
497
    {
498
        $this->prioritize = $enabled;
499
500
        return $this;
501
    }
502
503
    /**
504
     * Définissez le drapeau qui limite ou non les routes avec l'espace réservé {locale} à App::$supportedLocales
505
     */
506
    public function useSupportedLocalesOnly(bool $useOnly): self
507
    {
508
        $this->useSupportedLocalesOnly = $useOnly;
509
510
        return $this;
511
    }
512
513
    /**
514
     * Obtenez le drapeau qui limite ou non les routes avec l'espace réservé {locale} vers App::$supportedLocales
515
     */
516
    public function shouldUseSupportedLocalesOnly(): bool
517
    {
518
        return $this->useSupportedLocalesOnly;
519
    }
520
521
    /**
522
     * {@inheritDoc}
523
     */
524
    public function getRegisteredControllers(?string $verb = '*'): array
525
    {
526
        $controllers = [];
527
528
        if ($verb === '*') {
529
            foreach ($this->defaultHTTPMethods as $tmpVerb) {
530
                foreach ($this->routes[$tmpVerb] as $route) {
531
                    $controller = $this->getControllerName($route['handler']);
532
                    if ($controller !== null) {
533
                        $controllers[] = $controller;
534
                    }
535
                }
536
            }
537
        } else {
538
            $routes = $this->getRoutes($verb);
539
540
            foreach ($routes as $handler) {
541
                $controller = $this->getControllerName($handler);
542
                if ($controller !== null) {
543
                    $controllers[] = $controller;
544
                }
545
            }
546
        }
547
548
        return array_unique($controllers);
549
    }
550
551
    /**
552
     * {@inheritDoc}
553
     */
554
    public function getRoutes(?string $verb = null, bool $includeWildcard = true): array
555
    {
556
        if (empty($verb)) {
557
            $verb = $this->getHTTPVerb();
558
        }
559
560
        // Puisqu'il s'agit du point d'entrée du routeur,
561
        // prenez un moment pour faire toute découverte de route
562
        // que nous pourrions avoir besoin de faire.
563
        $this->discoverRoutes();
564
565
        $routes     = [];
566
        if (isset($this->routes[$verb])) {
567
            // Conserve les itinéraires du verbe actuel au début afin qu'ils soient
568
            // mis en correspondance avant l'un des itinéraires génériques "add".
569
			$collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
570
571
		    foreach ($collection as $routeKey => $r) {
572
                $routes[$routeKey] = $r['handler'];
573
            }
574
        }
575
576
        // tri des routes par priorité
577
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
578
            $order = [];
579
580
            foreach ($routes as $key => $value) {
581
                $key                    = $key === '/' ? $key : ltrim($key, '/ ');
582
                $priority               = $this->getRoutesOptions($key, $verb)['priority'] ?? 0;
583
                $order[$priority][$key] = $value;
584
            }
585
586
            ksort($order);
587
            $routes = array_merge(...$order);
588
        }
589
590
        return $routes;
591
    }
592
593
    /**
594
     * Renvoie une ou toutes les options d'itinéraire
595
     */
596
    public function getRoutesOptions(?string $from = null, ?string $verb = null): array
597
    {
598
        $options = $this->loadRoutesOptions($verb);
599
600
        return $from ? $options[$from] ?? [] : $options;
601
    }
602
603
    /**
604
     * {@inheritDoc}
605
     */
606
    public function getHTTPVerb(): string
607
    {
608
        return $this->HTTPVerb;
609
    }
610
611
    /**
612
     * {@inheritDoc}
613
     */
614
    public function setHTTPVerb(string $verb): self
615
    {
616
        $this->HTTPVerb = strtolower($verb);
617
618
        return $this;
619
    }
620
621
    /**
622
     * Une méthode de raccourci pour ajouter un certain nombre d'itinéraires en une seule fois.
623
     * Il ne permet pas de définir des options sur l'itinéraire, ou de
624
     * définir la méthode utilisée.
625
     */
626
    public function map(array $routes = [], ?array $options = null): self
627
    {
628
        foreach ($routes as $from => $to) {
629
            $this->add($from, $to, $options);
630
        }
631
632
        return $this;
633
    }
634
635
    /**
636
     * {@inheritDoc}
637
     */
638
    public function add(string $from, $to, ?array $options = null): self
639
    {
640
        $this->create('*', $from, $to, $options);
641
642
        return $this;
643
    }
644
645
    /**
646
     * Ajoute une redirection temporaire d'une route à une autre. 
647
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
648
     *
649
     * @param string $from   Le modèle à comparer
650
     * @param string $to     Soit un nom de route ou un URI vers lequel rediriger
651
     * @param int    $status Le code d'état HTTP qui doit être renvoyé avec cette redirection
652
     */
653
    public function redirect(string $from, string $to, int $status = 302): self
654
    {
655
        // Utilisez le modèle de la route nommée s'il s'agit d'une route nommée.
656
        if (array_key_exists($to, $this->routesNames['*'])) {
657
			$routeName  = $to;
658
            $routeKey   = $this->routesNames['*'][$routeName];
659
            $redirectTo = [$routeKey => $this->routes['*'][$routeKey]['handler']];
660
661
        } elseif (array_key_exists($to, $this->routes['get'])) {
662
            $routeName  = $to;
663
            $routeKey   = $this->routesNames['get'][$routeName];
664
            $redirectTo = [$routeKey => $this->routes['get'][$routeKey]['handler']];
665
        } else {
666
            // La route nommee n'a pas ete trouvée
667
            $redirectTo = $to;
668
        }
669
670
        $this->create('*', $from, $redirectTo, ['redirect' => $status]);
671
672
        return $this;
673
    }
674
675
    /**
676
     * Ajoute une redirection permanente d'une route à une autre. 
677
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
678
     */
679
    public function permanentRedirect(string $from, string $to): self
680
    {
681
        return $this->redirect($from, $to, 301);
682
    }
683
684
    /**
685
     * @deprecated 0.9 Please use redirect() instead
686
     */
687
    public function addRedirect(string $from, string $to, int $status = 302): self
688
    {
689
        return $this->redirect($from, $to, $status);
690
    }
691
692
    /**
693
     * {@inheritDoc}
694
	 * 
695
	 * @param string $routeKey cle de route ou route nommee
696
     */
697
    public function isRedirect(string $routeKey): bool
698
    {
699
		return isset($this->routes['*'][$routeKey]['redirect']);
700
    }
701
702
    /**
703
     * {@inheritDoc}
704
	 * 
705
	 * @param string $routeKey cle de route ou route nommee
706
     */
707
    public function getRedirectCode(string $routeKey): int
708
    {
709
        if (isset($this->routes['*'][$routeKey]['redirect'])) {
710
            return $this->routes['*'][$routeKey]['redirect'];
711
        }
712
713
        return 0;
714
    }
715
716
    /**
717
     * Regroupez une série de routes sous un seul segment d'URL. C'est pratique
718
     * pour regrouper des éléments dans une zone d'administration, comme :
719
     *
720
     * Example:
721
     *     // Creates route: admin/users
722
     *     $route->group('admin', function() {
723
     *            $route->resource('users');
724
     *     });
725
     *
726
     * @param string         $name      Le nom avec lequel grouper/préfixer les routes.
727
     * @param array|callable ...$params
728
     */
729
    public function group(string $name, ...$params)
730
    {
731
        $oldGroup   = $this->group ?? '';
732
        $oldOptions = $this->currentOptions;
733
734
        // Pour enregistrer une route, nous allons définir un indicateur afin que notre routeur
735
        // donc il verra le nom du groupe.
736
        // Si le nom du groupe est vide, nous continuons à utiliser le nom du groupe précédemment construit.
737
        $this->group = implode('/', array_unique(explode('/', $name ? (rtrim($oldGroup ?? '', '/') . '/' . ltrim($name, '/')) : $oldGroup)));
738
739
        $callback = array_pop($params);
740
741
        if ($params && is_array($params[0])) {
742
            $this->currentOptions = array_shift($params);
743
        }
744
745
        if (is_callable($callback)) {
746
            $callback($this);
747
        }
748
749
        $this->group          = $oldGroup;
750
        $this->currentOptions = $oldOptions;
751
    }
752
753
    /*
754
     * ------------------------------------------------- -------------------
755
     * Routage basé sur les verbes HTTP
756
     * ------------------------------------------------- -------------------
757
     * Le routage fonctionne ici car, comme le fichier de configuration des routes est lu,
758
     * les différentes routes basées sur le verbe HTTP ne seront ajoutées qu'à la mémoire en mémoire
759
     * routes s'il s'agit d'un appel qui doit répondre à ce verbe.
760
     *
761
     * Le tableau d'options est généralement utilisé pour transmettre un 'as' ou var, mais peut
762
     * être étendu à l'avenir. Voir le docblock pour la méthode 'add' ci-dessus pour
763
     * liste actuelle des options disponibles dans le monde.*/
764
765
    /**
766
     * Crée une collection d'itinéraires basés sur HTTP-verb pour un contrôleur.
767
     *
768
     * Options possibles :
769
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
770
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
771
     * 'websafe' - - '1' si seuls les verbes HTTP GET et POST sont pris en charge
772
     *
773
     * Exemple:
774
     *
775
     *      $route->resource('photos');
776
     *
777
     *      // Genère les routes suivantes:
778
     *      HTTP Verb | Path        | Action        | Used for...
779
     *      ----------+-------------+---------------+-----------------
780
     *      GET         /photos             index           un tableau d'objets photo
781
     *      GET         /photos/new         new             un objet photo vide, avec des propriétés par défaut
782
     *      GET         /photos/{id}/edit   edit            un objet photo spécifique, propriétés modifiables
783
     *      GET         /photos/{id}        show            un objet photo spécifique, toutes les propriétés
784
     *      POST        /photos             create          un nouvel objet photo, à ajouter à la ressource
785
     *      DELETE      /photos/{id}        delete          supprime l'objet photo spécifié
786
     *      PUT/PATCH   /photos/{id}        update          propriétés de remplacement pour la photo existante
787
     *
788
     *  Si l'option 'websafe' est présente, les chemins suivants sont également disponibles :
789
     *
790
     *      POST		/photos/{id}/delete delete
791
     *      POST        /photos/{id}        update
792
     *
793
     * @param string     $name    Le nom de la ressource/du contrôleur vers lequel router.
794
     * @param array|null $options Une liste des façons possibles de personnaliser le routage.
795
     */
796
    public function resource(string $name, ?array $options = null): self
797
    {
798
        // Afin de permettre la personnalisation de la route, le
799
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
800
        // pour stocker les valeurs.
801
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
802
803
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
804
        // valeur de $name avec le nom du nouveau contrôleur.
805
        if (isset($options['controller'])) {
806
            $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

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

923
            $newName = ucfirst(/** @scrutinizer ignore-type */ esc(strip_tags($options['controller'])));
Loading history...
924
        }
925
926
        // Afin de permettre la personnalisation des valeurs d'identifiant autorisées
927
        // nous avons besoin d'un endroit pour les stocker.
928
        $id = $options['placeholder'] ?? $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';
929
930
        // On s'assure de capturer les références arrière
931
        $id = '(' . trim($id, '()') . ')';
932
933
        $methods = isset($options['only']) ? (is_string($options['only']) ? explode(',', $options['only']) : $options['only']) : ['index', 'show', 'new', 'create', 'edit', 'update', 'remove', 'delete'];
934
935
        if (isset($options['except'])) {
936
            $options['except'] = is_array($options['except']) ? $options['except'] : explode(',', $options['except']);
937
938
            foreach ($methods as $i => $method) {
939
                if (in_array($method, $options['except'], true)) {
940
                    unset($methods[$i]);
941
                }
942
            }
943
        }
944
945
		$routeName = $name;
946
		if (isset($options['as']) || isset($options['name'])) {
947
			$routeName = trim($options['as'] ?? $options['name'], " .");
948
			unset($options['name'], $options['as']);
949
		}
950
951
        if (in_array('index', $methods, true)) {
952
            $this->get($name, $newName . '::index', $options + [
953
				'as' => $routeName . '.index',
954
			]);
955
        }
956
        if (in_array('show', $methods, true)) {
957
            $this->get($name . '/show/' . $id, $newName . '::show/$1', $options + [
958
				'as' => $routeName . '.view'
959
			]);
960
			$this->get($name . '/' . $id, $newName . '::show/$1', $options + [
961
				'as' => $routeName . '.show'
962
			]);
963
        }
964
        if (in_array('new', $methods, true)) {
965
            $this->get($name . '/new', $newName . '::new', $options + [
966
				'as' => $routeName . '.new'
967
			]);
968
        }
969
        if (in_array('create', $methods, true)) {
970
            $this->post($name . '/create', $newName . '::create', $options + [
971
				'as' => $routeName . '.create'
972
			]);
973
			$this->post($name, $newName . '::create', $options + [
974
				'as' => $routeName . '.store'
975
			]);
976
        }
977
        if (in_array('edit', $methods, true)) {
978
            $this->get($name . '/edit/' . $id, $newName . '::edit/$1', $options + [
979
				'as' => $routeName . '.edit'
980
			]);
981
        }
982
        if (in_array('update', $methods, true)) {
983
            $this->post($name . '/update/' . $id, $newName . '::update/$1', $options + [
984
				'as' => $routeName . '.update',
985
			]);
986
        }
987
        if (in_array('remove', $methods, true)) {
988
            $this->get($name . '/remove/' . $id, $newName . '::remove/$1', $options + [
989
				'as' => $routeName . '.remove',
990
			]);
991
        }
992
        if (in_array('delete', $methods, true)) {
993
            $this->post($name . '/delete/' . $id, $newName . '::delete/$1', $options + [
994
				'as' => $routeName . '.delete',
995
			]);
996
        }
997
        
998
        return $this;
999
    }
1000
1001
    /**
1002
     * Spécifie une seule route à faire correspondre pour plusieurs verbes HTTP.
1003
     *
1004
     * Exemple:
1005
     *  $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
1006
     *
1007
     * @param array|Closure|string $to
1008
     */
1009
    public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): self
1010
    {
1011
        if (empty($from) || empty($to)) {
1012
            throw new InvalidArgumentException('Vous devez fournir les paramètres : $from, $to.');
1013
        }
1014
1015
        foreach ($verbs as $verb) {
1016
            $verb = strtolower($verb);
1017
1018
            $this->{$verb}($from, $to, $options);
1019
        }
1020
1021
        return $this;
1022
    }
1023
1024
    /**
1025
     * Spécifie une route qui n'est disponible que pour les requêtes GET.
1026
     *
1027
     * @param array|Closure|string $to
1028
     */
1029
    public function get(string $from, $to, ?array $options = null): self
1030
    {
1031
        $this->create('get', $from, $to, $options);
1032
1033
        return $this;
1034
    }
1035
1036
    /**
1037
     * Spécifie une route qui n'est disponible que pour les requêtes POST.
1038
     *
1039
     * @param array|Closure|string $to
1040
     */
1041
    public function post(string $from, $to, ?array $options = null): self
1042
    {
1043
        $this->create('post', $from, $to, $options);
1044
1045
        return $this;
1046
    }
1047
1048
    /**
1049
     * Spécifie une route qui n'est disponible que pour les requêtes PUT.
1050
     *
1051
     * @param array|Closure|string $to
1052
     */
1053
    public function put(string $from, $to, ?array $options = null): self
1054
    {
1055
        $this->create('put', $from, $to, $options);
1056
1057
        return $this;
1058
    }
1059
1060
    /**
1061
     * Spécifie une route qui n'est disponible que pour les requêtes DELETE.
1062
     *
1063
     * @param array|Closure|string $to
1064
     */
1065
    public function delete(string $from, $to, ?array $options = null): self
1066
    {
1067
        $this->create('delete', $from, $to, $options);
1068
1069
        return $this;
1070
    }
1071
1072
    /**
1073
     * Spécifie une route qui n'est disponible que pour les requêtes HEAD.
1074
     *
1075
     * @param array|Closure|string $to
1076
     */
1077
    public function head(string $from, $to, ?array $options = null): self
1078
    {
1079
        $this->create('head', $from, $to, $options);
1080
1081
        return $this;
1082
    }
1083
1084
    /**
1085
     * Spécifie une route qui n'est disponible que pour les requêtes PATCH.
1086
     *
1087
     * @param array|Closure|string $to
1088
     */
1089
    public function patch(string $from, $to, ?array $options = null): self
1090
    {
1091
        $this->create('patch', $from, $to, $options);
1092
1093
        return $this;
1094
    }
1095
1096
    /**
1097
     * Spécifie une route qui n'est disponible que pour les requêtes OPTIONS.
1098
     *
1099
     * @param array|Closure|string $to
1100
     */
1101
    public function options(string $from, $to, ?array $options = null): self
1102
    {
1103
        $this->create('options', $from, $to, $options);
1104
1105
        return $this;
1106
    }
1107
1108
    
1109
	/**
1110
     * Spécifie une route qui n'est disponible que pour les requêtes GET et POST.
1111
     *
1112
     * @param array|Closure|string $to
1113
     */
1114
	public function form(string $from, $to, ?array $options = null): self
1115
	{
1116
		return $this->match(['GET', 'POST'], $from, $to, $options);
1117
	}
1118
	
1119
	/**
1120
     * Spécifie une route qui n'est disponible que pour les requêtes de ligne de commande.
1121
     *
1122
     * @param array|Closure|string $to
1123
     */
1124
    public function cli(string $from, $to, ?array $options = null): self
1125
    {
1126
        $this->create('cli', $from, $to, $options);
1127
1128
        return $this;
1129
    }
1130
1131
    /**
1132
     * Spécifie une route qui n'affichera qu'une vue.
1133
     * Ne fonctionne que pour les requêtes GET.
1134
     */
1135
    public function view(string $from, string $view, ?array $options = null): self
1136
    {
1137
        $to = static fn (...$data) => Services::viewer()
1138
            ->setData(['segments' => $data], 'raw')
1139
            ->display($view)
1140
            ->setOptions($options)
1141
            ->render();
1142
1143
		$routeOptions = $options ?? [];
1144
		$routeOptions = array_merge($routeOptions, ['view' => $view]);
1145
1146
		$this->create('get', $from, $to, $routeOptions);
1147
1148
        return $this;
1149
    }
1150
1151
    /**
1152
     * Limite les itinéraires à un ENVIRONNEMENT spécifié ou ils ne fonctionneront pas.
1153
     */
1154
    public function environment(string $env, Closure $callback): self
1155
    {
1156
        if (environment($env)) {
1157
            $callback($this);
1158
        }
1159
1160
        return $this;
1161
    }
1162
1163
    /**
1164
     * {@inheritDoc}
1165
     */
1166
    public function reverseRoute(string $search, ...$params)
1167
    {
1168
        // Les routes nommées ont une priorité plus élevée.
1169
        foreach ($this->routesNames as $verb => $collection) {
1170
            if (array_key_exists($search, $collection)) {
1171
                $routeKey = $collection[$search];
1172
1173
                $from = $this->routes[$verb][$routeKey]['from'];
1174
1175
                return $this->buildReverseRoute($from, $params);
1176
            }
1177
        }
1178
1179
        // Ajoutez l'espace de noms par défaut si nécessaire.
1180
        $namespace = trim($this->defaultNamespace, '\\') . '\\';
1181
        if (
1182
            substr($search, 0, 1) !== '\\'
1183
            && substr($search, 0, strlen($namespace)) !== $namespace
1184
        ) {
1185
            $search = $namespace . $search;
1186
        }
1187
1188
        // Si ce n'est pas une route nommée, alors bouclez
1189
        // toutes les routes pour trouver une correspondance.
1190
        foreach ($this->routes as $collection) {
1191
            foreach ($collection as $route) {
1192
                $to   = $route['handler'];
1193
                $from = $route['from'];
1194
1195
                // on ignore les closures
1196
                if (! is_string($to)) {
1197
                    continue;
1198
                }
1199
1200
                // Perd toute barre oblique d'espace de noms au début des chaînes
1201
                // pour assurer une correspondance plus cohérente.$to     = ltrim($to, '\\');
1202
                $to     = ltrim($to, '\\');
1203
                $search = ltrim($search, '\\');
1204
1205
                // S'il y a une chance de correspondance, alors ce sera
1206
                // soit avec $search au début de la chaîne $to.
1207
                if (strpos($to, $search) !== 0) {
1208
                    continue;
1209
                }
1210
1211
                // Assurez-vous que le nombre de $params donné ici
1212
                // correspond au nombre de back-references dans la route
1213
                if (substr_count($to, '$') !== count($params)) {
1214
                    continue;
1215
                }
1216
1217
                return $this->buildReverseRoute($from, $params);
1218
            }
1219
        }
1220
1221
        // Si nous sommes toujours là, alors nous n'avons pas trouvé de correspondance.
1222
        return false;
1223
    }
1224
1225
    /**
1226
     * Vérifie une route (en utilisant le "from") pour voir si elle est filtrée ou non.
1227
     */
1228
    public function isFiltered(string $search, ?string $verb = null): bool
1229
    {
1230
        return $this->getFiltersForRoute($search, $verb) !== [];
1231
    }
1232
1233
    /**
1234
     * Renvoie les filtres qui doivent être appliqués pour un seul itinéraire, ainsi que
1235
     * avec tous les paramètres qu'il pourrait avoir. Les paramètres sont trouvés en divisant
1236
     * le nom du paramètre entre deux points pour séparer le nom du filtre de la liste des paramètres,
1237
     * et le fractionnement du résultat sur des virgules. Alors:
1238
     *
1239
     *    'role:admin,manager'
1240
     *
1241
     * a un filtre de "rôle", avec des paramètres de ['admin', 'manager'].
1242
     */
1243
    public function getFiltersForRoute(string $search, ?string $verb = null): array
1244
    {
1245
        $options = $this->loadRoutesOptions($verb);
1246
1247
        $middlewares = $options[$search]['middlewares'] ?? (
1248
            $options[$search]['middleware'] ?? ($options[$search]['filter'] ?? [])
1249
        );
1250
1251
        return (array) $middlewares;
1252
    }
1253
1254
    /**
1255
     * Construit une route inverse
1256
     *
1257
     * @param array $params Un ou plusieurs paramètres à transmettre à la route.
1258
     *                      Le dernier paramètre vous permet de définir la locale.
1259
     */
1260
    protected function buildReverseRoute(string $from, array $params): string
1261
    {
1262
        $locale = null;
1263
1264
        // Retrouvez l'ensemble de nos rétro-références dans le parcours d'origine.
1265
        preg_match_all('/\(([^)]+)\)/', $from, $matches);
1266
1267
        if (empty($matches[0])) {
1268
            if (strpos($from, '{locale}') !== false) {
1269
                $locale = $params[0] ?? null;
1270
            }
1271
1272
            $from = $this->replaceLocale($from, $locale);
1273
1274
            return '/' . ltrim($from, '/');
1275
        }
1276
1277
        // Les paramètres régionaux sont passés ?
1278
        $placeholderCount = count($matches[0]);
1279
        if (count($params) > $placeholderCount) {
1280
            $locale = $params[$placeholderCount];
1281
        }
1282
1283
        // Construisez notre chaîne résultante, en insérant les $params aux endroits appropriés.
1284
        foreach ($matches[0] as $index => $placeholder) {
1285
            if (! isset($params[$index])) {
1286
                throw new InvalidArgumentException(
1287
                    'Argument manquant pour "' . $placeholder . '" dans la route "' . $from . '".'
1288
                );
1289
            }
1290
1291
            // Supprimez `(:` et `)` lorsque $placeholder est un espace réservé.
1292
            $placeholderName = substr($placeholder, 2, -1);
1293
            // ou peut-être que $placeholder n'est pas un espace réservé, mais une regex.
1294
            $pattern = $this->placeholders[$placeholderName] ?? $placeholder;
1295
1296
            if (! preg_match('#^' . $pattern . '$#u', $params[$index])) {
1297
                throw RouterException::invalidParameterType();
1298
            }
1299
1300
            // Assurez-vous que le paramètre que nous insérons correspond au type de paramètre attendu.
1301
            $pos  = strpos($from, $placeholder);
1302
            $from = substr_replace($from, $params[$index], $pos, strlen($placeholder));
1303
        }
1304
1305
        $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

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

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

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

1436
        $name = $this->formatRouteName(/** @scrutinizer ignore-type */ $options['as'] ?? $routeKey);
Loading history...
1437
1438
        // Ne remplacez aucun 'from' existant afin que les routes découvertes automatiquement
1439
        // n'écrase pas les paramètres app/Config/Routes.
1440
        // les routes manuelement définies doivent toujours être la "source de vérité".
1441
        // cela ne fonctionne que parce que les routes découvertes sont ajoutées juste avant
1442
        // pour tenter de router la requête.
1443
        $routeKeyExists = isset($this->routes[$verb][$routeKey]);
1444
        if ((isset($this->routesNames[$verb][$name]) || $routeKeyExists) && ! $overwrite) {
1445
            return;
1446
        }
1447
1448
		$this->routes[$verb][$routeKey] = [
1449
            'name'    => $name,
1450
            'handler' => $to,
1451
            'from'    => $from,
1452
        ];
1453
        $this->routesOptions[$verb][$routeKey] = $options;
1454
        $this->routesNames[$verb][$name]       = $routeKey;
1455
1456
        // C'est une redirection ?
1457
        if (isset($options['redirect']) && is_numeric($options['redirect'])) {
1458
            $this->routes['*'][$routeKey]['redirect'] = $options['redirect'];
1459
        }
1460
    }
1461
1462
    /**
1463
     * Compare le nom d'hôte transmis avec le nom d'hôte actuel sur cette demande de page.
1464
     *
1465
     * @param string $hostname Nom d'hôte dans les options d'itinéraire
1466
     */
1467
    private function checkHostname(string $hostname): bool
1468
    {
1469
        // Les appels CLI ne peuvent pas être sur le nom d'hôte.
1470
        if (! isset($this->httpHost) || is_cli()) {
1471
            return false;
1472
        }
1473
1474
        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

1474
        return strtolower(/** @scrutinizer ignore-type */ $this->httpHost) === strtolower($hostname);
Loading history...
1475
    }
1476
1477
    /**
1478
     * Compare le ou les sous-domaines transmis avec le sous-domaine actuel
1479
     * sur cette page demande.
1480
     *
1481
     * @param mixed $subdomains
1482
     */
1483
    private function checkSubdomains($subdomains): bool
1484
    {
1485
        // Les appels CLI ne peuvent pas être sur le sous-domaine.
1486
        if (! isset($this->httpHost)) {
1487
            return false;
1488
        }
1489
1490
        if ($this->currentSubdomain === null) {
1491
            $this->currentSubdomain = $this->determineCurrentSubdomain();
1492
        }
1493
1494
        if (! is_array($subdomains)) {
1495
            $subdomains = [$subdomains];
1496
        }
1497
1498
        // Les routes peuvent être limitées à n'importe quel sous-domaine. Dans ce cas, cependant,
1499
        // il nécessite la présence d'un sous-domaine.
1500
        if (! empty($this->currentSubdomain) && in_array('*', $subdomains, true)) {
1501
            return true;
1502
        }
1503
1504
        return in_array($this->currentSubdomain, $subdomains, true);
1505
    }
1506
1507
    /**
1508
     * Examine le HTTP_HOST pour obtenir une meilleure correspondance pour le sous-domaine. Ce
1509
     * ne sera pas parfait, mais devrait répondre à nos besoins.
1510
     *
1511
     * Ce n'est surtout pas parfait puisqu'il est possible d'enregistrer un domaine
1512
     * avec un point (.) dans le cadre du nom de domaine.
1513
     *
1514
     * @return mixed
1515
     */
1516
    private function determineCurrentSubdomain()
1517
    {
1518
        // Nous devons nous assurer qu'un schéma existe
1519
        // sur l'URL sinon parse_url sera mal interprété
1520
        // 'hôte' comme 'chemin'.
1521
        $url = $this->httpHost;
1522
        if (strpos($url, 'http') !== 0) {
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type null; however, parameter $haystack of strpos() 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

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

1628
            if (! in_array($locale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
1629
                $locale = null;
1630
            }
1631
        }
1632
1633
        if ($locale === null) {
1634
            $locale = Services::request()->getLocale();
1635
        }
1636
1637
        return strtr($route, ['{locale}' => $locale]);
1638
    }
1639
}
1640