├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── route-discovery.php ├── docs ├── _index.md ├── about-us.md ├── advanced-usage │ ├── _index.md │ └── using-route-transformers.md ├── changelog.md ├── discovering-routes-for-controllers │ ├── _index.md │ ├── getting-started.md │ └── mapping-controllers-to-routes.md ├── discovering-routes-for-views │ ├── _index.md │ ├── getting-started.md │ └── mapping-views-to-routes.md ├── installation-setup.md ├── introduction.md ├── questions-issues.md ├── requirements.md └── support-us.md ├── phpstan-baseline.neon ├── phpstan.neon.dist └── src ├── Attributes ├── DiscoveryAttribute.php ├── DoNotDiscover.php ├── DontDiscover.php ├── Prefix.php ├── Route.php └── Where.php ├── Config.php ├── Discovery ├── Discover.php └── DiscoverControllers.php ├── PendingRouteTransformers ├── AddControllerUriToActions.php ├── AddDefaultRouteName.php ├── HandleDoNotDiscoverAttribute.php ├── HandleDomainAttribute.php ├── HandleFullUriAttribute.php ├── HandleHttpMethodsAttribute.php ├── HandleMiddlewareAttribute.php ├── HandleRouteNameAttribute.php ├── HandleUriAttribute.php ├── HandleUrisOfNestedControllers.php ├── HandleWheresAttribute.php ├── MoveRoutesStartingWithParametersLast.php ├── PendingRouteTransformer.php └── RejectDefaultControllerMethodRoutes.php ├── PendingRoutes ├── PendingRoute.php ├── PendingRouteAction.php └── PendingRouteFactory.php ├── RouteDiscoveryServiceProvider.php └── RouteRegistrar.php /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/*') 5 | ->notPath('storage/*') 6 | ->notPath('resources/view/mail/*') 7 | ->in([ 8 | __DIR__ . '/src', 9 | __DIR__ . '/tests', 10 | ]) 11 | ->name('*.php') 12 | ->notName('*.blade.php') 13 | ->ignoreDotFiles(true) 14 | ->ignoreVCS(true); 15 | 16 | return (new PhpCsFixer\Config()) 17 | ->setRules([ 18 | '@PSR2' => true, 19 | 'array_syntax' => ['syntax' => 'short'], 20 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 21 | 'no_unused_imports' => true, 22 | 'not_operator_with_successor_space' => true, 23 | 'trailing_comma_in_multiline' => true, 24 | 'phpdoc_scalar' => true, 25 | 'unary_operator_spaces' => true, 26 | 'binary_operator_spaces' => true, 27 | 'blank_line_before_statement' => [ 28 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 29 | ], 30 | 'phpdoc_single_line_var_spacing' => true, 31 | 'phpdoc_var_without_name' => true, 32 | 'class_attributes_separation' => [ 33 | 'elements' => [ 34 | 'method' => 'one', 35 | ], 36 | ], 37 | 'method_argument_space' => [ 38 | 'on_multiline' => 'ensure_fully_multiline', 39 | 'keep_multiple_spaces_after_comma' => true, 40 | ], 41 | 'single_trait_insert_per_statement' => true, 42 | ]) 43 | ->setFinder($finder); 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-route-discovery` will be documented in this file 4 | 5 | ## 1.0.3 - 2023-02-15 6 | 7 | - support Laravel 10 8 | 9 | ## 1.0.2 - 2022-12-06 10 | 11 | ### What's Changed 12 | 13 | - Update introduction.md by @LeonvanderHaas in https://github.com/spatie/laravel-route-discovery/pull/19 14 | - Update questions-issues.md by @LeonvanderHaas in https://github.com/spatie/laravel-route-discovery/pull/20 15 | - Fix issue where union typed parameters caused discovery errors by @27pchrisl in https://github.com/spatie/laravel-route-discovery/pull/25 16 | 17 | ### New Contributors 18 | 19 | - @LeonvanderHaas made their first contribution in https://github.com/spatie/laravel-route-discovery/pull/19 20 | - @27pchrisl made their first contribution in https://github.com/spatie/laravel-route-discovery/pull/25 21 | 22 | **Full Changelog**: https://github.com/spatie/laravel-route-discovery/compare/1.0.1...1.0.2 23 | 24 | ## 1.0.1 - 2022-03-08 25 | 26 | ## What's Changed 27 | 28 | - Change to allow forward-slashes in Windows by @vanyamil in https://github.com/spatie/laravel-route-discovery/pull/15 29 | 30 | ## New Contributors 31 | 32 | - @vanyamil made their first contribution in https://github.com/spatie/laravel-route-discovery/pull/15 33 | 34 | **Full Changelog**: https://github.com/spatie/laravel-route-discovery/compare/1.0.0...1.0.1 35 | 36 | ## 1.0.0 - 2022-02-07 37 | 38 | - initial release 39 | 40 | ## 0.12.0 - 2022-01-20 41 | 42 | - experimental release 43 | 44 | ## 0.0.1 - 2022-01-03 45 | 46 | - experimental release 47 | 48 | ## 1.9.0 - 2021-10-04 49 | 50 | - add support for the names parameter to the Resource attribute (#56) 51 | 52 | ## 1.8.1 - 2021-09-20 53 | 54 | - fix: use \ReflectionAttribute::IS_INSTANCEOF (#55) 55 | 56 | ## 1.8.0 - 2021-09-17 57 | 58 | - add group attribute (#54) 59 | 60 | ## 1.7.0 - 2021-08-31 61 | 62 | - add additional functionality to get domain from a config file (#50) 63 | 64 | ## 1.6.0 - 2021-08-15 65 | 66 | - add apiResource parameter to Resource attribute (#49) 67 | 68 | ## 1.5.0 - 2021-08-15 69 | 70 | - add support for resource controllers 71 | 72 | ## 1.4.2 - 2021-07-05 73 | 74 | - do not register routes when they are cached (#38) 75 | 76 | ## 1.4.1 - 2021-06-09 77 | 78 | - remove facade 79 | 80 | ## 1.4.0 - 2021-04-08 81 | 82 | - adds optional Wheres attribute on class and method level (#31) 83 | 84 | ## 1.3.0 - 2021-03-03 85 | 86 | - add support for multi-verb routes 87 | 88 | ## 1.2.3 - 2021-02-15 89 | 90 | - global middleware from config file will be registered first (#24) 91 | 92 | ## 1.2.2 - 2021-02-12 93 | 94 | - change approach to handle routes with prefix (#23) 95 | 96 | ## 1.2.1 - 2021-02-07 97 | 98 | - add new check to make sure TestClasses directory exists when running tests (#20) 99 | 100 | ## 1.2.0 - 2020-12-19 101 | 102 | - add middleware config option 103 | 104 | ## 1.1.0 - 2020-11-30 105 | 106 | - add `Domain` attribute 107 | 108 | ## 1.0.0 - 2020-10-29 109 | 110 | - initial release 111 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 | # Automatically discover routes in a Laravel app 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-route-discovery.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-route-discovery) 4 | ![Tests](https://github.com/spatie/laravel-route-discovery/workflows/Tests/badge.svg) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-route-discovery.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-route-discovery) 6 | 7 | This package can automatically discover routes for controllers and views in your Laravel application. This isn't an all-in approach. While using use auto discovery, you can still register routes like you're used to. 8 | 9 | ```php 10 | // typically in a routes file 11 | 12 | Discover::controllers()->in($whateverDirectoryYouPrefer); 13 | Discover::views()->in($whateverDirectoryYouPrefer); 14 | 15 | // other routes 16 | ``` 17 | 18 | Using PHP attributes you can manipulate discovered routes: you can set a route name, add some middleware, or ... 19 | 20 | Here's how you would add middleware to a controller whose's route will be auto discovered. 21 | 22 | ```php 23 | namespace App\Http\Controllers; 24 | 25 | use Illuminate\Routing\Middleware\ValidateSignature; 26 | use Laravel\RouteDiscovery\Attributes\Route; 27 | 28 | class MyController 29 | { 30 | #[Route(middleware: ValidateSignature::class)] 31 | public function myMethod() { /* ... */ } 32 | } 33 | ``` 34 | 35 | ## Support us 36 | 37 | [](https://spatie.be/github-ad-click/laravel-route-discovery) 38 | 39 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 40 | 41 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 42 | 43 | ## Documentation 44 | 45 | You'll find full documentation [at the Spatie website](https://spatie.be/docs/laravel-route-discovery). 46 | 47 | ## A note on performance 48 | 49 | Discovering routes during each application request may have a small impact on performance. For increased performance, we highly recommend [caching your routes](https://laravel.com/docs/8.x/routing#route-caching) as part of your deployment process. 50 | 51 | ## Testing 52 | 53 | ``` bash 54 | composer test 55 | ``` 56 | 57 | ## Changelog 58 | 59 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 60 | 61 | ## Contributing 62 | 63 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 64 | 65 | ## Security Vulnerabilities 66 | 67 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 68 | 69 | ## Credits 70 | 71 | - [Freek Van der Herten](https://github.com/freekmurze) 72 | - [All Contributors](../../contributors) 73 | 74 | ## License 75 | 76 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/route-discovery", 3 | "description": "Auto register routes using PHP attributes.", 4 | "keywords": [ 5 | "laravel-route-discovery" 6 | ], 7 | "homepage": "https://github.com/laravel/route-discovery", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Freek Van der Herten", 12 | "email": "freek@spatie.be", 13 | "homepage": "https://spatie.be", 14 | "role": "Developer" 15 | }, 16 | { 17 | "name": "Taylor Otwell", 18 | "email": "taylor@laravel.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.0", 24 | "illuminate/contracts": "^8.67|^9.0|^10.0|^11.0", 25 | "illuminate/support": "^8.77|^9.0|^10.0|^11.0", 26 | "symfony/finder": "^5.4.2|^6.0|^7.0" 27 | }, 28 | "require-dev": { 29 | "nunomaduro/larastan": "^1.0.2|^2.0", 30 | "orchestra/testbench": "^6.23.2|^7.0|^8.0", 31 | "pestphp/pest": "^1.21", 32 | "phpstan/extension-installer": "^1.1", 33 | "phpstan/phpstan-deprecation-rules": "^1.0", 34 | "phpstan/phpstan-phpunit": "^1.0", 35 | "spatie/laravel-package-tools": "^1.10", 36 | "spatie/laravel-ray": "^1.27" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Laravel\\RouteDiscovery\\": "src", 41 | "Laravel\\RouteDiscovery\\Database\\Factories\\": "database/factories" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Laravel\\RouteDiscovery\\Tests\\": "tests" 47 | } 48 | }, 49 | "scripts": { 50 | "analyse": "vendor/bin/phpstan analyse", 51 | "test": "vendor/bin/pest --colors=always", 52 | "test-coverage": "vendor/bin/pest --coverage" 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "phpstan/extension-installer": true, 58 | "pestphp/pest-plugin": true 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /config/route-discovery.php: -------------------------------------------------------------------------------- 1 | [ 9 | // app_path('Http/Controllers'), 10 | ], 11 | 12 | /* 13 | * Routes will be registered for all views found in these directories. 14 | * The key of an item will be used as the prefix of the uri. 15 | */ 16 | 'discover_views_in_directory' => [ 17 | // 'docs' => resource_path('views/docs'), 18 | ], 19 | 20 | /* 21 | * After having discovered all controllers, these classes will manipulate the routes 22 | * before registering them to Laravel. 23 | * 24 | * In most cases, you shouldn't change these. 25 | */ 26 | 'pending_route_transformers' => [ 27 | ...Laravel\RouteDiscovery\Config::defaultRouteTransformers(), 28 | // 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /docs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: v1 3 | slogan: Automatically discover routes in a Laravel app 4 | githubUrl: https://github.com/spatie/laravel-route-discovery 5 | branch: main 6 | --- 7 | -------------------------------------------------------------------------------- /docs/about-us.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About us 3 | --- 4 | 5 | [Spatie](https://spatie.be) is a webdesign agency based in Antwerp, Belgium. 6 | 7 | Open source software is used in all projects we deliver. Laravel, Nginx, Ubuntu are just a few 8 | of the free pieces of software we use every single day. For this, we are very grateful. 9 | When we feel we have solved a problem in a way that can help other developers, 10 | we release our code as open source software [on GitHub](https://spatie.be/open-source). 11 | -------------------------------------------------------------------------------- /docs/advanced-usage/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced usage 3 | weight: 3 4 | --- 5 | -------------------------------------------------------------------------------- /docs/advanced-usage/using-route-transformers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using route transformers 3 | weight: 1 4 | --- 5 | 6 | Under the hood, the package will build up a collection of `Spatie\RouteDiscovery\PendingRoutes\PendingRoute` instances by looking at the controllers and views in your project. This collection of `PendingRoutes` can be modified by a `PendingRouteTransformer`. After that, each `action` inside a `PendingRoute` will be registered as a regular Laravel route. 7 | 8 | In the `route-discovery` config file you'll see the registered route transformers. 9 | 10 | ```php 11 | // in config/route-discovery.php 12 | 13 | /* 14 | * After having discovered all controllers, these classes will manipulate the routes 15 | * before registering them to Laravel. 16 | * 17 | * In most cases, you shouldn't change these. 18 | */ 19 | 'pending_route_transformers' => [ 20 | ...Spatie\RouteDiscovery\Config::defaultRouteTransformers(), 21 | // 22 | ], 23 | ``` 24 | 25 | This is the returned value of `Spatie\RouteDiscovery\Config::defaultRouteTransformers()`: 26 | 27 | ```php 28 | [ 29 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleDoNotDiscoverAttribute::class, 30 | Spatie\RouteDiscovery\PendingRouteTransformers\AddControllerUriToActions::class, 31 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleUrisOfNestedControllers::class, 32 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleRouteNameAttribute::class, 33 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleMiddlewareAttribute::class, 34 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleHttpMethodsAttribute::class, 35 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleUriAttribute::class, 36 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleFullUriAttribute::class, 37 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleWheresAttribute::class, 38 | Spatie\RouteDiscovery\PendingRouteTransformers\AddDefaultRouteName::class, 39 | Spatie\RouteDiscovery\PendingRouteTransformers\HandleDomainAttribute::class, 40 | Spatie\RouteDiscovery\PendingRouteTransformers\MoveRoutesStartingWithParametersLast::class, 41 | ]; 42 | ``` 43 | 44 | These transformers will handle specific `Route` attributes, make sure the routes are registered in the correct orders, ... 45 | 46 | ## Creating your own route transformer 47 | 48 | You can create your own route transformer by letting a class implement the `Spatie\RouteDiscovery\PendingRouteTransformers\PendingRouteTransformer` interface. Here's how that interface looks like: 49 | 50 | ```php 51 | use Illuminate\Support\Collection; 52 | use Laravel\RouteDiscovery\PendingRoutes\PendingRoute; 53 | 54 | interface PendingRouteTransformer 55 | { 56 | /** 57 | * @param Collection $pendingRoutes 58 | * 59 | * @return Collection 60 | */ 61 | public function transform(Collection $pendingRoutes): Collection; 62 | } 63 | ``` 64 | 65 | After you've created your transformer, register it in the `pending_route_transformers` key of the `route-discovery` config file. 66 | 67 | Take a look at one of the default route transformers for an example implementation. 68 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | weight: 6 4 | --- 5 | 6 | All notable changes to laravel-route-discovery are documented [on GitHub](https://github.com/spatie/laravel-route-discovery/blob/main/CHANGELOG.md) 7 | -------------------------------------------------------------------------------- /docs/discovering-routes-for-controllers/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Discovering routes for controllers 3 | weight: 1 4 | --- 5 | -------------------------------------------------------------------------------- /docs/discovering-routes-for-controllers/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | weight: 1 4 | --- 5 | 6 | This package can automatically discover and register routes for a directory containing controllers. 7 | 8 | ## Via the routes file 9 | 10 | You can enable route discovery via the routes file. 11 | 12 | ```php 13 | // in a routes file 14 | 15 | use Laravel\RouteDiscovery\Discovery\Discover; 16 | 17 | Discover::controllers()->in(app_path('Http/Controllers')); 18 | ``` 19 | 20 | ## Via the config file 21 | 22 | Alternatively, you can discover routes using the config file. 23 | 24 | First, you need to publish the config file. This will create a file at `config/route-discovery.php` 25 | 26 | ```bash 27 | php artisan vendor:publish --tag="route-discovery-config" 28 | ``` 29 | 30 | In the `discover_controllers_in_directory` key of the `route-discovery` config file, you can specify a directory that contains controllers. 31 | 32 | Here you can uncomment the line to register controllers in the `app_path('Http/Controllers')` directory. Of course you can use any directory you want. 33 | 34 | ```php 35 | // config/route-discovery 36 | 37 | /* 38 | * Routes will be registered for all controllers found in 39 | * these directories. 40 | */ 41 | 'discover_controllers_in_directory' => [ 42 | app_path('Http/Controllers'), 43 | ], 44 | // ... 45 | ``` 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/discovering-routes-for-controllers/mapping-controllers-to-routes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mapping controllers to routes 3 | weight: 2 4 | --- 5 | 6 | All the examples assume that you've registered to auto discover routes for all public methods of controller in the `app_path('Http/Controllers')` directory. 7 | 8 | By default, the package will add a route using the name of both the controller and method. 9 | 10 | For this controller, the `/news/my-method` route will be registered. 11 | 12 | ```php 13 | namespace App\Http\Controllers; 14 | 15 | class NewsController 16 | { 17 | public function myMethod() { /* ... */ } 18 | } 19 | ``` 20 | 21 | Of course, multiple methods in a controller will result in multiple routes being registered. 22 | 23 | For this controller, `/news/my-method` and `/news/my-other-method` routes will be registered. 24 | 25 | ```php 26 | namespace App\Http\Controllers; 27 | 28 | class NewsController 29 | { 30 | public function myMethod() { /* ... */ } 31 | public function myOtherMethod() { /* ... */ } 32 | } 33 | ``` 34 | 35 | ## Index methods 36 | 37 | When a method is named `index`, the method name is not used when registering a route. 38 | 39 | For this controller, the `/news` route will be registered. 40 | 41 | ```php 42 | namespace App\Http\Controllers; 43 | 44 | class NewsController 45 | { 46 | public function index() { /* ... */ } 47 | } 48 | ``` 49 | 50 | ## Nested controllers 51 | 52 | When a controller is in a sub-namespace, the sub-namespace names will be used when generating the URL. 53 | 54 | For this controller, the `/nested/news` route will be registered. 55 | 56 | ```php 57 | namespace App\Http\Controllers\Nested; 58 | 59 | class NewsController 60 | { 61 | public function index() { /* ... */ } 62 | } 63 | ``` 64 | 65 | ## Customizing the URL 66 | 67 | You can override the last segment of the generated URL by using adding a `Route` attribute to your method and passing a value to the `uri` parameter. 68 | 69 | For this controller, the `/news/alternative-uri` route will be registered instead of `/news/my-method`. 70 | 71 | ```php 72 | namespace App\Http\Controllers; 73 | 74 | use Laravel\RouteDiscovery\Attributes\Route; 75 | 76 | class NewsController 77 | { 78 | #[Route(uri: 'alternative-uri')] 79 | public function myMethod() { /* ... */ } 80 | } 81 | ``` 82 | 83 | If you want override the whole URL, pass a value to the `fullUri` method. 84 | 85 | For this controller, the `/alternative-uri` route will be registered instead of `/news/my-method`. 86 | 87 | ```php 88 | namespace App\Http\Controllers; 89 | 90 | use Laravel\RouteDiscovery\Attributes\Route; 91 | 92 | class NewsController 93 | { 94 | #[Route(fullUri: 'alternative-uri')] 95 | public function myMethod() { /* ... */ } 96 | } 97 | ``` 98 | 99 | ## HTTP verbs 100 | 101 | By default, all registered routes are `GET` routes. 102 | 103 | There are a couple of method names that will result in another HTTP verb. 104 | 105 | - `store`: `POST` 106 | - `update`: `PUT` and `PATCH` 107 | - `destroy` and `delete`: `DELETE` 108 | 109 | You can customize the verb to be used by adding a `Route` attribute to a method 110 | 111 | ```php 112 | namespace App\Http\Controllers; 113 | 114 | use Laravel\RouteDiscovery\Attributes\Route; 115 | 116 | class NewsController 117 | { 118 | #[Route(method: 'post')] 119 | public function myMethod() { /* ... */ } 120 | } 121 | ``` 122 | 123 | ## Adding a route name 124 | 125 | By default, the package will automatically add route names for each route that is registered. For this we'll use the controller name and the method name. 126 | 127 | For a `NewsController` with a method `myMethod`, the route name will be `news.my-method`. If that controller in a sub namespace, for example `App\Http\Controllers\Nested\NewsController`, the route name will become `nested.news.my-method`. 128 | 129 | You can customize the route name that will be added by adding a `Route` attribute and pass a string to the `name` argument. 130 | 131 | For the controller below, the discovered route will have the name `special-name`. 132 | 133 | ```php 134 | namespace App\Http\Controllers; 135 | 136 | use Laravel\RouteDiscovery\Attributes\Route; 137 | 138 | class NewsController 139 | { 140 | #[Route(name: 'special-name')] 141 | public function specialMethod() { /* ... */ } 142 | } 143 | ``` 144 | 145 | ## Adding middleware 146 | 147 | You can apply middleware to a route by adding a `Route` attribute and pass a middleware class to the `middleware` argument. 148 | 149 | ```php 150 | namespace App\Http\Controllers; 151 | 152 | use Illuminate\Routing\Middleware\ValidateSignature; 153 | use Laravel\RouteDiscovery\Attributes\Route; 154 | 155 | class DownloadController 156 | { 157 | #[Route(middleware: ValidateSignature::class)] 158 | public function download() { /* ... */ } 159 | } 160 | ``` 161 | 162 | To apply a middleware on all methods of a controller, use the `Route` attribute at the class level. In the example below, the middleware will be applied on both the routes of both `download` and `otherMethod`. 163 | 164 | ```php 165 | namespace App\Http\Controllers; 166 | 167 | use Illuminate\Routing\Middleware\ValidateSignature; 168 | use Laravel\RouteDiscovery\Attributes\Route; 169 | 170 | #[Route(middleware: ValidateSignature::class)] 171 | class DownloadController 172 | { 173 | public function download() { /* ... */ } 174 | 175 | public function otherMethod() { /* ... */ } 176 | } 177 | ``` 178 | 179 | Instead of a string, you can also pass an array with middleware to the `middleware` argument of the `Route` attribute. 180 | 181 | ```php 182 | #[Route(middleware: [ValidateSignature::class, AnotherMiddleware::class])] 183 | ``` 184 | 185 | ## Models as route parameters 186 | 187 | A URL segment will be used for a parameter of a method that accepts an eloquent model. 188 | 189 | For this controller, the `/users/edit/{user}` route will be registered. 190 | 191 | ```php 192 | namespace App\Http\Controllers; 193 | 194 | use Laravel\RouteDiscovery\Attributes\Route; 195 | 196 | class UsersController 197 | { 198 | public function edit(User $user) { /* ... */ } 199 | } 200 | ``` 201 | 202 | ## Route parameter constraints 203 | 204 | You can constrain the format of a route parameter with the `Where` attribute. 205 | 206 | In this following example this route will be registered: `/users/edit/{user}`. By adding the `Where::uuid` constraint to the `Where` attribute, we make sure that only UUIDs will match the `{user}` parameter. 207 | 208 | ```php 209 | namespace App\Http\Controllers; 210 | 211 | use Laravel\RouteDiscovery\Attributes\Route; 212 | use Laravel\RouteDiscovery\Attributes\Where; 213 | 214 | class UsersController 215 | { 216 | #[Where('user', constraint: Where::uuid)] 217 | public function edit(User $user) { /* ... */ } 218 | } 219 | ``` 220 | 221 | The package ships with these `Where` constraint constants: 222 | 223 | - `Where::alpha` 224 | - `Where::numeric` 225 | - `Where::alphanumeric` 226 | - `Where::uuid` 227 | 228 | You can also specify your own regex, by using the `Where` attribute. 229 | 230 | ```php 231 | namespace App\Http\Controllers; 232 | 233 | use Laravel\RouteDiscovery\Attributes\Route; 234 | use Laravel\RouteDiscovery\Attributes\Where; 235 | 236 | class UsersController 237 | { 238 | #[Where('user', constraint: '[0-9]+')] 239 | public function edit(User $user) { /* ... */ } 240 | } 241 | ``` 242 | 243 | ## Setting a domain 244 | 245 | You can specify which domain the routes should be registered by passing a value to the `domain` parameter of the `Route` attribute. This can be done on both the class and method level. 246 | 247 | Using this controller, the route to `firstMethod` will only listen for request to the `first.example.com` domain, the `secondMethod` to the `second.example.com` domain. 248 | 249 | ```php 250 | namespace App\Http\Controllers; 251 | 252 | use Illuminate\Routing\Middleware\ValidateSignature; 253 | use Laravel\RouteDiscovery\Attributes\Route; 254 | 255 | #[Route(domain: 'first.example.com')] 256 | class ExampleController 257 | { 258 | public function firstMethod() { /* ... */ } 259 | 260 | #[Route(domain: 'second.example.com')] 261 | public function secondMethod() { /* ... */ } 262 | } 263 | ``` 264 | 265 | ## Preventing routes from being discovered 266 | 267 | You can prevent a certain controller from being discovered by using the `DoNotDiscover` attribute. 268 | 269 | For this controller, only a route for the `anotherMethod` will be registered. 270 | 271 | ```php 272 | namespace App\Http\Controllers; 273 | 274 | use Laravel\RouteDiscovery\Attributes\DoNotDiscover; 275 | 276 | class UsersController 277 | { 278 | #[DoNotDiscover] 279 | public function myMethod() { /* ... */} 280 | 281 | public function anotherMethod() { /* ... */} 282 | } 283 | ``` 284 | 285 | You can also prevent an entire controller from being discovered by adding the `DoNotDiscover` attribute on the class level. 286 | 287 | For this controller, not a single route will be registered. 288 | 289 | ```php 290 | namespace App\Http\Controllers; 291 | 292 | use Laravel\RouteDiscovery\Attributes\DoNotDiscover; 293 | 294 | #[DoNotDiscover] 295 | class UsersController 296 | { 297 | public function myMethod() { /* ... */} 298 | 299 | public function anotherMethod() { /* ... */} 300 | } 301 | ``` 302 | -------------------------------------------------------------------------------- /docs/discovering-routes-for-views/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Discovering routes for views 3 | weight: 2 4 | --- 5 | -------------------------------------------------------------------------------- /docs/discovering-routes-for-views/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Discovering routes for views 3 | weight: 1 4 | --- 5 | 6 | This package can automatically discover and register routes for a directory containing Blade views. 7 | 8 | ## Via the routes file 9 | 10 | You can also enable route discovery via the routes file. 11 | 12 | ```php 13 | // in a routes file 14 | 15 | use Laravel\RouteDiscovery\Discovery\Discover; 16 | 17 | Discover::views()->in(resource_path('views/auto')); 18 | ``` 19 | 20 | To use a prefix, add middleware, and more, you can put that call to `Discover::views()` in a group. 21 | 22 | ```php 23 | // in a routes file 24 | 25 | use Laravel\RouteDiscovery\Discovery\Discover; 26 | 27 | Route::prefix('my-discovered-views')->group(function() { 28 | Discover::views()->in(resource_path('views/auto')); 29 | }); 30 | ``` 31 | 32 | ## Via the config file 33 | 34 | In the `discover_view_in_directory` key of the `route-discovery` config file, you can specify a directory that contains views. 35 | 36 | ```php 37 | // config/route-discovery.php 38 | 39 | // ... 40 | 41 | /* 42 | * Routes will be registered for all views found in these directories. 43 | * The key of an item will be used as the prefix of the uri. 44 | */ 45 | 'discover_views_in_directory' => [ 46 | 'docs' => resource_path('views/docs'), 47 | ], 48 | 49 | // .. 50 | ``` 51 | 52 | Using this example above, routes will be registered for all views in the `resource_path('views/docs')` directory. The key of the item will be used as a prefix. If you don't want to prefix your discovered routes, simply do not use a key. 53 | 54 | ```php 55 | // config/route-discovery.php 56 | 57 | 'discover_views_in_directory' => [ 58 | resource_path('views/discovery'), 59 | ], 60 | ``` 61 | 62 | Of course, you can also discover routes for multiple directories in one go. 63 | 64 | ```php 65 | // config/route-discovery.php 66 | 67 | 'discover_views_in_directory' => [ 68 | resource_path('views/discovery'), 69 | resource_path('views/another-directory'), 70 | ], 71 | ``` 72 | 73 | If you want to register multiple directories with the same prefix, you can use array syntax 74 | 75 | ```php 76 | // config/route-discovery.php 77 | 78 | 'discover_views_in_directory' => [ 79 | 'docs' => [ 80 | resource_path('views/docs'), 81 | resource_path('views/other-docs') 82 | ], 83 | ], 84 | ``` 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/discovering-routes-for-views/mapping-views-to-routes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mapping views to routes 3 | weight: 2 4 | --- 5 | 6 | The package will add a route for each `blade.php` that it finds in [directory you've specified](getting-started). 7 | 8 | Imagine you've configured view discovery this way. 9 | 10 | ```php 11 | // config/route-discovery 12 | 13 | 'discover_views_in_directory' => [ 14 | 'docs' => resource_path('views/docs'), 15 | ], 16 | ``` 17 | 18 | And image that `views/docs` contains these Blade views... 19 | 20 | - index.blade.php 21 | - pageA.blade.php 22 | - pageB.blade.php 23 | - nested/index.blade.php 24 | - nested/pageC.blade.php 25 | 26 | ... then these routes will be registered: 27 | 28 | - /docs --> index.blade.php 29 | - /docs/page-a --> pageA.blade.php 30 | - /docs/page-b --> pageB.blade.php 31 | - /docs/nested --> nested/index.blade.php 32 | - /docs/nested/page-c --> nested/pageC.blade.php 33 | 34 | The registered routes will also be named automatically. The route name will be generated by replacing the `/` with `.`. So there routes will have these names: 35 | 36 | - /docs --> docs 37 | - /docs/page-a --> docs.page-a 38 | - /docs/page-b --> docs.page-b 39 | - /docs/nested --> docs.nested 40 | - /docs/nested/page-c --> docs.nested.page-c 41 | -------------------------------------------------------------------------------- /docs/installation-setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation & setup 3 | weight: 4 4 | --- 5 | 6 | You can install the package via composer: 7 | 8 | ```bash 9 | composer require spatie/laravel-route-discovery 10 | ``` 11 | 12 | ## Publishing the config file 13 | 14 | Optionally, you can publish the `route-discovery` config file with this command. 15 | 16 | ```bash 17 | php artisan vendor:publish --tag="route-discovery-config" 18 | ``` 19 | 20 | This is the content of the published config file: 21 | 22 | ```php 23 | return [ 24 | /* 25 | * Routes will be registered for all controllers found in 26 | * these directories. 27 | */ 28 | 'discover_controllers_in_directory' => [ 29 | // app_path('Http/Controllers'), 30 | ], 31 | 32 | /* 33 | * Routes will be registered for all views found in these directories. 34 | * The key of an item will be used as the prefix of the uri. 35 | */ 36 | 'discover_views_in_directory' => [ 37 | // 'docs' => resource_path('views/docs'), 38 | ], 39 | 40 | /* 41 | * After having discovered all controllers, these classes will manipulate the routes 42 | * before registering them to Laravel. 43 | * 44 | * In most cases, you shouldn't change these. 45 | */ 46 | 'pending_route_transformers' => [ 47 | ...Spatie\RouteDiscovery\Config::defaultRouteTransformers(), 48 | // 49 | ], 50 | ]; 51 | ``` 52 | 53 | ## A word on performance 54 | 55 | Discovering routes during each application request may have a small impact on performance. For increased performance, we highly recommend [caching your routes](https://laravel.com/docs/8.x/routing#route-caching) as part of your deployment process. 56 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | weight: 1 4 | --- 5 | 6 | This package can automatically discover routes for controllers and views in your Laravel application. This isn't an all-in approach. While using auto discovery, you can still register routes like you're used to. 7 | 8 | ```php 9 | // typically in a routes file 10 | 11 | Discover::controllers()->in($whateverDirectoryYouPrefer); 12 | Discover::views()->in($whateverDirectoryYouPrefer); 13 | 14 | // other routes 15 | ``` 16 | 17 | Using PHP attributes you can manipulate discovered routes: you can set a route name, add some middleware, or ... 18 | 19 | Here's how you would add middleware to a controller whose route will be auto discovered. 20 | 21 | ```php 22 | namespace App\Http\Controllers; 23 | 24 | use Illuminate\Routing\Middleware\ValidateSignature; 25 | use Laravel\RouteDiscovery\Attributes\Route; 26 | 27 | class MyController 28 | { 29 | #[Route(middleware: ValidateSignature::class)] 30 | public function myMethod() { /* ... */ } 31 | } 32 | ``` 33 | 34 | ## A note on performance 35 | 36 | Discovering routes during each application request may have a small impact on performance. For increased performance, we highly recommend [caching your routes](https://laravel.com/docs/8.x/routing#route-caching) as part of your deployment process. 37 | -------------------------------------------------------------------------------- /docs/questions-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Questions and issues 3 | weight: 5 4 | --- 5 | 6 | Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the health of the library? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-route-discovery/issues), we'll try to address it as soon as possible. 7 | 8 | If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. 9 | -------------------------------------------------------------------------------- /docs/requirements.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Requirements 3 | weight: 3 4 | --- 5 | 6 | The laravel-route-discovery package requires **PHP 8.0+**, **Laravel 8+**. 7 | -------------------------------------------------------------------------------- /docs/support-us.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Support us 3 | weight: 2 4 | --- 5 | 6 | We invest a lot of resources into creating our [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 7 | 8 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 9 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Class App\\\\Http\\\\Controllers\\\\Controller not found\\.$#" 5 | count: 1 6 | path: src/PendingRouteTransformers/RejectDefaultControllerMethodRoutes.php 7 | 8 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 8 6 | paths: 7 | - src 8 | - config 9 | tmpDir: build/phpstan 10 | checkOctaneCompatibility: true 11 | checkModelProperties: true 12 | checkMissingIterableValueType: true 13 | 14 | -------------------------------------------------------------------------------- /src/Attributes/DiscoveryAttribute.php: -------------------------------------------------------------------------------- 1 | */ 13 | public array $methods; 14 | 15 | /** @var array */ 16 | public array $middleware; 17 | 18 | /** 19 | * @param array|string $method 20 | * @param string|null $uri 21 | * @param string|null $fullUri 22 | * @param string|null $name 23 | * @param array|string $middleware 24 | */ 25 | public function __construct( 26 | array | string $method = [], 27 | public ?string $uri = null, 28 | public ?string $fullUri = null, 29 | public ?string $name = null, 30 | array | string $middleware = [], 31 | public ?string $domain = null, 32 | ) { 33 | $methods = Arr::wrap($method); 34 | 35 | $this->methods = collect($methods) 36 | ->map(fn (string $method) => strtoupper($method)) 37 | ->filter(fn (string $method) => in_array($method, Router::$verbs)) 38 | ->toArray(); 39 | 40 | $this->middleware = Arr::wrap($middleware); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Attributes/Where.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public static function defaultRouteTransformers(): array 25 | { 26 | return [ 27 | RejectDefaultControllerMethodRoutes::class, 28 | HandleDoNotDiscoverAttribute::class, 29 | AddControllerUriToActions::class, 30 | HandleUrisOfNestedControllers::class, 31 | HandleRouteNameAttribute::class, 32 | HandleMiddlewareAttribute::class, 33 | HandleHttpMethodsAttribute::class, 34 | HandleUriAttribute::class, 35 | HandleFullUriAttribute::class, 36 | HandleWheresAttribute::class, 37 | AddDefaultRouteName::class, 38 | HandleDomainAttribute::class, 39 | MoveRoutesStartingWithParametersLast::class, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Discovery/Discover.php: -------------------------------------------------------------------------------- 1 | rootNamespace = ''; 16 | 17 | $this->basePath = base_path(); 18 | } 19 | 20 | public function useRootNamespace(string $rootNamespace): self 21 | { 22 | $this->rootNamespace = $rootNamespace; 23 | 24 | return $this; 25 | } 26 | 27 | public function useBasePath(string $basePath): self 28 | { 29 | $this->basePath = $basePath; 30 | 31 | return $this; 32 | } 33 | 34 | public function in(string $directory): void 35 | { 36 | /** @phpstan-ignore-next-line */ 37 | $router = app()->router; 38 | 39 | app(RouteRegistrar::class, [$router]) 40 | ->useRootNamespace($this->rootNamespace) 41 | ->useBasePath($this->basePath) 42 | ->registerDirectory($directory); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/AddControllerUriToActions.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 13 | * 14 | * @return Collection 15 | */ 16 | public function transform(Collection $pendingRoutes): Collection 17 | { 18 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 19 | $pendingRoute->actions->each(function (PendingRouteAction $action) use ($pendingRoute) { 20 | $originalActionUri = $action->uri; 21 | 22 | $action->uri = $pendingRoute->uri; 23 | 24 | if ($originalActionUri) { 25 | $action->uri .= "/{$originalActionUri}"; 26 | } 27 | }); 28 | }); 29 | 30 | return $pendingRoutes; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/AddDefaultRouteName.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 13 | * 14 | * @return Collection 15 | */ 16 | public function transform(Collection $pendingRoutes): Collection 17 | { 18 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 19 | $pendingRoute->actions 20 | ->reject(fn (PendingRouteAction $action) => $action->name) 21 | /** @phpstan-ignore-next-line */ 22 | ->each(fn (PendingRouteAction $action) => $action->name = $this->generateRouteName($action)); 23 | }); 24 | 25 | return $pendingRoutes; 26 | } 27 | 28 | protected function generateRouteName(PendingRouteAction $pendingRouteAction): string 29 | { 30 | return collect(explode('/', $pendingRouteAction->uri)) 31 | ->reject(fn (string $segment) => str_starts_with($segment, '{')) 32 | ->join('.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleDoNotDiscoverAttribute.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 15 | * 16 | * @return Collection 17 | */ 18 | public function transform(Collection $pendingRoutes): Collection 19 | { 20 | return $pendingRoutes 21 | ->reject(fn (PendingRoute $pendingRoute) => $pendingRoute->getAttribute(DoNotDiscover::class) || $pendingRoute->getAttribute(DontDiscover::class)) 22 | ->each(function (PendingRoute $pendingRoute) { 23 | $pendingRoute->actions = $pendingRoute 24 | ->actions 25 | ->reject(fn (PendingRouteAction $action) => $action->getAttribute(DoNotDiscover::class) || $action->getAttribute(DontDiscover::class)); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleDomainAttribute.php: -------------------------------------------------------------------------------- 1 | each(function (PendingRoute $pendingRoute) { 14 | $pendingRoute->actions->each(function (PendingRouteAction $action) use ($pendingRoute) { 15 | if ($pendingRouteAttribute = $pendingRoute->getRouteAttribute()) { 16 | $action->domain = $pendingRouteAttribute->domain; 17 | } 18 | 19 | if ($actionAttribute = $action->getRouteAttribute()) { 20 | $action->domain = $actionAttribute->domain; 21 | } 22 | }); 23 | }); 24 | 25 | return $pendingRoutes; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleFullUriAttribute.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 13 | * 14 | * @return Collection 15 | */ 16 | public function transform(Collection $pendingRoutes): Collection 17 | { 18 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 19 | $pendingRoute->actions->each(function (PendingRouteAction $action) { 20 | if (! $routeAttribute = $action->getRouteAttribute()) { 21 | return; 22 | } 23 | 24 | if (! $routeAttributeFullUri = $routeAttribute->fullUri) { 25 | return; 26 | } 27 | 28 | $action->uri = $routeAttributeFullUri; 29 | }); 30 | }); 31 | 32 | return $pendingRoutes; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleHttpMethodsAttribute.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 13 | * 14 | * @return Collection 15 | */ 16 | public function transform(Collection $pendingRoutes): Collection 17 | { 18 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 19 | $pendingRoute->actions->each(function (PendingRouteAction $action) { 20 | if (! $routeAttribute = $action->getRouteAttribute()) { 21 | return; 22 | } 23 | 24 | if (! $httpMethods = $routeAttribute->methods) { 25 | return; 26 | } 27 | 28 | $action->methods = $httpMethods; 29 | }); 30 | }); 31 | 32 | return $pendingRoutes; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleMiddlewareAttribute.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 13 | * 14 | * @return Collection 15 | */ 16 | public function transform(Collection $pendingRoutes): Collection 17 | { 18 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 19 | $pendingRoute->actions->each(function (PendingRouteAction $action) use ($pendingRoute) { 20 | if ($pendingRouteAttribute = $pendingRoute->getRouteAttribute()) { 21 | $action->addMiddleware($pendingRouteAttribute->middleware); 22 | } 23 | 24 | if ($actionRouteAttribute = $action->getRouteAttribute()) { 25 | $action->addMiddleware($actionRouteAttribute->middleware); 26 | } 27 | }); 28 | }); 29 | 30 | return $pendingRoutes; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleRouteNameAttribute.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 13 | * 14 | * @return Collection 15 | */ 16 | public function transform(Collection $pendingRoutes): Collection 17 | { 18 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 19 | $pendingRoute->actions->each(function (PendingRouteAction $action) { 20 | if (! $routeAttribute = $action->getRouteAttribute()) { 21 | return; 22 | } 23 | 24 | if (! $name = $routeAttribute->name) { 25 | return; 26 | } 27 | 28 | $action->name = $name; 29 | }); 30 | }); 31 | 32 | return $pendingRoutes; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleUriAttribute.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 14 | * 15 | * @return Collection 16 | */ 17 | public function transform(Collection $pendingRoutes): Collection 18 | { 19 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 20 | $pendingRoute->actions->each(function (PendingRouteAction $action) { 21 | if (! $routeAttribute = $action->getRouteAttribute()) { 22 | return; 23 | } 24 | 25 | if (! $routeAttributeUri = $routeAttribute->uri) { 26 | return; 27 | } 28 | 29 | $baseUri = Str::beforeLast($action->uri, '/'); 30 | $action->uri = $baseUri . '/' . $routeAttributeUri; 31 | }); 32 | }); 33 | 34 | return $pendingRoutes; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleUrisOfNestedControllers.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 14 | * 15 | * @return Collection 16 | */ 17 | public function transform(Collection $pendingRoutes): Collection 18 | { 19 | $pendingRoutes->each(function (PendingRoute $parentPendingRoute) use ($pendingRoutes) { 20 | $childNode = $this->findChild($pendingRoutes, $parentPendingRoute); 21 | 22 | if (! $childNode) { 23 | return; 24 | } 25 | 26 | /** @var PendingRouteAction|null $parentAction */ 27 | $parentAction = $parentPendingRoute->actions->first(function (PendingRouteAction $action) { 28 | return in_array($action->method->name, ['show', 'edit', 'update', 'destroy', 'delete']); 29 | }); 30 | 31 | if (is_null($parentAction)) { 32 | return; 33 | } 34 | 35 | $childNode->actions->each(function (PendingRouteAction $action) use ($parentPendingRoute, $parentAction) { 36 | $result = Str::replace($parentPendingRoute->uri, $parentAction->uri, $action->uri); 37 | 38 | $action->uri = $result; 39 | }); 40 | }); 41 | 42 | return $pendingRoutes; 43 | } 44 | 45 | protected function findChild(Collection $pendingRoutes, PendingRoute $parentRouteAction): ?PendingRoute 46 | { 47 | $childNamespace = $parentRouteAction->childNamespace(); 48 | 49 | return $pendingRoutes->first( 50 | fn (PendingRoute $potentialChildRoute) => $potentialChildRoute->namespace() === $childNamespace 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/HandleWheresAttribute.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 14 | * 15 | * @return Collection 16 | */ 17 | public function transform(Collection $pendingRoutes): Collection 18 | { 19 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 20 | $pendingRoute->actions->each(function (PendingRouteAction $action) use ($pendingRoute) { 21 | if ($pendingRouteWhereAttribute = $pendingRoute->getAttribute(Where::class)) { 22 | $action->addWhere($pendingRouteWhereAttribute); 23 | } 24 | 25 | if ($actionWhereAttribute = $action->getAttribute(Where::class)) { 26 | $action->addWhere($actionWhereAttribute); 27 | } 28 | }); 29 | }); 30 | 31 | return $pendingRoutes; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/MoveRoutesStartingWithParametersLast.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 13 | * 14 | * @return Collection 15 | */ 16 | public function transform(Collection $pendingRoutes): Collection 17 | { 18 | return $pendingRoutes->sortBy(function (PendingRoute $pendingRoute) { 19 | $containsRouteStartingWithUri = $pendingRoute->actions->contains(function (PendingRouteAction $action) { 20 | return str_starts_with($action->uri, '{'); 21 | }); 22 | 23 | if (! $containsRouteStartingWithUri) { 24 | return 0; 25 | } 26 | 27 | return $pendingRoute->actions->max(function (PendingRouteAction $action) { 28 | if (! str_starts_with($action->uri, '{')) { 29 | return PHP_INT_MAX; 30 | } 31 | 32 | return PHP_INT_MAX - count(explode('/', $action->uri)); 33 | }); 34 | }) 35 | ->values(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/PendingRouteTransformer.php: -------------------------------------------------------------------------------- 1 | $pendingRoutes 12 | * 13 | * @return Collection 14 | */ 15 | public function transform(Collection $pendingRoutes): Collection; 16 | } 17 | -------------------------------------------------------------------------------- /src/PendingRouteTransformers/RejectDefaultControllerMethodRoutes.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public array $rejectMethodsInClasses = [ 18 | ControllerWithDefaultLaravelTraits::class, 19 | DefaultAppController::class, 20 | Controller::class, 21 | ]; 22 | 23 | /** 24 | * @param Collection $pendingRoutes 25 | * 26 | * @return Collection 27 | */ 28 | public function transform(Collection $pendingRoutes): Collection 29 | { 30 | return $pendingRoutes->each(function (PendingRoute $pendingRoute) { 31 | $pendingRoute->actions = $pendingRoute 32 | ->actions 33 | ->reject(fn (PendingRouteAction $pendingRouteAction) => in_array( 34 | $pendingRouteAction->method->class, 35 | $this->rejectMethodsInClasses 36 | )); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PendingRoutes/PendingRoute.php: -------------------------------------------------------------------------------- 1 | $actions 21 | */ 22 | public function __construct( 23 | public SplFileInfo $fileInfo, 24 | public ReflectionClass $class, 25 | public string $uri, 26 | public string $fullyQualifiedClassName, 27 | public Collection $actions, 28 | ) { 29 | } 30 | 31 | public function namespace(): string 32 | { 33 | return Str::beforeLast($this->fullyQualifiedClassName, '\\'); 34 | } 35 | 36 | public function shortControllerName(): string 37 | { 38 | return Str::of($this->fullyQualifiedClassName) 39 | ->afterLast('\\') 40 | ->beforeLast('Controller'); 41 | } 42 | 43 | public function childNamespace(): string 44 | { 45 | return $this->namespace() . '\\' . $this->shortControllerName(); 46 | } 47 | 48 | public function getRouteAttribute(): ?Route 49 | { 50 | return $this->getAttribute(Route::class); 51 | } 52 | 53 | /** 54 | * @template TDiscoveryAttribute of DiscoveryAttribute 55 | * 56 | * @param class-string $attributeClass 57 | * 58 | * @return ?TDiscoveryAttribute 59 | */ 60 | public function getAttribute(string $attributeClass): ?DiscoveryAttribute 61 | { 62 | $attributes = $this->class->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF); 63 | 64 | if (! count($attributes)) { 65 | return null; 66 | } 67 | 68 | return $attributes[0]->newInstance(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/PendingRoutes/PendingRouteAction.php: -------------------------------------------------------------------------------- 1 | */ 21 | public array $methods = []; 22 | 23 | /** @var array{class-string, string} */ 24 | public array $action; 25 | 26 | /** @var array */ 27 | public array $middleware = []; 28 | 29 | /** @var array */ 30 | public array $wheres = []; 31 | public ?string $name = null; 32 | 33 | public ?string $domain = null; 34 | 35 | /** 36 | * @param ReflectionMethod $method 37 | * @param class-string $controllerClass 38 | */ 39 | public function __construct(ReflectionMethod $method, string $controllerClass) 40 | { 41 | $this->method = $method; 42 | 43 | $this->uri = $this->relativeUri(); 44 | 45 | $this->methods = $this->discoverHttpMethods(); 46 | 47 | $this->action = [$controllerClass, $method->name]; 48 | } 49 | 50 | public function relativeUri(): string 51 | { 52 | /** @var ReflectionParameter $modelParameter */ 53 | $modelParameter = collect($this->method->getParameters())->first(function (ReflectionParameter $parameter) { 54 | $type = $parameter->getType(); 55 | 56 | return $type instanceof ReflectionNamedType && is_a($type->getName(), Model::class, true); 57 | }); 58 | 59 | $uri = ''; 60 | 61 | if (! in_array($this->method->getName(), $this->commonControllerMethodNames())) { 62 | $uri = Str::kebab($this->method->getName()); 63 | } 64 | 65 | /** @phpstan-ignore-next-line */ 66 | if ($modelParameter) { 67 | if ($uri !== '') { 68 | $uri .= '/'; 69 | } 70 | 71 | $uri .= "{{$modelParameter->getName()}}"; 72 | } 73 | 74 | return $uri; 75 | } 76 | 77 | public function addWhere(Where $whereAttribute): self 78 | { 79 | $this->wheres[$whereAttribute->param] = $whereAttribute->constraint; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @param array|class-string $middleware 86 | * 87 | * @return self 88 | */ 89 | public function addMiddleware(array|string $middleware): self 90 | { 91 | $middleware = Arr::wrap($middleware); 92 | 93 | $allMiddleware = array_merge($middleware, $this->middleware); 94 | 95 | $this->middleware = array_unique($allMiddleware); 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * @return array 102 | */ 103 | protected function discoverHttpMethods(): array 104 | { 105 | return match ($this->method->name) { 106 | 'index', 'create', 'show', 'edit' => ['GET'], 107 | 'store' => ['POST'], 108 | 'update' => ['PUT', 'PATCH'], 109 | 'destroy', 'delete' => ['DELETE'], 110 | default => ['GET'], 111 | }; 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | protected function commonControllerMethodNames(): array 118 | { 119 | return [ 120 | 'index', '__invoke', 'get', 121 | 'show', 'store', 'update', 122 | 'destroy', 'delete', 123 | ]; 124 | } 125 | 126 | /** 127 | * @return string|array{string, string} 128 | */ 129 | public function action(): string|array 130 | { 131 | return $this->action[1] === '__invoke' 132 | ? $this->action[0] 133 | : $this->action; 134 | } 135 | 136 | public function getRouteAttribute(): ?Route 137 | { 138 | return $this->getAttribute(Route::class); 139 | } 140 | 141 | /** 142 | * @template TDiscoveryAttribute of DiscoveryAttribute 143 | * 144 | * @param class-string $attributeClass 145 | * 146 | * @return ?TDiscoveryAttribute 147 | */ 148 | public function getAttribute(string $attributeClass): ?DiscoveryAttribute 149 | { 150 | $attributes = $this->method->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF); 151 | 152 | if (! count($attributes)) { 153 | return null; 154 | } 155 | 156 | return $attributes[0]->newInstance(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/PendingRoutes/PendingRouteFactory.php: -------------------------------------------------------------------------------- 1 | fullyQualifiedClassNameFromFile($fileInfo); 22 | 23 | if (! class_exists($fullyQualifiedClassName)) { 24 | return null; 25 | } 26 | 27 | $class = new ReflectionClass($fullyQualifiedClassName); 28 | 29 | if ($class->isAbstract()) { 30 | return null; 31 | } 32 | 33 | $actions = collect($class->getMethods()) 34 | ->filter(function (ReflectionMethod $method) { 35 | return $method->isPublic(); 36 | }) 37 | ->map(function (ReflectionMethod $method) use ($fullyQualifiedClassName) { 38 | return new PendingRouteAction($method, $fullyQualifiedClassName); 39 | }); 40 | 41 | $uri = $this->discoverUri($class); 42 | 43 | return new PendingRoute($fileInfo, $class, $uri, $fullyQualifiedClassName, $actions); 44 | } 45 | 46 | protected function discoverUri(ReflectionClass $class): string 47 | { 48 | $parts = Str::of((string) $class->getFileName()) 49 | ->after(str_replace('/', DIRECTORY_SEPARATOR, $this->registeringDirectory)) 50 | ->beforeLast('Controller') 51 | ->explode(DIRECTORY_SEPARATOR); 52 | 53 | return collect($parts) 54 | ->filter() 55 | ->reject(function (string $part) { 56 | return strtolower($part) === 'index'; 57 | }) 58 | ->map(fn (string $part) => Str::of($part)->kebab()) 59 | ->implode('/'); 60 | } 61 | 62 | protected function fullyQualifiedClassNameFromFile(SplFileInfo $file): string 63 | { 64 | $class = trim(Str::replaceFirst($this->basePath, '', (string)$file->getRealPath()), DIRECTORY_SEPARATOR); 65 | 66 | $class = str_replace( 67 | [DIRECTORY_SEPARATOR, 'App\\'], 68 | ['\\', app()->getNamespace()], 69 | ucfirst(Str::replaceLast('.php', '', $class)) 70 | ); 71 | 72 | return $this->rootNamespace . $class; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/RouteDiscoveryServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-route-discovery') 17 | ->hasConfigFile(); 18 | } 19 | 20 | public function packageRegistered(): void 21 | { 22 | if ($this->app->routesAreCached()) { 23 | return; 24 | } 25 | 26 | $this 27 | ->registerRoutesForControllers() 28 | ->registerRoutesForViews(); 29 | } 30 | 31 | public function registerRoutesForControllers(): self 32 | { 33 | collect(config('route-discovery.discover_controllers_in_directory')) 34 | ->each( 35 | fn (string $directory) => Discover::controllers()->in($directory) 36 | ); 37 | 38 | return $this; 39 | } 40 | 41 | public function registerRoutesForViews(): self 42 | { 43 | collect(config('route-discovery.discover_views_in_directory')) 44 | ->each(function (array|string $directories, int|string $prefix) { 45 | if (is_numeric($prefix)) { 46 | $prefix = ''; 47 | } 48 | 49 | $directories = Arr::wrap($directories); 50 | 51 | foreach ($directories as $directory) { 52 | Route::prefix($prefix)->group(function () use ($directory) { 53 | Discover::views()->in($directory); 54 | }); 55 | } 56 | }); 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/RouteRegistrar.php: -------------------------------------------------------------------------------- 1 | router = $router; 27 | 28 | $this->basePath = app()->path(); 29 | } 30 | 31 | public function useBasePath(string $basePath): self 32 | { 33 | $this->basePath = $basePath; 34 | 35 | return $this; 36 | } 37 | 38 | public function useRootNamespace(string $rootNamespace): self 39 | { 40 | $this->rootNamespace = $rootNamespace; 41 | 42 | return $this; 43 | } 44 | 45 | public function registerDirectory(string $directory): void 46 | { 47 | $this->registeringDirectory = $directory; 48 | 49 | $pendingRoutes = $this->convertToPendingRoutes($directory); 50 | 51 | $pendingRoutes = $this->transformPendingRoutes($pendingRoutes); 52 | 53 | $this->registerRoutes($pendingRoutes); 54 | } 55 | 56 | /** 57 | * @param string $directory 58 | * 59 | * @return Collection<\Spatie\RouteDiscovery\PendingRoutes\PendingRoute> 60 | */ 61 | protected function convertToPendingRoutes(string $directory): Collection 62 | { 63 | $files = (new Finder())->files()->depth(0)->name('*.php')->in($directory); 64 | 65 | $pendingRouteFactory = new PendingRouteFactory( 66 | $this->basePath, 67 | $this->rootNamespace, 68 | $this->registeringDirectory, 69 | ); 70 | 71 | $pendingRoutes = collect($files) 72 | ->map(fn (SplFileInfo $file) => $pendingRouteFactory->make($file)) 73 | ->filter(); 74 | 75 | collect((new Finder())->directories()->depth(0)->in($directory)) 76 | ->flatMap(function (SplFileInfo $subDirectory) { 77 | return $this->convertToPendingRoutes($subDirectory); 78 | }) 79 | ->filter() 80 | /** @phpstan-ignore-next-line */ 81 | ->each(fn (PendingRoute $pendingRoute) => $pendingRoutes->push($pendingRoute)); 82 | 83 | return $pendingRoutes; 84 | } 85 | 86 | /** 87 | * @param Collection $pendingRoutes 88 | * 89 | * @return Collection $pendingRoutes 90 | */ 91 | protected function transformPendingRoutes(Collection $pendingRoutes): Collection 92 | { 93 | /** @var array> $transformers */ 94 | $transformers = config('route-discovery.pending_route_transformers', Config::defaultRouteTransformers()); 95 | 96 | /** @var Collection */ 97 | $transformers = collect($transformers) 98 | ->map(fn (string $transformerClass): PendingRouteTransformer => app($transformerClass)); 99 | 100 | foreach ($transformers as $transformer) { 101 | $pendingRoutes = $transformer->transform($pendingRoutes); 102 | } 103 | 104 | return $pendingRoutes; 105 | } 106 | 107 | protected function registerRoutes(Collection $pendingRoutes): void 108 | { 109 | $pendingRoutes->each(function (PendingRoute $pendingRoute) { 110 | $pendingRoute->actions->each(function (PendingRouteAction $action) { 111 | $route = $this->router->addRoute($action->methods, $action->uri, $action->action()); 112 | 113 | $route->middleware($action->middleware); 114 | 115 | /** @phpstan-ignore-next-line */ 116 | $route->name($action->name); 117 | 118 | if (count($action->wheres)) { 119 | $route->setWheres($action->wheres); 120 | } 121 | 122 | if ($action->domain) { 123 | $route->domain($action->domain); 124 | } 125 | }); 126 | }); 127 | } 128 | } 129 | --------------------------------------------------------------------------------