Router::setMatchedRoute()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Router;
13
14
use BlitzPHP\Container\Services;
15
use BlitzPHP\Contracts\Router\AutoRouterInterface;
16
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
17
use BlitzPHP\Contracts\Router\RouterInterface;
18
use BlitzPHP\Enums\Method;
19
use BlitzPHP\Exceptions\BadRequestException;
20
use BlitzPHP\Exceptions\PageNotFoundException;
21
use BlitzPHP\Exceptions\RedirectException;
22
use BlitzPHP\Exceptions\RouterException;
23
use BlitzPHP\Http\Request;
24
use BlitzPHP\Utilities\String\Text;
25
use Closure;
26
use Psr\Http\Message\ServerRequestInterface;
27
28
/**
29
 * Analyse l'URL de la requête dans le contrôleur, action et paramètres. Utilise les routes connectées
30
 * pour faire correspondre la chaîne d'URL entrante aux paramètres qui permettront à la requête d'être envoyée. Aussi
31
 * gère la conversion des listes de paramètres en chaînes d'URL, en utilisant les routes connectées. Le routage vous permet de découpler
32
 * la façon dont le monde interagit avec votre application (URL) et l'implémentation (contrôleurs et actions).
33
 */
34
class Router implements RouterInterface
35
{
36
    /**
37
     * Liste des méthodes HTTP autorisées (et CLI pour l'utilisation de la ligne de commande).
38
     */
39
    public const HTTP_METHODS = [
40
        Method::GET,
41
        Method::HEAD,
42
        Method::POST,
43
        Method::PATCH,
44
        Method::PUT,
45
        Method::DELETE,
46
        Method::OPTIONS,
47
        Method::TRACE,
48
        Method::CONNECT,
49
        'CLI',
50
    ];
51
52
    /**
53
     * Une instance de la classe RouteCollection.
54
     *
55
     * @var RouteCollection
56
     */
57
    protected $collection;
58
59
    /**
60
     * Sous-répertoire contenant la classe de contrôleur demandée.
61
     * Principalement utilisé par 'autoRoute'.
62
     */
63
    protected ?string $directory = null;
64
65
    /**
66
     * Le nom de la classe contrôleur
67
     *
68
     * @var Closure|string
69
     */
70
    protected $controller;
71
72
    /**
73
     * Le nom de la méthode à utiliser
74
     */
75
    protected string $method = '';
76
77
    /**
78
     * Un tableau de liens qui ont été collectés afin
79
     * qu'ils puissent être envoyés aux routes de fermeture.
80
     */
81
    protected array $params = [];
82
83
    /**
84
     * Le nom du du front-controller.
85
     */
86
    protected string $indexPage = 'index.php';
87
88
    /**
89
     * Si les tirets dans les URI doivent être convertis
90
     * pour les traits de soulignement lors de la détermination des noms de méthode.
91
     */
92
    protected bool $translateURIDashes = true;
93
94
    /**
95
     * Les routes trouvées pour la requête courrante
96
     */
97
    protected ?array $matchedRoute = null;
98
99
    /**
100
     * Les options de la route matchée.
101
     */
102
    protected ?array $matchedRouteOptions = null;
103
104
    /**
105
     * Le locale (langue) qui a été detectée dans la route.
106
     *
107
     * @var string
108
     */
109
    protected $detectedLocale;
110
111
    /**
112
     * Les informations des middlewares à executer
113
     * Si la route matchée necessite des filtres.
114
     *
115
     * @var list<string>
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Router\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
116
     */
117
    protected array $middlewaresInfo = [];
118
119
    protected ?AutoRouterInterface $autoRouter = null;
120
121
    /**
122
     * Caractères URI autorisés
123
     *
124
     * La valeur par défaut est `''` (ne pas vérifier) pour des raisons de compatibilité ascendante.
125
     */
126
    protected string $permittedURIChars = '';
127
128
    /**
129
     * @param RouteCollection $routes
130
     * @param Request         $request
131
     */
132
    public function __construct(RouteCollectionInterface $routes, ServerRequestInterface $request)
133
    {
134 20
        $this->permittedURIChars = config('app.permitted_uri_chars', '');
135 20
        $this->collection        = $routes;
0 ignored issues
show
Documentation Bug introduced by
$routes is of type BlitzPHP\Contracts\Router\RouteCollectionInterface, but the property $collection was declared to be of type BlitzPHP\Router\RouteCollection. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
136
137 20
        $this->setController($this->collection->getDefaultController());
138 20
        $this->setMethod($this->collection->getDefaultMethod());
139
140 20
        $this->collection->setHTTPVerb($request->getMethod());
141
142 20
        $this->translateURIDashes = $this->collection->shouldTranslateURIDashes();
143
144
        if ($this->collection->shouldAutoRoute()) {
145
            $this->autoRouter = new AutoRouter(
146
                $this->collection->getRegisteredControllers('*'),
147
                $this->collection->getDefaultNamespace(),
148
                $this->collection->getDefaultController(),
149
                $this->collection->getDefaultMethod(),
150
                $this->translateURIDashes
151 20
            );
152
        }
153
    }
154
155
    /**
156
     * @return Closure|string Nom de classe du contrôleur ou closure
157
     *
158
     * @throws PageNotFoundException
159
     * @throws RedirectException
160
     */
161
    public function handle(?string $uri = null)
162
    {
163
        // Si nous ne trouvons pas d'URI à comparer, alors
164
        // tout fonctionne à partir de ses paramètres par défaut.
165
        if ($uri === null || $uri === '') {
166 2
            $uri = '/';
167
        }
168
169
        // Décoder la chaîne de caractères codée par l'URL
170 20
        $uri = urldecode($uri);
171
172 20
        $this->checkDisallowedChars($uri);
173
174 20
        $this->middlewaresInfo = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type BlitzPHP\Router\list of property $middlewaresInfo.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
175
176
        if ($this->checkRoutes($uri)) {
177
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
178 4
                $this->middlewaresInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
179
            }
180
181
            // met a jour le routeur dans le conteneur car permet notament de recupere les bonnes
182
            // info du routing (route actuelle, controleur et methode mappés)
183 20
            Services::set(static::class, $this);
184
185 20
            return $this->controllerName();
186
        }
187
188
        // Toujours là ? Ensuite, nous pouvons essayer de faire correspondre l'URI avec
189
        // Contrôleurs/répertoires, mais l'application peut ne pas
190
        // vouloir ceci, comme dans le cas des API.
191
        if (! $this->collection->shouldAutoRoute()) {
192 6
            throw new PageNotFoundException("Impossible de trouver une route pour '{$this->collection->getHTTPVerb()}: {$uri}'.");
193
        }
194
195 2
        $this->autoRoute($uri);
196
197
        // met a jour le routeur dans le conteneur car permet notament de recupere les bonnes
198
        // info du routing (route actuelle, controleur et methode mappés)
199 2
        Services::set(static::class, $this);
200
201 2
        return $this->controllerName();
202
    }
203
204
    /**
205
     * Renvoie les informations des middlewares de la routes matchée
206
     *
207
     * @return list<string>
208
     */
209
    public function getMiddlewares(): array
210
    {
211 4
        return $this->middlewaresInfo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->middlewaresInfo returns the type array which is incompatible with the documented return type BlitzPHP\Router\list.
Loading history...
212
    }
213
214
    /**
215
     * Renvoie le nom du contrôleur matché
216
     *
217
     * @return Closure|string
218
     */
219
    public function controllerName()
220
    {
221
        if (! is_string($this->controller)) {
222 4
            return $this->controller;
223
        }
224
225
        $controller = str_contains($this->controller, '\\')
226
            ? $this->controller
227
            : trim($this->collection->getDefaultNamespace(), '\\') . '\\' . $this->controller;
228
229
        $controller = preg_replace(
230
            ['#(\_)?Controller$#i', '#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i'],
231
            '',
232
            ucfirst($controller)
233 20
        );
234
235 20
        $controller = trim($controller, '/\\');
236
        $controller = $this->translateURIDashes
237
            ? str_replace('-', '_', $controller)
238 2
            : Text::convertTo($controller, 'pascal');
239
240 20
        return $controller . 'Controller';
241
    }
242
243
    /**
244
     * Retourne le nom de la méthode à exécuter
245
     */
246
    public function methodName(): string
247
    {
248 10
        return str_replace('-', '_', $this->method);
249
    }
250
251
    /**
252
     * Renvoie les paramètres de remplacement 404 de la collection.
253
     * Si le remplacement est une chaîne, sera divisé en tableau contrôleur/index.
254
     *
255
     * @return array|callable|null
256
     */
257
    public function get404Override()
258
    {
259
        $route = $this->collection->get404Override();
260
261
        if (is_string($route)) {
262
            $routeArray = explode('::', $route);
263
264
            return [
265
                $routeArray[0], // Controleur
266
                $routeArray[1] ?? 'index',   // Methode
267
            ];
268
        }
269
270
        if (is_callable($route)) {
271
            return $route;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $route also could return the type mixed which is incompatible with the documented return type array|callable|null.
Loading history...
272
        }
273
274
        return null;
275
    }
276
277
    /**
278
     * Renvoie les liaisons qui ont été mises en correspondance et collectées
279
     * pendant le processus d'analyse sous forme de tableau, prêt à être envoyé à
280
     * instance->method(...$params).
281
     */
282
    public function params(): array
283
    {
284 8
        return $this->params;
285
    }
286
287
    /**
288
     * Renvoie le nom du sous-répertoire dans lequel se trouve le contrôleur.
289
     * Relatif à APPPATH.'Controllers'.
290
     *
291
     * Uniquement utilisé lorsque le routage automatique est activé.
292
     */
293
    public function directory(): string
294
    {
295
        if ($this->autoRouter instanceof AutoRouter) {
296
            return $this->autoRouter->directory();
0 ignored issues
show
Bug introduced by
The method directory() does not exist on null. ( Ignorable by Annotation )

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

296
            return $this->autoRouter->/** @scrutinizer ignore-call */ directory();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
297
        }
298
299
        return '';
300
    }
301
302
    /**
303
     * Renvoie les informations de routage qui correspondaient à ce
304
     * requête, si une route a été définie.
305
     */
306
    public function getMatchedRoute(): ?array
307
    {
308
        return $this->matchedRoute;
309
    }
310
311
    /**
312
     * Renvoie toutes les options définies pour la route correspondante
313
     */
314
    public function getMatchedRouteOptions(): ?array
315
    {
316 2
        return $this->matchedRouteOptions;
317
    }
318
319
    /**
320
     * Définit la valeur qui doit être utilisée pour correspondre au fichier index.php. Valeurs par défaut
321
     * à index.php mais cela vous permet de le modifier au cas où vous utilisez
322
     * quelque chose comme mod_rewrite pour supprimer la page. Vous pourriez alors le définir comme une chaine vide=
323
     */
324
    public function setIndexPage(string $page): self
325
    {
326
        $this->indexPage = $page;
327
328
        return $this;
329
    }
330
331
    /**
332
     * Renvoie vrai/faux selon que la route actuelle contient ou non
333
     * un placeholder {locale}.
334
     */
335
    public function hasLocale(): bool
336
    {
337 2
        return (bool) $this->detectedLocale;
338
    }
339
340
    /**
341
     * Renvoie la locale (langue) détectée, le cas échéant, ou null.
342
     */
343
    public function getLocale(): ?string
344
    {
345 2
        return $this->detectedLocale;
346
    }
347
348
    /**
349
     * Compare la chaîne uri aux routes que la
350
     * classe RouteCollection a définie pour nous, essayant de trouver une correspondance.
351
     * Cette méthode modifiera $this->controller, si nécessaire.
352
     *
353
     * @param string $uri Le chemin URI à comparer aux routes
354
     *
355
     * @return bool Si la route a été mis en correspondance ou non.
356
     *
357
     * @throws RedirectException
358
     */
359
    protected function checkRoutes(string $uri): bool
360
    {
361 20
        $routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
362
363
        // S'il n'y a pas de routes definies pour la methode HTTP, c'est pas la peine d'aller plus loin
364
        if ($routes === []) {
365 4
            return false;
366
        }
367
368
        $uri = $uri === '/'
369
            ? $uri
370 16
            : trim($uri, '/ ');
371
372
        // Boucle dans le tableau de routes à la recherche de caractères génériques
373
        foreach ($routes as $routeKey => $handler) {
374
            $routeKey = $routeKey === '/'
375
                ? $routeKey
376
                // $routeKey peut être int, car il s'agit d'une clé de tableau, et l'URI `/1` est valide.
377
                // Le `/` de tête est supprimé.
378 16
                : ltrim((string) $routeKey, '/ ');
379
380 20
            $matchedKey = $routeKey;
381
382
            // A-t-on affaire à une locale ?
383
            if (str_contains($routeKey, '{locale}')) {
384 8
                $routeKey = str_replace('{locale}', '[^/]+', $routeKey);
385
            }
386
387
            // Est-ce que RegEx correspond ?
388
            if (preg_match('#^' . $routeKey . '$#u', $uri, $matches)) {
389
                // Cette route est-elle censée rediriger vers une autre ?
390
                if ($this->collection->isRedirect($routeKey)) {
391
                    // remplacement des groupes de routes correspondants par des références : post/([0-9]+) -> post/$1
392
                    $redirectTo = preg_replace_callback('/(\([^\(]+\))/', static function (): string {
393
                        static $i = 1;
394
395
                        return '$' . $i++;
396
                    }, is_array($handler) ? key($handler) : $handler);
397
398
                    throw new RedirectException(
399
                        preg_replace('#\A' . $routeKey . '\z#u', $redirectTo, $uri),
400
                        $this->collection->getRedirectCode($routeKey)
401
                    );
402
                }
403
                // Stocke nos paramètres régionaux afin que l'objet CodeIgniter puisse l'affecter à la requête.
404
                if (str_contains($matchedKey, '{locale}')) {
405
                    preg_match(
406
                        '#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
407
                        $uri,
408
                        $matched
409 20
                    );
410
411
                    if ($this->collection->shouldUseSupportedLocalesOnly()
412
                        && ! in_array($matched['locale'], config('app.supported_locales'), true)) {
0 ignored issues
show
Bug introduced by
config('app.supported_locales') of type T|null|object 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

412
                        && ! in_array($matched['locale'], /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
413
                        // Lancer une exception pour empêcher l'autorouteur,
414
                        // si activé, essayer de trouver une route
415 2
                        throw PageNotFoundException::localeNotSupported($matched['locale']);
416
                    }
417
418 4
                    $this->detectedLocale = $matched['locale'];
419 4
                    unset($matched);
420
                }
421
422
                // Utilisons-nous Closures ? Si tel est le cas, nous devons collecter les paramètres dans un tableau
423
                // afin qu'ils puissent être transmis ultérieurement à la méthode du contrôleur.
424
                if (! is_string($handler) && is_callable($handler)) {
425 4
                    $this->controller = $handler;
426
427
                    // Supprime la chaîne d'origine du tableau matches
428 4
                    array_shift($matches);
429
430 4
                    $this->params = $matches;
431
432 4
                    $this->setMatchedRoute($matchedKey, $handler);
433
434 4
                    return true;
435
                }
436
437
                if (is_array($handler)) {
438 20
                    $handler = implode('::', $handler);
439
                }
440
441
                if (str_contains($handler, '::')) {
442 18
                    [$controller, $methodAndParams] = explode('::', $handler);
443
                } else {
444 2
                    $controller      = $handler;
445 2
                    $methodAndParams = '';
446
                }
447
448
                // Vérifie `/` dans le nom du contrôleur
449
                if (str_contains($controller, '/')) {
450 2
                    throw RouterException::invalidControllerName($handler);
451
                }
452
453
                if (str_contains($handler, '$') && str_contains($routeKey, '(')) {
454
                    // Vérifie le contrôleur dynamique
455
                    if (str_contains($controller, '$')) {
456 2
                        throw RouterException::dynamicController($handler);
457
                    }
458
459
                    if (config('routing.multiple_segments_one_param') === false) {
460
                        // Utilisation de back-references
461 10
                        $segments = explode('/', preg_replace('#^' . $routeKey . '$#u', $handler, $uri));
462
                    } else {
463
                        if (str_contains($methodAndParams, '/')) {
464
                            [$method, $handlerParams] = explode('/', $methodAndParams, 2);
465
                            $params                   = explode('/', $handlerParams);
466
                            $handlerSegments          = array_merge([$controller . '::' . $method], $params);
467
                        } else {
468
                            $handlerSegments = [$handler];
469
                        }
470
471
                        $segments = [];
472
473
                        foreach ($handlerSegments as $segment) {
474 10
                            $segments[] = $this->replaceBackReferences($segment, $matches);
475
                        }
476
                    }
477
                } else {
478 18
                    $segments = explode('/', $handler);
479
                }
480
481 20
                $this->setRequest($segments);
482
483 20
                $this->setMatchedRoute($matchedKey, $handler);
484
485 20
                return true;
486
            }
487
        }
488
489 4
        return false;
490
    }
491
492
    /**
493
     * Replace string `$n` with `$matches[n]` value.
494
     */
495
    private function replaceBackReferences(string $input, array $matches): string
496
    {
497
        $pattern = '/\$([1-' . count($matches) . '])/u';
498
499
        return preg_replace_callback(
500
            $pattern,
501
            static function ($match) use ($matches) {
502
                $index = (int) $match[1];
503
504
                return $matches[$index] ?? '';
505
            },
506
            $input
507
        );
508
    }
509
510
    /**
511
     * Tente de faire correspondre un chemin d'URI avec des contrôleurs et des répertoires
512
     * trouvé dans CONTROLLER_PATH, pour trouver une route correspondante.
513
     */
514
    public function autoRoute(string $uri)
515
    {
516
        [$this->directory, $this->controller, $this->method, $this->params]
517 2
            = $this->autoRouter->getRoute($uri, $this->collection->getHTTPVerb());
518
    }
519
520
    /**
521
     * Définir la route de la requête
522
     *
523
     * Prend un tableau de segments URI en entrée et définit la classe/méthode
524
     * être appelé.
525
     *
526
     * @param array $segments segments d'URI
527
     */
528
    protected function setRequest(array $segments = [])
529
    {
530
        // Si nous n'avons aucun segment - essayez le contrôleur par défaut ;
531
        if ($segments === []) {
532
            return;
533
        }
534
535 20
        [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null);
536
537 20
        $this->setController($controller);
538
539
        // $this->method contient déjà le nom de la méthode par défaut,
540
        // donc ne l'écrasez pas avec le vide.
541
        if (! empty($method)) {
542 18
            $this->setMethod($method);
543
        }
544
545 20
        array_shift($segments);
546
547 20
        $this->params = $segments;
548
    }
549
550
    /**
551
     * @param callable|string $handler
552
     */
553
    protected function setMatchedRoute(string $route, $handler): void
554
    {
555 20
        $this->matchedRoute = [$route, $handler];
556
557 20
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
558
    }
559
560
    /**
561
     * Modifie le nom du controleur
562
     */
563
    private function setController(string $name): void
564
    {
565 20
        $this->controller = $this->makeController($name);
566
    }
567
568
    /**
569
     * Construit un nom de contrôleur valide
570
     */
571
    private function makeController(string $name): string
572
    {
573
        if ($this->autoRouter instanceof AutoRouter) {
574 20
            return $this->autoRouter->makeController($name);
575
        }
576
577 20
        return $name;
578
    }
579
580
    /**
581
     * Modifie le nom de la méthode
582
     */
583
    private function setMethod(string $name): void
584
    {
585 20
        $this->method = preg_replace('#' . config('app.url_suffix') . '$#i', '', $name);
0 ignored issues
show
Bug introduced by
Are you sure config('app.url_suffix') of type T|null|object can be used in concatenation? ( Ignorable by Annotation )

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

585
        $this->method = preg_replace('#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i', '', $name);
Loading history...
586
    }
587
588
    /**
589
     * Vérifie les caractères non autorisés
590
     */
591
    private function checkDisallowedChars(string $uri): void
592
    {
593
        foreach (explode('/', $uri) as $segment) {
594
            if ($segment !== '' && $this->permittedURIChars !== ''
595
                && preg_match('/\A[' . $this->permittedURIChars . ']+\z/iu', $segment) !== 1
596
            ) {
597
                throw new BadRequestException(
598
                    'L\'URI que vous avez soumis contient des caractères non autorisés : "' . $segment . '"'
599 2
                );
600
            }
601
        }
602
    }
603
}
604