Passed
Push — main ( d71b68...a743f7 )
by Dimitri
08:12 queued 04:09
created

AutoRouter::getRoute()   C

Complexity

Conditions 15
Paths 156

Size

Total Lines 105
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 28.7511

Importance

Changes 0
Metric Value
cc 15
eloc 51
c 0
b 0
f 0
nc 156
nop 2
dl 0
loc 105
ccs 20
cts 33
cp 0.6061
crap 28.7511
rs 5.4499

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\Exceptions\MethodNotFoundException;
16
use BlitzPHP\Exceptions\PageNotFoundException;
17
use BlitzPHP\Utilities\Helpers;
18
use BlitzPHP\Utilities\String\Text;
19
use ReflectionClass;
20
use ReflectionException;
21
22
/**
23
 * Routeur sécurisé pour le routage automatique
24
 *
25
 * @credit <a href="http://www.codeigniter.com">CodeIgniter 4.4 - CodeIgniter\Router\AutoRouterImproved</a>
26
 */
27
final class AutoRouter implements AutoRouterInterface
28
{
29
    /**
30
     * Sous-répertoire contenant la classe contrôleur demandée.
31
     * Principalement utilisé par 'autoRoute'.
32
     */
33
    private ?string $directory = null;
34
35
    /**
36
     * Le nom de la classe du contrôleur.
37
     */
38
    private string $controller;
39
40
    /**
41
     * Nom de la méthode à utiliser.
42
     */
43
    private string $method;
44
45
    /**
46
     * Tableau de paramètres de la méthode du contrôleur.
47
     *
48
     * @var string[]
49
     */
50
    private array $params = [];
51
52
    /**
53
     *  Indique si on doit traduire les tirets de l'URL pour les controleurs/methodes en CamelCase.
54
     *  E.g., blog-controller -> BlogController
55
     */
56
    private readonly bool $translateUriToCamelCase;
57
58
    /**
59
     * Namespace des controleurs
60
     */
61
    private string $namespace;
62
63
    /**
64
     * Segments de l'URI
65
     *
66
     * @var string[]
67
     */
68
    private array $segments = [];
69
70
    /**
71
     * Position du contrôleur dans les segments URI.
72
     * Null pour le contrôleur par défaut.
73
     */
74
    private ?int $controllerPos = null;
75
76
    /**
77
     * Position de la méthode dans les segments URI.
78
     * Null pour la méthode par défaut.
79
     */
80
    private ?int $methodPos = null;
81
82
    /**
83
     * Position du premier Paramètre dans les segments URI.
84
     * Null pour les paramètres non definis.
85
     */
86
    private ?int $paramPos = null;
87
88
    /**
89
     * Carte des segments URI et des namespace.
90
     *
91
     * La clé est le premier segment URI. La valeur est le namespace du contrôleur.
92
     * Ex.,
93
     *   [
94
     *       'blog' => 'Acme\Blog\Controllers',
95
     *   ]
96
     *
97
     * @var array [ uri_segment => namespace ]
98
     */
99
    private array $moduleRoutes;
100
101
    /**
102
     * URI courant
103
     */
104
    private ?string $uri = null;
105
106
    /**
107
     * Constructeur
108
     *
109
     * @param class-string[] $protectedControllers Liste des contrôleurs enregistrés pour le verbe CLI qui ne doivent pas être accessibles sur le Web.
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string[] at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string[].
Loading history...
110
     * @param string         $namespace            Espace de noms par défaut pour les contrôleurs.
111
     * @param string         $defaultController    Nom du controleur par defaut.
112
     * @param string         $defaultMethod        Nom de la methode par defaut.
113
     * @param bool           $translateURIDashes   Indique si les tirets dans les URI doivent être convertis en traits de soulignement lors de la détermination des noms de méthode.
114
     */
115
    public function __construct(
116
        private readonly array $protectedControllers,
117
        string $namespace,
118
        private string $defaultController,
119
        private readonly string $defaultMethod,
120
        private readonly bool $translateURIDashes
121
    ) {
122 2
        $this->namespace = rtrim($namespace, '\\');
123
124 2
        $routingConfig                 = (object) config('routing');
125 2
        $this->moduleRoutes            = $routingConfig->module_routes;
0 ignored issues
show
Bug introduced by
The property module_routes does not seem to exist on BlitzPHP\Config\Config.
Loading history...
126 2
        $this->translateUriToCamelCase = $routingConfig->translate_uri_to_camel_case;
0 ignored issues
show
Bug introduced by
The property translate_uri_to_camel_case does not seem to exist on BlitzPHP\Config\Config.
Loading history...
Bug introduced by
The property translateUriToCamelCase is declared read-only in BlitzPHP\Router\AutoRouter.
Loading history...
127
128
        // Definir les valeurs par defaut
129 2
        $this->controller = $this->defaultController;
130
    }
131
132
    private function createSegments(string $uri): array
133
    {
134 2
        $segments = explode('/', $uri);
135 2
        $segments = array_filter($segments, static fn ($segment) => $segment !== '');
136
137
        // réindexer numériquement le tableau, en supprimant les lacunes
138 2
        return array_values($segments);
139
    }
140
141
    /**
142
     * Recherchez le premier contrôleur correspondant au segment URI.
143
     *
144
     * S'il y a un contrôleur correspondant au premier segment, la recherche s'arrête là.
145
     * Les segments restants sont des paramètres du contrôleur.
146
     *
147
     * @return bool true si une classe de contrôleur est trouvée.
148
     */
149
    private function searchFirstController(): bool
150
    {
151 2
        $segments = $this->segments;
152
153 2
        $controller = '\\' . $this->namespace;
154
155 2
        $controllerPos = -1;
156
157
        while ($segments !== []) {
158 2
            $segment = array_shift($segments);
159 2
            $controllerPos++;
160
161 2
            $class = $this->translateURI($segment);
162
163
            // dès que nous rencontrons un segment qui n'est pas compatible PSR-4, arrêter la recherche
164
            if (! $this->isValidSegment($class)) {
165
                return false;
166
            }
167
168 2
            $controller = $this->makeController($controller . '\\' . $class);
169
170
            if (class_exists($controller)) {
171 2
                $this->controller    = $controller;
172 2
                $this->controllerPos = $controllerPos;
173
174 2
                $this->checkUriForController($controller);
175
176
                // Le premier élément peut être un nom de méthode.
177 2
                $this->params = $segments;
178
                if ($segments !== []) {
179
                    $this->paramPos = $this->controllerPos + 1;
180
                }
181
182 2
                return true;
183
            }
184
        }
185
186
        return false;
187
    }
188
189
    /**
190
     * Recherchez le dernier contrôleur par défaut correspondant aux segments URI.
191
     *
192
     * @return bool true si une classe de contrôleur est trouvée.
193
     */
194
    private function searchLastDefaultController(): bool
195
    {
196
        $segments = $this->segments;
197
198
        $segmentCount = count($this->segments);
199
        $paramPos     = null;
200
        $params       = [];
201
202
        while ($segments !== []) {
203
            if ($segmentCount > count($segments)) {
204
                $paramPos = count($segments);
205
            }
206
207
            $namespaces = array_map(
208
                fn ($segment) => $this->translateURI($segment),
209
                $segments
210
            );
211
212
            $controller = '\\' . $this->namespace
213
                . '\\' . implode('\\', $namespaces)
214
                . '\\' . $this->defaultController;
215
216
            if (class_exists($controller)) {
217
                $this->controller = $controller;
218
                $this->params     = $params;
219
220
                if ($params !== []) {
221
                    $this->paramPos = $paramPos;
222
                }
223
224
                return true;
225
            }
226
227
            // ajoutons le dernier élément dans $segments au début de $params.
228
            array_unshift($params, array_pop($segments));
229
        }
230
231
        // Vérifiez le contrôleur par défaut dans le répertoire des contrôleurs.
232
        $controller = '\\' . $this->namespace
233
            . '\\' . $this->defaultController;
234
235
        if (class_exists($controller)) {
236
            $this->controller = $controller;
237
            $this->params     = $params;
238
239
            if ($params !== []) {
240
                $this->paramPos = 0;
241
            }
242
243
            return true;
244
        }
245
246
        return false;
247
    }
248
249
    /**
250
     * Recherche contrôleur, méthode et params dans l'URI.
251
     *
252
     * @return array [directory_name, controller_name, controller_method, params]
253
     */
254
    public function getRoute(string $uri, string $httpVerb): array
255
    {
256 2
        $this->uri = $uri;
257 2
        $httpVerb  = strtolower($httpVerb);
258
259
        // Reinitialise les parametres de la methode du controleur.
260 2
        $this->params = [];
261
262 2
        $defaultMethod = $httpVerb . ucfirst($this->defaultMethod);
263 2
        $this->method  = $defaultMethod;
264
265 2
        $this->segments = $this->createSegments($uri);
266
267
        // Verifier les routes de modules
268
        if ($this->segments !== [] && array_key_exists($this->segments[0], $this->moduleRoutes)) {
269 2
            $uriSegment      = array_shift($this->segments);
270
            $this->namespace = rtrim($this->moduleRoutes[$uriSegment], '\\');
271
        }
272
273
        if ($this->searchFirstController()) {
274
            // Le contrôleur a ete trouvé.
275 2
            $baseControllerName = Helpers::classBasename($this->controller);
276
277
            // Empêcher l'accès au chemin de contrôleur par défaut
278
            if (strtolower($baseControllerName) === strtolower($this->defaultController)) {
279
                throw new PageNotFoundException(
280
                    'Impossible d\'accéder au contrôleur par défaut "' . $this->controller . '" avec le nom du contrôleur comme chemin de l\'URI.'
281 2
                );
282
            }
283
        } elseif ($this->searchLastDefaultController()) {
284
            // Le controleur par defaut a ete trouve.
285
            $baseControllerName = Helpers::classBasename($this->controller);
286
        } else {
287
            // Aucun controleur trouvé
288
            throw new PageNotFoundException('Aucun contrôleur trouvé pour: ' . $uri);
289
        }
290
291
        // Le premier élément peut être un nom de méthode.
292
        /** @var string[] $params */
293 2
        $params = $this->params;
294
295 2
        $methodParam = array_shift($params);
296
297 2
        $method = '';
298
        if ($methodParam !== null) {
299 2
            $method = $httpVerb . $this->translateURI($methodParam);
300
301
            $this->checkUriForMethod($method);
302
        }
303
304
        if ($methodParam !== null && method_exists($this->controller, $method)) {
305
            // Methode trouvee.
306
            $this->method = $method;
307
            $this->params = $params;
308
309
            // Mise a jour des positions.
310
            $this->methodPos = $this->paramPos;
311
            if ($params === []) {
312
                $this->paramPos = null;
313
            }
314
            if ($this->paramPos !== null) {
315
                $this->paramPos++;
316
            }
317
318
            // Empêcher l'accès à la méthode du contrôleur par défaut
319
            if (strtolower($baseControllerName) === strtolower($this->defaultController)) {
320
                throw new PageNotFoundException(
321
                    'Impossible d\'accéder au contrôleur par défaut "' . $this->controller . '::' . $this->method . '"'
322
                );
323
            }
324
325
            // Empêcher l'accès au chemin de méthode par défaut
326
            if (strtolower($this->method) === strtolower($defaultMethod)) {
327
                throw new PageNotFoundException(
328
                    'Impossible d\'accéder à la méthode par défaut "' . $this->method . '" avec le nom de méthode comme chemin d\'URI.'
329
                );
330
            }
331
        } elseif (method_exists($this->controller, $defaultMethod)) {
332
            // La methode par defaut a ete trouvée
333 2
            $this->method = $defaultMethod;
334
        } else {
335
            // Aucune methode trouvee
336
            throw PageNotFoundException::controllerNotFound($this->controller, $method);
337
        }
338
339
        // Vérifiez le contrôleur n'est pas défini dans les routes.
340 2
        $this->protectDefinedRoutes();
341
342
        // Assurez-vous que le contrôleur n'a pas la méthode _remap().
343 2
        $this->checkRemap();
344
345
        // Assurez-vous que les segments URI pour le contrôleur et la méthode
346
        // ne contiennent pas de soulignement lorsque $translateURIDashes est true.
347 2
        $this->checkUnderscore();
348
349
        // Verifier le nombre de parametres
350
        try {
351 2
            $this->checkParameters();
352
        } catch (MethodNotFoundException) {
353
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
354
        }
355
356 2
        $this->setDirectory();
357
358 2
        return [$this->directory, $this->controllerName(), $this->methodName(), $this->params];
359
    }
360
361
    /**
362
     * @internal Juste pour les tests.
363
     *
364
     * @return array<string, int|null>
365
     */
366
    public function getPos(): array
367
    {
368
        return [
369
            'controller' => $this->controllerPos,
370
            'method'     => $this->methodPos,
371
            'params'     => $this->paramPos,
372
        ];
373
    }
374
375
    private function checkParameters(): void
376
    {
377
        try {
378 2
            $refClass = new ReflectionClass($this->controller);
379
        } catch (ReflectionException) {
380
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
381
        }
382
383
        try {
384 2
            $refMethod = $refClass->getMethod($this->method);
385 2
            $refParams = $refMethod->getParameters();
386
        } catch (ReflectionException) {
387
            throw new MethodNotFoundException();
388
        }
389
390
        if (! $refMethod->isPublic()) {
391 2
            throw new MethodNotFoundException();
392
        }
393
394
        if (count($refParams) < count($this->params)) {
395
            throw new PageNotFoundException(
396
                'Le nombre de param dans l\'URI est supérieur aux paramètres de la méthode du contrôleur.'
397
                . ' Handler:' . $this->controller . '::' . $this->method
398
                . ', URI:' . $this->uri
399 2
            );
400
        }
401
    }
402
403
    private function checkRemap(): void
404
    {
405
        try {
406 2
            $refClass = new ReflectionClass($this->controller);
407 2
            $refClass->getMethod('_remap');
408
409
            throw new PageNotFoundException(
410
                'AutoRouterImproved ne prend pas en charge la methode `_remap()`.'
411
                . ' Contrôleur:' . $this->controller
412
            );
413
        } catch (ReflectionException) {
414
            // Ne rien faire
415
        }
416
    }
417
418
    private function checkUnderscore(): void
419
    {
420
        if ($this->translateURIDashes === false) {
421
            return;
422
        }
423
424 2
        $paramPos = $this->paramPos ?? count($this->segments);
425
426 2
        for ($i = 0; $i < $paramPos; $i++) {
427
            if (str_contains($this->segments[$i], '_')) {
428
                throw new PageNotFoundException(
429
                    'AutoRouterImproved interdit l\'accès à l\'URI'
430
                    . ' contenant les undescore ("' . $this->segments[$i] . '")'
431
                    . ' quand $translate_uri_dashes est activé.'
432
                    . ' Veuillez utiliser les tiret.'
433
                    . ' Handler:' . $this->controller . '::' . $this->method
434
                    . ', URI:' . $this->uri
435 2
                );
436
            }
437
        }
438
    }
439
440
    /**
441
     * Vérifier l'URI du contrôleur pour $translateUriToCamelCase
442
     *
443
     * @param string $classname Nom de classe du contrôleur généré à partir de l'URI.
444
     *                          La casse peut être un peu incorrecte.
445
     */
446
    private function checkUriForController(string $classname): void
447
    {
448
        if ($this->translateUriToCamelCase === false) {
449 2
            return;
450
        }
451
452
        if (! in_array(ltrim($classname, '\\'), get_declared_classes(), true)) {
453
            throw new PageNotFoundException(
454
                '"' . $classname . '" n\'a pas été trouvé.'
455
            );
456
        }
457
    }
458
459
    /**
460
     * Vérifier l'URI pour la méthode $translateUriToCamelCase
461
     *
462
     * @param string $method Nom de la méthode du contrôleur généré à partir de l'URI.
463
     *                       La casse peut être un peu incorrecte.
464
     */
465
    private function checkUriForMethod(string $method): void
466
    {
467
        if ($this->translateUriToCamelCase === false) {
468
            return;
469
        }
470
471
        if (
472
            // Par exemple, si `getSomeMethod()` existe dans le contrôleur, seul l'URI `controller/some-method` devrait être accessible.
473
            // Mais si un visiteur navigue vers l'URI `controller/somemethod`, `getSomemethod()` sera vérifié, et `method_exists()` retournera true parce que les noms de méthodes en PHP sont insensibles à la casse.
474
            method_exists($this->controller, $method)
475
            // Mais nous n'autorisons pas `controller/somemethod`, donc vérifiez le nom exact de la méthode.
476
            && ! in_array($method, get_class_methods($this->controller), true)
477
        ) {
478
            throw new PageNotFoundException(
479
                '"' . $this->controller . '::' . $method . '()" n\'a pas été trouvé.'
480
            );
481
        }
482
    }
483
484
    /**
485
     * Renvoie true si la chaîne $segment fournie représente un segment d'espace de noms/répertoire valide conforme à PSR-4
486
     *
487
     * La regex vient de https://www.php.net/manual/en/language.variables.basics.php
488
     */
489
    private function isValidSegment(string $segment): bool
490
    {
491 2
        return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
492
    }
493
494
    private function translateURI(string $segment): string
495
    {
496
        if ($this->translateUriToCamelCase) {
497
            if (strtolower($segment) !== $segment) {
498
                throw new PageNotFoundException(
499
                    'AutoRouter interdit l\'accès à l\'URI'
500
                    . ' contenant des lettres majuscules ("' . $segment . '")'
501
                    . ' lorsque $translateUriToCamelCase est activé.'
502
                    . ' Veuillez utiliser le tiret.'
503
                    . ' URI:' . $this->uri
504
                );
505
            }
506
507
            if (str_contains($segment, '--')) {
508
                throw new PageNotFoundException(
509
                    'AutoRouter interdit l\'accès à l\'URI'
510
                    . ' contenant un double tiret ("' . $segment . '")'
511
                    . ' lorsque $translateUriToCamelCase est activé.'
512
                    . ' Veuillez utiliser le tiret simple.'
513
                    . ' URI:' . $this->uri
514
                );
515
            }
516
517
            return str_replace(
518
                ' ',
519
                '',
520
                ucwords(
521
                    preg_replace('/[\-]+/', ' ', $segment)
522
                )
523
            );
524
        }
525
526 2
        $segment = ucfirst($segment);
527
528
        if ($this->translateURIDashes) {
529 2
            return str_replace('-', '_', $segment);
530
        }
531
532
        return $segment;
533
    }
534
535
    /**
536
     * Obtenez le chemin du dossier du contrôleur et définissez-le sur la propriété.
537
     */
538
    private function setDirectory(): void
539
    {
540 2
        $segments = explode('\\', trim($this->controller, '\\'));
541
542
        // Supprimer le court nom de classe.
543 2
        array_pop($segments);
544
545 2
        $namespaces = implode('\\', $segments);
546
547
        $dir = str_replace(
548
            '\\',
549
            '/',
550
            ltrim(substr($namespaces, strlen($this->namespace)), '\\')
551 2
        );
552
553
        if ($dir !== '') {
554 2
            $this->directory = $dir . '/';
555
        }
556
    }
557
558
    private function protectDefinedRoutes(): void
559
    {
560 2
        $controller = strtolower($this->controller);
561
562
        foreach ($this->protectedControllers as $controllerInRoutes) {
563 2
            $routeLowerCase = strtolower($controllerInRoutes);
564
565
            if ($routeLowerCase === $controller) {
566
                throw new PageNotFoundException(
567
                    'Impossible d\'accéder à un contrôleur définie dans les routes. Contrôleur : ' . $controllerInRoutes
568
                );
569
            }
570
        }
571
    }
572
573
    /**
574
     * Renvoie le nom du sous-répertoire dans lequel se trouve le contrôleur.
575
     * Relatif à CONTROLLER_PATH
576
     *
577
     * @deprecated 1.0
578
     */
579
    public function directory(): string
580
    {
581
        return ! empty($this->directory) ? $this->directory : '';
582
    }
583
584
    /**
585
     * Renvoie le nom du contrôleur matché
586
     */
587
    private function controllerName(): string
588
    {
589
        return $this->translateURIDashes
590
            ? str_replace('-', '_', trim($this->controller, '/\\'))
591 2
            : Text::convertTo($this->controller, 'pascal');
592
    }
593
594
    /**
595
     * Retourne le nom de la méthode à exécuter
596
     */
597
    private function methodName(): string
598
    {
599
        return $this->translateURIDashes
600
            ? str_replace('-', '_', $this->method)
601 2
            : Text::convertTo($this->method, 'camel');
602
    }
603
604
    /**
605
     * Construit un nom de contrôleur valide
606
     */
607
    public function makeController(string $name): string
608
    {
609
        return preg_replace(
610
            ['#(\_)?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

610
            ['#(\_)?Controller$#i', '#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i'],
Loading history...
611
            '',
612
            ucfirst($name)
613 2
        ) . 'Controller';
614
    }
615
}
616