Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor feature flag overrider architecture to make use of the manager pattern #85

Merged
merged 2 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
jeremynikolic marked this conversation as resolved.
Show resolved Hide resolved

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