Passed
Push — main ( 1b5a5a...dfaed8 )
by Dimitri
13:41 queued 09:37
created

Routes::execute()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 21
ccs 0
cts 7
cp 0
crap 20
rs 9.9332
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\Cli\Commands\Routes;
13
14
use Ahc\Cli\Output\Color;
15
use BlitzPHP\Cli\Console\Command;
16
use BlitzPHP\Container\Services;
17
use BlitzPHP\Http\Request;
18
use BlitzPHP\Router\DefinedRouteCollector;
19
use BlitzPHP\Router\RouteCollection;
20
use BlitzPHP\Router\Router;
21
use BlitzPHP\Utilities\Helpers;
22
use BlitzPHP\Utilities\Iterable\Arr;
23
use BlitzPHP\Utilities\Iterable\Collection;
24
use BlitzPHP\Utilities\String\Text;
25
use Closure;
26
use ReflectionClass;
27
use ReflectionFunction;
28
29
/**
30
 * Répertorie toutes les routes.
31
 * Cela inclura tous les fichiers Routes qui peuvent être découverts, et inclura les routes qui ne sont pas définies
32
 * dans les fichiers de routes, mais sont plutôt découverts via le routage automatique.
33
 */
34
class Routes extends Command
35
{
36
    /**
37
     * @var string Groupe
38
     */
39
    protected $group = 'BlitzPHP';
40
41
    /**
42
     * @var string Nom
43
     */
44
    protected $name = 'route:list';
45
46
    /**
47
     * @var string Description
48
     */
49
    protected $description = 'Affiche toutes les routes.';
50
51
    /**
52
     * @var string
53
     */
54
    protected $service = 'Service de routing';
55
56
    /**
57
     * Les options de la commande.
58
     *
59
     * @var array<string, string>
60
     */
61
    protected $options = [
62
        '--host'          => 'Spécifiez nom d\'hôte dans la demande URI.',
63
        '--domain'        => 'Filtrer les routes par le domaine',
64
        '--handler'       => 'Filtrer les routes par le gestionnaire',
65
        '--method'        => 'Filtrer les routes par la méthode',
66
        '--name'          => 'Filtrer les routes par le nom',
67
        '--json'          => 'Produire la liste des routes au format JSON',
68
        '--show-stats'    => 'Afficher les statistiques de collecte de routes',
69
        '-r|--reverse'    => "Inverser l'ordre des routes",
70
        '--sort'          => ['La colonne (domain, method, uri, name, handler, middleware, definition) à trier', 'uri'],
71
        '--path'          => 'Afficher uniquement les routes correspondant au modèle de chemin donné',
72
        '--except-path'   => 'Ne pas afficher les routes correspondant au modèle de chemin donné',
73
        '--except-vendor' => 'Ne pas afficher les routes définis par les paquets des fournisseurs',
74
        '--only-vendor'   => 'Afficher uniquement les routes définis par les paquets des fournisseurs',
75
    ];
76
77
    /**
78
     * Les en-têtes du tableau pour la commande.
79
     *
80
     * @var list<string>
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Cli\Commands\Routes\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...
81
     */
82
    protected array $headers = ['Domain', 'Method', 'Route', 'Name', 'Handler', 'Middleware'];
83
84
    /**
85
     * @var array<string,string>
86
     */
87
    protected array $verbColors = [
88
        'GET'     => Color::BLUE,
89
        'HEAD'    => Color::CYAN,
90
        'OPTIONS' => Color::CYAN,
91
        'POST'    => Color::YELLOW,
92
        'PUT'     => Color::YELLOW,
93
        'PATCH'   => Color::YELLOW,
94
        'DELETE'  => Color::RED,
95
    ];
96
97
    /**
98
     * {@inheritDoc}
99
     */
100
    public function execute(array $params)
101
    {
102
        if (null !== $host = $this->option('host')) {
103
            Services::set(Request::class, service('request')->withHeader('HTTP_HOST', $host));
104
        }
105
106
        if ([] === $routes = $this->collectRoutes($collection = service('routes')->loadRoutes())) {
107
            $this->error("Votre application n'a pas de routes.");
108
109
            return;
110
        }
111
112
        $total = count($routes);
113
114
        if ([] === $routes = $this->getRoutes($routes, new SampleURIGenerator($collection))) {
115
            $this->error("Votre application n'a pas de routes correspondant aux critères donnés.");
116
117
            return;
118
        }
119
120
        $this->displayRoutes($routes, $total);
121
    }
122
123
    /**
124
     * Collecte les routes et les routes découvertes automatiquement.
125
     */
126
    protected function collectRoutes(RouteCollection $collection): array
127
    {
128
        $definedRouteCollector = new DefinedRouteCollector($collection);
129
        $routes                = $definedRouteCollector->collect();
130
131
        if ($collection->shouldAutoRoute()) {
132
            $methods = $this->option('method') ? [$this->option('method')] : Router::HTTP_METHODS;
133
134
            $autoRouteCollector = new AutoRouteCollector(
135
                $collection->getDefaultNamespace(),
136
                $collection->getDefaultController(),
137
                $collection->getDefaultMethod(),
138
                $methods,
139
                $collection->getRegisteredControllers('*')
140
            );
141
142
            $autoRoutes = $autoRouteCollector->get();
143
144
            // Verification des routes de modules
145
            if ([] !== $routingConfig = config('routing')) {
0 ignored issues
show
introduced by
The condition array() !== $routingConfig = config('routing') is always true.
Loading history...
146
                foreach ($routingConfig['module_routes'] as $uri => $namespace) {
147
                    $autoRouteCollector = new AutoRouteCollector(
148
                        $namespace,
149
                        $collection->getDefaultController(),
150
                        $collection->getDefaultMethod(),
151
                        $methods,
152
                        $collection->getRegisteredControllers('*'),
153
                        $uri
154
                    );
155
156
                    $autoRoutes = [...$autoRoutes, ...$autoRouteCollector->get()];
157
                }
158
            }
159
160
            foreach ($autoRoutes as $route) {
161
                $routes[] = [
162
                    'method'     => $route[0],
163
                    'route'      => $route[1],
164
                    'name'       => $route[2],
165
                    'handler'    => $route[3],
166
                    'middleware' => $route[4],
167
                ];
168
            }
169
        }
170
171
        return $routes;
172
    }
173
174
    /**
175
     * Compiler les routes dans un format affichable.
176
     */
177
    protected function getRoutes(array $routes, SampleURIGenerator $uriGenerator): array
178
    {
179
        $routes = collect($routes)
180
            ->map(fn ($route) => $this->getRouteInformation($route, $uriGenerator, new MiddlewareCollector()))
181
            ->filter()
182
            ->all();
183
184
        if (($sort = $this->option('sort')) !== null) {
185
            $routes = $this->sortRoutes($sort, $routes);
186
        } else {
187
            $routes = $this->sortRoutes('route', $routes);
188
        }
189
190
        if ($this->option('reverse')) {
191
            $routes = array_reverse($routes);
192
        }
193
194
        return $this->pluckColumns($routes);
195
    }
196
197
    /**
198
     * Obtenir les informations relatives à une route donnée.
199
     */
200
    protected function getRouteInformation(array $route, SampleURIGenerator $uriGenerator, MiddlewareCollector $middlewareCollector): ?array
201
    {
202
        if (! isset($route['middleware'])) {
203
            $sampleUri           = $uriGenerator->get($route['route']);
204
            $middlewares         = $middlewareCollector->get($route['method'], $sampleUri);
205
            $route['middleware'] = implode(' ', array_map(Helpers::classBasename(...), $middlewares));
206
        }
207
208
        return $this->filterRoute([
209
            'domain'     => $route['domain'] ?? '',
210
            'method'     => $route['method'],
211
            'route'      => $route['route'],
212
            'uri'        => $sampleUri,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sampleUri does not seem to be defined for all execution paths leading up to this point.
Loading history...
213
            'name'       => $route['name'],
214
            'handler'    => ltrim($route['handler'], '\\'),
215
            'middleware' => $route['middleware'],
216
            'vendor'     => $this->isVendorRoute($route),
217
        ]);
218
    }
219
220
    /**
221
     * Déterminer si la route a été définie en dehors de l'application.
222
     */
223
    protected function isVendorRoute(array $route): bool
224
    {
225
        if ($route['handler'] instanceof Closure) {
226
            $path = (new ReflectionFunction($route['handler']))->getFileName();
227
        } elseif (is_string($route['handler']) && ! (str_contains($route['handler'], '(View) ') || str_contains($route['handler'], '(Closure) '))) {
228
            if (! class_exists($classname = explode('::', $route['handler'])[0])) {
229
                return false;
230
            }
231
            $path = (new ReflectionClass($classname))->getFileName();
232
        } else {
233
            return false;
234
        }
235
236
        return str_starts_with($path, base_path('vendor'));
237
    }
238
239
    /**
240
     * Filtrer la route par URI et/ou nom.
241
     */
242
    protected function filterRoute(array $route): ?array
243
    {
244
        if (($this->option('name') && ! Text::contains((string) $route['name'], $this->option('name')))
245
            || ($this->option('handler') && isset($route['handler']) && is_string($route['handler']) && ! Text::contains($route['handler'], $this->option('handler')))
246
            || ($this->option('path') && ! Text::contains($route['uri'], $this->option('path')))
247
            || ($this->option('method') && ! Text::contains($route['method'], strtoupper($this->option('method'))))
0 ignored issues
show
Bug introduced by
strtoupper($this->option('method')) of type string is incompatible with the type iterable expected by parameter $needles of BlitzPHP\Utilities\String\Text::contains(). ( Ignorable by Annotation )

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

247
            || ($this->option('method') && ! Text::contains($route['method'], /** @scrutinizer ignore-type */ strtoupper($this->option('method'))))
Loading history...
248
            || ($this->option('domain') && ! Text::contains((string) $route['domain'], $this->option('domain')))
249
            || ($this->option('except-vendor') && $route['vendor'])
250
            || ($this->option('only-vendor') && ! $route['vendor'])) {
251
            return null;
252
        }
253
254
        if ($this->option('except-path')) {
255
            foreach (explode(',', $this->option('except-path')) as $path) {
256
                if (str_contains($route['uri'], $path)) {
257
                    return null;
258
                }
259
            }
260
        }
261
262
        return $route;
263
    }
264
265
    /**
266
     * Trier les routes en fonction d'un élément donné.
267
     */
268
    protected function sortRoutes(string $sort, array $routes): array
269
    {
270
        if ($sort === 'definition') {
271
            return $routes;
272
        }
273
274
        if (Text::contains($sort, ',')) {
0 ignored issues
show
Bug introduced by
',' of type string is incompatible with the type iterable expected by parameter $needles of BlitzPHP\Utilities\String\Text::contains(). ( Ignorable by Annotation )

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

274
        if (Text::contains($sort, /** @scrutinizer ignore-type */ ',')) {
Loading history...
275
            $sort = explode(',', $sort);
276
        }
277
278
        return collect($routes)->sortBy($sort)->toArray();
279
    }
280
281
    /**
282
     * Supprimer les colonnes inutiles des routes.
283
     */
284
    protected function pluckColumns(array $routes): array
285
    {
286
        return array_map(fn ($route) => Arr::only($route, $this->getColumns()), $routes);
287
    }
288
289
    /**
290
     * Obtenir les en-têtes de tableau pour les colonnes visibles.
291
     */
292
    protected function getHeaders(): array
293
    {
294
        return Arr::only($this->headers, array_keys($this->getColumns()));
295
    }
296
297
    /**
298
     * Obtenir les noms de colonnes à afficher (en-têtes de tableaux en minuscules).
299
     */
300
    protected function getColumns(): array
301
    {
302
        return array_map('strtolower', $this->headers);
303
    }
304
305
    /**
306
     * Convertir les routes donnees en JSON.
307
     */
308
    protected function asJson(Collection $routes)
309
    {
310
        $this->json(
311
            $routes->map(static function ($route) {
312
                $route['middleware'] = empty($route['middleware']) ? [] : explode(' ', $route['middleware']);
313
314
                return $route;
315
            })
316
                ->values()
317
                ->toArray()
318
        );
319
    }
320
321
    /**
322
     * Affiche les informations relatives à la route sur la console.
323
     *
324
     * @param int $total Nombre de route total collecté, indépendement des filtres appliqués
325
     */
326
    protected function displayRoutes(array $routes, int $total): void
327
    {
328
        $routes = collect($routes)->map(static fn ($route) => array_merge($route, [
329
            'route' => $route['domain'] ? ($route['domain'] . '/' . ltrim($route['route'], '/')) : $route['route'],
330
            'name'  => $route['route'] === $route['name'] ? null : $route['name'],
331
        ]));
332
333
        if ($this->option('json')) {
334
            $this->asJson($routes);
335
336
            return;
337
        }
338
339
        $maxMethodLength = $routes->map(static fn ($route) => strlen($route['method']))->max();
340
341
        foreach ($routes->values()->toArray() as $route) {
342
            $left = implode('', [
343
                $this->color->line(str_pad($route['method'], $maxMethodLength), ['fg' => $this->verbColors[$route['method']]]),
344
                ' ',
345
                $route['route'],
346
            ]);
347
            $right = implode(' > ', array_filter([$route['name'], $route['handler']]));
348
349
            $this->justify($left, $right, [
350
                'second' => ['fg' => Color::fg256(6), 'bold' => 1],
351
            ]);
352
        }
353
354
        if ($this->option('show-stats')) {
355
            $this->displayStats($routes, $total);
356
        }
357
    }
358
359
    /**
360
     * Affichage des stats de collecte des routes
361
     */
362
    protected function displayStats(Collection $routes, int $total): void
363
    {
364
        $this->eol()->border(char: '*');
365
366
        $options = ['sep' => '-', 'second' => ['fg' => Color::GREEN]];
367
        $this->justify('Nombre total de routes définies', (string) $total, $options);
368
        $this->justify('Nombre de routes affichées', (string) $routes->count(), $options);
369
        if (! $this->option('method')) {
370
            $this->border(char: '.');
371
            $methods = $routes->map(static fn ($route) => $route['method'])->unique()->sort()->all();
372
373
            foreach ($methods as $method) {
374
                $this->justify(
375
                    $method,
376
                    (string) $routes->where('method', $method)->count(),
377
                    $options
378
                );
379
            }
380
        }
381
        $this->border(char: '*');
382
    }
383
}
384