innocenzi / discovery-for-laravel
Discover code anywhere in your Laravel codebase without relying on path conventions
Package info
github.com/innocenzi/discovery-for-laravel
pkg:composer/innocenzi/discovery-for-laravel
Fund package maintenance!
Requires
- php: ~8.5
- illuminate/contracts: ^13.2
- tempest/discovery: ^3.9
Requires (Dev)
- carthage-software/mago: ^1.17
- hybridly/laravel: 0.10.0-beta.14
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.5.15
README
Automatically locate controller actions, console commands, configuration files and other components of your application without relying on filesystem-based conventions nor manual configuration.
composer require innocenzi/discovery-for-laravel
What it does
If you ever wanted to architecture your application however you want, for instance by organizing your code in modules or in vertical slices, you probably had to give up on some of Laravel's conveniences, such as automatic Artisan command registration.
This package brings Tempest's discovery into Laravel applications, which allows for:
- Registering routes from controller method attributes,
- Registering artisan commands by discovering command classes,
- Loading modular config files ending with
.config.php, - Registering container bindings through dedicated initializer classes,
- Registering global middleware using attributes.
Of course, each of these features can easily be disabled individually if you don't want them.
Installation
Install via Composer:
composer require innocenzi/discovery-for-laravel # Optional, if you need to customize behavior php artisan vendor:publish --provider="Discovery\DiscoveryServiceProvider"
That's it, you don't need to do anything else. You can read what the built-in discoveries do to understand how you can benefit from them.
Production
To avoid any performance overhead in production, it is important to generate the discovery cache. The cache will store all discovered items, preventing the need to scan the filesystem.
You may do so by adding php artisan discovery:generate to your deployment script, typically before the optimize step.
php artisan discovery:generate php artisan optimize
Discoveries
By default, we provide the ability to discover artisan commands, event handlers, controller actions, configuration files, schedules, global middleware and dependency initializers.
Of course, each of these can be disabled individually by updating the skip_classes option in config/discovery.php.
You can also create your own discoveries if you have another use case for it.
Artisan commands
Any class extending Illuminate\Console\Command is discovered and registered, no matter where it's placed. It does not have to be in the app/Console/Commands directory.
namespace Module\Reports; use Illuminate\Console\Attributes\Signature; use Illuminate\Console\Command; #[Signature('reports:prune')] final class PruneReportsCommand extends Command { public function handle(): int { // ... return self::SUCCESS; } }
Event handlers
Methods annotated with the Discovery\Events\EventHandler attribute will be discovered and registered as event listeners. The method must accept a single parameter, which is the event class to listen to:
namespace Modules\Billing; use Discovery\Events\EventHandler; final class InvoiceEventHandler { #[EventHandler] public function onInvoicePaid(InvoicePaid $event): void { // ... } }
Scheduling
Methods annotated with Discovery\Scheduling\Schedule are discovered and registered as scheduled tasks.
You can use either one of the built-in Discovery\Scheduling\Every values or a raw cron expression:
namespace Modules\Billing; use Discovery\Scheduling\Every; use Discovery\Scheduling\Schedule; final class BillingScheduler { #[Schedule(Every::SECOND)] public function billEverySecond(): void { // ... } #[Schedule(Every::DAY, time: '13:30')] public function billDaily(): void { // Runs every day at 13:30. } }
Routing
One of the best features of discovery is the ability to register routes from attributes on controller methods.
This is something that Spatie implemented when PHP attributes were initially released, but for some reason, this practice was never really popular in the Laravel ecosystem.
If you want to see the routes your application has, you can use php artisan route:list as usual. With attribute-based routes, you no longer need to go back-and-forth between your routes/web.php and your controllers—and for large applications, you no longer have to deal with gigantic route files.
This example declares two routes: GET /billing/invoices and POST /billing/invoices:
namespace Module\Billing; use Discovery\Routing\Api; use Discovery\Routing\Get; use Discovery\Routing\Post; use Discovery\Routing\Prefix; use Discovery\Routing\Web; #[Prefix(name: 'billing', uri: 'billing')] final class InvoiceController { #[Get(uri: 'invoices', name: 'index'), Web] public function index() { // ... } #[Post(uri: 'invoices', name: 'store'), Web] public function store() { // ... } }
Decorators
The #[Prefix] and #[Web] attributes in the example above are route decorators—another concept borrowed from Tempest.
A decorator is a class that modifies the underlying route definition in some way. In this case, the Web decorator pushes the route into the web middleware group, and the Prefix decorator adds a URI prefix and a name prefix to all routes in the controller.
They are a great alternative to the group method of Laravel's router. This is how the Web one is implemented:
use Attribute; #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class Web implements RouteDecorator { public function decorate(Route $route): Route { $route->middleware[] = 'web'; return $route; } }
Configuration files
Any file ending in <name>.config.php will be registered under the <name> namespace. This is useful to co-locate configuration files with the components they configure.
// src/Modules/Billing/billing.config.php return [ 'enabled' => true, 'retry_attempts' => 3, ];
The example above becomes available as config('billing.enabled').
Dependency initializers
This package provides dependency initializers, a concept borrowed from Tempest. Initializers are simple classes responsible for initializing and configuring a specific dependency.
In a typical Laravel application, this is done in service providers. Using this package, you can create a class implementing Discovery\Container\Initializer anywhere (preferably, near related code), and it will be automatically discovered and registered in the container.
namespace Modules\Strip; use Discovery\Container\Initializer; use Illuminate\Container\Attributes\Singleton; use Psr\Container\ContainerInterface; #[Singleton] final class StripeClientInitializer implements Initializer { public function initialize(ContainerInterface $container): StripeClient { return new StripeClient( key: config('services.stripe.key'), ); } }
To register the initialized service as a singleton, add the built-in Illuminate\Container\Attributes\Singleton attribute on the initializer class.
Global middleware
Classes annotated with Discovery\Routing\Middleware are pushed into the selected middleware group. Again, this class may be located anywhere in the application, and it will be automatically discovered and registered.
namespace Infrastructure; use Discovery\Routing\Middleware; #[Middleware('api')] final class TraceRequestId { public function __invoke(mixed $request, \Closure $next): mixed { // ... return $next($request); } }
Troubleshooting
- Discovery relies on reflection and filesystem scanning. If you don't want some files to be loaded at discovery time, you may adapt
skip_matchesinconfig/discovery.phpto exclude them.