Skip to content

Installation

phil/feature-flags-bundle

This page takes a bare application to a working bundle: requirements, Composer, the bundle class, the database tables, the admin routes and the firewall in front of them. The full list of options is in configuration.md.

Requirements

RequirementVersionWhy
PHP8.5 or laterDeclared as >=8.5; the code uses typed class constants, readonly classes and #[Override].
ext-mbstringanyReasons, actors, descriptions and owners are checked as UTF-8 and measured in characters.
ext-hashbundled with PHPThe stable buckets of rollouts and experiments use the murmur3a algorithm of hash(). It is always compiled in since PHP 7.4, so it is not declared.
doctrine/dbal^4.5The database store and its schema.
doctrine/orm^3.7The schema listener hooks into the ORM's postGenerateSchema event.
phil/circuit-breaker, phil/circuit-breaker-bundle^0.1@devThe CircuitOverride port the force-open flags implement, and the circuits whose keys declare those flags.
phil/ddd-bundle^0.1@devThe InvalidValue and DomainError exceptions of the domain.
phil/enums^0.3@devAsValuableEnumTrait on FlagKind and TargetingAttribute, whose values() writes the error messages and the kinds of the admin form.
psr/clock^1.0Every timestamp (audit lines, expiry) comes from a PSR-20 clock.
symfony/config, console, dependency-injection, http-foundation, http-kernel, routing^8.2Configuration tree, listing command, wiring, admin controller, controller attribute, route condition.
twig/twig^3.21The admin templates and the #[AsTwigFunction] functions.

Doctrine DBAL 4.5. FlagTables builds its tables with Schema::edit() and the table, column and primary key editors, and FlagSchemaListener hands the schema back with GenerateSchemaEventArgs::setSchema(), which throws unless Schema::edit() exists (DBAL 4.5 and later), which is why the bundle requires ^4.5.

The bundle registers its services with tags that other bundles act on, so in practice the application also has:

BundleNeeded for
symfony/framework-bundleThe default clock service (with symfony/clock), the router service the admin uses, kernel.reset, console.command, kernel.event_listener and routing.condition_service tags.
symfony/twig-bundleThe twig service of the admin, and the twig.attribute_extension / twig.runtime tags of the Twig functions.
doctrine/doctrine-bundleOnly with the database store: the doctrine.dbal.<name>_connection service and the doctrine.event_listener tag.
symfony/expression-languageOnly for route conditions (condition: "service('feature_flags')…").

Databases: the database store is tested on SQLite, MySQL 8 and PostgreSQL.

Install with Composer

composer require phil/feature-flags-bundle

The bundle requires phil/ddd-bundle, phil/circuit-breaker and phil/circuit-breaker-bundle at a development stability (^0.1@dev). Until they are published, the application declares a path or vcs repository for them and accepts @dev for these packages.

Register the bundle

With Symfony Flex the bundle is added to config/bundles.php for you; otherwise add it by hand, after the bundles it relies on:

// config/bundles.php
return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],   // database store only
    Phil\CircuitBreakerBundle\PhilCircuitBreakerBundle::class => ['all' => true],   // circuits forced open by a flag
    Phil\FeatureFlagsBundle\PhilFeatureFlagsBundle::class => ['all' => true],
];

Without PhilCircuitBreakerBundle, no circuit is declared and no force-open flag either; the flags work alike.

The smallest configuration

The configuration key is phil_feature_flags. With no adapter at all, flags declared in YAML and an empty context:

# config/packages/phil_feature_flags.yaml
phil_feature_flags:
    context_resolver: phil_feature_flags.context_resolver.anonymous
    flags:
        new-checkout: true

Without context_resolver, the bundle expects a service named after the Phil\FeatureFlagsBundle\Application\Flag\EvaluationContextResolver interface: implement it in src/ (with autowiring, a single implementation is aliased to the interface) — see extension-points.md.

The database store and its migration

Enable the store to keep active states and admin-created flags in the database:

phil_feature_flags:
    doctrine:
        connection: default     # a DBAL connection name of DoctrineBundle

The store owns two tables, phil_feature_flags_definition and phil_feature_flags_state (database-store.md describes their columns). The bundle does not ship a migration: FlagSchemaListener adds both tables to the schema the ORM generates for that connection, so the usual tools see them:

bin/console doctrine:migrations:diff        # writes the migration that creates the two tables
bin/console doctrine:migrations:migrate
bin/console doctrine:schema:validate        # does not ask to drop them

The listener only runs when the ORM's schema tool runs for an entity manager on that same connection. An application without an entity manager on that connection creates the tables from the same definition:

use Doctrine\DBAL\Schema\Schema;
use Phil\FeatureFlagsBundle\Infrastructure\Doctrine\FlagTables;

foreach (FlagTables::configure(new Schema())->toSql($connection->getDatabasePlatform()) as $sql) {
    $connection->executeStatement($sql);
}

The toggle

Changing a flag at runtime needs the audited toggle, which needs a state store (the database store gives one) and two adapters of the application, an authorizer and an auditor:

phil_feature_flags:
    doctrine: true
    toggle:
        authorizer: App\Flags\PermissionAuthorizer
        auditor: App\Flags\AuditTrailAuditor

See toggles-and-edits.md and extension-points.md.

The admin routes

Enable the screens (they need the toggle) and name the service that says who the operator is:

phil_feature_flags:
    admin:
        actor_provider: App\Flags\OperatorProvider

Then import the bundle's routes under a prefix of your choice:

# config/routes/phil_feature_flags.yaml
phil_feature_flags_admin:
    resource: '@PhilFeatureFlagsBundle/config/routes/admin.php'
    prefix: /admin/flags

The five routes it declares are listed in admin.md.

The firewall in front of the admin

The bundle does not authenticate anyone. Put the prefix behind the application's firewall and require a role:

# config/packages/security.yaml
security:
    access_control:
        - { path: ^/admin/flags, roles: ROLE_FLAG_OPERATOR }

Inside, the ActorProvider names the operator (a screen answers 403 when it returns null), the authorizer decides each change, and every form post is checked as same-origin. Details in admin.md.

Check the installation

bin/console debug:config phil_feature_flags
bin/console phil:feature-flags:list

The listing shows every flag the configuration and the database declare, with its state and source (console.md).