Passed
Push — main ( bfe281...a04a11 )
by Dimitri
11:19
created

RouteCollection::getDefaultController()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
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\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->fallback ?: ($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
        // 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));
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...
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
            $placeholder = [$placeholder => $pattern];
325
        }
326
327
        $this->placeholders = array_merge($this->placeholders, $placeholder);
328
329
        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
        $this->defaultController = esc(strip_tags($value));
349
350
        return $this;
351
    }
352
353
    /**
354
     * {@inheritDoc}
355
     */
356
    public function setDefaultMethod(string $value): self
357
    {
358
        $this->defaultMethod = esc(strip_tags($value));
359
360
        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
            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
        $routes = $this;
0 ignored issues
show
Unused Code introduced by
The assignment to $routes is dead and can be removed.
Loading history...
425
426
        $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
                continue;
432
            }
433
434
            include_once $file;
435
        }
436
437
        $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
        return preg_replace('#Controller$#i', '', $this->defaultController) . 'Controller';
459
    }
460
461
    /**
462
     * {@inheritDoc}
463
     */
464
    public function getDefaultMethod(): string
465
    {
466
        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
            $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
        $this->discoverRoutes();
569
570
        $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
			$collection = $includeWildcard ? $this->routes[$verb] + ($this->routes['*'] ?? []) : $this->routes[$verb];
575
576
		    foreach ($collection as $routeKey => $r) {
577
                $routes[$routeKey] = $r['handler'];
578
            }
579
        }
580
581
        // tri des routes par priorité
582
        if ($this->prioritizeDetected && $this->prioritize && $routes !== []) {
583
            $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
        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
        $options = $this->loadRoutesOptions($verb);
604
605
        return $from ? $options[$from] ?? [] : $options;
606
    }
607
608
    /**
609
     * {@inheritDoc}
610
     */
611
    public function getHTTPVerb(): string
612
    {
613
        return $this->HTTPVerb;
614
    }
615
616
    /**
617
     * {@inheritDoc}
618
     */
619
    public function setHTTPVerb(string $verb): self
620
    {
621
        $this->HTTPVerb = strtolower($verb);
622
623
        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
        $this->create('*', $from, $to, $options);
646
647
        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
666
        } elseif (array_key_exists($to, $this->routes['get'])) {
667
            $routeName  = $to;
668
            $routeKey   = $this->routesNames['get'][$routeName];
669
            $redirectTo = [$routeKey => $this->routes['get'][$routeKey]['handler']];
670
        } else {
671
            // La route nommee n'a pas ete trouvée
672
            $redirectTo = $to;
673
        }
674
675
        $this->create('*', $from, $redirectTo, ['redirect' => $status]);
676
677
        return $this;
678
    }
679
680
    /**
681
     * Ajoute une redirection permanente d'une route à une autre. 
682
     * Utilisé pour rediriger le trafic des anciennes routes inexistantes vers les nouvelles routes déplacés.
683
     */
684
    public function permanentRedirect(string $from, string $to): self
685
    {
686
        return $this->redirect($from, $to, 301);
687
    }
688
689
    /**
690
     * @deprecated 0.9 Please use redirect() instead
691
     */
692
    public function addRedirect(string $from, string $to, int $status = 302): self
693
    {
694
        return $this->redirect($from, $to, $status);
695
    }
696
697
    /**
698
     * {@inheritDoc}
699
	 * 
700
	 * @param string $routeKey cle de route ou route nommee
701
     */
702
    public function isRedirect(string $routeKey): bool
703
    {
704
		return isset($this->routes['*'][$routeKey]['redirect']);
705
    }
706
707
    /**
708
     * {@inheritDoc}
709
	 * 
710
	 * @param string $routeKey cle de route ou route nommee
711
     */
712
    public function getRedirectCode(string $routeKey): int
713
    {
714
        if (isset($this->routes['*'][$routeKey]['redirect'])) {
715
            return $this->routes['*'][$routeKey]['redirect'];
716
        }
717
718
        return 0;
719
    }
720
721
    /**
722
     * Regroupez une série de routes sous un seul segment d'URL. C'est pratique
723
     * pour regrouper des éléments dans une zone d'administration, comme :
724
     *
725
     * Example:
726
     *     // Creates route: admin/users
727
     *     $route->group('admin', function() {
728
     *            $route->resource('users');
729
     *     });
730
     *
731
     * @param string         $name      Le nom avec lequel grouper/préfixer les routes.
732
     * @param array|callable ...$params
733
     */
734
    public function group(string $name, ...$params)
735
    {
736
        $oldGroup   = $this->group ?? '';
737
        $oldOptions = $this->currentOptions;
738
739
        // Pour enregistrer une route, nous allons définir un indicateur afin que notre routeur
740
        // donc il verra le nom du groupe.
741
        // Si le nom du groupe est vide, nous continuons à utiliser le nom du groupe précédemment construit.
742
        $this->group = implode('/', array_unique(explode('/', $name ? (rtrim($oldGroup ?? '', '/') . '/' . ltrim($name, '/')) : $oldGroup)));
743
744
        $callback = array_pop($params);
745
746
        if ($params && is_array($params[0])) {
747
            $this->currentOptions = array_merge(
748
                $this->currentOptions ?? [],
749
                array_shift($params)
750
            );
751
        }
752
753
        if (is_callable($callback)) {
754
            $callback($this);
755
        }
756
757
        $this->group          = $oldGroup;
758
        $this->currentOptions = $oldOptions;
759
    }
760
761
    /*
762
     * ------------------------------------------------- -------------------
763
     * Routage basé sur les verbes HTTP
764
     * ------------------------------------------------- -------------------
765
     * Le routage fonctionne ici car, comme le fichier de configuration des routes est lu,
766
     * les différentes routes basées sur le verbe HTTP ne seront ajoutées qu'à la mémoire en mémoire
767
     * routes s'il s'agit d'un appel qui doit répondre à ce verbe.
768
     *
769
     * Le tableau d'options est généralement utilisé pour transmettre un 'as' ou var, mais peut
770
     * être étendu à l'avenir. Voir le docblock pour la méthode 'add' ci-dessus pour
771
     * liste actuelle des options disponibles dans le monde.*/
772
773
    /**
774
     * Crée une collection d'itinéraires basés sur HTTP-verb pour un contrôleur.
775
     *
776
     * Options possibles :
777
     * 'controller' - Personnalisez le nom du contrôleur utilisé dans la route 'to'
778
     * 'placeholder' - L'expression régulière utilisée par le routeur. La valeur par défaut est '(:any)'
779
     * 'websafe' - - '1' si seuls les verbes HTTP GET et POST sont pris en charge
780
     *
781
     * Exemple:
782
     *
783
     *      $route->resource('photos');
784
     *
785
     *      // Genère les routes suivantes:
786
     *      HTTP Verb | Path        | Action        | Used for...
787
     *      ----------+-------------+---------------+-----------------
788
     *      GET         /photos             index           un tableau d'objets photo
789
     *      GET         /photos/new         new             un objet photo vide, avec des propriétés par défaut
790
     *      GET         /photos/{id}/edit   edit            un objet photo spécifique, propriétés modifiables
791
     *      GET         /photos/{id}        show            un objet photo spécifique, toutes les propriétés
792
     *      POST        /photos             create          un nouvel objet photo, à ajouter à la ressource
793
     *      DELETE      /photos/{id}        delete          supprime l'objet photo spécifié
794
     *      PUT/PATCH   /photos/{id}        update          propriétés de remplacement pour la photo existante
795
     *
796
     *  Si l'option 'websafe' est présente, les chemins suivants sont également disponibles :
797
     *
798
     *      POST		/photos/{id}/delete delete
799
     *      POST        /photos/{id}        update
800
     *
801
     * @param string     $name    Le nom de la ressource/du contrôleur vers lequel router.
802
     * @param array|null $options Une liste des façons possibles de personnaliser le routage.
803
     */
804
    public function resource(string $name, ?array $options = null): self
805
    {
806
        // Afin de permettre la personnalisation de la route, le
807
        // les ressources sont envoyées à, nous devons avoir un nouveau nom
808
        // pour stocker les valeurs.
809
        $newName = implode('\\', array_map('ucfirst', explode('/', $name)));
810
811
        // Si un nouveau contrôleur est spécifié, alors nous remplaçons le
812
        // valeur de $name avec le nom du nouveau contrôleur.
813
        if (isset($options['controller'])) {
814
            $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

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

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

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

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

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

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

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

1636
            if (! in_array($locale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
1637
                $locale = null;
1638
            }
1639
        }
1640
1641
        if ($locale === null) {
1642
            $locale = Services::request()->getLocale();
1643
        }
1644
1645
        return strtr($route, ['{locale}' => $locale]);
1646
    }
1647
}
1648