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 Flags Laravel Package

ryangjchandler/laravel-feature-flags

A lightweight feature flag package for Laravel. Define features, check whether they are active from your code, and conditionally render UI with Blade directives. A simple API for toggling features and rolling out changes without redeploying.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with composer require ryangjchandler/laravel-feature-flags. Package discovery wires up the service provider and the Feature facade alias automatically; publish the config only if you need to change defaults (php artisan vendor:publish --tag=feature-flags-config).
  • Look first at src/Feature.php and the manager class behind the Feature facade. This package is small — the entire contract is essentially "extend Feature, implement one method that returns a bool, ask the facade about it." Reading those two files (plus the make:feature stub) takes ten minutes and tells you more than the README.
  • First use case — a flag for a single screen or endpoint:
php artisan make:feature BetaDashboard
namespace App\Features;

use RyanChandler\FeatureFlags\Feature;

class BetaDashboard extends Feature
{
    public function resolve(): bool
    {
        return $this->scope?->isBetaTester() ?? false;
    }
}
use RyanChandler\FeatureFlags\Facades\Feature;

if (Feature::active(BetaDashboard::class)) {
    // new dashboard
}
  • In Blade the package registers a directive, so the same check reads as:
@feature(\App\Features\BetaDashboard::class)
    <x-new-dashboard />
@else
    <x-old-dashboard />
@endfeature

Start with one flag, ship it, then grow. Because flags are classes, the cost of adding the tenth one is near zero.

Implementation Patterns

One class per flag, never boolean config sprawl. The whole value of this package over config('features.x') is that a flag has a name, a place, an owner, and a reviewable diff. Keep resolve() to a few lines and derive the answer from data you already have (role, plan, cohort, tenant settings).

Scope explicitly, per request. Pass the user or tenant in rather than reaching for auth() inside resolve():

Feature::for($request->user())->active(BetaDashboard::class);

This keeps the flag testable, keeps it safe in queued jobs, and makes "what does this flag depend on?" answerable by looking at the call site.

Deterministic percentage rollouts. Stable bucketing is the one piece of logic worth centralising, because a bad hash flaps users in and out:

public function resolve(): bool
{
    return crc32($this->scope->id . 'beta-dashboard') % 100 < 10;
}

Include the flag's own name in the hash input so two 10% flags don't select the same 10% of users.

Bridge into Laravel's authorisation layer so the rest of the app doesn't import the facade everywhere:

Gate::define('see-beta-dashboard', fn (User $u) =>
    Feature::for($u)->active(BetaDashboard::class)
);

Then @can('see-beta-dashboard'), $this->authorize(...), and policies all work for free.

Route-level gating with the middleware. Register the middleware alias in bootstrap/app.php (or Http/Kernel.php on older versions) and attach it to a route group:

Route::middleware('feature:'.BetaDashboard::class)->group(function () {
    Route::get('/dashboard', NewDashboardController::class);
});

Use this for whole surfaces; use Feature::active() inline for small in-page variations. Don't put the middleware on everything — it's a coarse tool.

Compose flags rather than duplicating logic. A flag may consult another flag, but keep the graph a DAG and shallow:

public function resolve(): bool
{
    return Feature::for($this->scope)->active(BetaProgramme::class)
        && $this->scope->hasBillingAccount();
}

Cycles here are silent infinite recursion, so sketch the graph before you hit five flags.

Testing. Never write assertions that depend on the current state of a flag's internals. Instead, resolve the flag against a fixture user and assert the flag's decision, and in feature tests swap in a stub:

$this->app->bind(BetaDashboard::class, fn () => new class extends BetaDashboard {
    public function resolve(): bool { return true; }
});

If the manager caches instances, clear the resolved cache between tests (see below) or the stub won't take effect.

Gradual removal is part of the pattern. Once a flag is at 100%, delete the class, the @feature branch, and the old code path in the same PR. Flags that outlive their rollout become permanent if statements with a fancy name.

Gotchas and Tips

  • Everything is a class-string. @feature('BetaDashboard'), Feature::active('beta-dashboard'), or a class name with the wrong namespace all fail the same way: the flag reads as inactive (or throws a container error). Import the class or define a const so a rename is a static-analysis error rather than a silent off-switch.
  • The facade caches resolved features for the request. That's what makes per-request memoisation cheap, but it also means a long-running worker (Octane, queue listener) can carry stale answers across requests. If you deploy under Octane, confirm the manager is flushed per request, and definitely clear it in test tearDown when you swap in stubs.
  • Feature::for() mutates the shared manager. Chaining for() and forgetting to reset leaves the next check scoped to the wrong user. Prefer passing scope at each call site, or wrap in try/finally in loops over users.
  • resolve() runs often — keep it cheap and side-effect free. No queries inside it if you can help it, no logging that matters, absolutely no writes. If a flag needs a DB hit, load the data once and stash it on the scope object.
  • There is no persistence layer, by design. You cannot flip a boolean in a database at 2am; the "opinionated" trade-off is that flag state is code. If product needs runtime toggles, either read config()/env inside resolve() (accepting a deploy-per-change is not always acceptable) or move that specific flag to Laravel Pennant, which is first-party and persists to a store.
  • Config quirks around middleware and Blade. The middleware alias is not registered for you — add it to the kernel/bootstrap or feature:... throws a "target class does not exist"-style binding error at route resolution, not at boot. Likewise, if the Blade directive name clashes with another package, whichever registers last wins; grep Blade::directive if @feature starts behaving oddly.
  • Package health is the biggest gotcha. Zero dependents and no recent activity means you are the maintainer. Pin an exact version, vendor-review the source before upgrading, and treat a Laravel major bump as "check this package compiles" rather than assuming. For a new project on Laravel 10+, Laravel Pennant is the lower-risk choice with a very similar class-per-flag mental model; this package remains a fine fit for an existing app where the facade-based API is already in use.
  • Debugging tip: when a flag is unexpectedly false, drop a temporary dump([$this->scope?->id, $this->resolve()]) inside the feature class — it's faster than tracing the facade, and it immediately reveals the two usual culprits: null scope (unauthenticated request, queued job) and a stale cached answer.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource