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

Featurit Sdk Symfony Laravel Package

featurit/featurit-sdk-symfony

Symfony SDK for integrating Featurit feature flags into your PHP applications. Manage toggles, target users, and control feature rollout from Symfony services with a simple, configurable client.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up (5 minutes)

composer require featurit/featurit-sdk-symfony

If you're on Flex the bundle is usually auto-registered; otherwise add it to config/bundles.php:

Featurit\Symfony\FeaturitBundle::class => ['all' => true],

Configure once, per environment

# config/packages/featurit.yaml
featurit:
    featurit_enabled: '%env(bool:FEATURIT_ENABLED)%'
    tenant: '%env(FEATURIT_TENANT)%'
    api_key: '%env(FEATURIT_API_KEY)%'
    environment: '%env(FEATURIT_ENVIRONMENT)%'   # e.g. production / staging
    cache:
        service: 'cache.app'                     # any PSR-6 pool / cache service
        ttl: 60

Keep api_key out of the repo, mirror FEATURIT_ENABLED=false in .env.test and .env.local, and give each deploy target its own tenant or environment value so flags don't bleed between staging and prod.

Since v0.8.0 (PHP SDK 1.0.0) cache injection is the headline change: instead of being limited to a hardcoded adapter, you can point the bundle at your own cache service — a dedicated pool, a chain adapter, Redis, whatever you already run. The exact YAML key moved around across versions, so confirm it against Configuration.php before you trust the snippet above.

Where to look first

  1. The bundle's README.md for the exact config keys and the service id it exposes.
  2. vendor/featurit/featurit-sdk-symfony/ — DependencyInjection/Configuration.php is the authoritative list of options (especially the cache node after 0.8.0), and Resources/config/services.yaml shows you the service ids and aliases to autowire.
  3. vendor/featurit/featurit-sdk-php/ — the Symfony bundle is a thin wrapper; the real API surface (isFeatureActive, getFeatureTreatment, user context, cache interface) lives here. When docs are thin, read this. The 1.0.0 tag is worth reading specifically: that's where the cache contract now lives.

First use case

The classic one: ship a new checkout flow behind a flag and kill it without a deploy.

use Featurit\Client\Featurit;

final class CheckoutController
{
    public function __construct(private Featurit $featurit) {}

    public function __invoke(): Response
    {
        if ($this->featurit->isFeatureActive('new-checkout')) {
            return $this->render('checkout/new.html.twig');
        }

        return $this->render('checkout/legacy.html.twig');
    }
}

The client is registered as an autowirable service (typically also aliased to featurit if you prefer container ids). If isFeatureActive() returns false for everything on the first run, your tenant/api-key/environment combination is wrong — check the logs before assuming a code problem. If it's slow on the first run, your cache service isn't wired yet.

Implementation Patterns

Don't sprinkle the SDK through your codebase

Wrap it once. Flag checks are a vendor API and will leak into every controller, Twig template and listener if you let them.

interface FeatureFlags
{
    public function isActive(string $flag, bool $default = false): bool;
    public function treatment(string $flag, string $default = 'control'): string;
}

Implement it with Featurit, bind the interface in services.yaml, and inject that everywhere. Two payoffs: you can swap vendors later, and you can fake flags in tests without touching the SDK.

Feed the user context — this is the extension point that matters

Feature targeting, percentage rollouts and A/B bucketing all depend on FeaturIT knowing who is asking. Out of the box the SDK has no idea about your Symfony security token. Implement the SDK's user-context provider interface (look for FeaturitUserContextProviderInterface under vendor/featurit/) and register it as a service so the bundle picks it up:

final class SecurityUserContextProvider implements FeaturitUserContextProviderInterface
{
    public function __construct(private Security $security) {}

    public function getUserContext(): FeaturitUserContext
    {
        $user = $this->security->getUser();

        return new FeaturitUserContext(
            userId: $user?->getUserIdentifier() ?? 'anonymous',
            sessionId: /* request session id, or a cookie-backed id */ null,
            attributes: [
                'plan'    => $user?->getPlan() ?? 'free',
                'country' => $user?->getCountry() ?? 'unknown',
            ],
        );
    }
}

Keep attributes to a small, stable set of segmentation keys — plan tier, country, role, company id. Every attribute you add becomes part of the targeting model and part of the payload sent to a third party.

Treatments for A/B, booleans for on/off

Reserve isFeatureActive() for kill switches and rollout gates. For anything with variants use getFeatureTreatment(), which returns a stable string per user:

$variant = $this->featurit->getFeatureTreatment('pricing-page'); // 'control' | 'variant-b'

Always code a default branch so an unknown/empty treatment can't blow up a template.

Twig

If the bundle ships a Twig extension you'll get something like {% if featurit_is_active('new-nav') %}. If it doesn't, expose a single Twig function through your own extension that delegates to your FeatureFlags wrapper — do not let templates call the client directly, or flags become invisible to grep.

Cache deliberately — and now take advantage of the new injection point

Flags are fetched over HTTP, so production needs a shared cache. Before 0.8.0 you were largely at the mercy of whatever the bundle defaulted to; with SDK 1.0.0 you should register a dedicated pool and inject it:

# config/packages/cache.yaml
framework:
    cache:
        pools:
            featurit.cache:
                adapter: cache.adapter.redis
                default_lifetime: 60
                provider: 'redis://%env(REDIS_URL)%'
# config/packages/featurit.yaml
featurit:
    cache:
        service: 'featurit.cache'
        ttl: 60

A dedicated pool buys you three things a shared cache.app doesn't:

  • Surgical invalidation. bin/console cache:pool:clear featurit.cache kills flag state without nuking every cached query in the app — exactly what you want mid-incident after flipping a kill switch.
  • No key collisions. Small packages are careless about key prefixes. A dedicated pool makes collisions with your own cache keys structurally impossible.
  • Independent sizing/TTL. Flag payloads share a lifetime profile with nothing else in your app.

A 30–120s TTL is a reasonable compromise between "flag changes take effect quickly" and "we don't hammer the API on every request." Whatever you pick, write the purge command down in your runbook before you need it.

Test the cache wiring, not just the flags

Point the pool at cache.adapter.array in .env.test (or inject an array adapter directly) so CI never depends on Redis or the vendor. If you use the new custom-cache path, add one test that asserts the pool actually receives writes — a misconfigured service id is silent and just means every request pays full latency.

Testing (flags)

Bind a fake FeatureFlags in config/services_test.yaml that returns a fixed set of values, and write separate tests that exercise both branches of every flag. Also add one integration test against the real SDK with a stub HTTP client so you notice when the vendor's response parsing changes — that's doubly relevant after the 1.0.0 bump.

Gotchas and Tips

Treat the 1.0.0 SDK bump as a breaking change until proven otherwise. Going from the pre-1.0 line to 1.0.0 under a 0.8.0 bundle version is exactly the shape of release where method signatures and FeaturitUserContext constructor arguments shift. Before you deploy: re-read your SecurityUserContextProvider against the new context class, run the integration test, and grep for any direct Featurit method calls in your wrapper. Semver on the bundle says patch; semver on the SDK says major. Believe the SDK.

The custom cache must match the interface the SDK expects. SDK 1.0.0 made cache injection flexible, which means it will happily accept a service that looks right and fails at call time if the contract differs (PSR-6 CacheItemPoolInterface vs PSR-16 CacheInterface, or a missing getItem/save combination). bin/console debug:container featurit.cache to confirm the service exists and debug:config featurit to confirm the key is read — the bundle will not warn you if you point it at the wrong service id, it'll just silently fall back to an in-memory adapter and you'll pay an HTTP call per request in production.

Fail-open vs fail-closed is a decision you have to make explicitly. When the FeaturIT API is unreachable and the cache is cold, the SDK cannot tell you the flag state. Whatever it returns becomes your de-facto policy. For a risky new feature you want false (fail closed); for a "disable the banner" flag you probably want the safe state too. Encode the desired default at the call site (isActive('x', default: false)) rather than trusting the library, and test it by pointing environment at a bogus value with the cache cleared. Note that your cache service is now part of the failure path: if Redis is down, you're on the cold path more often than you think.

Never call the SDK inside a loop or a Doctrine hydrator. Even with a warm cache you're doing repeated string/array lookups and, on cold cache, potentially blocking I/O. Resolve flags once at the top of the request (controller, listener, or a request-scoped service) and pass plain booleans down.

Anonymous users break percentage rollouts. If userId is null and sessionId is null, the SDK has nothing to bucket on and you'll get inconsistent or uniformly-default results. Generate a stable session id early (cookie or session) for logged-out visitors if any flag targets them.

Config key names are the #1 source of "it does nothing." The bundle's documented YAML and the actual Configuration.php drift between versions in small packages like this — and the cache node just changed under you in 0.8.0. When a flag never activates, dump the resolved config (bin/console debug:config featurit) before debugging anything else. Same for the service id — bin/console debug:container featurit confirms what's actually registered and autowirable.

Pin the version. With 4 stars and no dependents, you are effectively the QA team. A ^1.0 constraint is fine, but read the diff before bumping the minor — the underlying PHP SDK has changed method signatures, context constructor arguments, and cache contracts across releases. Lock it in composer.lock and treat upgrades as a deliberate task with a test run, not something a composer update does to you at 5pm on a Friday.

GDPR / data flow. User identifiers and attributes leave your infrastructure for a third-party SaaS on every uncached evaluation. If you're in the EU, that needs to be in your processor list and your privacy notice. Prefer pseudonymous ids over emails, and audit what your context provider is actually sending — it's very easy to accidentally attach an entire user entity. A longer TTL is, marginally, a privacy control too.

Disable it in dev and CI. featurit_enabled: false locally avoids network calls, flaky tests and confusing "why is this flag on?" moments. Combine with the test fake and the array cache so CI never depends on the vendor's uptime or your Redis. Also confirm the bundle doesn't register a listener that fires on every request when disabled — with small packages this is worth a quick profiler check, particularly after a release that touches service wiring.

Extend through services, not forks. The three legitimate extension points are the user-context provider, the cache service (newly first-class in 0.8.0), and your own FeatureFlags wrapper. Anything else you need (multi-tenant flag scoping, flag audit logging, a /flags debug endpoint) belongs in the wrapper, not in a patched vendor file. If you find yourself editing vendor/, that's a signal to add a thin layer above the interface instead.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route