Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Feature Laravel Package

francescomalatesta/laravel-feature

Laravel Feature helps manage feature flags in Laravel apps. Define features, toggle them globally or per user, and check them in code to enable or disable functionality without new deployments. Blade directives and middleware support gradual rollouts and experiments.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its assets:

composer require francescomalatesta/laravel-feature
php artisan vendor:publish --provider="Francescomalatesta\LaravelFeature\LaravelFeatureServiceProvider"
php artisan migrate

Look at three places first:

  1. config/feature.php — table name and default behaviour for unknown flags. This is the file you will actually touch when behaviour surprises you.
  2. The published migration — a features table with name and enabled. Everything the package does is just reads and writes against that table.
  3. The Feature facade — the entire public surface lives there (isEnabled, enable, disable, and feature creation/removal helpers).

First useful use case: wrap a risky new code path.

use Feature;

if (Feature::isEnabled('new-checkout')) {
    return $this->newCheckout($request);
}

return $this->legacyCheckout($request);

Then toggle it without deploying code:

php artisan feature:enable new-checkout
php artisan feature:disable new-checkout
php artisan feature:list

And in Blade, for UI that should only render when a flag is on:

@feature('new-dashboard')
    @include('dashboard.v2')
@endfeature

That is the whole package. Everything below is about using it without creating operational debt.

Implementation Patterns

Check once per request, then pass the boolean down. A feature check is a DB read; looping over it is an N+1 you will not notice until it hurts.

class ReportController extends Controller
{
    public function index(Request $request)
    {
        $v2 = Feature::isEnabled('reports.v2');

        return $v2
            ? view('reports.v2', $this->data($request))
            : view('reports.v1', $this->data($request));
    }
}

If you need it in many layers, resolve it once in a middleware or service provider and bind it into the container as a small value object rather than calling the facade from everywhere.

Use constants or an enum for flag names. Typos on an unknown flag are silent (you get the default), which is the single most common source of “the flag is on but nothing changed.”

final class Flags
{
    public const NEW_CHECKOUT = 'checkout.v2';
    public const REPORTS_V2   = 'reports.v2';
}

Wrap the facade behind your own thin service. You get request-level memoization, a single place to add logging, and an easy migration path if you outgrow the package later.

class Flags
{
    private array $cache = [];

    public function on(string $name): bool
    {
        return $this->cache[$name] ??= Feature::isEnabled($name);
    }
}

Seed flags as part of environment setup. Local machines and staging databases should not depend on a human running feature:enable after a fresh migrate:fresh. A seeder that asserts the expected baseline is cheap and removes a whole class of “works on my machine.”

// database/seeders/FeatureSeeder.php
Feature::create('checkout.v2', false);
Feature::enable('reports.v2');

Tie flags to deploy scripts, not to manual SSH sessions. In your release pipeline, after migrate:

php artisan feature:enable checkout.v2

This creates an audit trail in CI and means a rollback is an explicit step, not an act of memory.

Combine with gates for authorization, not as a substitute for it. A feature flag says “this code exists”; a gate says “this user may use it.” Keep them separate and compose them.

Gate::define('use-reports-v2', fn (User $user) =>
    Feature::isEnabled(Flags::REPORTS_V2) && $user->isAdmin()
);

Roll out by percentage on top of the flag. The package has no percentage targeting. Combine the global flag with a stable per-user bucket so a user never flips back and forth.

public function onForUser(string $flag, User $user, int $percent): bool
{
    if (! Feature::isEnabled($flag)) {
        return false;
    }

    return (crc32($flag . ':' . $user->id) % 100) < $percent;
}

Test flags explicitly, in every test that depends on them. Never rely on leftover DB state. Set the flag in setUp() (or in the specific test), and disable it in tearDown() so a failure cannot poison the next test.

protected function setUp(): void
{
    parent::setUp();
    Feature::enable(Flags::NEW_CHECKOUT);
}

Use RefreshDatabase and, where possible, write two tests per flag: one with the flag on and one with it off. The “off” test is the one that actually protects rollback.

Remove the flag and the dead branch on a schedule. A flag that has been on for two releases is technical debt. Track creation dates in a README or a ticket, and delete the losing branch as part of the cleanup PR — not “later.”

Gotchas and Tips

Unknown flags resolve to the default, not to an error. If you mistype checkout.v2 as checkout-v2, you will not get an exception; you will get whatever config/feature.php specifies. When debugging “flag is on but code not taken,” check spelling and the exact row in the features table before anything else.

Enable/disable expects the row to exist. The artisan commands operate on an existing feature; creating and toggling are separate operations. In a pipeline, create the flag in a seeder or migration step before you try to enable it, otherwise the command fails and takes your deploy down with it.

There is a real DB read per check. No caching layer is provided out of the box. In a loop, a Blade partial included many times, or a queued job batch, hoist the check. If you need it, add a request-scoped cache in your own wrapper and flush it in tests.

Flags are global — there is no per-user or per-tenant storage. The table has one row per flag. Anything resembling targeting (per-plan, per-cohort, per-account) is your responsibility on top of the boolean. Do not try to encode targeting into flag names; you will end up with checkout.v2.enterprise.eu and regret it.

In tests, state leaks silently. Because checks are pure DB reads, a flag enabled in one test stays enabled for the next unless you reset it. Prefer a trait that disables all flags in tearDown(), or wrap the flag setup per test rather than in a base class shared across the suite.

Config changes need config:cache cleared. If you edit config/feature.php (table name, defaults) and see stale behaviour, run php artisan config:clear. In production this is almost always the cause of “the default changed but the app didn't.”

Table name must match the config. If you have multiple databases or a non-standard prefix, verify the migration and config/feature.php agree. A mismatch surfaces as a “table not found” query error pointing at the package's internals, which is easy to misread as a package bug.

Blade directive changes require recompiled views. If you add or change custom handling around the @feature directive, clear compiled views (php artisan view:clear). Otherwise you will be debugging the old compiled PHP.

Never put irreversible side effects behind a flag without a guard. Sending email, charging cards, dispatching jobs, writing to external systems — a flag flip mid-flight can duplicate work or strand records. Gate the read path with the flag; keep the write path unconditional or idempotent.

Enabling in production requires DB write access from your deploy environment. If your pipeline runs migrations with a restricted user, confirm the same credentials can write to features. A flag toggle is a data change, and treating it as a config change is how teams get surprised at 2am.

Staging and production diverge unless you seed them. Because the state lives in the database, not in code, a flag disabled in staging can be enabled in production and vice versa. Document the expected matrix, or better, drive it from your release script so it is reproducible.

Extension points are limited, so extend at the edges. The package is small and the surface is the facade. If you need caching, percentage rollouts, per-user targeting, or an admin UI, wrap the facade in your own service and build up — do not fork the package. For an admin panel, a simple resource controller against the features table plus Feature::enable() / Feature::disable() is enough, and it keeps the package as the single source of truth.

Watch Laravel version compatibility. This is an older, low-traffic package. On recent Laravel versions, confirm auto-discovery and the facade alias still register correctly; if the alias does not appear in config/app.php, add it manually and clear config:cache and bootstrap/cache.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin