Skip to content

Commit

Permalink
feat: Make overriders definition more extendable
Browse files Browse the repository at this point in the history
Adjust the way config works around overriders so that other overriders may be defined outside the package using the manager pattern
  • Loading branch information
jeremynikolic committed Jun 14, 2024
1 parent d2201dd commit a6dd040
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 132 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
27 changes: 20 additions & 7 deletions src/Overriders/ConfigOverrider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,47 @@
{
public function __construct(
private Repository $config,
) {
)
{
}

/**
* Note: a flag key with null as value is considered not present, will return false
*/
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));
return $this;
}

public function setAll(bool|null $value): static
{
$this->config->set('feature-flags.overriders.config.override_all', $value);
return $this;
}
}
69 changes: 69 additions & 0 deletions src/Overriders/InMemoryOverrider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?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 a6dd040

Please sign in to comment.