Skip to content

Commit

Permalink
Merge pull request #85 from worksome/feat_overrider_refactor
Browse files Browse the repository at this point in the history
Refactor feature flag overrider architecture to make use of the manager pattern
  • Loading branch information
jeremynikolic committed Jun 19, 2024
2 parents d2201dd + 9a635a0 commit 7479bdc
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 110 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ testbench.yaml
vendor
node_modules
.php-cs-fixer.cache

.phpunit.result.cache
52 changes: 33 additions & 19 deletions config/feature-flags.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
declare(strict_types=1);

use Worksome\FeatureFlags\ModelFeatureFlagConvertor;
use Worksome\FeatureFlags\Overriders\ConfigOverrider;

// config for Worksome/FeatureFlags
return [
Expand All @@ -17,7 +16,7 @@
/**
* Overrides implementing FeatureFlagOverrider contract
*/
'overrider' => ConfigOverrider::class,
'overrider' => 'config',

'providers' => [
'launchdarkly' => [
Expand All @@ -36,24 +35,39 @@
],

/**
* Overrides all feature flags directly without hitting the provider.
* This is particularly useful for running things in the CI,
* e.g. Cypress tests.
* List of available overriders.
* Key is to be used to specify which overrider should be active
*
* Be careful in setting a default value as said value will be applied to all flags.
* Use `null` value if needing the key to be present but act as if it was not
*/
'override-all' => env('FEATURE_FLAGS_OVERRIDE_ALL'),
'overriders' => [
'config' => [
/**
* Overrides all feature flags directly without hitting the provider.
* This is particularly useful for running things in the CI,
* e.g. Cypress tests.
*
* Be careful in setting a default value as said value will be applied to all flags.
* Use `null` value if needing the key to be present but act as if it was not
*/
'override-all' => null,

/**
* Override flags. If a feature flag is set inside an override,
* it will be used instead of the flag set in the provider.
*
* Usage: ['feature-flag-key' => true]
*
* Be careful in setting a default value as it will be applied.
* Use `null` value if needing the key to be present but act as if it was not
*
*/
'overrides' => [
// ...
],
],
'in-memory' => [
// ...
]
],

/**
* Override flags. If a feature flag is set inside an override,
* it will be used instead of the flag set in the provider.
*
* Usage: ['feature-flag-key' => true]
*
* Be careful in setting a default value as it will be applied.
* Use `null` value if needing the key to be present but act as if it was not
*
*/
'overrides' => [],
];
4 changes: 4 additions & 0 deletions src/Contracts/FeatureFlagOverrider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ public function get(FeatureFlagEnum $key): bool;
public function hasAll(): bool;

public function getAll(): bool;

public function set(FeatureFlagEnum $key, bool|null $value): static;

public function setAll(bool|null $value): static;
}
5 changes: 2 additions & 3 deletions src/FeatureFlagsApiManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ class FeatureFlagsApiManager extends Manager
public function createLaunchDarklyDriver(): LaunchDarklyApiProvider
{
$token = $this->config->get('feature-flags.providers.launchdarkly.access-token');
if (! is_string($token)) {
throw new LaunchDarklyMissingAccessTokenException();
}

assert(is_string($token), new LaunchDarklyMissingAccessTokenException());

return new LaunchDarklyApiProvider(
accessToken: $token,
Expand Down
29 changes: 29 additions & 0 deletions src/FeatureFlagsOverriderManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Worksome\FeatureFlags;

use Illuminate\Support\Manager;
use Worksome\FeatureFlags\Overriders\ConfigOverrider;
use Worksome\FeatureFlags\Overriders\InMemoryOverrider;

class FeatureFlagsOverriderManager extends Manager
{
public function createConfigDriver(): ConfigOverrider
{
return new ConfigOverrider(
$this->config,
);
}

public function createInMemoryDriver(): InMemoryOverrider
{
return new InMemoryOverrider();
}

public function getDefaultDriver(): string
{
return strval($this->config->get('feature-flags.overrider')); // @phpstan-ignore-line
}
}
41 changes: 19 additions & 22 deletions src/FeatureFlagsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,10 @@ public function register(): void
'feature-flags'
);

$this->app->extend(FeatureFlagsProviderContract::class, function ($provider, Container $app) {
return $app->makeWith(FeatureFlagsOverrideProvider::class, [
'provider' => $provider,
]);
});
$this->app->singleton(FeatureFlagsManager::class);

$this->app->singleton(FeatureFlagsOverriderManager::class);

$this->app->singleton(
FeatureFlagsManager::class,
static fn (Container $container) => new FeatureFlagsManager($container)
);

$this->app->singleton(FeatureFlagsProviderContract::class, function (Container $app) {
/** @var FeatureFlagsManager $manager */
Expand All @@ -62,6 +56,22 @@ public function register(): void
return $manager->driver();
});

$this->app->singleton(
FeatureFlagOverrider::class,
function (Container $app) {
/** @var FeatureFlagsOverriderManager $manager */
$manager = $app->get(FeatureFlagsOverriderManager::class);

return $manager->driver();
}
);

$this->app->extend(FeatureFlagsProviderContract::class, function ($provider, Container $app) {
return $app->makeWith(FeatureFlagsOverrideProvider::class, [
'provider' => $provider,
]);
});

$this->app->singleton(FeatureFlagUserConvertor::class, function (Container $app) {
/** @var ConfigRepository $config */
$config = $app->get('config');
Expand All @@ -80,19 +90,6 @@ function (Container $app) {
return $manager->driver();
}
);

$this->app->singleton(
FeatureFlagOverrider::class,
function (Container $app) {
/** @var ConfigRepository $config */
$config = $app->get('config');

/** @var class-string<FeatureFlagOverrider> $convertor */
$convertor = $config->get('feature-flags.overrider');

return $app->get($convertor);
}
);
}

public function provides(): array
Expand Down
26 changes: 20 additions & 6 deletions src/Overriders/ConfigOverrider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,40 @@ public function __construct(
*/
public function has(FeatureFlagEnum $key): bool
{
return $this->config->has(sprintf('feature-flags.overrides.%s', $key->value))
&& $this->config->get(sprintf('feature-flags.overrides.%s', $key->value)) !== null;
return $this->config->has(sprintf('feature-flags.overriders.config.overrides.%s', $key->value))
&& $this->config->get(sprintf('feature-flags.overriders.config.overrides.%s', $key->value)) !== null;
}

public function get(FeatureFlagEnum $key): bool
{
return (bool) $this->config->get(sprintf('feature-flags.overrides.%s', $key->value));
return (bool) $this->config->get(sprintf('feature-flags.overriders.config.overrides.%s', $key->value));
}

/**
* Note: null value is considered not present, will return false
*/
public function hasAll(): bool
{
return $this->config->has('feature-flags.override-all')
&& $this->config->get('feature-flags.override-all') !== null;
return $this->config->has('feature-flags.overriders.config.override_all')
&& $this->config->get('feature-flags.overriders.config.override_all') !== null;
}

public function getAll(): bool
{
return (bool) $this->config->get('feature-flags.override-all');
return (bool) $this->config->get('feature-flags.overriders.config.override_all');
}

public function set(FeatureFlagEnum $key, bool|null $value): static
{
$this->config->set(sprintf('feature-flags.overriders.config.overrides.%s', $key->value), $value);

return $this;
}

public function setAll(bool|null $value): static
{
$this->config->set('feature-flags.overriders.config.override_all', $value);

return $this;
}
}
70 changes: 70 additions & 0 deletions src/Overriders/InMemoryOverrider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Worksome\FeatureFlags\Overriders;

use Illuminate\Support\Arr;
use Worksome\FeatureFlags\Contracts\FeatureFlagEnum;
use Worksome\FeatureFlags\Contracts\FeatureFlagOverrider;

class InMemoryOverrider implements FeatureFlagOverrider
{
/**
* @var array<string, bool|null> $overrides
*/
private array $overrides = [];

/**
* @var bool|null $overrideAll
*/
private bool|null $overrideAll = null;

/**
* Note: a flag key with null as value is considered not present, will return false
*/
public function has(FeatureFlagEnum $key): bool
{
return Arr::has($this->overrides, $key->value)
&& Arr::get($this->overrides, $key->value) !== null;
}

public function get(FeatureFlagEnum $key): bool
{
return (bool) Arr::get($this->overrides, $key->value, false);
}

/**
* Note: null value is considered not present, will return false
*/
public function hasAll(): bool
{
return $this->overrideAll !== null;
}

public function getAll(): bool
{
return (bool) $this->overrideAll;
}

public function setAll(bool|null $value = null): static
{
$this->overrideAll = $value;
return $this;
}

public function set(FeatureFlagEnum $key, mixed $value): static
{
Arr::set($this->overrides, $key->value, $value);
return $this;
}

public function overrides(array|null $overriders): array|self
{
if ($overriders) {
$this->overrides = $overriders;
return $this;
}
return $this->overrides;
}
}
40 changes: 38 additions & 2 deletions src/Traits/InteractsWithFeatureFlags.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
namespace Worksome\FeatureFlags\Traits;

use Worksome\FeatureFlags\Contracts\FeatureFlagEnum;
use Worksome\FeatureFlags\Contracts\FeatureFlagOverrider;

/**
* This class is intended for usage mostly in testing context
* It provides the necessary methods to interact with the current feature flag overrider.
* Therefore, easily turning flag ON and OFF
*/
trait InteractsWithFeatureFlags
{
public function switchFeatureFlag(FeatureFlagEnum $key, bool $onOff): void
public function switchFeatureFlag(FeatureFlagEnum $key, bool|null $onOffNull): void
{
$this->app['config']->set("feature-flags.overrides.{$key->value}", $onOff);
$this->featureFlagOverrider()->set($key, $onOffNull);
}

public function enableFeatureFlag(FeatureFlagEnum $key): void
Expand All @@ -22,4 +28,34 @@ public function disableFeatureFlag(FeatureFlagEnum $key): void
{
$this->switchFeatureFlag($key, false);
}

public function switchFeatureFlagAll(bool|null $onOffNull): void
{
$this->featureFlagOverrider()->setAll($onOffNull);
}

public function enableFeatureFlagAll(): void
{
$this->switchFeatureFlagAll(true);
}

public function disableFeatureFlagAll(): void
{
$this->switchFeatureFlagAll(false);
}

public function clearFeatureFlag(FeatureFlagEnum $key): void
{
$this->switchFeatureFlag($key, null);
}

public function clearFeatureFlagAll(): void
{
$this->switchFeatureFlagAll(null);
}

protected function featureFlagOverrider(): FeatureFlagOverrider
{
return $this->app->get(FeatureFlagOverrider::class);
}
}
Loading

0 comments on commit 7479bdc

Please sign in to comment.