├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── docs ├── laravel-route-menu.png ├── route-list.png └── route-menu.png └── src ├── Commands └── LaravelRouteMenuCommand.php └── LaravelRouteMenuServiceProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-route-menu` will be documented in this file. 4 | 5 | ## 1.0.0 - 202X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) morrislaptop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Route Menu 2 | 3 | Your `route:list`, sir. 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/morrislaptop/laravel-route-menu.svg?style=flat-square)](https://packagist.org/packages/morrislaptop/laravel-route-menu) 6 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/morrislaptop/laravel-route-menu/run-tests?label=tests)](https://github.com/morrislaptop/laravel-route-menu/actions?query=workflow%3ATests+branch%3Amaster) 7 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/morrislaptop/laravel-route-menu/Check%20&%20fix%20styling?label=code%20style)](https://github.com/morrislaptop/laravel-route-menu/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amaster) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/morrislaptop/laravel-route-menu.svg?style=flat-square)](https://packagist.org/packages/morrislaptop/laravel-route-menu) 9 | 10 | `route:menu` gives you a beautiful route list which is friendly on smaller terminals and brings a few new features in. 11 | 12 | ![route:menu](docs/route-menu.png) 13 | 14 | Features: 15 | 16 | * Shows all routes in a list view for smaller terminals 17 | * Groups routes by namespace or filename for easy navigating 18 | * Extracts parameters and their types 19 | * Displays an IDE friendly file path for code jumping 🌟 20 | * Extra support for `Route::redirect` and `Route::view` methods 21 | * Additional `file` filter to only show relevant routes by filename / namespace 22 | * Lots of 🏷️, 🎬, 🤹, 🧅, ☕️, 🏰, 🛫, 👀, ⚡, ☁️, 🌙, 🌅, 🔭, 💵, 🔐, 🛂, 👨‍🚀️ 23 | 24 | ## Installation 25 | 26 | You can install the package via composer: 27 | 28 | ```bash 29 | composer require morrislaptop/laravel-route-menu --dev 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```php 35 | php artisan route:menu 36 | ``` 37 | 38 | In addition to the `name`, `method` and `path` filters on `route:list`, an additional `file` filter is supported which will filter the routes based on the namespace (for classes) or file (for Closures). 39 | 40 | ```php 41 | php artisan route:menu --file=app 42 | ``` 43 | 44 | ## Testing 45 | 46 | ```bash 47 | composer test 48 | ``` 49 | 50 | ## Changelog 51 | 52 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 53 | 54 | ## Contributing 55 | 56 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 57 | 58 | ## Security Vulnerabilities 59 | 60 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 61 | 62 | ## Credits 63 | 64 | - [Craig Morris](https://github.com/morrislaptop) 65 | - [All Contributors](../../contributors) 66 | 67 | ## License 68 | 69 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "morrislaptop/laravel-route-menu", 3 | "description": "Your route:list, sir.", 4 | "keywords": [ 5 | "morrislaptop", 6 | "laravel-route-menu" 7 | ], 8 | "homepage": "https://github.com/morrislaptop/laravel-route-menu", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Craig Morris", 13 | "email": "craig.michael.morris@gmail.com", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.4|^8.0", 19 | "illuminate/contracts": "^7.0|^8.0", 20 | "spatie/laravel-package-tools": "^1.4.3" 21 | }, 22 | "require-dev": { 23 | "friendsofphp/php-cs-fixer": "^2.18", 24 | "inertiajs/inertia-laravel": "^0.4.0", 25 | "livewire/livewire": "^2.4", 26 | "orchestra/testbench": "^5.0|^6.13", 27 | "phpunit/phpunit": "^9.3", 28 | "spatie/laravel-ray": "^1.17", 29 | "vimeo/psalm": "^4.4" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Morrislaptop\\LaravelRouteMenu\\": "src", 34 | "Morrislaptop\\LaravelRouteMenu\\Database\\Factories\\": "database/factories" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Morrislaptop\\LaravelRouteMenu\\Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "psalm": "vendor/bin/psalm", 44 | "test": "vendor/bin/phpunit --colors=always", 45 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 46 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "laravel": { 53 | "providers": [ 54 | "Morrislaptop\\LaravelRouteMenu\\LaravelRouteMenuServiceProvider" 55 | ], 56 | "aliases": { 57 | "LaravelRouteMenu": "Morrislaptop\\LaravelRouteMenu\\LaravelRouteMenuFacade" 58 | } 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /docs/laravel-route-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morrislaptop/laravel-route-menu/a622e6fce5efa06e392639fd76b1165d225716de/docs/laravel-route-menu.png -------------------------------------------------------------------------------- /docs/route-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morrislaptop/laravel-route-menu/a622e6fce5efa06e392639fd76b1165d225716de/docs/route-list.png -------------------------------------------------------------------------------- /docs/route-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morrislaptop/laravel-route-menu/a622e6fce5efa06e392639fd76b1165d225716de/docs/route-menu.png -------------------------------------------------------------------------------- /src/Commands/LaravelRouteMenuCommand.php: -------------------------------------------------------------------------------- 1 | router, 'flushMiddlewareGroups')) { 30 | $this->router->flushMiddlewareGroups(); 31 | } 32 | 33 | if (empty($this->router->getRoutes()->getRoutes())) { 34 | $this->error("Your application doesn't have any routes."); 35 | 36 | return; 37 | } 38 | 39 | if (empty($routes = $this->getRoutes())) { 40 | $this->error("Your application doesn't have any routes matching the given criteria."); 41 | 42 | return; 43 | } 44 | 45 | $groups = $this->groupRoutes($routes); 46 | 47 | $this->displayGroups($groups); 48 | } 49 | 50 | /** 51 | * Compile the routes into a displayable format. 52 | * 53 | * @return Route[] 54 | */ 55 | protected function getRoutes() 56 | { 57 | return collect($this->router->getRoutes()) 58 | ->filter(fn (Route $route) => $this->filterRawRoute($route)) 59 | ->all(); 60 | } 61 | 62 | /** 63 | * @param Route[] $routes 64 | * @return array 65 | */ 66 | protected function groupRoutes(array $routes) 67 | { 68 | return collect($routes)->groupBy( 69 | fn (Route $route) => $this->getNamespaceOrFile($route) 70 | )->all(); 71 | } 72 | 73 | protected function getNamespaceOrFile(Route $route): string 74 | { 75 | $reflection = $this->resolveReflection($route); 76 | 77 | if ($reflection instanceof ReflectionMethod) { 78 | return $reflection->getDeclaringClass()->getNamespaceName(); 79 | } 80 | 81 | if ($reflection instanceof ReflectionClass) { 82 | return $reflection->getNamespaceName(); 83 | } 84 | 85 | return str_replace(base_path() . '/', '', $reflection->getFileName()); 86 | } 87 | 88 | /** 89 | * @param array $groups 90 | */ 91 | protected function displayGroups(array $groups): void 92 | { 93 | foreach ($groups as $namespace => $routes) { 94 | $emoji = $this->getEmoji($namespace); 95 | 96 | $this->line(" $emoji $namespace"); 97 | 98 | $this->line('-----'); 99 | 100 | foreach ($routes as $route) { 101 | $this->displayRoute($route); 102 | } 103 | } 104 | } 105 | 106 | protected function getEmoji(string $namespace): string 107 | { 108 | $map = collect([ 109 | 'Fortify' => '🏰', 110 | 'Jetstream' => '🛫', 111 | 'Livewire' => '👀', 112 | 'Inertia' => '⏩', 113 | 'Spark' => '⚡', 114 | 'Vapor' => '☁️', 115 | 'Dusk' => '🌙', 116 | 'Horizon' => '🌅', 117 | 'Telescope' => '🔭', 118 | 'Cashier' => '💵', 119 | 'Paddle' => '💵', 120 | 'Sanctum' => '🔐', 121 | 'Passport' => '🛂', 122 | 'Nova' => '👨‍🚀️', 123 | ]); 124 | 125 | return $map->first( 126 | fn ($_e, $search) => Str::contains($namespace, $search), 127 | '💻' 128 | ); 129 | } 130 | 131 | protected function displayRoute(Route $route): void 132 | { 133 | $reflection = $this->resolveReflection($route); 134 | $method = $this->resolveMethod($route, $reflection); 135 | $isSpecialMethod = in_array($method, ['REDIRECT', 'VIEW']); 136 | $padLength = 10; 137 | 138 | $this->line( 139 | "" . $method . "" 140 | . ' ' 141 | . '/' . ltrim($route->uri(), '/') 142 | ); 143 | 144 | $this->line(''); 145 | 146 | if ($method === 'REDIRECT') { 147 | $this->line('👉 ' . $route->defaults['destination'] . ' ' . $route->defaults['status']); 148 | } 149 | 150 | if ($method === 'VIEW') { 151 | $this->line('🎨 ' . 'resources/views/' . $route->defaults['view'] . '.php'); 152 | } 153 | 154 | // @todo Find relevant file for React, Svelte, Typescript etc.. 155 | if ($method === 'INERTIA') { 156 | $this->line('🎨 ' . 'resources/js/pages/' . $route->defaults['component'] . '.vue'); 157 | } 158 | 159 | if ($route->getName()) { 160 | $this->line('🏷️ ' . str_pad("Name: ", $padLength) . $route->getName()); 161 | } 162 | 163 | if ($route->domain()) { 164 | $this->line('🌏 ' . str_pad("Domain: ", $padLength) . $route->domain()); 165 | } 166 | 167 | if ($route->getActionName()) { 168 | $this->line('🎬 ' . str_pad("Action: ", $padLength) . $route->getActionName()); 169 | } 170 | 171 | if (! $isSpecialMethod && $params = $route->signatureParameters()) { 172 | $this->line('🤹 ' . str_pad("Params: ", $padLength) . implode(', ', $this->paramString($params))); 173 | } 174 | 175 | if ($middleware = $this->getMiddleware($route)) { 176 | $this->line('🧅 ' . str_pad("Middles: ", $padLength) . $middleware); 177 | } 178 | 179 | if (! $isSpecialMethod) { 180 | $fileName = str_replace(base_path() . '/', '', $reflection->getFileName()); 181 | $this->line('☕️ ' . str_pad("Code: ", $padLength) . "" . $fileName . ':' . $reflection->getStartLine() . ''); 182 | } 183 | 184 | $this->line(''); 185 | $this->line(''); 186 | } 187 | 188 | protected function resolveMethod(Route $route, $reflection) 189 | { 190 | $class = null; 191 | 192 | if ($reflection instanceof ReflectionClass) { 193 | $class = $reflection; 194 | } 195 | if ($reflection instanceof ReflectionMethod) { 196 | $class = $reflection->getDeclaringClass(); 197 | } 198 | 199 | if ($class && $class->getName() === RedirectController::class) { 200 | return 'REDIRECT'; 201 | } 202 | 203 | if ($class && $class->getName() === InertiaController::class) { 204 | return 'INERTIA'; 205 | } 206 | 207 | if ($class && $class->getName() === ViewController::class) { 208 | return 'VIEW'; 209 | } 210 | 211 | return collect($route->methods()) 212 | ->reject(fn (string $method) => $method === 'HEAD') 213 | ->implode(', '); 214 | } 215 | 216 | /** 217 | * @param ReflectionParameter[] $params 218 | * @return string[] 219 | */ 220 | protected function paramString($params) 221 | { 222 | return collect($params)->map(function (ReflectionParameter $param) { 223 | $str = $param->allowsNull() ? '?' : ''; 224 | $str .= "" . ($param->getType() instanceof ReflectionNamedType ? $param->getType()->getName() : 'mixed') . ""; 225 | $str .= ' '; 226 | $str .= '$' . $param->getName(); 227 | $str .= $param->isDefaultValueAvailable() ? ' = ' . $param->getDefaultValue() . '' : ''; 228 | 229 | return $str; 230 | })->all(); 231 | } 232 | 233 | protected function resolveReflection(Route $route) 234 | { 235 | if (! $route->getAction('controller')) { 236 | $closure = $route->getAction('uses'); 237 | 238 | return new ReflectionFunction($closure); 239 | } 240 | 241 | [$class, $method] = Str::parseCallback($route->getAction('controller'), '__invoke'); 242 | 243 | if (is_a($class, Component::class, true)) { 244 | return new ReflectionClass($class); 245 | } 246 | 247 | return new ReflectionMethod($class, $method); 248 | } 249 | 250 | protected function getMiddleware($route) 251 | { 252 | return collect($this->router->gatherRouteMiddleware($route))->map(function ($middleware) { 253 | return $middleware instanceof Closure ? 'Closure' : $middleware; 254 | })->implode(", "); 255 | } 256 | 257 | protected function filterRawRoute(Route $route): ?Route 258 | { 259 | if (($this->option('name') && ! Str::contains($route->getName(), $this->option('name'))) || 260 | $this->option('path') && ! Str::contains($route->uri(), $this->option('path')) || 261 | $this->option('file') && ! Str::contains(strtolower($this->getNamespaceOrFile($route)), strtolower($this->option('file'))) || 262 | $this->option('method') && ! Str::contains(implode('|', $route->methods()), strtoupper($this->option('method')))) { 263 | return null; 264 | } 265 | 266 | return $route; 267 | } 268 | 269 | /** 270 | * Get the console command options. 271 | * 272 | * @return array 273 | */ 274 | protected function getOptions() 275 | { 276 | return [ 277 | ['method', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by method'], 278 | ['name', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by name'], 279 | ['path', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by path'], 280 | ['file', null, InputOption::VALUE_OPTIONAL, 'Filter the routes by namespace or file'], 281 | ]; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/LaravelRouteMenuServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-route-menu') 20 | ->hasCommand(LaravelRouteMenuCommand::class); 21 | } 22 | } 23 | --------------------------------------------------------------------------------