flagception/flagception-bundle
A Symfony bundle for feature flags and toggles. Define flags, activate them by context or constraints, and check them in controllers, services, Twig templates, and annotations. Integrates Flagception with Symfony configuration and dependency injection.
The first thing to internalize: this is a Symfony bundle, not a Laravel package. composer require flagception/flagception-bundle in a Laravel app will install fine and then do nothing — there is no service provider, no facade, no Blade directive, and the bundle's DI extension only runs under Symfony's kernel. (As of 6.1.1 this is unchanged; the only release activity is Symfony 5.4 re-added to the CI matrix, which tells you the bundle still targets older Symfony LTS lines, not Laravel.) A Laravel developer has two realistic entry points:
flagception/flagception) and wire it yourself in a service provider. The bundle is mostly glue: it reads YAML, builds FeatureManager, registers validators, and (since 6.1.1) reports inactive features into route metadata for the profiler. You can replicate the useful 90% in ~40 lines.config/features.php gets you the same shape with first-class framework support.If you are in a Symfony app, the minimal path is:
# config/packages/flagception.yaml
flagception:
features:
checkout.new_flow:
default: false
constraints:
- name: environment
options: { env: ['dev', 'test'] }
public function __construct(private FeatureManagerInterface $features) {}
if ($this->features->isActive('checkout.new_flow')) {
// new path
}
Where to read first, in order:
DependencyInjection/Configuration.php — this is the entire feature schema. Everything you can express is here.Resources/config/*.xml — shows exactly which services get built and with what arguments. This is your wiring blueprint for Laravel.vendor/flagception/flagception/src/...) — the built-in vocabulary (environment, cookie, request, user, expression, time-based, etc.). These are the whole product.EventListener/RouteMetadataSubscriber.php (or wherever the subscriber now lives) — it annotates matched routes with the list of features that evaluated inactive. Worth thirty seconds because it doubles as documentation of the intended "flag per route" integration style.FeatureManager::isActive($name, array $context) — the single method you'll call day-to-day.Your first use case should be deliberately boring: gate one code path that is on in dev/test and off in prod, checked from exactly one place. Don't start with expression constraints or percentage rollouts.
Config-first, code-second. Features belong in a config file (YAML in Symfony, config/features.php in Laravel), not scattered as booleans. Code should only ever ask isActive('some.flag'). If a flag's logic lives in a closure inside a controller, you've lost the audit trail that makes feature toggles worth having.
Build a request-scoped context once. The context array is how constraints see the world. Assemble it in a middleware and reuse it:
// app/Http/Middleware/FeatureContext.php
public function handle($request, Closure $next)
{
app(FeatureContext::class)->set([
'user' => $request->user()?->only('id', 'roles'),
'request' => ['ip' => $request->ip(), 'header' => $request->headers->all()],
'env' => app()->environment(),
]);
return $next($request);
}
Wrap it at the boundaries, not in the domain. Blade directive, route middleware, and a job-level check are the right seams:
Blade::if('feature', fn (string $name) =>
app(FeatureManagerInterface::class)->isActive($name, app(FeatureContext::class)->all())
);
// @feature('checkout.new_flow') ... @endfeature
Never let isActive() leak into entities, models, or domain services — it makes them untestable and turns flags into permanent branches.
All constraints are ANDed. If you need "admin OR internal env," express it inside a single expression constraint or split it into two flags with an explicit any at the call site. Trying to make the constraint list itself disjunctive is the most common source of confusion.
Rollout pattern that actually works: environment constraint for non-prod, plus an expression/percentage constraint keyed on user.id in prod. Keep both in the same flag so dev and prod don't diverge structurally. Hash on a stable user identifier, never on session or request ID, or users will flip mid-session.
Route-level gating is a first-class path in Symfony. The bundle already hooks route metadata (extended in 6.1.1 to surface inactive features), so "this controller action is behind flag X" is a supported shape rather than something you invent. Mirror it in Laravel with a route middleware that reads $route->getName() → flag map from config, rather than sprinkling isActive() in controllers.
Testing: bind a stub FeatureManagerInterface in the container, or add a test helper Feature::force('checkout.new_flow', true). Assert both branches of every gated path at least once. If your tests exercise expression constraints, you're testing the expression language, not your code — stub those out.
Retire flags on a schedule. Add a features:list Artisan command that greps for isActive('...') and reports flags with no call sites. A bundle that makes flags cheap to add makes them expensive to remove; budget for it.
Resources/config/services.xml is your copy-paste source for constructor arguments.false) silently. A typo in isActive('checkout.new_flwo') disables your feature with zero diagnostics. Build a lint step that extracts flag names from your code and diffs them against the config.user/request/cookie constraints evaluate against an empty array in workers and console commands, so features silently turn off in jobs. Either pass context explicitly into the job, or evaluate the flag at dispatch time and put the boolean in the payload.default vs. constraints semantics are subtle. The default is the fallback; once constraints exist they decide. Verify against the source before relying on defaults for "constraints didn't match" — the distinction matters for flags that are on-by-default with an opt-out.eval-adjacent. They're powerful and they'll be written by whoever is on call. No sandbox, arbitrary context access, and errors may surface as "inactive" rather than as exceptions. Treat expression strings as production code: review them, and log when one throws.config:cache plus an opcache will do the same thing to your features.php.dump() calls. Don't expect an equivalent in Laravel; if you build a wrapper, consider logging the evaluated flag set (name → active/inactive → matched constraint) once per request behind a debug switch, because "silently false" is this package's defining failure mode.Annotations/ directory before adopting the attribute style. In Laravel, use a middleware attribute instead; don't pull in doctrine/annotations just for this.interface Constraint { public function passes(array $context): bool; } and a registry keyed by config name.FeatureManager interface, and assume you may own it eventually.How can I help you explore Laravel packages today?