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.
Architecture fit. The package is intentionally narrow: one service (FeatureCheckerInterface), one provider contract (ProviderInterface), a chain aggregator, an in-memory/service-backed provider, a Twig bridge, and a profiler collector. That is the correct shape for a feature-flag primitive in a Symfony 4.4 application. It follows ordinary bundle conventions (auto-configuring tagged services, a compiler pass in FeatureFlagPass, XML service wiring), so it slots into existing DI patterns rather than introducing a parallel configuration model. The provider abstraction is the strongest design decision: it lets teams front the same feature_is_enabled() API with a simple callable today and later swap in Unleash/GitLab or a Doctrine-backed assignment table without touching call sites. This is the difference between a two-week demo and something an engineering org can standardize on.
Integration feasibility. Low friction. composer require em411/feature-flag-bundle, register Ajgarlag\FeatureFlagBundle\FeatureFlagBundle::class in config/bundles.php (Flex handles this), and start declaring features via @AsFeature annotations or the ajgarlag.feature_flag.feature service tag. The Twig functions and profiler collector are available automatically if Twig and WebProfiler are present. There are no database migrations, no queue workers, no outbound network calls unless a provider adds them. The realistic integration cost is measured in hours, not sprints.
Technical risk. This is the highest-signal section and should gate adoption. Specific concerns:
config.audit.block-insecure: false in CI because Composer's security audit blocks the pinned Symfony/Twig versions. That is a defensible posture for a backport, but it means the consumer's Composer install will also surface audit failures unless they override the same setting. Enterprises with hardened CI pipelines need a documented policy for this.ajgarlag/feature-flag-bundle carries the credibility; this fork carries the compatibility burden. There is no evidence of external adoption, which means no community bug-catching and no de facto SLO. The changelog is professional (semver discipline, explicit upstream attribution, clear scoping to CI vs. runtime), which is reassuring but does not substitute for a support commitment.em411/feature-flag-bundle but the PHP namespace is Ajgarlag\FeatureFlagBundle\. This is deliberate for drop-in compatibility, but it creates real confusion during code review, SBOM generation, and incident response ("which package owns this class?").@AsFeature requires the doctrine/annotations runtime. Doctrine deprecated annotations in favor of native attributes, which this fork cannot use on PHP 7.4. Teams must keep doctrine/annotations alive and accept that the annotation syntax will be a code-review and onboarding friction point for engineers who have moved to PHP 8 attributes.condition expressions cannot evaluate services in Symfony 4.4, so feature gating of routes must be done in controllers or a kernel.controller listener. This is documented but easy to miss and pushes gating logic into controllers, which is where it is hardest to audit. Any team with a large routing surface should plan a listener abstraction before rollout.Key questions for the maintainer / sponsor team.
doctrine/annotations, symfony/framework-bundle, or twig/twig be backported here, or is the expectation strictly "your app pins the same versions"? The 1.0.4 audit note suggests the latter, but it should be explicit.ajgarlag/feature-flag-bundle, or is the fork permanently divergent?FeatureCheckerInterface today, will that API exist unchanged in the upstream when we upgrade? (The README says yes; get it in writing in an ADR.)Stack fit. This package exists to serve exactly one integration scenario: an application that is on Symfony 4.4 and PHP 7.4 and cannot move to Symfony 6+/PHP 8.1+ within the current planning horizon. If the application can reach PHP 8.1 and Symfony 6.x, the correct decision is to use upstream ajgarlag/feature-flag-bundle and skip this package entirely. The README states this plainly and TPMs should treat that statement as the primary acceptance criterion. Do not adopt this package as a hedge against an eventual upgrade — adopt it as an explicit, time-boxed bridge.
Migration path. Two directions matter:
ajgarlag/feature-flag-bundle with em411/feature-flag-bundle in composer.json. Because the namespace is preserved, no use statements change and no bundle class registration changes. The only code change is #[AsFeature] attributes becoming @AsFeature Doctrine annotations — mechanical, but a real diff that must be reviewed. Treat this as a one-way, in-place substitution and do not run both packages side by side.@AsFeature annotation becomes a #[AsFeature] attribute, and route-level gating can move from controllers/listeners into Symfony's native route condition expressions. The FeatureCheckerInterface callsites should be identical. Budget this as part of the eventual Symfony 5.4/6.x upgrade, not as a separate workstream.Compatibility. Confirmed supported combinations: PHP 7.4, PHP 8.0; Symfony 4.4; Twig 2.x (Twig is require-dev only and therefore optional at runtime, which is correct for a bundle with a Twig bridge). Not supported / untested: PHP 8.1+ (the changelog explicitly capped twig/twig to keep the CI matrix green on PHP 7.4 and 8.0, so do not assume 8.1 compatibility), Symfony 5+, PHP 8 attributes. If your application runs PHP 8.0 you are technically within scope but at the edge of the maintainer's CI matrix — flag this in your risk register.
Sequencing. A pragmatic rollout:
@AsFeature on an invokable class, and one on a method of an existing service), and exercise feature_is_enabled() from a controller, a service, and a Twig template. Confirm the profiler panel resolves feature values per request.config.audit.block-insecure: false) is acceptable to the security team. If either answer is no, stop here.ProviderInterface implementation backed by a table or config store; or (c) a third-party flag service via a provider (the README shows an Unleash/GitLab pattern that is copy-pasteable). For most teams, (b) is the right target because it gives product/ops a way to flip flags without a deploy.condition expressions do not work on 4.4, standardize on either a kernel.controller listener or a small base-controller check. Write this once, in a shared trait or listener, so it does not get copy-pasted across the codebase.Maintenance. The package is small and the changelog shows disciplined semver. 1.0.5 was a CI-only release; 1.0.4 was a CI/security-audit fix; 1.0.1–1.0.3 were packaging and Twig-2 compatibility fixes. There is no evidence of runtime churn, which is the right signal for a low-level primitive. The real maintenance burden is not in the package — it is in the EOL stack underneath it. Symfony 4.4, PHP 7.4, and Doctrine annotations are all receiving at most security-only fixes (and Symfony 4.4 is past its free support window), which means the application's patching burden dominates this bundle's. TPMs should not credit the package for the health of its dependencies.
Support. Community-only via GitHub issues. No commercial backing, no published SLA, no external adopters to fall back on. Two consequences: (1) internal code ownership matters more than usual — assign a named engineer to be the internal point of contact; (2) for anything touching a revenue path, fork-and-patch internally rather than waiting on upstream. The package is small enough that this is realistic, but it should be an explicit decision, not a hope.
Scaling. Two axes:
ChainProvider iterates providers in priority order and stops at the first hit. Complexity is O(number of providers) per lookup, not O(number of features), so annotation- and tag-declared features scale linearly in class metadata and constant in lookup cost. Thousands of features are fine; tens of thousands would suggest an architectural review of why so many flags exist, not a change to this bundle.isEnabled() call invokes a provider callable, and any network- or database-backed provider will hit its backend on every call unless the provider itself caches. For Unleash/GitLab providers, the README's PSR-16 cache wiring is mandatory, not optional; budget a cache pool and a TTL for it. For a Doctrine-backed provider, expect a query per lookup unless you memoize per request — easy to add, easy to forget.Failure modes to plan for.
FeatureChecker and can take down a request. Wrap external providers with try/catch that fails off for risky new features and on for kill-switch-style flags, and document the choice per flag.config.audit.block-insecure: false override will see their pipeline go red on next Composer update. Document the override alongside the install instructions in your internal runbook, not in the README.Ramp-up. Low for engineers: the public API is three concepts (FeatureCheckerInterface, ProviderInterface, @AsFeature), and the README is complete enough to onboard a mid-level engineer in an afternoon. The friction points are Doctrine annotation syntax for engineers who have already moved to PHP 8 attributes, and the conceptual "feature vs. configuration vs. experiment" distinction, which the bundle does not help with — that is a product-policy problem, not a software problem. Budget a short internal doc and a naming convention before rollout; it will save more time than any tooling decision.
How can I help you explore Laravel packages today?