edalzell / laravel-features
Self-contained feature modules for Laravel with auto-registration of routes, migrations, views, events, and seeders
Requires
- php: ^8.3
- composer/composer: ^2.8
- edalzell/laravel-features-plugin: ^0.2
- illuminate/contracts: ^12.0 || ^13.0
Requires (Dev)
- brain/monkey: ^2.7
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.40
README
Add self-contained features to your Laravel app or package, including all resources/routes/etc.
.
└── app/
...
└── features/
│ └── MyGreatFeature/
│ ├── config/
│ │ └── my-great-feature.php
│ ├── database/
│ │ ├── factories
│ │ ├── migrations
│ │ └── seeders
│ ├── resources
│ ├── routes
│ └── src/
│ ├── Models
│ ├── ...
│ └── ServiceProvider.php
Each feature behaves like a mini Laravel app. The following are auto-registered and booted:
| Phase | What |
|---|---|
| Register | Config, Migrations, Routes, Seeders, Views |
| Boot | Config publishing, Listeners, Policies, Seeders |
Route groups
A feature's route files are put in a route group, chosen by filename, the same way the framework does it for an application's own route files:
| File | Group |
|---|---|
routes/web.php |
web middleware |
routes/api.php |
api middleware, api prefix |
| anything else | no middleware group, no prefix |
Without this, loadRoutesFrom() is a bare require — a feature's routes/web.php would get no session or CSRF, and routes/api.php no throttling and no prefix.
Publish the config to change a group for all features at once — to add an API version, for example:
php artisan vendor:publish --tag=features-config
// config/features.php 'route_groups' => [ 'web' => ['middleware' => 'web'], 'api' => ['middleware' => 'api', 'prefix' => 'api/v1', 'as' => 'api.v1.'], ],
Or override routeGroups() on one feature's service provider:
protected function routeGroups(): array { return ['api' => ['middleware' => ['api', 'auth:sanctum'], 'prefix' => 'api/internal']]; }
Set an entry to null, or remove it, and that file gets no middleware group and no prefix — only what it declares itself.
Installation
You can install the package via composer:
composer require edalzell/laravel-features
Usage
To add a new feature in your app:
php artisan make:feature MyGreatFeature
To add feature to a package:
php artisan make:feature MyGreatFeature the-dev/my-package
This creates a ServiceProvider that extends FeatureServiceProvider — everything is auto-registered with no further code required.
Option 1: Extend FeatureServiceProvider
The zero-friction path. Your provider gets boot() and register() for free:
class MyGreatFeatureServiceProvider extends FeatureServiceProvider { // nothing needed — everything is auto-registered }
Override any of these protected methods to customise behaviour:
protected function configFileName(): string // default: kebab-cased feature name protected function configGroup(): string // default: '' (no subdirectory) protected function configPublishHandle(): string // default: kebab-cased feature name protected function featuresPath(): string // default: derived from the provider's own location protected function routeGroups(): array // default: config('features.route_groups')
Option 2: Standalone Features object
When your provider already extends another class, wire up Features directly:
use Edalzell\Features\Features; class MyServiceProvider extends SomeOtherProvider { private Features $features; public function __construct(Application $app) { parent::__construct($app); $this->features = (new Features($this)) ->path($this->featuresPath()) ->name($this->name()) ->configFileName($this->configFileName()) ->configGroup($this->configGroup()) ->configPublishHandle($this->configPublishHandle()); } public function boot(): void { $this->features->bootFeature(); } public function register(): void { $this->features->registerFeature(); } }
Features derives the path, namespace, and app from your provider via reflection. You only need to call the fluent setters when overriding the defaults.
Auto-discovering features
Use the HasFeatures trait in any service provider to automatically register all features from a directory. In your app, add it to AppServiceProvider:
use Edalzell\Features\Concerns\HasFeatures; class AppServiceProvider extends ServiceProvider { use HasFeatures; public function register(): void { $this->registerFeatures(app_path('../features'), 'App\\Features'); } }
For a package, add it to your package's main service provider:
use Edalzell\Features\Concerns\HasFeatures; class MyPackageServiceProvider extends ServiceProvider { use HasFeatures; public function register(): void { $this->registerFeatures(); } }
In a package, registerFeatures() defaults to looking in <package-root>/features/ and registering providers under YourPackage\Features\FeatureName\ServiceProvider. Pass explicit arguments to override either default:
$this->registerFeatures('/path/to/features', 'My\\Namespace\\Features');
Features outside the app
A feature works from anywhere — the app, a package, or a directory outside the app entirely, such as a monorepo where two apps share one set of features:
gym/
├── apps/
│ ├── server/
│ └── mobile/
└── shared/
└── features/
└── Scheduling/
Two things need wiring, in each app that uses them.
Autoloading — declare the directory and its namespace in composer.json, and the plugin generates PSR-4 entries for every feature it finds:
"extra": { "laravel-features": { "paths": { "../../shared/features": "Shared\\Features" } } }
Registration — point registerFeatures() at the same directory:
$this->registerFeatures(base_path('../../shared/features'), 'Shared\\Features');
The app's own features/ directory is still scanned, so app-local and shared features can coexist.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
License
The MIT License (MIT). Please see License File for more information.