Passed
Push — main ( 4cd58e...3399b6 )
by Dimitri
03:02
created

Router::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
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 __construct(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
        if ($this->collection->shouldAutoRoute()) {
123
            $this->autoRouter = new AutoRouter(
124
                $this->collection->getRegisteredControllers('cli'),
125
                $this->collection->getDefaultNamespace(),
126
                $this->collection->getDefaultController(),
127
                $this->collection->getDefaultMethod(),
128
                $this->translateURIDashes,
129
                $this->collection->getHTTPVerb()
130
            );   
131
        }
132
    }
133
134
    /**
135
     * @deprecated
136
     */
137
    public function init(RouteCollectionInterface $routes, ServerRequestInterface $request)
138
    {
139
        return $this;
140
    }
141
142
    /**
143
     * @return Closure|string Nom de classe du contrôleur ou closure
144
     *
145
     * @throws PageNotFoundException
146
     * @throws RedirectException
147
     */
148
    public function handle(?string $uri = null)
149
    {
150
        // Si nous ne trouvons pas d'URI à comparer, alors
151
        // tout fonctionne à partir de ses paramètres par défaut.
152
        if ($uri === null || $uri === '') {
153
            $uri = '/';
154
        }
155
156
        $uri                   = urldecode($uri);
157
        $this->middlewaresInfo = [];
158
159
        if ($this->checkRoutes($uri)) {
160
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
161
                $this->middlewaresInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
162
            }
163
164
            return $this->controllerName();
165
        }
166
167
        // Toujours là ? Ensuite, nous pouvons essayer de faire correspondre l'URI avec
168
        // Contrôleurs/répertoires, mais l'application peut ne pas
169
        // vouloir ceci, comme dans le cas des API.
170
        if (! $this->collection->shouldAutoRoute()) {
171
            $verb = strtolower($this->collection->getHTTPVerb());
172
173
            throw new PageNotFoundException("Impossible de trouver une route pour '{$verb}: {$uri}'.");
174
        }
175
176
        $this->autoRoute($uri);
177
178
        return $this->controllerName();
179
    }
180
181
    /**
182
     * Renvoie les informations des middlewares de la routes matchée
183
     *
184
     * @return string[]
185
     */
186
    public function getMiddlewares(): array
187
    {
188
        return $this->middlewaresInfo;
189
    }
190
191
    /**
192
     * Renvoie le nom du contrôleur matché
193
     *
194
     * @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...
195
     */
196
    public function controllerName()
197
    {
198
        if (! is_string($this->controller)) {
199
            return $this->controller;
200
        }
201
202
        $controller = preg_replace(
203
            ['#(\_)?Controller$#i', '#' . config('app.url_suffix') . '$#i'],
204
            '',
205
            ucfirst($this->controller)
206
        ) . 'Controller';
207
208
        return $this->translateURIDashes
209
            ? str_replace('-', '_', trim($controller, '/\\'))
210
            : Text::toPascalCase($controller);
211
    }
212
213
    /**
214
     * Retourne le nom de la méthode à exécuter
215
     */
216
    public function methodName(): string
217
    {
218
        return $this->translateURIDashes
219
            ? str_replace('-', '_', $this->method)
220
            : $this->method;
221
    }
222
223
    /**
224
     * Renvoie les paramètres de remplacement 404 de la collection.
225
     * Si le remplacement est une chaîne, sera divisé en tableau contrôleur/index.
226
     *
227
     * @return array|callable|null
228
     */
229
    public function get404Override()
230
    {
231
        $route = $this->collection->get404Override();
232
233
        if (is_string($route)) {
234
            $routeArray = explode('::', $route);
235
236
            return [
237
                $routeArray[0], // Controller
238
                $routeArray[1] ?? 'index',   // Method
239
            ];
240
        }
241
242
        if (is_callable($route)) {
243
            return $route;
244
        }
245
246
        return null;
247
    }
248
249
    /**
250
     * Renvoie les liaisons qui ont été mises en correspondance et collectées
251
     * pendant le processus d'analyse sous forme de tableau, prêt à être envoyé à
252
     * instance->method(...$params).
253
     */
254
    public function params(): array
255
    {
256
        return $this->params;
257
    }
258
259
    /**
260
     * Renvoie le nom du sous-répertoire dans lequel se trouve le contrôleur.
261
     * Relatif à APPPATH.'Controllers'.
262
     *
263
     * Uniquement utilisé lorsque le routage automatique est activé.
264
     */
265
    public function directory(): string
266
    {
267
        if ($this->autoRouter instanceof AutoRouter) {
268
            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

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