Passed
Push — main ( 57b38b...aec17e )
by Dimitri
04:13
created

Router::checkRoutes()   F

Complexity

Conditions 19
Paths 171

Size

Total Lines 109
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 21.8878

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 19
eloc 54
c 2
b 2
f 0
nc 171
nop 1
dl 0
loc 109
ccs 24
cts 30
cp 0.8
crap 21.8878
rs 3.925

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\Exceptions\PageNotFoundException;
19
use BlitzPHP\Exceptions\RedirectException;
20
use BlitzPHP\Exceptions\RouterException;
21
use BlitzPHP\Http\Request;
22
use BlitzPHP\Utilities\String\Text;
23
use Closure;
24
use Psr\Http\Message\ServerRequestInterface;
25
26
/**
27
 * Analyse l'URL de la requête dans le contrôleur, action et paramètres. Utilise les routes connectées
28
 * pour faire correspondre la chaîne d'URL entrante aux paramètres qui permettront à la requête d'être envoyée. Aussi
29
 * 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
30
 * la façon dont le monde interagit avec votre application (URL) et l'implémentation (contrôleurs et actions).
31
 */
32
class Router implements RouterInterface
33
{
34
    /**
35
     * Une instance de la classe RouteCollection.
36
     *
37
     * @var RouteCollection
38
     */
39
    protected $collection;
40
41
    /**
42
     * Sous-répertoire contenant la classe de contrôleur demandée.
43
     * Principalement utilisé par 'autoRoute'.
44
     */
45
    protected ?string $directory = null;
46
47
    /**
48
     * Le nom de la classe contrôleur
49
     *
50
     * @var Closure|string
51
     */
52
    protected $controller;
53
54
    /**
55
     * Le nom de la méthode à utiliser
56
     */
57
    protected string $method = '';
58
59
    /**
60
     * Un tableau de liens qui ont été collectés afin
61
     * qu'ils puissent être envoyés aux routes de fermeture.
62
     */
63
    protected array $params = [];
64
65
    /**
66
     * Le nom du du front-controller.
67
     */
68
    protected string $indexPage = 'index.php';
69
70
    /**
71
     * Si les tirets dans les URI doivent être convertis
72
     * pour les traits de soulignement lors de la détermination des noms de méthode.
73
     */
74
    protected bool $translateURIDashes = true;
75
76
    /**
77
     * Les routes trouvées pour la requête courrante
78
     */
79
    protected ?array $matchedRoute = null;
80
81
    /**
82
     * Les options de la route matchée.
83
     */
84
    protected ?array $matchedRouteOptions = null;
85
86
    /**
87
     * Le locale (langue) qui a été detectée dans la route.
88
     *
89
     * @var string
90
     */
91
    protected $detectedLocale;
92
93
    /**
94
     * Les informations des middlewares à executer
95
     * Si la route matchée necessite des filtres.
96
     *
97
     * @var string[]
98
     */
99
    protected array $middlewaresInfo = [];
100
101
    protected ?AutoRouterInterface $autoRouter = null;
102
103
    /**
104
     * @param RouteCollection $routes
105
     * @param Request         $request
106
     */
107
    public function __construct(RouteCollectionInterface $routes, ServerRequestInterface $request)
108
    {
109 8
        $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...
110
111 8
        $this->setController($this->collection->getDefaultController());
112 8
        $this->setMethod($this->collection->getDefaultMethod());
113
114 8
        $this->collection->setHTTPVerb($request->getMethod());
115
116 8
        $this->translateURIDashes = $this->collection->shouldTranslateURIDashes();
117
118
        if ($this->collection->shouldAutoRoute()) {
119
            $this->autoRouter = new AutoRouter(
120
                $this->collection->getRegisteredControllers('*'),
121
                $this->collection->getDefaultNamespace(),
122
                $this->collection->getDefaultController(),
123
                $this->collection->getDefaultMethod(),
124
                $this->translateURIDashes
125 8
            );
126
        }
127
    }
128
129
    /**
130
     * @return Closure|string Nom de classe du contrôleur ou closure
131
     *
132
     * @throws PageNotFoundException
133
     * @throws RedirectException
134
     */
135
    public function handle(?string $uri = null)
136
    {
137
        // Si nous ne trouvons pas d'URI à comparer, alors
138
        // tout fonctionne à partir de ses paramètres par défaut.
139
        if ($uri === null || $uri === '') {
140 2
            $uri = '/';
141
        }
142
143 8
        $uri                   = urldecode($uri);
144 8
        $this->middlewaresInfo = [];
145
146
        if ($this->checkRoutes($uri)) {
147
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
148 2
                $this->middlewaresInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
149
            }
150
151
            // met a jour le routeur dans le conteneur car permet notament de recupere les bonnes
152
            // info du routing (route actuelle, controleur et methode mappés)
153 8
            Services::set(static::class, $this);
154
155 8
            return $this->controllerName();
156
        }
157
158
        // Toujours là ? Ensuite, nous pouvons essayer de faire correspondre l'URI avec
159
        // Contrôleurs/répertoires, mais l'application peut ne pas
160
        // vouloir ceci, comme dans le cas des API.
161
        if (! $this->collection->shouldAutoRoute()) {
162 4
            throw new PageNotFoundException("Impossible de trouver une route pour '{$this->collection->getHTTPVerb()}: {$uri}'.");
163
        }
164
165
        $this->autoRoute($uri);
166
167
        // met a jour le routeur dans le conteneur car permet notament de recupere les bonnes
168
        // info du routing (route actuelle, controleur et methode mappés)
169
        Services::set(static::class, $this);
170
171
        return $this->controllerName();
172
    }
173
174
    /**
175
     * Renvoie les informations des middlewares de la routes matchée
176
     *
177
     * @return string[]
178
     */
179
    public function getMiddlewares(): array
180
    {
181 2
        return $this->middlewaresInfo;
182
    }
183
184
    /**
185
     * Renvoie le nom du contrôleur matché
186
     *
187
     * @return Closure|string
188
     */
189
    public function controllerName()
190
    {
191
        if (! is_string($this->controller)) {
192 4
            return $this->controller;
193
        }
194
195
        $controller = str_contains($this->controller, '\\')
196
            ? $this->controller
197
            : trim($this->collection->getDefaultNamespace(), '\\') . '\\' . $this->controller;
198
199
        $controller = preg_replace(
200
            ['#(\_)?Controller$#i', '#' . config('app.url_suffix') . '$#i'],
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

200
            ['#(\_)?Controller$#i', '#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i'],
Loading history...
201
            '',
202
            ucfirst($controller)
203 8
        );
204
205 8
        $controller = trim($controller, '/\\');
206
        $controller = $this->translateURIDashes
207
            ? str_replace('-', '_', $controller)
208 2
            : Text::convertTo($controller, 'pascal');
209
210 8
        return $controller . 'Controller';
211
    }
212
213
    /**
214
     * Retourne le nom de la méthode à exécuter
215
     */
216
    public function methodName(): string
217
    {
218 8
        return str_replace('-', '_', $this->method);
219
    }
220
221
    /**
222
     * Renvoie les paramètres de remplacement 404 de la collection.
223
     * Si le remplacement est une chaîne, sera divisé en tableau contrôleur/index.
224
     *
225
     * @return array|callable|null
226
     */
227
    public function get404Override()
228
    {
229
        $route = $this->collection->get404Override();
230
231
        if (is_string($route)) {
232
            $routeArray = explode('::', $route);
233
234
            return [
235
                $routeArray[0], // Controller
236
                $routeArray[1] ?? 'index',   // Method
237
            ];
238
        }
239
240
        if (is_callable($route)) {
241
            return $route;
242
        }
243
244
        return null;
245
    }
246
247
    /**
248
     * Renvoie les liaisons qui ont été mises en correspondance et collectées
249
     * pendant le processus d'analyse sous forme de tableau, prêt à être envoyé à
250
     * instance->method(...$params).
251
     */
252
    public function params(): array
253
    {
254 6
        return $this->params;
255
    }
256
257
    /**
258
     * Renvoie le nom du sous-répertoire dans lequel se trouve le contrôleur.
259
     * Relatif à APPPATH.'Controllers'.
260
     *
261
     * Uniquement utilisé lorsque le routage automatique est activé.
262
     */
263
    public function directory(): string
264
    {
265
        if ($this->autoRouter instanceof AutoRouter) {
266
            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

266
            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...
267
        }
268
269
        return '';
270
    }
271
272
    /**
273
     * Renvoie les informations de routage qui correspondaient à ce
274
     * requête, si une route a été définie.
275
     */
276
    public function getMatchedRoute(): ?array
277
    {
278
        return $this->matchedRoute;
279
    }
280
281
    /**
282
     * Renvoie toutes les options définies pour la route correspondante
283
     */
284
    public function getMatchedRouteOptions(): ?array
285
    {
286 2
        return $this->matchedRouteOptions;
287
    }
288
289
    /**
290
     * Définit la valeur qui doit être utilisée pour correspondre au fichier index.php. Valeurs par défaut
291
     * à index.php mais cela vous permet de le modifier au cas où vous utilisez
292
     * quelque chose comme mod_rewrite pour supprimer la page. Vous pourriez alors le définir comme une chaine vide=
293
     */
294
    public function setIndexPage(string $page): self
295
    {
296
        $this->indexPage = $page;
297
298
        return $this;
299
    }
300
301
    /**
302
     * Renvoie vrai/faux selon que la route actuelle contient ou non
303
     * un placeholder {locale}.
304
     */
305
    public function hasLocale(): bool
306
    {
307 2
        return (bool) $this->detectedLocale;
308
    }
309
310
    /**
311
     * Renvoie la locale (langue) détectée, le cas échéant, ou null.
312
     */
313
    public function getLocale(): ?string
314
    {
315 2
        return $this->detectedLocale;
316
    }
317
318
    /**
319
     * Compare la chaîne uri aux routes que la
320
     * classe RouteCollection a définie pour nous, essayant de trouver une correspondance.
321
     * Cette méthode modifiera $this->controller, si nécessaire.
322
     *
323
     * @param string $uri Le chemin URI à comparer aux routes
324
     *
325
     * @return bool Si la route a été mis en correspondance ou non.
326
     *
327
     * @throws RedirectException
328
     */
329
    protected function checkRoutes(string $uri): bool
330
    {
331 8
        $routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
332
333
        // S'il n'y a pas de routes definies pour la methode HTTP, c'est pas la peine d'aller plus loin
334
        if (empty($routes)) {
335
            return false;
336
        }
337
338
        $uri = $uri === '/'
339
            ? $uri
340 8
            : trim($uri, '/ ');
341
342
        // Boucle dans le tableau de routes à la recherche de caractères génériques
343
        foreach ($routes as $routeKey => $handler) {
344
            $routeKey = $routeKey === '/'
345
                ? $routeKey
346 8
                : ltrim($routeKey, '/ ');
347
348 8
            $matchedKey = $routeKey;
349
350
            // A-t-on affaire à une locale ?
351
            if (str_contains($routeKey, '{locale}')) {
352 6
                $routeKey = str_replace('{locale}', '[^/]+', $routeKey);
353
            }
354
355
            // Est-ce que RegEx correspond ?
356
            if (preg_match('#^' . $routeKey . '$#u', $uri, $matches)) {
357
                // Cette route est-elle censée rediriger vers une autre ?
358
                if ($this->collection->isRedirect($routeKey)) {
359
                    // remplacement des groupes de routes correspondants par des références : post/([0-9]+) -> post/$1
360
                    $redirectTo = preg_replace_callback('/(\([^\(]+\))/', static function () {
361
                        static $i = 1;
362
363
                        return '$' . $i++;
364
                    }, is_array($handler) ? key($handler) : $handler);
365
366
                    throw new RedirectException(
367
                        preg_replace('#^' . $routeKey . '$#u', $redirectTo, $uri),
368
                        $this->collection->getRedirectCode($routeKey)
369
                    );
370
                }
371
                // Stocke nos paramètres régionaux afin que l'objet CodeIgniter puisse l'affecter à la requête.
372
                if (str_contains($matchedKey, '{locale}')) {
373
                    preg_match(
374
                        '#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
375
                        $uri,
376
                        $matched
377 8
                    );
378
379
                    if ($this->collection->shouldUseSupportedLocalesOnly()
380
                        && ! in_array($matched['locale'], config('App')->supportedLocales, true)) {
0 ignored issues
show
Bug introduced by
The property supportedLocales does not seem to exist on BlitzPHP\Config\Config.
Loading history...
381
                        // Lancer une exception pour empêcher l'autorouteur,
382
                        // si activé, essayer de trouver une route
383
                        throw PageNotFoundException::localeNotSupported($matched['locale']);
384
                    }
385
386 2
                    $this->detectedLocale = $matched['locale'];
387 2
                    unset($matched);
388
                }
389
390
                // Utilisons-nous Closures ? Si tel est le cas, nous devons collecter les paramètres dans un tableau
391
                // afin qu'ils puissent être transmis ultérieurement à la méthode du contrôleur.
392
                if (! is_string($handler) && is_callable($handler)) {
393 4
                    $this->controller = $handler;
394
395
                    // Supprime la chaîne d'origine du tableau matches
396 4
                    array_shift($matches);
397
398 4
                    $this->params = $matches;
399
400 4
                    $this->setMatchedRoute($matchedKey, $handler);
401
402 4
                    return true;
403
                }
404
405
                if (is_array($handler)) {
406 8
                    $handler = implode('::', $handler);
407
                }
408
409 8
                [$controller] = explode('::', $handler);
410
411
                // Vérifie `/` dans le nom du contrôleur
412
                if (str_contains($controller, '/')) {
413 2
                    throw RouterException::invalidControllerName($handler);
414
                }
415
416
                if (str_contains($handler, '$') && str_contains($routeKey, '(')) {
417
                    // Vérifie le contrôleur dynamique
418
                    if (str_contains($controller, '$')) {
419 2
                        throw RouterException::dynamicController($handler);
420
                    }
421
422
                    // Utilisation de back-references
423 8
                    $handler = preg_replace('#^' . $routeKey . '$#u', $handler, $uri);
424
                } else {
425 8
                    array_shift($matches);
426 8
                    $handler .= '/' . implode('/', $matches);
427
                }
428
429 8
                $this->setRequest(explode('/', $handler));
430
431 8
                $this->setMatchedRoute($matchedKey, $handler);
432
433 8
                return true;
434
            }
435
        }
436
437 4
        return false;
438
    }
439
440
    /**
441
     * Tente de faire correspondre un chemin d'URI avec des contrôleurs et des répertoires
442
     * trouvé dans CONTROLLER_PATH, pour trouver une route correspondante.
443
     */
444
    public function autoRoute(string $uri)
445
    {
446
        [$this->directory, $this->controller, $this->method, $this->params]
447
            = $this->autoRouter->getRoute($uri, $this->collection->getHTTPVerb());
448
    }
449
450
    /**
451
     * Définir la route de la requête
452
     *
453
     * Prend un tableau de segments URI en entrée et définit la classe/méthode
454
     * être appelé.
455
     *
456
     * @param array $segments segments d'URI
457
     */
458
    protected function setRequest(array $segments = [])
459
    {
460
        // Si nous n'avons aucun segment - essayez le contrôleur par défaut ;
461
        if (empty($segments)) {
462
            return;
463
        }
464
465 8
        [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null);
466
467 8
        $this->setController($controller);
468
469
        // $this->method contient déjà le nom de la méthode par défaut,
470
        // donc ne l'écrasez pas avec le vide.
471
        if (! empty($method)) {
472 8
            $this->setMethod($method);
473
        }
474
475 8
        array_shift($segments);
476
477 8
        $this->params = $segments;
478
    }
479
480
    /**
481
     * Modifie le nom du controleur
482
     */
483
    private function setController(string $name): void
484
    {
485 8
        $this->controller = $this->makeController($name);
486
    }
487
488
    /**
489
     * Construit un nom de contrôleur valide
490
     */
491
    private function makeController(string $name): string
492
    {
493
        if ($this->autoRouter instanceof AutoRouter) {
494 8
            return $this->autoRouter->makeController($name);
495
        }
496
497 8
        return $name;
498
    }
499
500
    /**
501
     * Modifie le nom de la méthode
502
     */
503
    private function setMethod(string $name): void
504
    {
505 8
        $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

505
        $this->method = preg_replace('#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i', '', $name);
Loading history...
506
    }
507
508
    /**
509
     * @param callable|string $handler
510
     */
511
    protected function setMatchedRoute(string $route, $handler): void
512
    {
513 8
        $this->matchedRoute = [$route, $handler];
514
515 8
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
516
    }
517
}
518