Shopware 6 Hidden Gems #4: The feature flag system can do way more than FEATURE_ALL=1
Fabian Blechschmidt
Most Shopware developers know exactly two things about feature flags: there is a feature.yaml, and you can put FEATURE_ALL=1 in your .env to see what breaks. That was my level of knowledge too, until I read Feature.php and FeatureFlagRegistry.php top to bottom for this series. Turns out there is a whole feature management system in there — with database persistence, a REST API, events and a Twig tag. Almost none of it is documented outside of a couple of ADRs.
Let’s go through it, from „nice to know“ to „wait, what?“.
FEATURE_ALL=major
FEATURE_ALL with any truthy value activates all minor flags. But the string major is special-cased (Framework/Feature.php:114-131): it additionally activates the major flags — the breaking-change ones like V6_8_0_0.
# activate all minor flags (experimental features)
FEATURE_ALL=1
# additionally activate major/breaking flags — hello, future
FEATURE_ALL=major
That second line is a one-env-var way to run your CI against next-major behavior today. Individual flags still win over FEATURE_ALL, so you can exclude one flag while enabling everything else:
FEATURE_ALL=major
V6_8_0_0=0
What’s actually in the box (6.7.0.0)
bin/console feature:list shows what’s registered. On a stock 6.7 that includes some interesting disabled minor flags, all declared in Framework/Resources/config/packages/feature.yaml:
Flag
What it gates
PERFORMANCE_TWEAKS
Alternative algorithms in promotion/cart calculation (PromotionCalculator.php:268, CartScopeDiscountPackager.php:52)
FLOW_EXECUTION_AFTER_BUSINESS_PROCESS
Flows execute after the main unit of work instead of inline
TELEMETRY_METRICS
OpenTelemetry metrics support
Each entry has major, toggleable and default properties — and toggleable is where it gets interesting.
Flags can be toggled at runtime — and persisted in the database
This one genuinely surprised me. FeatureFlagRegistry (Framework/Feature/FeatureFlagRegistry.php) merges the static flags from YAML with flags stored in the key-value storage (app_config table, key feature.flags). Toggling goes through enable()/disable() and:
only works for flags declared toggleable: true,
refuses major flags — stored major toggles are even filtered out on load (line 53-55), so nobody can permanently flip a breaking change under your feet,
dispatches BeforeFeatureFlagToggleEvent and FeatureFlagToggledEvent, so your plugin can react (clear caches, warm things up),
persists, so it survives restarts and deployments.
The whole mechanism sits behind shopware.feature_toggle.enable — which defaults to true. This is not an opt-in experiment; it’s on in your shop right now.
GET /api/_action/feature-flag # list all flags + state
POST /api/_action/feature-flag/enable/{feature}
POST /api/_action/feature-flag/disable/{feature}
All three require the ACL privilege api_feature_flag_toggle. So: gradual rollout of an experimental feature in production, from your deployment pipeline, without touching .env, without a restart. We have built similar switches in the past for Magento 1 — Shopware ships a small version of it and tells nobody.
# try the performance tweaks on the live shop, revert if the numbers look bad
curl -X POST https://shop.example/api/_action/feature-flag/enable/PERFORMANCE_TWEAKS \
-H "Authorization: Bearer $TOKEN"
The supporting cast
A few smaller pieces that make the system rounder than expected:
{% sw_silent_feature_call %} — a Twig tag (Framework/Adapter/Twig/TokenParser/FeatureFlagCallTokenParser.php) that wraps template code and suppresses deprecation warnings while a flag is inactive:
{% sw_silent_feature_call "v6.8.0.0" %}
{# legacy template code that would spam deprecations #}
{% endsw_silent_feature_call %}
Symfony profiler integration — core/Profiling/FeatureFlag/FeatureFlagProfiler.php puts all flags with state and description into the debug toolbar. No console round-trip to check „is the flag on in this request?“.
Test helpers — Feature::fake() swaps the entire flag registry for a closure and restores it afterwards; the PHPUnit attribute #[DisabledFeatures(['v6.8.0.0'])] (from core/Test/Annotation) disables flags per test with automatic cleanup.
Flexible naming — Feature::normalizeName() means v6.8.0.0, V6_8_0_0 and v6_8_0_0 are all the same flag. One less way to shoot yourself in the foot.
Custom flags for your own project
Since flags are just config, your project (or plugin config) can declare its own:
Now your code gates on Feature::isActive('ACME_NEW_CHECKOUT'), QA enables it per API call on staging, and the rollout on production is one POST request — reversible with another one. In dev mode, checking an unregistered flag even triggers a warning (Feature.php:102-107), which catches typos before they become „why is the feature never active“.
TL;DR
The feature flag system is a real feature management tool: FEATURE_ALL=major for next-major CI runs, toggleable: true flags persist runtime toggles in the database (major flags are protected), GET/POST /api/_action/feature-flag/* manages them remotely behind the api_feature_flag_toggle ACL, and there’s a Twig tag, profiler panel and test attributes to go with it. Documented in two ADRs and zero guides.
Next up: turning down the noise — the four undocumented shopware.logger.* options that decide what lands in your logs.
If you’re using flags for rollouts already, I’d love to hear how — drop me an email!
Found in Shopware 6.7.0.0: Framework/Feature.php:114-131 (FEATURE_ALL), Framework/Feature/FeatureFlagRegistry.php:36-105 (persistence + events), Framework/Api/Controller/FeatureFlagController.php:26-46 (REST API), Framework/DependencyInjection/Configuration.php:793-795 (feature_toggle.enable, default true), Framework/Adapter/Twig/TokenParser/FeatureFlagCallTokenParser.php (Twig tag).