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

Feature Flag Bundle Laravel Package

em411/feature-flag-bundle

A Symfony bundle for feature flags: enable or disable code paths based on context-aware callables. This is a Symfony 4.4 / PHP 7.4 backport of ajgarlag/feature-flag-bundle, keeping the same API and namespace for a drop-in upgrade from the upstream package.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you install anything: this is not a Laravel package. It's a Symfony 4.4 / PHP 7.4 bundle with a hard dependency on Symfony components (symfony/framework-bundle, symfony/dependency-injection, Twig, Doctrine annotations). Dropping it into a modern Laravel app will not autoload, will not register a service provider, and will almost certainly create Composer conflicts against illuminate/* and current symfony/* versions. Your realistic options are:

  1. Adopt the pattern, not the package — copy the tiny API surface (FeatureChecker + ProviderInterface + a chain provider) into a Laravel service provider, or use Laravel Pennant and keep this README as the design reference.
  2. Use the package for real only if you maintain a separate Symfony 4.4 legacy service and want per-request feature gating there.

If you are in case 2, the install is two steps:

composer require em411/feature-flag-bundle
// config/bundles.php
return [
    // ...
    Ajgarlag\FeatureFlagBundle\FeatureFlagBundle::class => ['all' => true],
];

Where to look first, in order:

  • FeatureCheckerInterface — the only thing you inject in application code. It exposes isEnabled(string $name) and, judging by the Twig feature_get_value() function, a value-returning counterpart.
  • Provider\ProviderInterface — get(string $featureName): ?callable. This is the extension point; everything else is wiring.
  • Attribute\AsFeature — despite the name, it's a Doctrine annotation (@AsFeature), not a PHP 8 attribute. PHP 7.4 has no attributes.

First use case: gate a checkout flow. Declare a feature class, inject the checker, fail with a 404 when it's off.

namespace App\Feature;

use Ajgarlag\FeatureFlagBundle\Attribute\AsFeature;

/**
 * @AsFeature("new_checkout")
 */
final class NewCheckoutFeature
{
    public function __invoke(): bool
    {
        return getenv('NEW_CHECKOUT') === '1';
    }
}
public function index(): Response
{
    if (!$this->featureChecker->isEnabled('new_checkout')) {
        throw new NotFoundHttpException();
    }
    // ...
}

That's the whole loop. Everything below is variations on it.

Implementation Patterns

Declaring features. Four shapes, pick deliberately:

// 1. Invokable class, explicit name (preferred)
/** @AsFeature("xmas") */
final class XmasFeature { public function __invoke(): bool { ... } }

// 2. Class + method property
/** @AsFeature(name="xmas", method="isXmas") */
final class XmasFeature { public function isXmas(): bool { ... } }

// 3. Method-level, explicit name
/** @AsFeature(name="weekend") */
public function isWeekend(): bool { ... }

// 4. Method-level, no name → "App\Feature\FeatureService::isAnotherFeature"
/** @AsFeature */
public function isAnotherFeature(): bool { ... }

Prefer explicit names in cases 1–3. The defaults are FQCN-derived and silently change if you rename or move the class.

Multiple features on one class. Doctrine annotations can't repeat, so use the service tag instead of stacking @AsFeature:

services:
    App\Feature\CheckoutFlags:
        tags:
            - { name: 'ajgarlag.feature_flag.feature', feature: 'new_checkout' }

Custom providers — the pattern you'll actually spend time on. Any service implementing ProviderInterface is autoconfigured. Return a closure when you own the feature, null when you don't:

final class DoctrineProvider implements ProviderInterface
{
    public function __construct(private FeatureAssignmentRepository $repo) {}

    public function get(string $featureName): ?callable
    {
        $context = ['user' => $this->security->getUser()?->getUsername()];
        return fn () => $this->repo->featureIsEnabled($featureName, $context);
    }
}

Three provider archetypes cover most needs: static/env (ServiceProvider / the built-in in-memory one), database (assignment table — the Doctrine example), and remote (Unleash/GitLab). The README's GitLab example is worth skimming even if you don't use Unleash, because it shows the caching-service and context-object wiring you'll need for any HTTP-backed provider.

Priority and the chain. Higher priority on the ajgarlag.feature_flag.provider tag is checked first; first provider that returns a feature wins. Use this to put a cheap override provider (an admin "kill switch" from env, or a test double) ahead of expensive DB/API providers.

Route gating. Symfony 4.4 can't evaluate services in route.condition. Two options:

  • Controller guard as above — fine for a handful of routes.
  • A kernel.controller listener that maps controller/attribute names to features and throws NotFoundHttpException centrally. Once you have more than ~3 gated routes, do this instead of copy-pasting the guard.

Twig. feature_is_enabled('x') and feature_get_value('x') for template-level toggles. feature_get_value is also your gradual-rollout lever: return a bucket integer or variant string from the callable and compare in Twig (== 'some_value').

Wrap it. Inject FeatureCheckerInterface through one thin application-level service (App\Flags\Flags) rather than directly into every controller. When you eventually migrate to Laravel Pennant or the upstream PHP 8 attribute API, you change one class instead of fifty constructors.

Gotchas and Tips

Dependency conflicts are the first wall. A Laravel app on PHP 8.1+ will fail composer require on PHP constraint, Symfony version pins, or Twig ^2.12 | >=3.0,<3.7. Don't fight it — the package is not intended for that runtime.

EOL posture is explicit. Symfony 4.4 and PHP 7.4 are both EOL; the maintainers had to set config.audit.block-insecure: false in CI just to resolve dependencies. Every composer install will report security advisories. Fine for an already-frozen legacy app, unacceptable for anything you plan to keep shipping on. Treat that as a scheduling constraint, not a nuisance.

The namespace does not match the package name. It's em411/feature-flag-bundle on Packagist but Ajgarlag\FeatureFlagBundle\ in PHP, deliberately, for drop-in compatibility with the upstream. Consequences:

  • IDE autocomplete and "go to definition" will lead you to the upstream repo's docs. Mostly correct — except the README examples that use #[AsFeature] (PHP 8 attributes). Here it is @AsFeature in a docblock.
  • Never install both this package and ajgarlag/feature-flag-bundle in the same project. Identical class namespaces and class names → fatal redeclaration or unpredictable autoload resolution.

Annotations need infrastructure. @AsFeature requires doctrine/annotations and a configured annotation reader. If the legacy app has no Doctrine annotation setup, class-level autoconfiguration won't fire and features will appear "missing" with no error — just isEnabled() returning false. Symptom to watch for: the profiler shows the feature as unresolved even though the class exists.

Providers that always return a closure shadow everything below them. get() must return null for features it doesn't know. A provider that returns a closure unconditionally becomes a catch-all and the chain silently degrades to a single provider. Also: a provider returning a closure has "won" — the chain does not compare values, only ownership.

No built-in result caching. The callable is invoked every time isEnabled() / feature_get_value() is called. A DB-backed provider called inside a Twig loop is an N+1. Memoize inside your provider (or decorate FeatureCheckerInterface with a per-request cache) as a matter of course.

Context capture timing. The closure captures context at get() time. In a Symfony request this is usually the same moment as evaluation, so it works — but if you resolve the user lazily (sub-requests, CLI, message consumers), capture inside the closure body rather than before it.

Failure mode is undefined by you, not by the bundle. If a provider throws (DB down, Unleash API timeout), the exception propagates. Decide explicitly per provider: fail-open for UI cosmetics, fail-closed for anything that changes money or data — and wrap the callable in try/catch to enforce it. The default "let it throw" is a 500, which is a valid but rarely intended choice.

Naming drift. Default feature names are FQCN or Class::method. Refactor a class, silently lose a feature. Explicit names everywhere is cheap insurance.

Service id archaeology. The in-memory provider service id is ajgarlag.feature_flag.provider.in_memory but it's actually backed by ServiceProvider (the changelog calls this out as a leftover). Don't infer implementation from the id when overriding services in services_test.yaml.

Testing. Override ChainProvider (or prepend a test provider with high priority) in the test container and return static closures. Never let the test suite hit real DB/HTTP providers — those calls are uncached and will make the suite slow and flaky.

Profiler. The bundle ships a web profiler data collector showing resolved feature values per request. In dev this is your primary debugging tool for "why is this flag off" — check the resolved value there before adding dump() calls.

Migration path is a codemod. When you move to PHP 8.1+/Symfony 6 and switch to upstream, every @AsFeature docblock becomes a #[AsFeature] attribute and the one-annotation-per-method limitation disappears. Keeping feature names explicit and the checker behind your own service makes that a mechanical change rather than a rewrite.

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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect