Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Foundation\Console;
4
 
5
use Closure;
6
use Illuminate\Console\Command;
7
use Illuminate\Contracts\Routing\UrlGenerator;
8
use Illuminate\Routing\Route;
9
use Illuminate\Routing\Router;
10
use Illuminate\Routing\ViewController;
11
use Illuminate\Support\Arr;
12
use Illuminate\Support\Str;
13
use ReflectionClass;
14
use ReflectionFunction;
15
use Symfony\Component\Console\Attribute\AsCommand;
16
use Symfony\Component\Console\Input\InputOption;
17
use Symfony\Component\Console\Terminal;
18
 
19
#[AsCommand(name: 'route:list')]
20
class RouteListCommand extends Command
21
{
22
    /**
23
     * The console command name.
24
     *
25
     * @var string
26
     */
27
    protected $name = 'route:list';
28
 
29
    /**
30
     * The name of the console command.
31
     *
32
     * This name is used to identify the command during lazy loading.
33
     *
34
     * @var string|null
35
     *
36
     * @deprecated
37
     */
38
    protected static $defaultName = 'route:list';
39
 
40
    /**
41
     * The console command description.
42
     *
43
     * @var string
44
     */
45
    protected $description = 'List all registered routes';
46
 
47
    /**
48
     * The router instance.
49
     *
50
     * @var \Illuminate\Routing\Router
51
     */
52
    protected $router;
53
 
54
    /**
55
     * The table headers for the command.
56
     *
57
     * @var string[]
58
     */
59
    protected $headers = ['Domain', 'Method', 'URI', 'Name', 'Action', 'Middleware'];
60
 
61
    /**
62
     * The terminal width resolver callback.
63
     *
64
     * @var \Closure|null
65
     */
66
    protected static $terminalWidthResolver;
67
 
68
    /**
69
     * The verb colors for the command.
70
     *
71
     * @var array
72
     */
73
    protected $verbColors = [
74
        'ANY' => 'red',
75
        'GET' => 'blue',
76
        'HEAD' => '#6C7280',
77
        'OPTIONS' => '#6C7280',
78
        'POST' => 'yellow',
79
        'PUT' => 'yellow',
80
        'PATCH' => 'yellow',
81
        'DELETE' => 'red',
82
    ];
83
 
84
    /**
85
     * Create a new route command instance.
86
     *
87
     * @param  \Illuminate\Routing\Router  $router
88
     * @return void
89
     */
90
    public function __construct(Router $router)
91
    {
92
        parent::__construct();
93
 
94
        $this->router = $router;
95
    }
96
 
97
    /**
98
     * Execute the console command.
99
     *
100
     * @return void
101
     */
102
    public function handle()
103
    {
104
        $this->router->flushMiddlewareGroups();
105
 
106
        if (! $this->router->getRoutes()->count()) {
107
            return $this->components->error("Your application doesn't have any routes.");
108
        }
109
 
110
        if (empty($routes = $this->getRoutes())) {
111
            return $this->components->error("Your application doesn't have any routes matching the given criteria.");
112
        }
113
 
114
        $this->displayRoutes($routes);
115
    }
116
 
117
    /**
118
     * Compile the routes into a displayable format.
119
     *
120
     * @return array
121
     */
122
    protected function getRoutes()
123
    {
124
        $routes = collect($this->router->getRoutes())->map(function ($route) {
125
            return $this->getRouteInformation($route);
126
        })->filter()->all();
127
 
128
        if (($sort = $this->option('sort')) !== null) {
129
            $routes = $this->sortRoutes($sort, $routes);
130
        } else {
131
            $routes = $this->sortRoutes('uri', $routes);
132
        }
133
 
134
        if ($this->option('reverse')) {
135
            $routes = array_reverse($routes);
136
        }
137
 
138
        return $this->pluckColumns($routes);
139
    }
140
 
141
    /**
142
     * Get the route information for a given route.
143
     *
144
     * @param  \Illuminate\Routing\Route  $route
145
     * @return array
146
     */
147
    protected function getRouteInformation(Route $route)
148
    {
149
        return $this->filterRoute([
150
            'domain' => $route->domain(),
151
            'method' => implode('|', $route->methods()),
152
            'uri' => $route->uri(),
153
            'name' => $route->getName(),
154
            'action' => ltrim($route->getActionName(), '\\'),
155
            'middleware' => $this->getMiddleware($route),
156
            'vendor' => $this->isVendorRoute($route),
157
        ]);
158
    }
159
 
160
    /**
161
     * Sort the routes by a given element.
162
     *
163
     * @param  string  $sort
164
     * @param  array  $routes
165
     * @return array
166
     */
167
    protected function sortRoutes($sort, array $routes)
168
    {
169
        return Arr::sort($routes, function ($route) use ($sort) {
170
            return $route[$sort];
171
        });
172
    }
173
 
174
    /**
175
     * Remove unnecessary columns from the routes.
176
     *
177
     * @param  array  $routes
178
     * @return array
179
     */
180
    protected function pluckColumns(array $routes)
181
    {
182
        return array_map(function ($route) {
183
            return Arr::only($route, $this->getColumns());
184
        }, $routes);
185
    }
186
 
187
    /**
188
     * Display the route information on the console.
189
     *
190
     * @param  array  $routes
191
     * @return void
192
     */
193
    protected function displayRoutes(array $routes)
194
    {
195
        $routes = collect($routes);
196
 
197
        $this->output->writeln(
198
            $this->option('json') ? $this->asJson($routes) : $this->forCli($routes)
199
        );
200
    }
201
 
202
    /**
203
     * Get the middleware for the route.
204
     *
205
     * @param  \Illuminate\Routing\Route  $route
206
     * @return string
207
     */
208
    protected function getMiddleware($route)
209
    {
210
        return collect($this->router->gatherRouteMiddleware($route))->map(function ($middleware) {
211
            return $middleware instanceof Closure ? 'Closure' : $middleware;
212
        })->implode("\n");
213
    }
214
 
215
    /**
216
     * Determine if the route has been defined outside of the application.
217
     *
218
     * @param  \Illuminate\Routing\Route  $route
219
     * @return bool
220
     */
221
    protected function isVendorRoute(Route $route)
222
    {
223
        if ($route->action['uses'] instanceof Closure) {
224
            $path = (new ReflectionFunction($route->action['uses']))
225
                                ->getFileName();
226
        } elseif (is_string($route->action['uses']) &&
227
                  str_contains($route->action['uses'], 'SerializableClosure')) {
228
            return false;
229
        } elseif (is_string($route->action['uses'])) {
230
            if ($this->isFrameworkController($route)) {
231
                return false;
232
            }
233
 
234
            $path = (new ReflectionClass($route->getControllerClass()))
235
                                ->getFileName();
236
        } else {
237
            return false;
238
        }
239
 
240
        return str_starts_with($path, base_path('vendor'));
241
    }
242
 
243
    /**
244
     * Determine if the route uses a framework controller.
245
     *
246
     * @param  \Illuminate\Routing\Route  $route
247
     * @return bool
248
     */
249
    protected function isFrameworkController(Route $route)
250
    {
251
        return in_array($route->getControllerClass(), [
252
            '\Illuminate\Routing\RedirectController',
253
            '\Illuminate\Routing\ViewController',
254
        ], true);
255
    }
256
 
257
    /**
258
     * Filter the route by URI and / or name.
259
     *
260
     * @param  array  $route
261
     * @return array|null
262
     */
263
    protected function filterRoute(array $route)
264
    {
265
        if (($this->option('name') && ! Str::contains((string) $route['name'], $this->option('name'))) ||
266
            ($this->option('path') && ! Str::contains($route['uri'], $this->option('path'))) ||
267
            ($this->option('method') && ! Str::contains($route['method'], strtoupper($this->option('method')))) ||
268
            ($this->option('domain') && ! Str::contains((string) $route['domain'], $this->option('domain'))) ||
269
            ($this->option('except-vendor') && $route['vendor']) ||
270
            ($this->option('only-vendor') && ! $route['vendor'])) {
271
            return;
272
        }
273
 
274
        if ($this->option('except-path')) {
275
            foreach (explode(',', $this->option('except-path')) as $path) {
276
                if (str_contains($route['uri'], $path)) {
277
                    return;
278
                }
279
            }
280
        }
281
 
282
        return $route;
283
    }
284
 
285
    /**
286
     * Get the table headers for the visible columns.
287
     *
288
     * @return array
289
     */
290
    protected function getHeaders()
291
    {
292
        return Arr::only($this->headers, array_keys($this->getColumns()));
293
    }
294
 
295
    /**
296
     * Get the column names to show (lowercase table headers).
297
     *
298
     * @return array
299
     */
300
    protected function getColumns()
301
    {
302
        return array_map('strtolower', $this->headers);
303
    }
304
 
305
    /**
306
     * Parse the column list.
307
     *
308
     * @param  array  $columns
309
     * @return array
310
     */
311
    protected function parseColumns(array $columns)
312
    {
313
        $results = [];
314
 
315
        foreach ($columns as $column) {
316
            if (str_contains($column, ',')) {
317
                $results = array_merge($results, explode(',', $column));
318
            } else {
319
                $results[] = $column;
320
            }
321
        }
322
 
323
        return array_map('strtolower', $results);
324
    }
325
 
326
    /**
327
     * Convert the given routes to JSON.
328
     *
329
     * @param  \Illuminate\Support\Collection  $routes
330
     * @return string
331
     */
332
    protected function asJson($routes)
333
    {
334
        return $routes
335
            ->map(function ($route) {
336
                $route['middleware'] = empty($route['middleware']) ? [] : explode("\n", $route['middleware']);
337
 
338
                return $route;
339
            })
340
            ->values()
341
            ->toJson();
342
    }
343
 
344
    /**
345
     * Convert the given routes to regular CLI output.
346
     *
347
     * @param  \Illuminate\Support\Collection  $routes
348
     * @return array
349
     */
350
    protected function forCli($routes)
351
    {
352
        $routes = $routes->map(
353
            fn ($route) => array_merge($route, [
354
                'action' => $this->formatActionForCli($route),
355
                'method' => $route['method'] == 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS' ? 'ANY' : $route['method'],
356
                'uri' => $route['domain'] ? ($route['domain'].'/'.ltrim($route['uri'], '/')) : $route['uri'],
357
            ]),
358
        );
359
 
360
        $maxMethod = mb_strlen($routes->max('method'));
361
 
362
        $terminalWidth = $this->getTerminalWidth();
363
 
364
        $routeCount = $this->determineRouteCountOutput($routes, $terminalWidth);
365
 
366
        return $routes->map(function ($route) use ($maxMethod, $terminalWidth) {
367
            [
368
                'action' => $action,
369
                'domain' => $domain,
370
                'method' => $method,
371
                'middleware' => $middleware,
372
                'uri' => $uri,
373
            ] = $route;
374
 
375
            $middleware = Str::of($middleware)->explode("\n")->filter()->whenNotEmpty(
376
                fn ($collection) => $collection->map(
377
                    fn ($middleware) => sprintf('         %s⇂ %s', str_repeat(' ', $maxMethod), $middleware)
378
                )
379
            )->implode("\n");
380
 
381
            $spaces = str_repeat(' ', max($maxMethod + 6 - mb_strlen($method), 0));
382
 
383
            $dots = str_repeat('.', max(
384
                $terminalWidth - mb_strlen($method.$spaces.$uri.$action) - 6 - ($action ? 1 : 0), 0
385
            ));
386
 
387
            $dots = empty($dots) ? $dots : " $dots";
388
 
389
            if ($action && ! $this->output->isVerbose() && mb_strlen($method.$spaces.$uri.$action.$dots) > ($terminalWidth - 6)) {
390
                $action = substr($action, 0, $terminalWidth - 7 - mb_strlen($method.$spaces.$uri.$dots)).'…';
391
            }
392
 
393
            $method = Str::of($method)->explode('|')->map(
394
                fn ($method) => sprintf('<fg=%s>%s</>', $this->verbColors[$method] ?? 'default', $method),
395
            )->implode('<fg=#6C7280>|</>');
396
 
397
            return [sprintf(
398
                '  <fg=white;options=bold>%s</> %s<fg=white>%s</><fg=#6C7280>%s %s</>',
399
                $method,
400
                $spaces,
401
                preg_replace('#({[^}]+})#', '<fg=yellow>$1</>', $uri),
402
                $dots,
403
                str_replace('   ', ' › ', $action ?? ''),
404
            ), $this->output->isVerbose() && ! empty($middleware) ? "<fg=#6C7280>$middleware</>" : null];
405
        })
406
            ->flatten()
407
            ->filter()
408
            ->prepend('')
409
            ->push('')->push($routeCount)->push('')
410
            ->toArray();
411
    }
412
 
413
    /**
414
     * Get the formatted action for display on the CLI.
415
     *
416
     * @param  array  $route
417
     * @return string
418
     */
419
    protected function formatActionForCli($route)
420
    {
421
        ['action' => $action, 'name' => $name] = $route;
422
 
423
        if ($action === 'Closure' || $action === ViewController::class) {
424
            return $name;
425
        }
426
 
427
        $name = $name ? "$name   " : null;
428
 
429
        $rootControllerNamespace = $this->laravel[UrlGenerator::class]->getRootControllerNamespace()
430
            ?? ($this->laravel->getNamespace().'Http\\Controllers');
431
 
432
        if (str_starts_with($action, $rootControllerNamespace)) {
433
            return $name.substr($action, mb_strlen($rootControllerNamespace) + 1);
434
        }
435
 
436
        $actionClass = explode('@', $action)[0];
437
 
438
        if (class_exists($actionClass) && str_starts_with((new ReflectionClass($actionClass))->getFilename(), base_path('vendor'))) {
439
            $actionCollection = collect(explode('\\', $action));
440
 
441
            return $name.$actionCollection->take(2)->implode('\\').'   '.$actionCollection->last();
442
        }
443
 
444
        return $name.$action;
445
    }
446
 
447
    /**
448
     * Determine and return the output for displaying the number of routes in the CLI output.
449
     *
450
     * @param  \Illuminate\Support\Collection  $routes
451
     * @param  int  $terminalWidth
452
     * @return string
453
     */
454
    protected function determineRouteCountOutput($routes, $terminalWidth)
455
    {
456
        $routeCountText = 'Showing ['.$routes->count().'] routes';
457
 
458
        $offset = $terminalWidth - mb_strlen($routeCountText) - 2;
459
 
460
        $spaces = str_repeat(' ', $offset);
461
 
462
        return $spaces.'<fg=blue;options=bold>Showing ['.$routes->count().'] routes</>';
463
    }
464
 
465
    /**
466
     * Get the terminal width.
467
     *
468
     * @return int
469
     */
470
    public static function getTerminalWidth()
471
    {
472
        return is_null(static::$terminalWidthResolver)
473
            ? (new Terminal)->getWidth()
474
            : call_user_func(static::$terminalWidthResolver);
475
    }
476
 
477
    /**
478
     * Set a callback that should be used when resolving the terminal width.
479
     *
480
     * @param  \Closure|null  $resolver
481
     * @return void
482
     */
483
    public static function resolveTerminalWidthUsing($resolver)
484
    {
485
        static::$terminalWidthResolver = $resolver;
486
    }
487
 
488
    /**
489
     * Get the console command options.
490
     *
491
     * @return array
492
     */
493
    protected function getOptions()
494
    {
495
        return [
496
            ['json', null, InputOption::VALUE_NONE, 'Output the route list as JSON'],
497
            ['method', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by method'],
498
            ['name', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by name'],
499
            ['domain', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by domain'],
500
            ['path', null, InputOption::VALUE_OPTIONAL, 'Only show routes matching the given path pattern'],
501
            ['except-path', null, InputOption::VALUE_OPTIONAL, 'Do not display the routes matching the given path pattern'],
502
            ['reverse', 'r', InputOption::VALUE_NONE, 'Reverse the ordering of the routes'],
503
            ['sort', null, InputOption::VALUE_OPTIONAL, 'The column (domain, method, uri, name, action, middleware) to sort by', 'uri'],
504
            ['except-vendor', null, InputOption::VALUE_NONE, 'Do not display routes defined by vendor packages'],
505
            ['only-vendor', null, InputOption::VALUE_NONE, 'Only display routes defined by vendor packages'],
506
        ];
507
    }
508
}