Test Failed
Push — main ( 57b19c...1fe2cd )
by Dimitri
15:57
created

Router::getMiddlewares()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 1
cp 0
crap 2
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\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
        $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
        $this->setController($this->collection->getDefaultController());
112
        $this->setMethod($this->collection->getDefaultMethod());
113
114
        $this->collection->setHTTPVerb($request->getMethod());
115
116
        $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
            );
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
            $uri = '/';
141
        }
142
143
        $uri                   = urldecode($uri);
144
        $this->middlewaresInfo = [];
145
146
        if ($this->checkRoutes($uri)) {
147
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
148
                $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
            Services::set(static::class, $this);
154
155
            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
            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
        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
            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', '#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i'],
201
            '',
202
            ucfirst($controller)
203
        );
204
205
        $controller = trim($controller, '/\\');
206
        $controller = $this->translateURIDashes
207
            ? str_replace('-', '_', $controller)
208
            : Text::convertTo($controller, 'pascal');
209
210
        return $controller . 'Controller';
211
    }
212
213
    /**
214
     * Retourne le nom de la méthode à exécuter
215
     */
216
    public function methodName(): string
217
    {
218
        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
        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
        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
        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
        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
        $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
            : 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
                : ltrim($routeKey, '/ ');
347
348
            $matchedKey = $routeKey;
349
350
            // A-t-on affaire à une locale ?
351
            if (str_contains($routeKey, '{locale}')) {
352
                $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
                    );
378
379
                    if ($this->collection->shouldUseSupportedLocalesOnly()
380
                        && ! 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

380
                        && ! in_array($matched['locale'], /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
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
                    $this->detectedLocale = $matched['locale'];
387
                    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
                    $this->controller = $handler;
394
395
                    // Supprime la chaîne d'origine du tableau matches
396
                    array_shift($matches);
397
398
                    $this->params = $matches;
399
400
                    $this->setMatchedRoute($matchedKey, $handler);
401
402
                    return true;
403
                }
404
405
                if (is_array($handler)) {
406
                    $handler = implode('::', $handler);
407
                }
408
409
                [$controller] = explode('::', $handler);
410
411
                // Vérifie `/` dans le nom du contrôleur
412
                if (str_contains($controller, '/')) {
413
                    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
                        throw RouterException::dynamicController($handler);
420
                    }
421
422
                    // Utilisation de back-references
423
                    $handler = preg_replace('#^' . $routeKey . '$#u', $handler, $uri);
424
                } else {
425
                    array_shift($matches);
426
                    $handler .= '/' . implode('/', $matches);
427
                }
428
429
                $this->setRequest(explode('/', $handler));
430
431
                $this->setMatchedRoute($matchedKey, $handler);
432
433
                return true;
434
            }
435
        }
436
437
        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
        [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null);
466
467
        $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
            $this->setMethod($method);
473
        }
474
475
        array_shift($segments);
476
477
        $this->params = $segments;
478
    }
479
480
    /**
481
     * Modifie le nom du controleur
482
     */
483
    private function setController(string $name): void
484
    {
485
        $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
            return $this->autoRouter->makeController($name);
495
        }
496
497
        return $name;
498
    }
499
500
    /**
501
     * Modifie le nom de la méthode
502
     */
503
    private function setMethod(string $name): void
504
    {
505
        $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
        $this->matchedRoute = [$route, $handler];
514
515
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
516
    }
517
}
518