Passed
Push — main ( 1fe2cd...c1deb1 )
by Dimitri
04:10
created

Router::checkRoutes()   F

Complexity

Conditions 23
Paths 427

Size

Total Lines 129
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 30.6093

Importance

Changes 3
Bugs 2 Features 0
Metric Value
cc 23
eloc 68
c 3
b 2
f 0
nc 427
nop 1
dl 0
loc 129
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\PageNotFoundException;
20
use BlitzPHP\Exceptions\RedirectException;
21
use BlitzPHP\Exceptions\RouterException;
22
use BlitzPHP\Http\Request;
23
use BlitzPHP\Utilities\String\Text;
24
use Closure;
25
use Psr\Http\Message\ServerRequestInterface;
26
27
/**
28
 * Analyse l'URL de la requête dans le contrôleur, action et paramètres. Utilise les routes connectées
29
 * pour faire correspondre la chaîne d'URL entrante aux paramètres qui permettront à la requête d'être envoyée. Aussi
30
 * 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
31
 * la façon dont le monde interagit avec votre application (URL) et l'implémentation (contrôleurs et actions).
32
 */
33
class Router implements RouterInterface
34
{
35
    /**
36
     * List of allowed HTTP methods (and CLI for command line use).
37
     */
38
    public const HTTP_METHODS = [
39
        Method::GET,
40
        Method::HEAD,
41
        Method::POST,
42
        Method::PATCH,
43
        Method::PUT,
44
        Method::DELETE,
45
        Method::OPTIONS,
46
        Method::TRACE,
47
        Method::CONNECT,
48
        'CLI',
49
    ];
50
51
    /**
52
     * Une instance de la classe RouteCollection.
53
     *
54
     * @var RouteCollection
55
     */
56
    protected $collection;
57
58
    /**
59
     * Sous-répertoire contenant la classe de contrôleur demandée.
60
     * Principalement utilisé par 'autoRoute'.
61
     */
62
    protected ?string $directory = null;
63
64
    /**
65
     * Le nom de la classe contrôleur
66
     *
67
     * @var Closure|string
68
     */
69
    protected $controller;
70
71
    /**
72
     * Le nom de la méthode à utiliser
73
     */
74
    protected string $method = '';
75
76
    /**
77
     * Un tableau de liens qui ont été collectés afin
78
     * qu'ils puissent être envoyés aux routes de fermeture.
79
     */
80
    protected array $params = [];
81
82
    /**
83
     * Le nom du du front-controller.
84
     */
85
    protected string $indexPage = 'index.php';
86
87
    /**
88
     * Si les tirets dans les URI doivent être convertis
89
     * pour les traits de soulignement lors de la détermination des noms de méthode.
90
     */
91
    protected bool $translateURIDashes = true;
92
93
    /**
94
     * Les routes trouvées pour la requête courrante
95
     */
96
    protected ?array $matchedRoute = null;
97
98
    /**
99
     * Les options de la route matchée.
100
     */
101
    protected ?array $matchedRouteOptions = null;
102
103
    /**
104
     * Le locale (langue) qui a été detectée dans la route.
105
     *
106
     * @var string
107
     */
108
    protected $detectedLocale;
109
110
    /**
111
     * Les informations des middlewares à executer
112
     * Si la route matchée necessite des filtres.
113
     *
114
     * @var string[]
115
     */
116
    protected array $middlewaresInfo = [];
117
118
    protected ?AutoRouterInterface $autoRouter = null;
119
120
    /**
121
     * @param RouteCollection $routes
122
     * @param Request         $request
123
     */
124
    public function __construct(RouteCollectionInterface $routes, ServerRequestInterface $request)
125
    {
126 16
        $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...
127
128 16
        $this->setController($this->collection->getDefaultController());
129 16
        $this->setMethod($this->collection->getDefaultMethod());
130
131 16
        $this->collection->setHTTPVerb($request->getMethod());
132
133 16
        $this->translateURIDashes = $this->collection->shouldTranslateURIDashes();
134
135
        if ($this->collection->shouldAutoRoute()) {
136
            $this->autoRouter = new AutoRouter(
137
                $this->collection->getRegisteredControllers('*'),
138
                $this->collection->getDefaultNamespace(),
139
                $this->collection->getDefaultController(),
140
                $this->collection->getDefaultMethod(),
141
                $this->translateURIDashes
142 16
            );
143
        }
144
    }
145
146
    /**
147
     * @return Closure|string Nom de classe du contrôleur ou closure
148
     *
149
     * @throws PageNotFoundException
150
     * @throws RedirectException
151
     */
152
    public function handle(?string $uri = null)
153
    {
154
        // Si nous ne trouvons pas d'URI à comparer, alors
155
        // tout fonctionne à partir de ses paramètres par défaut.
156
        if ($uri === null || $uri === '') {
157 2
            $uri = '/';
158
        }
159
160 16
        $uri                   = urldecode($uri);
161 16
        $this->middlewaresInfo = [];
162
163
        if ($this->checkRoutes($uri)) {
164
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
165 2
                $this->middlewaresInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
166
            }
167
168
            // met a jour le routeur dans le conteneur car permet notament de recupere les bonnes
169
            // info du routing (route actuelle, controleur et methode mappés)
170 16
            Services::set(static::class, $this);
171
172 16
            return $this->controllerName();
173
        }
174
175
        // Toujours là ? Ensuite, nous pouvons essayer de faire correspondre l'URI avec
176
        // Contrôleurs/répertoires, mais l'application peut ne pas
177
        // vouloir ceci, comme dans le cas des API.
178
        if (! $this->collection->shouldAutoRoute()) {
179 4
            throw new PageNotFoundException("Impossible de trouver une route pour '{$this->collection->getHTTPVerb()}: {$uri}'.");
180
        }
181
182 2
        $this->autoRoute($uri);
183
184
        // met a jour le routeur dans le conteneur car permet notament de recupere les bonnes
185
        // info du routing (route actuelle, controleur et methode mappés)
186 2
        Services::set(static::class, $this);
187
188 2
        return $this->controllerName();
189
    }
190
191
    /**
192
     * Renvoie les informations des middlewares de la routes matchée
193
     *
194
     * @return string[]
195
     */
196
    public function getMiddlewares(): array
197
    {
198 2
        return $this->middlewaresInfo;
199
    }
200
201
    /**
202
     * Renvoie le nom du contrôleur matché
203
     *
204
     * @return Closure|string
205
     */
206
    public function controllerName()
207
    {
208
        if (! is_string($this->controller)) {
209 4
            return $this->controller;
210
        }
211
212
        $controller = str_contains($this->controller, '\\')
213
            ? $this->controller
214
            : trim($this->collection->getDefaultNamespace(), '\\') . '\\' . $this->controller;
215
216
        $controller = preg_replace(
217
            ['#(\_)?Controller$#i', '#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i'],
218
            '',
219
            ucfirst($controller)
220 16
        );
221
222 16
        $controller = trim($controller, '/\\');
223
        $controller = $this->translateURIDashes
224
            ? str_replace('-', '_', $controller)
225 2
            : Text::convertTo($controller, 'pascal');
226
227 16
        return $controller . 'Controller';
228
    }
229
230
    /**
231
     * Retourne le nom de la méthode à exécuter
232
     */
233
    public function methodName(): string
234
    {
235 10
        return str_replace('-', '_', $this->method);
236
    }
237
238
    /**
239
     * Renvoie les paramètres de remplacement 404 de la collection.
240
     * Si le remplacement est une chaîne, sera divisé en tableau contrôleur/index.
241
     *
242
     * @return array|callable|null
243
     */
244
    public function get404Override()
245
    {
246
        $route = $this->collection->get404Override();
247
248
        if (is_string($route)) {
249
            $routeArray = explode('::', $route);
250
251
            return [
252
                $routeArray[0], // Controller
253
                $routeArray[1] ?? 'index',   // Method
254
            ];
255
        }
256
257
        if (is_callable($route)) {
258
            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...
259
        }
260
261
        return null;
262
    }
263
264
    /**
265
     * Renvoie les liaisons qui ont été mises en correspondance et collectées
266
     * pendant le processus d'analyse sous forme de tableau, prêt à être envoyé à
267
     * instance->method(...$params).
268
     */
269
    public function params(): array
270
    {
271 8
        return $this->params;
272
    }
273
274
    /**
275
     * Renvoie le nom du sous-répertoire dans lequel se trouve le contrôleur.
276
     * Relatif à APPPATH.'Controllers'.
277
     *
278
     * Uniquement utilisé lorsque le routage automatique est activé.
279
     */
280
    public function directory(): string
281
    {
282
        if ($this->autoRouter instanceof AutoRouter) {
283
            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

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

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

560
        $this->method = preg_replace('#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i', '', $name);
Loading history...
561
    }
562
563
    /**
564
     * @param callable|string $handler
565
     */
566
    protected function setMatchedRoute(string $route, $handler): void
567
    {
568 16
        $this->matchedRoute = [$route, $handler];
569
570 16
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
571
    }
572
}
573