Passed
Push — main ( aa68b0...40dc38 )
by Dimitri
03:05
created

Router::checkRoutes()   D

Complexity

Conditions 17
Paths 163

Size

Total Lines 99
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 17
eloc 48
c 1
b 1
f 0
nc 163
nop 1
dl 0
loc 99
rs 4.6916

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\Contracts\Router\AutoRouterInterface;
15
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
16
use BlitzPHP\Contracts\Router\RouterInterface;
17
use BlitzPHP\Exceptions\PageNotFoundException;
18
use BlitzPHP\Exceptions\RedirectException;
19
use BlitzPHP\Exceptions\RouterException;
20
use BlitzPHP\Utilities\String\Text;
21
use Psr\Http\Message\ServerRequestInterface;
22
23
/**
24
 * Analyse l'URL de la requête dans le contrôleur, action et paramètres. Utilise les routes connectées
25
 * pour faire correspondre la chaîne d'URL entrante aux paramètres qui permettront à la requête d'être envoyée. Aussi
26
 * 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
27
 * la façon dont le monde interagit avec votre application (URL) et l'implémentation (contrôleurs et actions).
28
 */
29
class Router implements RouterInterface
30
{
31
    /**
32
     * Une instance de la classe RouteCollection.
33
     *
34
     * @var RouteCollection
35
     */
36
    protected $collection;
37
38
    /**
39
     * Sous-répertoire contenant la classe de contrôleur demandée.
40
     * Principalement utilisé par 'autoRoute'.
41
     *
42
     * @var string|null
43
     */
44
    protected $directory;
45
46
    /**
47
     * Le nom de la classe contrôleur
48
     *
49
     * @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...
50
     */
51
    protected $controller;
52
53
    /**
54
     * Le nom de la méthode à utiliser
55
     */
56
    protected string $method = '';
57
58
    /**
59
     * Un tableau de liens qui ont été collectés afin
60
     * qu'ils puissent être envoyés aux routes de fermeture.
61
     */
62
    protected array $params = [];
63
64
    /**
65
     * Le nom du du front-controller.
66
     */
67
    protected string $indexPage = 'index.php';
68
69
    /**
70
     * Si les tirets dans les URI doivent être convertis
71
     * pour les traits de soulignement lors de la détermination des noms de méthode.
72
     */
73
    protected bool $translateURIDashes = true;
74
75
    /**
76
     * Les routes trouvées pour la requête courrante
77
     *
78
     * @var array|null
79
     */
80
    protected $matchedRoute;
81
82
    /**
83
     * Les options de la route matchée.
84
     *
85
     * @var array|null
86
     */
87
    protected $matchedRouteOptions;
88
89
    /**
90
     * Le locale (langue) qui a été detectée dans la route.
91
     *
92
     * @var string
93
     */
94
    protected $detectedLocale;
95
96
    /**
97
     * Les informations des middlewares à executer
98
     * Si la route matchée necessite des filtres.
99
     *
100
     * @var string[]
101
     */
102
    protected array $middlewaresInfo = [];
103
104
    protected ?AutoRouterInterface $autoRouter = null;
105
106
    /**
107
     * @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...
108
     *
109
     * @return self
110
     */
111
    public function init(RouteCollectionInterface $routes, ServerRequestInterface $request)
112
    {
113
        $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...
114
115
        $this->setController($this->collection->getDefaultController());
116
        $this->setMethod($this->collection->getDefaultMethod());
117
118
        $this->collection->setHTTPVerb($request->getMethod() ?? strtolower($_SERVER['REQUEST_METHOD']));
119
120
        $this->translateURIDashes = $this->collection->shouldTranslateURIDashes();
121
122
        $this->autoRouter = new AutoRouter(
123
            $this->collection->getRegisteredControllers('cli'),
124
            $this->collection->getDefaultNamespace(),
125
            $this->collection->getDefaultController(),
126
            $this->collection->getDefaultMethod(),
127
            $this->translateURIDashes,
128
            $this->collection->getHTTPVerb()
129
        );
130
131
        return $this;
132
    }
133
134
    /**
135
     * @return Closure|string Controller classname or Closure
136
     *
137
     * @throws PageNotFoundException
138
     * @throws RedirectException
139
     */
140
    public function handle(?string $uri = null)
141
    {
142
        // Si nous ne trouvons pas d'URI à comparer, alors
143
        // tout fonctionne à partir de ses paramètres par défaut.
144
        if ($uri === null || $uri === '') {
145
            return strpos($this->controller, '\\') === false
146
                ? $this->collection->getDefaultNamespace() . $this->controller
147
                : $this->controller;
148
        }
149
150
        $uri = urldecode($uri);
151
152
        if ($this->checkRoutes($uri)) {
153
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
154
                $this->middlewaresInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
155
            }
156
157
            return $this->controller;
158
        }
159
160
        // Toujours là ? Ensuite, nous pouvons essayer de faire correspondre l'URI avec
161
        // Contrôleurs/répertoires, mais l'application peut ne pas
162
        // vouloir ceci, comme dans le cas des API.
163
        if (! $this->collection->shouldAutoRoute()) {
164
            $verb = strtolower($this->collection->getHTTPVerb());
165
166
            throw new PageNotFoundException("Can't find a route for '{$verb}: {$uri}'.");
167
        }
168
169
        $this->autoRoute($uri);
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
        return $this->middlewaresInfo;
182
    }
183
184
    /**
185
     * Renvoie le nom du contrôleur matché
186
     *
187
     * @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...
188
     */
189
    public function controllerName()
190
    {
191
        if (! is_string($this->controller)) {
192
            return $this->controller;
193
        }
194
195
        return $this->translateURIDashes
196
            ? str_replace('-', '_', trim($this->controller, '/\\'))
197
            : Text::toPascalCase($this->controller);
198
    }
199
200
    /**
201
     * Retourne le nom de la méthode à exécuter
202
     */
203
    public function methodName(): string
204
    {
205
        return $this->translateURIDashes
206
            ? str_replace('-', '_', $this->method)
207
            : $this->method;
208
    }
209
210
    /**
211
     * Renvoie les paramètres de remplacement 404 de la collection.
212
     * Si le remplacement est une chaîne, sera divisé en tableau contrôleur/index.
213
     *
214
     * @return array|callable|null
215
     */
216
    public function get404Override()
217
    {
218
        $route = $this->collection->get404Override();
219
220
        if (is_string($route)) {
221
            $routeArray = explode('::', $route);
222
223
            return [
224
                $routeArray[0], // Controller
225
                $routeArray[1] ?? 'index',   // Method
226
            ];
227
        }
228
229
        if (is_callable($route)) {
230
            return $route;
231
        }
232
233
        return null;
234
    }
235
236
    /**
237
     * Renvoie les liaisons qui ont été mises en correspondance et collectées
238
     * pendant le processus d'analyse sous forme de tableau, prêt à être envoyé à
239
     * instance->method(...$params).
240
     */
241
    public function params(): array
242
    {
243
        return $this->params;
244
    }
245
246
    /**
247
     * Renvoie le nom du sous-répertoire dans lequel se trouve le contrôleur.
248
     * Relatif à APPPATH.'Controllers'.
249
     *
250
     * Uniquement utilisé lorsque le routage automatique est activé.
251
     */
252
    public function directory(): string
253
    {
254
        if ($this->autoRouter instanceof AutoRouter) {
255
            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

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

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
502
    }
503
504
    /**
505
     * Modifie le nom du controleur
506
     */
507
    private function setController(string $name): void
508
    {
509
        $this->controller = $this->makeController($name);
510
    }
511
512
    /**
513
     * Construit un nom de contrôleur valide
514
     */
515
    private function makeController(string $name): string
516
    {
517
        if ($this->autoRouter instanceof AutoRouter) {
518
            return $this->autoRouter->makeController($name);
519
        }
520
521
        return $name;
522
    }
523
524
    /**
525
     * Modifie le nom de la méthode
526
     */
527
    private function setMethod(string $name): void
528
    {
529
        $this->method = preg_replace('#' . config('app.url_suffix') . '$#i', '', $name);
530
    }
531
532
    /**
533
     * @param callable|string $handler
534
     */
535
    protected function setMatchedRoute(string $route, $handler): void
536
    {
537
        $this->matchedRoute = [$route, $handler];
538
539
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
540
    }
541
}
542