Router::checkRoutes()   F
last analyzed

Complexity

Conditions 23
Paths 427

Size

Total Lines 131
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 30.6093

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 23
eloc 68
c 4
b 2
f 0
nc 427
nop 1
dl 0
loc 131
ccs 28
cts 37
cp 0.7568
crap 30.6093
rs 0.7957

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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