Passed
Push — main ( c6deb1...79ccdf )
by Dimitri
12:23
created

Router   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 477
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 140
dl 0
loc 477
rs 6.96
c 2
b 1
f 0
wmc 53

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getMatchedRoute() 0 3 1
A getLocale() 0 3 1
F checkRoutes() 0 106 19
A hasLocale() 0 3 1
A setController() 0 3 1
A setMethod() 0 3 1
A methodName() 0 5 2
A getMatchedRouteOptions() 0 3 1
A getMiddlewares() 0 3 1
A get404Override() 0 18 3
A setIndexPage() 0 5 1
A setRequest() 0 20 3
A makeController() 0 7 2
A handle() 0 37 6
A controllerName() 0 15 3
A setMatchedRoute() 0 5 1
A params() 0 3 1
A __construct() 0 18 2
A directory() 0 7 2
A autoRoute() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

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

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

260
            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...
261
        }
262
263
        return '';
264
    }
265
266
    /**
267
     * Renvoie les informations de routage qui correspondaient à ce
268
     * requête, si une route a été définie.
269
     */
270
    public function getMatchedRoute(): ?array
271
    {
272
        return $this->matchedRoute;
273
    }
274
275
    /**
276
     * Renvoie toutes les options définies pour la route correspondante
277
     */
278
    public function getMatchedRouteOptions(): ?array
279
    {
280
        return $this->matchedRouteOptions;
281
    }
282
283
    /**
284
     * Définit la valeur qui doit être utilisée pour correspondre au fichier index.php. Valeurs par défaut
285
     * à index.php mais cela vous permet de le modifier au cas où vous utilisez
286
     * quelque chose comme mod_rewrite pour supprimer la page. Vous pourriez alors le définir comme une chaine vide=
287
     */
288
    public function setIndexPage(string $page): self
289
    {
290
        $this->indexPage = $page;
291
292
        return $this;
293
    }
294
295
    /**
296
     * Renvoie vrai/faux selon que la route actuelle contient ou non
297
     * un placeholder {locale}.
298
     */
299
    public function hasLocale(): bool
300
    {
301
        return (bool) $this->detectedLocale;
302
    }
303
304
    /**
305
     * Renvoie la locale (langue) détectée, le cas échéant, ou null.
306
     */
307
    public function getLocale(): ?string
308
    {
309
        return $this->detectedLocale;
310
    }
311
312
    /**
313
     * Compare la chaîne uri aux routes que la
314
     * classe RouteCollection a définie pour nous, essayant de trouver une correspondance.
315
     * Cette méthode modifiera $this->controller, si nécessaire.
316
     *
317
     * @param string $uri Le chemin URI à comparer aux routes
318
     *
319
     * @return bool Si la route a été mis en correspondance ou non.
320
     *
321
     * @throws RedirectException
322
     */
323
    protected function checkRoutes(string $uri): bool
324
    {
325
        $routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
326
327
        // S'il n'y a pas de routes definies pour la methode HTTP, c'est pas la peine d'aller plus loin
328
        if (empty($routes)) {
329
            return false;
330
        }
331
332
        $uri = $uri === '/'
333
            ? $uri
334
            : trim($uri, '/ ');
335
336
        // Boucle dans le tableau de routes à la recherche de caractères génériques
337
        foreach ($routes as $routeKey => $handler) {
338
            $routeKey = $routeKey === '/'
339
                ? $routeKey
340
                : ltrim($routeKey, '/ ');
341
342
            $matchedKey = $routeKey;
343
344
            // A-t-on affaire à une locale ?
345
            if (strpos($routeKey, '{locale}') !== false) {
346
                $routeKey = str_replace('{locale}', '[^/]+', $routeKey);
347
            }
348
349
            // Est-ce que RegEx correspond ?
350
            if (preg_match('#^' . $routeKey . '$#u', $uri, $matches)) {
351
                // Cette route est-elle censée rediriger vers une autre ?
352
                if ($this->collection->isRedirect($routeKey)) {
353
                    // remplacement des groupes de routes correspondants par des références : post/([0-9]+) -> post/$1
354
                    $redirectTo = preg_replace_callback('/(\([^\(]+\))/', static function () {
355
                        static $i = 1;
356
357
                        return '$' . $i++;
358
                    }, is_array($handler) ? key($handler) : $handler);
359
360
                    throw new RedirectException(
361
                        preg_replace('#^' . $routeKey . '$#u', $redirectTo, $uri),
362
                        $this->collection->getRedirectCode($routeKey)
363
                    );
364
                }
365
                // Stocke nos paramètres régionaux afin que l'objet CodeIgniter puisse l'affecter à la requête.
366
                if (strpos($matchedKey, '{locale}') !== false) {
367
                    preg_match(
368
                        '#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
369
                        $uri,
370
                        $matched
371
                    );
372
373
                    if ($this->collection->shouldUseSupportedLocalesOnly()
374
                        && ! 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...
375
                        // Lancer une exception pour empêcher l'autorouteur,
376
                        // si activé, essayer de trouver une route
377
                        throw PageNotFoundException::localeNotSupported($matched['locale']);
378
                    }
379
380
                    $this->detectedLocale = $matched['locale'];
381
                    unset($matched);
382
                }
383
384
                // Utilisons-nous Closures ? Si tel est le cas, nous devons collecter les paramètres dans un tableau
385
                // afin qu'ils puissent être transmis ultérieurement à la méthode du contrôleur.
386
                if (! is_string($handler) && is_callable($handler)) {
387
                    $this->controller = $handler;
388
389
                    // Supprime la chaîne d'origine du tableau matches
390
                    array_shift($matches);
391
392
                    $this->params = $matches;
393
394
                    $this->setMatchedRoute($matchedKey, $handler);
395
396
                    return true;
397
                }
398
399
                if (is_array($handler)) {
400
                    $handler = implode('::', $handler);
401
                }
402
403
                [$controller] = explode('::', $handler);
404
405
                // Vérifie `/` dans le nom du contrôleur
406
                if (strpos($controller, '/') !== false) {
407
                    throw RouterException::invalidControllerName($handler);
408
                }
409
410
                if (strpos($handler, '$') !== false && strpos($routeKey, '(') !== false) {
411
                    // Vérifie le contrôleur dynamique
412
                    if (strpos($controller, '$') !== false) {
413
                        throw RouterException::dynamicController($handler);
414
                    }
415
416
                    // Utilisation de back-references
417
                    $handler = preg_replace('#^' . $routeKey . '$#u', $handler, $uri);
418
                }
419
420
                $this->setRequest(explode('/', $handler));
421
422
                $this->setMatchedRoute($matchedKey, $handler);
423
424
                return true;
425
            }
426
        }
427
428
        return false;
429
    }
430
431
    /**
432
     * Tente de faire correspondre un chemin d'URI avec des contrôleurs et des répertoires
433
     * trouvé dans CONTROLLER_PATH, pour trouver une route correspondante.
434
     */
435
    public function autoRoute(string $uri)
436
    {
437
        [$this->directory, $this->controller, $this->method, $this->params]
438
            = $this->autoRouter->getRoute($uri, $this->collection->getHTTPVerb());
0 ignored issues
show
Unused Code introduced by
The call to BlitzPHP\Contracts\Route...erInterface::getRoute() has too many arguments starting with $this->collection->getHTTPVerb(). ( Ignorable by Annotation )

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

438
        /** @scrutinizer ignore-call */ 
439
        [$this->directory, $this->controller, $this->method, $this->params]

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
439
    }
440
441
    /**
442
     * Définir la route de la requête
443
     *
444
     * Prend un tableau de segments URI en entrée et définit la classe/méthode
445
     * être appelé.
446
     *
447
     * @param array $segments segments d'URI
448
     */
449
    protected function setRequest(array $segments = [])
450
    {
451
        // Si nous n'avons aucun segment - essayez le contrôleur par défaut ;
452
        if (empty($segments)) {
453
            return;
454
        }
455
456
        [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null);
457
458
        $this->setController($controller);
459
460
        // $this->method contient déjà le nom de la méthode par défaut,
461
        // donc ne l'écrasez pas avec le vide.
462
        if (! empty($method)) {
463
            $this->setMethod($method);
464
        }
465
466
        array_shift($segments);
467
468
        $this->params = $segments;
469
    }
470
471
    /**
472
     * Modifie le nom du controleur
473
     */
474
    private function setController(string $name): void
475
    {
476
        $this->controller = $this->makeController($name);
477
    }
478
479
    /**
480
     * Construit un nom de contrôleur valide
481
     */
482
    private function makeController(string $name): string
483
    {
484
        if ($this->autoRouter instanceof AutoRouter) {
485
            return $this->autoRouter->makeController($name);
486
        }
487
488
        return $name;
489
    }
490
491
    /**
492
     * Modifie le nom de la méthode
493
     */
494
    private function setMethod(string $name): void
495
    {
496
        $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

496
        $this->method = preg_replace('#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i', '', $name);
Loading history...
497
    }
498
499
    /**
500
     * @param callable|string $handler
501
     */
502
    protected function setMatchedRoute(string $route, $handler): void
503
    {
504
        $this->matchedRoute = [$route, $handler];
505
506
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
507
    }
508
}
509