Open Source · Symfony Bundle

Shizuku Feature Flags.

Feature flag management for Symfony, backed by Doctrine ORM. zero configuration, Profiler integration.

PHP ≥ 8.2 Symfony 7.4 · 8.0 Doctrine ORM 3.x MIT
$ composer require devexploris/shizuku-feature-flags
01 — Introduction

Only Doctrine and cache

Shizuku stores your feature flags in the database via Doctrine ORM. No external service, no YAML file.

Origin story
In my job, every feature lived on its own long-lived branch. Merge day was a war — conflicts everywhere, deploys blocked, everyone waiting on everyone. The real problem wasn't the code, it was the coupling between shipping and toggling. I needed a way to merge early, deploy often, and decide when to switch a feature on without depending on an external dashboard, a third-party service, or a YAML file to redeploy.

So I built Shizuku. A flag in the database, a console command, and nothing else in the way.
Zero config
Cache and Profiler wire up automatically if the Symfony packages are present.
Full lifecycle
Create · Enable · Disable · Lock · Delete with explicit safeguards at every step.
Dev tooling
Profiler panel, Twig function feature(), cache TTL, caller-chain tracking.
02 — Requirements

Dependencies

PackageVersionRole
php≥ 8.2Required by Symfony 7.4+
symfony/http-kernel^7.4 · ^8.0AbstractBundle, DataCollector
symfony/console^7.4 · ^8.0CLI commands
doctrine/orm^3.0Entity persistence
doctrine/doctrine-bundle^2.0 · ^3.0ServiceEntityRepository
twig/twig^3.0Function feature()
symfony/cache-contracts^3.0Cache interface (optional)
Optional: symfony/cache for flag caching and symfony/web-profiler-bundle for the Profiler panel — wired up automatically if present.
03 — Installation

Setup

01 — Require

$ composer require devexploris/shizuku-feature-flags

02 — Register the bundle

// config/bundles.php
return [
    Devexploris\ShizukuFeatureFlags\ShizukuFeatureFlagsBundle::class => ['all' => true],
];

03 — Create the table

The bundle ships no migrations. Generate one from your application:

$ php bin/console doctrine:migrations:diff
$ php bin/console doctrine:migrations:migrate
04 — Usage

Usage

PHP

Inject FeatureFlagService and call isEnabled(). An unknown flag always returns false.

use Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService;

class MyService
{
    public function __construct(private FeatureFlagService $flags) {}

    public function doSomething(): void
    {
        if ($this->flags->isEnabled('my_feature')) {
            // do something
        }
    }
}

Twig

The bundle registers the global function feature(). Unknown flags silently return false — no exception.

{% if feature('my_feature') %}
    {# nouveau comportement #}
{% endif %}

{# inline — ex. bascule de classe CSS #}
Unknown flag? If the flag is missing from the database, isEnabled() and feature() return false. The Symfony Profiler reports it in its panel to catch silent typos.
05 — Commands

Console commands

commands

Flag names must be snake_case: lowercase letters, digits and underscores, starting with a letter.

List

$ php bin/console shizuku:list [filter]

filter is optional. All, Enabled, Disabled, Locked. Without argument, an interactive prompt is shown.

Create

$ php bin/console shizuku:flag:create
$ php bin/console shizuku:flag:create --name=my_feature --description="My feature" --enable

Flags are created disabled by default. Pass --enable to start them active.

Enable / Disable

$ php bin/console shizuku:flag:enable --name=my_feature
$ php bin/console shizuku:flag:disable --name=my_feature

Locked flags cannot be enabled or disabled.

Lock

$ php bin/console shizuku:flag:lock --name=my_feature

Locking freezes the flag state permanently. It can no longer be enabled or disabled. This is a one-way operation: there is no unlock command. A locked flag signals that its feature has reached its final state and that the surrounding isEnabled() calls are now dead weight to be removed from the codebase.

Delete

$ php bin/console shizuku:flag:delete --name=my_feature
$ php bin/console shizuku:flag:delete --name=my_feature --force

Only locked flags can be deleted by default. --force bypasses the restriction (with confirmation).

⚠ Clean your code before deleting

Deleting a flag does not remove its calls from your code.
Any remaining isEnabled('my_feature') or feature('my_feature') will silently return false once the flag is gone, your feature will appear broken with no error, no exception, no log.

A locked+enabled flag is not meant to live in the codebase forever. Remove every if (isEnabled(...)) section, then delete the flag. The Profiler panel lists locked flags as a reminder until they are gone.

06 — Lifecycle

Flag lifecycle

Created
disabled
Enabled
actif
Disabled
inactif
Locked
gelé
Deleted
supprimé
01Flag created disabled by default (or active with --enable).
02Can be freely toggled between enabled and disabled until locked.
03Once locked, the state is frozen and cannot be changed. No unlock exists. The feature is considered shipped or abandoned — the flag is now technical debt.
04Clean the code — remove every isEnabled() and feature() call. Keep the winning code path, delete the losing one.
05Only then, delete the flag. A flag deleted without code cleanup will silently return false — the feature breaks with no warning.
07 — Caching

Caching

When cache.app is available in the container, the bundle wires it up automatically. Flag state is cached for 300 seconds and immediately invalidated on any mutating command (enable, disable, lock, delete, create).

Custom pool

# config/services.yaml
Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService:
    arguments:
        $cache: '@cache.my_custom_pool'

Disable cache

# config/services.yaml
Devexploris\ShizukuFeatureFlags\Service\FeatureFlagService:
    arguments:
        $cache: ~
Direct database changes bypass cache invalidation. Changes will be visible after the TTL expires (300 s).
08 — Profiler

Symfony Profiler

When symfony/web-profiler-bundle is installed, a Feature Flags panel is added to the toolbar automatically.

ElementDescription
Checked / enabledNumber of isEnabled() calls and how many returned true.
Locked flagsFrozen flags awaiting cleanup, displayed as "Must be cleaned before removal".
Unknown flagsFlags called in code but missing from the database. The toolbar highlights in orange.
Caller chainFull Class::method chain that triggered each isEnabled(), vendor frames excluded.
09 — Entity Reference

Entity Reference

PropertyTypeDescription
namestringUnique snake_case identifier, validated at creation.
descriptionstringHuman-readable label.
isEnabledboolWhether the flag is currently active.
isLockedboolWhether the flag is frozen awaiting cleanup.
createdAtDateTimeImmutableSet automatically on persistence.
enabledAtDateTimeImmutable|nullSet on first activation, reset to null on disable.
lockedAtDateTimeImmutable|nullSet when locked.