php 146 lines · 4 tabs

Request-Scoped Feature Flags in Symfony with a Twig Extension and Controller Gate

Shared by codesnips Aug 2026
4 tabs
<?php

namespace App\Feature;

use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\Service\ResetInterface;

final class FeatureFlagManager implements ResetInterface
{
    private array $cache = [];

    public function __construct(
        private readonly RequestStack $requestStack,
        private readonly array $rollouts = [],
        private readonly bool $allowHeaderOverride = false,
    ) {
    }

    public function isEnabled(string $flag): bool
    {
        return $this->cache[$flag] ??= $this->evaluate($flag);
    }

    private function evaluate(string $flag): bool
    {
        $request = $this->requestStack->getCurrentRequest();

        if ($this->allowHeaderOverride && $request !== null) {
            $header = $request->headers->get('X-Feature-' . str_replace('_', '-', $flag));
            if ($header !== null) {
                return filter_var($header, FILTER_VALIDATE_BOOL);
            }
        }

        $percentage = $this->rollouts[$flag] ?? 0;
        if ($percentage <= 0) {
            return false;
        }
        if ($percentage >= 100) {
            return true;
        }

        $identity = $request?->getSession()->getId() ?? $request?->getClientIp() ?? 'anon';
        $bucket = crc32($flag . ':' . $identity) % 100;

        return $bucket < $percentage;
    }

    public function reset(): void
    {
        $this->cache = [];
    }
}
4 files · php Explain with highlit

This snippet shows how a small feature-flag system is wired into a Symfony application so that a flag is evaluated once per request and consumed consistently in services, templates, and controllers. The core idea is a request-scoped evaluator: flags often depend on the current user, request headers, or a percentage rollout, so their values should be stable for the lifetime of a single request but recomputed on the next one. Memoizing per request avoids both the cost of re-evaluating rules repeatedly and the correctness bugs that arise when a flag flips mid-request.

In FeatureFlagManager, the service holds an injected RequestStack and an in-memory $cache keyed by flag name. isEnabled() returns the memoized value if present, otherwise it delegates to evaluate() and stores the result. The evaluate() method demonstrates two common strategies: a header override (X-Feature-*) useful for QA and automated tests, and a deterministic percentage rollout computed with crc32 over the flag name plus a stable per-request identifier. Using a hash of a stable key rather than rand() means the same user consistently lands in or out of the rollout bucket, which is essential for a coherent experience and for A/B measurement.

Because the manager depends on RequestStack, it naturally behaves as request-scoped without needing custom container scopes; each request populates a fresh RequestStack frame, and the reset() method (invoked via kernel.reset) clears the memo cache between requests when the service container is reused, as with worker-based runtimes.

The FeatureExtension Twig extension exposes a single feature() function that forwards to the same manager, so templates can branch with {% if feature('new_checkout') %} and share exactly the evaluation logic used elsewhere. This keeps template logic thin and prevents drift between backend and view-layer decisions.

Finally, CheckoutController shows the controller gate. The #[IsFeatureEnabled('new_checkout')] attribute declares intent declaratively, while the inline isEnabled() check inside confirm() illustrates the imperative fallback for finer-grained branching. A pitfall worth noting: header overrides must be restricted to trusted environments, otherwise clients could toggle features at will. This pattern is worth reaching for whenever rollouts, kill switches, or per-user experiments need one source of truth across the stack.


Related snips

Share this code

Here's the card — post it anywhere.

Request-Scoped Feature Flags in Symfony with a Twig Extension and Controller Gate — share card
Link copied