edalzell/laravel-features

Self-contained feature modules for Laravel with auto-registration of routes, migrations, views, events, and seeders

Maintainers

Package info

github.com/edalzell/laravel-features

pkg:composer/edalzell/laravel-features

Transparency log

Statistics

Installs: 349

Dependents: 0

Suggesters: 0

Stars: 20

Open Issues: 0

v0.7.0 2026-08-08 05:15 UTC

This package is auto-updated.

Last update: 2026-08-08 05:15:26 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

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.