PHPackages                             gracious/feature-flag-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. gracious/feature-flag-bundle

ActiveSymfony-bundle[Utility &amp; Helpers](/categories/utility)

gracious/feature-flag-bundle
============================

Lightweight feature flag management for Symfony applications.

0.1(1mo ago)00MITPHPPHP &gt;=8.3

Since Jun 2Pushed 1mo agoCompare

[ Source](https://github.com/graciousagency/feature-flag)[ Packagist](https://packagist.org/packages/gracious/feature-flag-bundle)[ RSS](/packages/gracious-feature-flag-bundle/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (15)Versions (2)Used By (0)

Gracious Feature Flag Bundle
============================

[](#gracious-feature-flag-bundle)

Lightweight feature flags for Symfony. Define flags in config, then check them from services, controllers, Twig, routes, and controller attributes.

- PHP `>=8.3`, Symfony `^7.0 || ^8.0`, Twig 3 (optional).

**Features**

- Config-driven flags with optional descriptions.
- One default manager plus any number of named managers (separate flag groups).
- Check flags in PHP, Twig, route defaults, or `#[RequireFeature]` attributes.
- Per-process runtime overrides (`enable` / `disable` / `reset`).
- Optional read-only REST endpoint with a kill switch.

Installation
------------

[](#installation)

```
composer require gracious/feature-flag-bundle
```

With Symfony Flex this is all you need. Without Flex, register the bundle manually:

```
// config/bundles.php
return [
    // ...
    Gracious\FeatureFlagBundle\GraciousFeatureFlagBundle::class => ['all' => true],
];
```

Quick start
-----------

[](#quick-start)

```
# config/packages/gracious_feature_flag.yaml
gracious_feature_flag:
    flags:
        new_checkout:
            enabled: true
            description: 'New checkout flow'
        beta_search:
            enabled: false   # 'enabled' defaults to false
```

```
use Gracious\FeatureFlagBundle\Flag\FeatureFlagManagerInterface;

final class CheckoutService
{
    public function __construct(private FeatureFlagManagerInterface $flags) {}

    public function run(): void
    {
        if ($this->flags->isEnabled('new_checkout')) {
            // new flow
        }
    }
}
```

Configuration
-------------

[](#configuration)

```
gracious_feature_flag:
    # default manager flags
    flags:
        new_checkout: { enabled: true, description: 'New checkout flow' }
        beta_search:  { enabled: false }

    # optional: extra named managers, each its own flag group
    managers:
        billing:
            flags:
                invoices_v2: { enabled: true }

    # optional: exception thrown when a guard fails (route / attribute)
    exception:
        class: Gracious\FeatureFlagBundle\Exception\FeatureNotAvailableException
        status_code: 404
        factory: ~       # service id implementing ExceptionFactoryInterface; wins over 'class'

    # optional: REST endpoint kill switch
    api:
        enabled: true    # false => endpoints return 404
```

Manager API
-----------

[](#manager-api)

```
$flags->isEnabled('new_checkout'); // bool
$flags->has('new_checkout');       // bool: flag is defined
$flags->get('new_checkout');       // Flag VO (name, enabled, description)
$flags->all();                     // array

// runtime overrides (this PHP process only; see Limitations)
$flags->enable('beta_search');
$flags->disable('new_checkout');
$flags->reset('beta_search');      // back to the configured value
```

Unknown flag names throw `UnknownFeatureException`.

### Named managers

[](#named-managers)

Each named manager is its own service. Autowire it by the variable name `Manager`:

```
public function __construct(FeatureFlagManagerInterface $billingManager) {}
```

To resolve a manager by name at runtime, inject the `ManagerRegistry`:

```
use Gracious\FeatureFlagBundle\Flag\ManagerRegistry;

public function __construct(private ManagerRegistry $registry) {}

$this->registry->get('billing')->isEnabled('invoices_v2');
$this->registry->getDefault()->isEnabled('new_checkout');
```

Unknown manager names throw `UnknownManagerException`.

Twig
----

[](#twig)

```
{% if feature('new_checkout') %}
    Try the new checkout
{% endif %}

{# named manager (second argument) #}
{% if feature('invoices_v2', 'billing') %} ... {% endif %}

{# as a test #}
{% if 'beta_search' is feature_enabled %} ... {% endif %}
```

The Twig extension registers only when Twig is installed.

Route guards
------------

[](#route-guards)

Guard a route with the `_feature_flag` default. The string form requires the flag enabled:

```
beta_page:
    path: /beta
    controller: App\Controller\BetaController
    defaults:
        _feature_flag: beta_search
```

The array form sets the required state and an optional manager:

```
legacy_page:
    path: /legacy
    controller: App\Controller\LegacyController
    defaults:
        _feature_flag: { name: legacy, enabled: false, manager: default }
```

When the requirement is not met, the configured exception is thrown (404 by default).

Attribute guards
----------------

[](#attribute-guards)

`#[RequireFeature]` works on a controller class or method and is repeatable:

```
use Gracious\FeatureFlagBundle\Attribute\RequireFeature;

#[RequireFeature('new_checkout')]                 // class: require enabled
final class CheckoutController
{
    #[RequireFeature('legacy', enabled: false)]   // require disabled
    public function index(): Response { /* ... */ }

    #[RequireFeature('invoices_v2', manager: 'billing')]
    public function invoices(): Response { /* ... */ }
}
```

Custom exception
----------------

[](#custom-exception)

For full control over the failure response, provide a factory service:

```
use Gracious\FeatureFlagBundle\Exception\ExceptionFactoryInterface;
use Gracious\FeatureFlagBundle\Flag\Flag;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

final class AccessDeniedExceptionFactory implements ExceptionFactoryInterface
{
    public function create(Flag $flag, bool $required): \Throwable
    {
        return new AccessDeniedHttpException(
            sprintf('Feature "%s" gate failed.', $flag->name),
        );
    }
}
```

```
gracious_feature_flag:
    exception:
        factory: App\FeatureFlag\AccessDeniedExceptionFactory
```

Alternatively set `exception.class` to any class with a `(string $name, int $statusCode)`constructor. A `factory` always wins over `class`.

REST endpoint
-------------

[](#rest-endpoint)

The endpoint is read-only and opt-in. Import the routes to enable it:

```
# config/routes/gracious_feature_flag.yaml
feature_flags:
    resource: '@GraciousFeatureFlagBundle/config/routes.php'
    prefix: /_feature-flags
    trailing_slash_on_root: false
```

MethodPathDescriptionGET`/_feature-flags`list all flags (default manager)GET`/_feature-flags/{name}`read a single flagBoth accept `?manager=`. Unknown flag or manager returns 404.

```
curl http://localhost/_feature-flags
# [{"name":"new_checkout","enabled":true,"description":"New checkout flow"}]
```

`trailing_slash_on_root: false` keeps the list route at `/_feature-flags` so the request returns 200 directly instead of a 301 redirect to `/_feature-flags/`.

**Disabling the endpoint.** Set `api.enabled: false`. Both routes then return 404 even if still imported, a hard kill switch independent of routing.

> **Security:** these endpoints expose flag names and states and are not protected by the bundle. Restrict the prefix to a dev/internal firewall or guard the import with access control before exposing it in production.

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance90

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

52d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/21ae8d3f83f03a3c3a6f31de17ec1964ae26f1af703069284f650ed676211d4c?d=identicon)[gracious](/maintainers/gracious)

---

Top Contributors

[![michaelgracious](https://avatars.githubusercontent.com/u/24290969?v=4)](https://github.com/michaelgracious "michaelgracious (2 commits)")

---

Tags

symfonybundlefeature togglefeature flag

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/gracious-feature-flag-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/gracious-feature-flag-bundle/health.svg)](https://phpackages.com/packages/gracious-feature-flag-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M400](/packages/easycorp-easyadmin-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
