Skip to content

Commit

Permalink
NEXT-12133 - Extend feature flags with additional meta data
Browse files Browse the repository at this point in the history
  • Loading branch information
pweyck committed Nov 30, 2020
1 parent 367ad3f commit 1a949e8
Show file tree
Hide file tree
Showing 10 changed files with 546 additions and 82 deletions.
4 changes: 2 additions & 2 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4847,12 +4847,12 @@ parameters:

-
message: "#^Cannot call method end\\(\\) on Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\|null\\.$#"
count: 5
count: 4
path: src/Core/Framework/DependencyInjection/Configuration.php

-
message: "#^Cannot call method booleanNode\\(\\) on Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\|null\\.$#"
count: 1
count: 2
path: src/Core/Framework/DependencyInjection/Configuration.php

-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public function process(ContainerBuilder $container): void
{
$services = $container->findTaggedServiceIds('shopware.feature');

Feature::registerFeatures($container->getParameter('shopware.feature.flags'));

foreach ($services as $serviceId => $tags) {
foreach ($tags as $tag) {
if (!isset($tag['flag'])) {
Expand Down
25 changes: 24 additions & 1 deletion src/Core/Framework/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,31 @@ private function createFeatureSection(): ArrayNodeDefinition
$rootNode
->children()
->arrayNode('flags')
->prototype('scalar')->end()
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('name')->end()
->booleanNode('default')->defaultFalse()->end()
->booleanNode('major')->defaultFalse()->end()
->scalarNode('description')->end()
->end()
->end()
->beforeNormalization()
->always()->then(function ($flags) {
foreach ($flags as $key => $flag) {
// support old syntax
if (is_int($key) && is_string($flag)) {
unset($flags[$key]);

$flags[] = [
'name' => $flag,
];
}
}

return $flags;
})
->end()
->end();

return $rootNode;
Expand Down
125 changes: 96 additions & 29 deletions src/Core/Framework/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,12 @@

class Feature
{
/**
* @var string[]
*/
private static $registeredFeatures;
public const ALL_MAJOR = 'major';

/**
* @internal
* @var array[]
*/
public static function setRegisteredFeatures(iterable $registeredFeatures, string $dumpPath): void
{
self::$registeredFeatures = [];
foreach ($registeredFeatures as $flag) {
$flag = self::normalizeName($flag);
self::$registeredFeatures[$flag] = $flag;
}

self::dumpFeatures($dumpPath);
}
private static $registeredFeatures = [];

public static function normalizeName(string $name): string
{
Expand All @@ -48,11 +36,6 @@ public static function normalizeName(string $name): string
public static function isActive(string $feature): bool
{
$env = $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'prod';
$allEnabled = $_SERVER['FEATURE_ALL'] ?? false;
if ($allEnabled && $allEnabled !== 'false' && $allEnabled !== '0' && $allEnabled !== '') {
return true;
}

$feature = self::normalizeName($feature);

if (self::$registeredFeatures !== null
Expand All @@ -62,16 +45,25 @@ public static function isActive(string $feature): bool
trigger_error('Unknown feature "' . $feature . '"', E_USER_WARNING);
}

if (!isset($_SERVER[$feature])) {
return false;
$featureAll = $_SERVER['FEATURE_ALL'] ?? '';
if (self::isTrue((string) $featureAll) && array_key_exists($feature, self::$registeredFeatures ?? [])) {
if ($featureAll === Feature::ALL_MAJOR) {
return true;
}

// return true if it's registered and not a major feature
if (self::$registeredFeatures[$feature]['major'] === false) {
return true;
}
}

$value = trim($_SERVER[$feature]);
if (!array_key_exists($feature, $_SERVER)) {
$fallback = self::$registeredFeatures[$feature]['default'] ?? false;

return $value
&& $value !== 'false'
&& $value !== '0'
&& $value !== '';
return (bool) $fallback;
}

return self::isTrue(trim($_SERVER[$feature]));
}

public static function ifActive(string $flagName, \Closure $closure): void
Expand Down Expand Up @@ -110,13 +102,88 @@ public static function getAll(): array
{
$resolvedFlags = [];

foreach (self::$registeredFeatures as $name) {
foreach (self::$registeredFeatures as $name => $_) {
$resolvedFlags[$name] = self::isActive($name);
}

return $resolvedFlags;
}

/**
* @internal
*/
public static function registerFeature(string $name, array $metaData = []): void
{
// merge with existing data
$metaData = array_merge(
self::$registeredFeatures[$name] ?? [],
$metaData
);

// set defaults
$metaData['major'] = (bool) ($metaData['major'] ?? false);
$metaData['default'] = (bool) ($metaData['default'] ?? false);
$metaData['description'] = (string) ($metaData['description'] ?? '');

self::$registeredFeatures[$name] = $metaData;
}

/**
* @internal
*/
public static function registerFeatures(iterable $registeredFeatures, ?string $dumpPath = null): void
{
foreach ($registeredFeatures as $flag => $data) {
// old format
if (is_string($data)) {
$flag = $data;
$data = [];
}

self::registerFeature($flag, $data);
}

if ($dumpPath !== null) {
self::dumpFeatures($dumpPath);
}
}

/**
* @internal
*
* @deprecated tag:v6.4.0.0 Use `Feature::resetRegisteredFeatures` and `Feature::registerFeatures`
*/
public static function setRegisteredFeatures(iterable $registeredFeatures, ?string $dumpPath = null): void
{
self::resetRegisteredFeatures();
self::registerFeatures($registeredFeatures, $dumpPath);
}

/**
* @internal
*/
public static function resetRegisteredFeatures(): void
{
self::$registeredFeatures = [];
}

/**
* @internal
*/
public static function getRegisteredFeatures(): array
{
return self::$registeredFeatures;
}

private static function isTrue(string $value): bool
{
/* @var mixed $value */
return $value
&& $value !== 'false'
&& $value !== '0'
&& $value !== '';
}

private static function dumpFeatures(string $dumpPath): void
{
$env = $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'prod';
Expand All @@ -126,7 +193,7 @@ private static function dumpFeatures(string $dumpPath): void
}

$values = [];
foreach (self::$registeredFeatures as $flag) {
foreach (self::$registeredFeatures as $flag => $_) {
$values[$flag] = self::isActive($flag);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Core/Framework/Framework.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function boot(): void
{
parent::boot();

Feature::setRegisteredFeatures(
Feature::registerFeatures(
$this->container->getParameter('shopware.feature.flags'),
$this->container->getParameter('kernel.cache_dir') . '/shopware_features.php'
);
Expand Down
37 changes: 28 additions & 9 deletions src/Core/Framework/Resources/config/packages/shopware.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,31 @@ shopware:

feature:
flags:
- FEATURE_NEXT_1797
- FEATURE_NEXT_10549
- FEATURE_NEXT_10559
- FEATURE_NEXT_10078
- FEATURE_NEXT_11389
- FEATURE_NEXT_10536
- FEATURE_NEXT_7453
- FEATURE_NEXT_8172
- FEATURE_NEXT_10539
- name: FEATURE_NEXT_1797
default: false
major: true
description: "composer require for plugins"
- name: FEATURE_NEXT_7453
major: true
description: "use custom name prefix for nested routes"
- name: FEATURE_NEXT_8172
major: true
description: "prevent plugins to extend the first level menu entries"
- name: FEATURE_NEXT_10549
major: false
description: "save cart or products on wishlist"
- name: FEATURE_NEXT_10559
major: false
description: "vat handling for companies"
- name: FEATURE_NEXT_10078
major: false
description: "cms product page layouts"
- name: FEATURE_NEXT_11389
major: false
description: "layout assignment in CMS module"
- name: FEATURE_NEXT_10536
major: false
description: "filter settings per category"
- name: FEATURE_NEXT_10539
major: false
description: "prevent deletion of used rules"
Loading

0 comments on commit 1a949e8

Please sign in to comment.