ylsideas/feature-flags
An extendable Laravel package for managing feature flags in code, routes, Blade views, task scheduling, and validations—making continuous integration and deployment easier by safely guarding features behind toggles.
1. Install and publish config
composer require ylsideas/feature-flags:^3.0
php artisan vendor:publish \
--provider="YlsIdeas\FeatureFlags\FeatureFlagsServiceProvider" \
--tag=config
Version 3 is the current line (v3.1.0 added Laravel 13 support). If you're on Laravel 9/PHP 8 you want ^2.0; version 1 is unsupported.
2. Know what you got by default
The published config/features.php ships with an in-memory gateway that reads a .features.php file at the project root. That means you can start flagging code with zero database work — commit .features.php for local/dev defaults and treat it as code, not runtime state.
3. First use case — the three-line version
use YlsIdeas\FeatureFlags\Facades\Features;
if (Features::accessible('new-checkout')) {
// new path
} else {
// legacy path
}
Do this for the first flag only at the top of a controller or Blade view. Resist wrapping ten call sites on day one — pick one blast radius and confirm the plumbing works.
4. Where to look first
config/features.php — the gateways and pipeline definitions are the whole mental model of this package.php artisan feature:state my-flag — prints the current state of a flag; the fastest sanity check when a flag "isn't working".CHANGELOG.md / releases — this package ships features fast (debug system, query-builder mixin, maintenance mode, fakes all arrived as 2.x minors). Skim it before assuming a capability is missing.The v2+ architecture replaced "repositories" with a pipeline of gateways. A flag is resolved by walking gateways in order; each gateway can be configured to only handle certain flags via filters, and can carry its own cache. This is the single most important design decision you'll make.
A pragmatic production pipeline:
// config/features.php (shape, not verbatim)
'pipeline' => [
'database' => ['cache' => 60], // runtime overrides an admin/ops team flips
'in_memory' => [], // .features.php: per-environment defaults, committed
],
This gives you: ops can flip a flag from a console command or dashboard without a deploy; engineers can still set a deterministic default in code review.
Gateways implementing the toggleable interface (database, redis) are writable at runtime. Wire the commands into your runbook:
php artisan feature:on new-checkout
php artisan feature:off new-checkout
php artisan feature:state new-checkout
If feature:on seems to do nothing, your pipeline is resolving from a non-toggleable gateway (e.g. in-memory) first — see Gotchas.
Middleware is the right tool when the feature is a page, not a branch in a page:
Route::get('/reports/beta', ReportsController::class)
->middleware('feature:beta-reports');
Since v2.3 the middleware can be configured with a message, so you control what a user sees when they hit a gated route rather than exposing a generic denial. Pair this with a navigation check using Features::accessible() so you never link to a route the user can't reach.
For flags that change what data exists rather than which view renders, use the query-builder mixin added in v2.1:
Post::query()
->whenFeatureIsAccessible('rich-posts', fn ($q) => $q->with('media'))
->whenFeatureIsNotAccessible('rich-posts', fn ($q) => $q->whereNull('legacy_only'))
->get();
This keeps conditional query logic out of your controllers and makes it greppable later when the flag is retired.
use YlsIdeas\FeatureFlags\Facades\Features;
public function setUp(): void
{
parent::setUp();
Features::fake(['new-checkout' => true, 'beta-reports' => false]);
}
Notes that matter in practice:
tearDown rather than relying on one global fake per suite.The gate-based driver lets you express flag access as a Laravel Gate, which is how you get percentage or segment rollouts without new infrastructure:
Features::accessible('flag') then respects Gate::allows() for the authenticated user.Caveat: gate-driven flags are inherently request/user-scoped. Don't put them in the same pipeline position you use for global kill-switches.
v2.1 added a system for debugging features that are accessed and a handler for when features should be expired. Use both:
Since v2.4 you can drive maintenance mode with flags. This is a good pattern for "degrade gracefully per tenant" rather than a binary artisan down that hits everyone.
Cached gateway results were not always booleans. v3.0.1 explicitly fixed this ("force casting cache result to boolean"). On 3.0.0 or earlier, a cached "0" or "false" string can be truthy in a loose check. Pin ^3.0.2 and still prefer Features::accessible() over casting raw gateway values yourself.
update_on_resolve is easy to misread. It controls whether resolving a flag writes back to the underlying gateway. The Chain gateway historically ignored it (fixed in 1.2.1), which is a good reminder: if you change that setting, test the chain case specifically, not just a single gateway. If you're unsure, leave it at the default and let the pipeline order do the work.
A non-toggleable gateway in the pipeline silently swallows your toggles. In-memory/config gateways resolve from files and config; feature:on can't write to them. Check ordering with feature:state before you file a bug.
Long-running processes hold stale flag state. Under Octane or in queue workers, gateway caches and resolved values can outlive a toggle. Either keep the cache TTL short for runtime-controlled flags, clear the feature cache as part of your toggle procedure, or resolve flags from an uncached gateway in worker contexts.
Unregistered/unknown flags resolve to off. That's the safe default, but it means a typo in a flag name is indistinguishable from an intentional off state. Centralise flag names in a constants class or enum and reference those everywhere — middleware strings, Blade, validation, and queries.
Validation semantics are "must be accessible". The validation rule was inverted in 1.0.x and fixed in 1.1.0. If your form validation for a flagged field behaves backwards, check you're not on an ancient version and check whether your pipeline is returning the value you think it is.
Don't let gated middleware break API contracts. A flag middleware rejects the request; if the same route serves JSON and HTML clients, confirm the response shape per client (the middleware message config from v2.3 is your lever). Otherwise a flag flip turns a clean 200 into an opaque HTML error in a mobile client.
Retire flags on a schedule, not on vibes. Combine the debug-access log with the expiration handler: pick a date at creation, check the debug log at that date, delete the branch. Flags that live forever become unreadable conditional soup and are the main reason teams turn against feature flagging.
Extension points worth knowing: custom gateways (implement the gateway contract, add toggleable if writable), gateway filters (scope a gateway to a flag namespace), per-gateway caches, and the pipeline composition itself. Most "the package can't do X" problems are actually "write a small gateway that does X and drop it in the pipeline."
Upgrading between majors is not always free. v1→v2 renamed repositories to gateways, introduced the pipeline, and switched the default config to in-memory with .features.php. If you inherit a v1 app, use UPGRADE.md; v2→v3 needs no real work (only type-hint tightening), so ^3.0 is the right target on any actively maintained Laravel app.
How can I help you explore Laravel packages today?