AutoRouter::createSegments()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 7
ccs 3
cts 3
cp 1
crap 1
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\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 list<string>
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Router\list 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...
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 list<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 list<class-string> $protectedControllers Liste des contrôleurs enregistrés pour le verbe CLI qui ne doivent pas être accessibles sur le Web.
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 readonly string $defaultController,
119
        private readonly string $defaultMethod,
120
        private readonly bool $translateURIDashes
121
    ) {
122 4
        $this->namespace = rtrim($namespace, '\\');
123
124 4
        $routingConfig                 = (object) config('routing');
125 4
        $this->moduleRoutes            = $routingConfig->module_routes;
126 4
        $this->translateUriToCamelCase = $routingConfig->translate_uri_to_camel_case;
0 ignored issues
show
Bug introduced by
The property translateUriToCamelCase is declared read-only in BlitzPHP\Router\AutoRouter.
Loading history...
127
128
        // Definir les valeurs par defaut
129 4
        $this->controller = $this->defaultController;
130
    }
131
132
    private function createSegments(string $uri): array
133
    {
134 4
        $segments = explode('/', $uri);
135 4
        $segments = array_filter($segments, static fn ($segment) => $segment !== '');
136
137
        // réindexer numériquement le tableau, en supprimant les lacunes
138 4
        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 4
        $segments = $this->segments;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->segments of type array is incompatible with the declared type BlitzPHP\Router\list of property $segments.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
152
153 4
        $controller = '\\' . $this->namespace;
154
155 4
        $controllerPos = -1;
156
157
        while ($segments !== []) {
158 4
            $segment = array_shift($segments);
159 4
            $controllerPos++;
160
161 4
            $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 4
            $controller = $this->makeController($controller . '\\' . $class);
169
170
            if (class_exists($controller)) {
171 4
                $this->controller    = $controller;
172 4
                $this->controllerPos = $controllerPos;
173
174 4
                $this->checkUriForController($controller);
175
176
                // Le premier élément peut être un nom de méthode.
177 4
                $this->params = $segments;
178
                if ($segments !== []) {
179 2
                    $this->paramPos = $this->controllerPos + 1;
180
                }
181
182 4
                return true;
183
            }
184
        }
185
186 2
        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 2
        $segments = $this->segments;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->segments of type array is incompatible with the declared type BlitzPHP\Router\list of property $segments.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
197
198 2
        $segmentCount = count($this->segments);
199 2
        $paramPos     = null;
200 2
        $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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $params of type array is incompatible with the declared type BlitzPHP\Router\list of property $params.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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 2
            . '\\' . $this->defaultController;
234
235
        if (class_exists($controller)) {
236 2
            $this->controller = $controller;
237 2
            $this->params     = $params;
238
239
            if ($params !== []) {
240
                $this->paramPos = 0;
241
            }
242
243 2
            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 4
        $this->uri = $uri;
257 4
        $httpVerb  = strtolower($httpVerb);
258
259
        // Reinitialise les parametres de la methode du controleur.
260 4
        $this->params = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type BlitzPHP\Router\list of property $params.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
261
262 4
        $defaultMethod = $httpVerb . ucfirst($this->defaultMethod);
263 4
        $this->method  = $defaultMethod;
264
265 4
        $this->segments = $this->createSegments($uri);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->createSegments($uri) of type array is incompatible with the declared type BlitzPHP\Router\list of property $segments.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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 2
            $this->namespace = rtrim($this->moduleRoutes[$uriSegment], '\\');
271
        }
272
273
        if ($this->searchFirstController()) {
274
            // Le contrôleur a ete trouvé.
275 4
            $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 4
                );
282
            }
283
        } elseif ($this->searchLastDefaultController()) {
284
            // Le controleur par defaut a ete trouve.
285 2
            $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 list<string> $params */
293 4
        $params = $this->params;
294
295 4
        $methodParam = array_shift($params);
0 ignored issues
show
Bug introduced by
$params of type BlitzPHP\Router\list is incompatible with the type array expected by parameter $array of array_shift(). ( Ignorable by Annotation )

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

295
        $methodParam = array_shift(/** @scrutinizer ignore-type */ $params);
Loading history...
296
297 4
        $method = '';
298
        if ($methodParam !== null) {
299 2
            $method = $httpVerb . $this->translateURI($methodParam);
300
301 2
            $this->checkUriForMethod($method);
302
        }
303
304
        if ($methodParam !== null && method_exists($this->controller, $method)) {
305
            // Methode trouvee.
306 2
            $this->method = $method;
307 2
            $this->params = $params;
308
309
            // Mise a jour des positions.
310 2
            $this->methodPos = $this->paramPos;
311
            if ($params === []) {
312 2
                $this->paramPos = null;
313
            }
314
            if ($this->paramPos !== null) {
315 2
                $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 2
                );
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 2
                );
330
            }
331
        } elseif (method_exists($this->controller, $defaultMethod)) {
332
            // La methode par defaut a ete trouvée
333 4
            $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 4
        $this->protectDefinedRoutes();
341
342
        // Assurez-vous que le contrôleur n'a pas la méthode _remap().
343 4
        $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 4
        $this->checkUnderscore();
348
349
        // Verifier le nombre de parametres
350
        try {
351 4
            $this->checkParameters();
352
        } catch (MethodNotFoundException) {
353 2
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
354
        }
355
356 4
        $this->setDirectory();
357
358 4
        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 2
        ];
373
    }
374
375
    private function checkParameters(): void
376
    {
377
        try {
378 4
            $refClass = new ReflectionClass($this->controller);
379
        } catch (ReflectionException) {
380
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
381
        }
382
383
        try {
384 4
            $refMethod = $refClass->getMethod($this->method);
385 4
            $refParams = $refMethod->getParameters();
386
        } catch (ReflectionException) {
387
            throw new MethodNotFoundException();
388
        }
389
390
        if (! $refMethod->isPublic()) {
391 4
            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 4
            );
400
        }
401
    }
402
403
    private function checkRemap(): void
404
    {
405
        try {
406 4
            $refClass = new ReflectionClass($this->controller);
407 4
            $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 4
        $paramPos = $this->paramPos ?? count($this->segments);
425
426 4
        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 4
                );
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 4
            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 2
            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 4
        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 4
        $segment = ucfirst($segment);
527
528
        if ($this->translateURIDashes) {
529 4
            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 4
        $segments = explode('\\', trim($this->controller, '\\'));
541
542
        // Supprimer le court nom de classe.
543 4
        array_pop($segments);
544
545 4
        $namespaces = implode('\\', $segments);
546
547
        $dir = str_replace(
548
            '\\',
549
            '/',
550
            ltrim(substr($namespaces, strlen($this->namespace)), '\\')
551 4
        );
552
553
        if ($dir !== '') {
554 4
            $this->directory = $dir . '/';
555
        }
556
    }
557
558
    private function protectDefinedRoutes(): void
559
    {
560 4
        $controller = strtolower($this->controller);
561
562
        foreach ($this->protectedControllers as $controllerInRoutes) {
563 4
            $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 $this->directory !== null && $this->directory !== '' && $this->directory !== '0' ? $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 4
            : 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 4
            : 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 T|null|object 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 4
        ) . 'Controller';
614
    }
615
}
616