edalzell/laravel-features
A lightweight feature flag package for Laravel. Define named features in config, toggle them per environment or user, and check availability anywhere in your app with a simple API—ideal for gradual rollouts, beta access, and shipping code without new deploys.
This package lets you carve a Laravel application into self-contained "features" — each feature is a class that owns its own routes, commands, translations, migrations, seeders, and dependencies, and the package wires them into the framework for you. As of v1.2.0 you can also hook extensions into a feature's boot, which is the first supported way to add behaviour to a feature without subclassing it.
composer require edalzell/laravel-features
php artisan vendor:publish --tag=features-config
app/Features/Billing/BillingFeature.php or similar, depending on the configured namespace).config/features.php (or let auto-discovery pick it up if enabled).php artisan about and any feature-listing artisan command the package exposes, then php artisan route:list to confirm your feature's routes appear.Where to look first: the feature base class/stub and the published config file. The config tells you which directories are scanned, how namespaces are derived, whether discovery is on, and which disk the package uses. The base class tells you the exact method names the package will call (routes(), commands(), translations(), migrations(), seeders(), dependencies()) and — new in v1.2.0 — where the boot-extension hook lives.
First use case: pick one existing slab of your app — a billing module with a handful of routes, a console command, and a few lang files — and move it into a feature. That exercises routes, commands, and translations in one pass, which is the fastest way to validate your config before you migrate anything else.
Upgrading from v1.1.x: v1.2.0 is additive, but the maintainer's review pass touched the registry, discovery, route groups, and disk handling. Re-run your route list and your seeders after upgrading and delete any workarounds you'd added for those areas before assuming they're still needed.
Feature as the single entry point. Treat the feature class as a miniature service provider. Everything the feature needs the app to know about is declared in one place:
class BillingFeature extends Feature
{
public function commands(): array
{
return [SyncInvoicesCommand::class, PruneDraftsCommand::class];
}
public function translations(): array
{
return ['billing' => __DIR__.'/resources/lang'];
}
public function routes(): array
{
return [
'web' => [
'prefix' => 'billing',
'middleware' => ['web', 'auth'],
'routes' => __DIR__.'/routes/web.php',
],
];
}
}
Boot extensions (new in v1.2.0). The headline feature of this release: you can hook extensions into a feature's boot rather than only being able to add behaviour by editing or subclassing the feature class. The pattern is to keep the feature itself declarative — routes, commands, translations — and push cross-cutting boot-time behaviour (extra view composers, model observers, gates, middleware tweaks) into a small extension class registered from the feature. Check the base class for the exact hook signature; the important day-to-day win is that a package or a sibling module can contribute boot behaviour to a feature without you forking it.
// Sketch — confirm the exact hook name/signature against the base class.
class BillingFeature extends Feature
{
public function extensions(): array
{
return [RegisterBillingGates::class, AttachInvoiceObserver::class];
}
}
Treat extensions as the preferred seam for anything that would otherwise be a boot() body full of if (class_exists(...)) checks.
Routes as closures (new in v1.1.0). A route group can point at a file or, since v1.1.0, inline the definition. Handy for small features or a couple of endpoints that don't justify their own file:
public function routes(): array
{
return [
'web' => [
'prefix' => 'billing',
'middleware' => ['web'],
'routes' => function (Router $router) {
$router->get('/invoices', [InvoiceController::class, 'index']);
$router->post('/invoices/{invoice}/pay', [InvoiceController::class, 'pay']);
},
],
];
}
Use files for anything sizeable — closures are convenient but they're not route-cache friendly in the same way a file is, and they hide routes from a quick grep. Route-group handling was reworked in v1.2.0, so if you'd built a workaround around group prefixes or middleware, re-check it before keeping it.
Translations (new in v1.1.0). Declare one or more lang directories and reference them with the standard Laravel namespace syntax: __('billing::messages.paid'). Keep the key in the array aligned with the namespace you want to expose, and keep each feature's namespace distinct to avoid collisions.
Console commands (new in v1.1.0). Return command class names from commands(); Laravel resolves them from the container, so constructor injection works as normal. This is the cleanest place for feature-scoped scheduled jobs and maintenance commands.
Seeder discovery with ::class (fixed in v1.1.0). Seeders that reference another seeder via OtherSeeder::class inside $this->call([...]) are now discovered correctly. If you'd previously worked around this by using string class names, you can restore the ::class form — it's more refactor-safe.
Dependencies. Declare feature-to-feature dependencies in the feature itself (see the package docs on where these go — v1.1.0 clarified the location). Use this rather than relying on incidental load order; it makes it obvious when feature B needs feature A's migrations to have run first.
Day-to-day workflow. Keep the boundary honest: if a controller, model, or command only serves one feature, it belongs inside that feature's directory. Shared primitives go in app/Support or similar, not in a feature — otherwise you'll end up with features importing each other's internals, and the dependency declaration becomes a lie.
php artisan route:clear && php artisan route:cache and confirm the app still routes. A feature that works in dev and 404s in production is almost always a stale route cache — and because route-group handling changed in v1.2.0, clear the cache immediately after upgrading before you debug anything else.config/filesystems.php. A missing or misnamed disk surfaces as an odd "nothing loaded" failure rather than a clear error, so check it early when debugging.messages or admin.$signature will produce confusing behaviour — one shadows the other. Namespace your signatures (billing:sync-invoices).::class discovery now works, but it does not guarantee ordering across features. Anything order-sensitive should be expressed through the dependency declaration, not through hoping discovery runs in a particular sequence.register() that depend on another feature having booted; use boot() — or a boot extension — plus the dependency list.config/features.php won't apply under config:cache. Remember php artisan config:clear when adding a feature or changing discovery paths — this trips people up constantly.composer dump-autoload after moving classes into a new namespace. Nine times out of ten it's the autoloader.How can I help you explore Laravel packages today?