diff --git a/.gitignore b/.gitignore index 6127d06..f5004aa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ testbench.yaml vendor node_modules .php-cs-fixer.cache + +.phpunit.result.cache diff --git a/config/feature-flags.php b/config/feature-flags.php index 539b04a..cea4d32 100644 --- a/config/feature-flags.php +++ b/config/feature-flags.php @@ -3,7 +3,6 @@ declare(strict_types=1); use Worksome\FeatureFlags\ModelFeatureFlagConvertor; -use Worksome\FeatureFlags\Overriders\ConfigOverrider; // config for Worksome/FeatureFlags return [ @@ -17,7 +16,7 @@ /** * Overrides implementing FeatureFlagOverrider contract */ - 'overrider' => ConfigOverrider::class, + 'overrider' => 'config', 'providers' => [ 'launchdarkly' => [ @@ -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' => [], ]; diff --git a/src/Contracts/FeatureFlagOverrider.php b/src/Contracts/FeatureFlagOverrider.php index 48f365a..e107724 100644 --- a/src/Contracts/FeatureFlagOverrider.php +++ b/src/Contracts/FeatureFlagOverrider.php @@ -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; } diff --git a/src/FeatureFlagsApiManager.php b/src/FeatureFlagsApiManager.php index fb8b0cc..25d8397 100644 --- a/src/FeatureFlagsApiManager.php +++ b/src/FeatureFlagsApiManager.php @@ -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, diff --git a/src/FeatureFlagsOverriderManager.php b/src/FeatureFlagsOverriderManager.php new file mode 100644 index 0000000..f3093eb --- /dev/null +++ b/src/FeatureFlagsOverriderManager.php @@ -0,0 +1,29 @@ +config, + ); + } + + public function createInMemoryDriver(): InMemoryOverrider + { + return new InMemoryOverrider(); + } + + public function getDefaultDriver(): string + { + return strval($this->config->get('feature-flags.overrider')); // @phpstan-ignore-line + } +} diff --git a/src/FeatureFlagsServiceProvider.php b/src/FeatureFlagsServiceProvider.php index 320eb15..f7c1ea3 100644 --- a/src/FeatureFlagsServiceProvider.php +++ b/src/FeatureFlagsServiceProvider.php @@ -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 */ @@ -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'); @@ -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 $convertor */ - $convertor = $config->get('feature-flags.overrider'); - - return $app->get($convertor); - } - ); } public function provides(): array diff --git a/src/Overriders/ConfigOverrider.php b/src/Overriders/ConfigOverrider.php index 82056fd..0438d5c 100644 --- a/src/Overriders/ConfigOverrider.php +++ b/src/Overriders/ConfigOverrider.php @@ -20,13 +20,13 @@ 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)); } /** @@ -34,12 +34,26 @@ public function get(FeatureFlagEnum $key): bool */ 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; } } diff --git a/src/Overriders/InMemoryOverrider.php b/src/Overriders/InMemoryOverrider.php new file mode 100644 index 0000000..bbe1298 --- /dev/null +++ b/src/Overriders/InMemoryOverrider.php @@ -0,0 +1,70 @@ + $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; + } +} diff --git a/src/Traits/InteractsWithFeatureFlags.php b/src/Traits/InteractsWithFeatureFlags.php index d1a1ce3..206c032 100644 --- a/src/Traits/InteractsWithFeatureFlags.php +++ b/src/Traits/InteractsWithFeatureFlags.php @@ -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 @@ -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); + } } diff --git a/tests/Feature/ArrayOverriderTest.php b/tests/Feature/ArrayOverriderTest.php new file mode 100644 index 0000000..5cb5427 --- /dev/null +++ b/tests/Feature/ArrayOverriderTest.php @@ -0,0 +1,100 @@ +inMemoryOverrider = new InMemoryOverrider(); +}); + +test('has returns false if override key is not present', function () { + expect($this->inMemoryOverrider->has(TestFeatureFlag::TestFlag))->toBeFalse(); +}); + +test('has returns false if override key is present but null', function () { + $this->inMemoryOverrider->set(TestFeatureFlag::TestFlag, null); + expect($this->inMemoryOverrider->has(TestFeatureFlag::TestFlag))->toBeFalse(); +}); + +test('has returns true if override key is present with truthy value', function ($value) { + $this->inMemoryOverrider->set(TestFeatureFlag::TestFlag, $value); + expect($this->inMemoryOverrider->has(TestFeatureFlag::TestFlag))->toBeTrue(); +})->with([ + true, + 1, + 1.0, + 'test', + [1], +]); + +test('has returns true if override key is present with falsy value', function ($value) { + $this->inMemoryOverrider->set(TestFeatureFlag::TestFlag, $value); + expect($this->inMemoryOverrider->has(TestFeatureFlag::TestFlag))->toBeTrue(); +})->with([ + false, + 0, + 0.0, + '', + '0', + [[]], +]); + +test('get returns true if override key is present with truthy value', function ($value) { + $this->inMemoryOverrider->set(TestFeatureFlag::TestFlag, $value); + expect($this->inMemoryOverrider->get(TestFeatureFlag::TestFlag))->toBeTrue(); +})->with([ + true, + 1, + 1.0, + 'test', + [1], +]); + +test('get returns false if override key is present with falsy value', function ($value) { + $this->inMemoryOverrider->set(TestFeatureFlag::TestFlag, $value); + expect($this->inMemoryOverrider->get(TestFeatureFlag::TestFlag))->toBeFalse(); +})->with([ + null, + false, + 0, + 0.0, + '', + '0', + [[]], +]); + +test('get returns false if override key is not present', function () { + expect($this->inMemoryOverrider->get(TestFeatureFlag::TestFlag))->toBeFalse(); +}); + +test('getAll returns true if override key is present with truthy value', function ($value) { + $this->inMemoryOverrider->set(TestFeatureFlag::TestFlag, $value); + expect($this->inMemoryOverrider->get(TestFeatureFlag::TestFlag))->toBeTrue(); +})->with([ + true, + 1, + 1.0, + 'test', + [1], +]); + +test('getAll returns false if override key is present with falsy value', function ($value) { + $this->inMemoryOverrider->set(TestFeatureFlag::TestFlag, $value); + expect($this->inMemoryOverrider->get(TestFeatureFlag::TestFlag))->toBeFalse(); +})->with([ + null, + false, + 0, + 0.0, + '', + '0', + [[]], +]); + +test('getAll returns false if override key is not present', function () { + expect($this->inMemoryOverrider->get(TestFeatureFlag::TestFlag))->toBeFalse(); +}); diff --git a/tests/Feature/OverridesTest.php b/tests/Feature/ConfigOverriderTest.php similarity index 57% rename from tests/Feature/OverridesTest.php rename to tests/Feature/ConfigOverriderTest.php index 41b8ebf..10a4427 100644 --- a/tests/Feature/OverridesTest.php +++ b/tests/Feature/ConfigOverriderTest.php @@ -18,85 +18,105 @@ }); test('has returns false if override key is present but null', function () { - $this->configRepo->set('feature-flags.overrides.test', null); + $this->configRepo->set('feature-flags.overriders.config.overrides.test', null); expect($this->configOverrides->has(TestFeatureFlag::TestFlag))->toBeFalse(); }); test('has returns true if override key is present with truthy value', function ($value) { - $this->configRepo->set('feature-flags.overrides.test', $value); + $this->configRepo->set('feature-flags.overriders.config.overrides.test', $value); expect($this->configOverrides->has(TestFeatureFlag::TestFlag))->toBeTrue(); })->with([ - true, - 1, - 1.0, - "test", - [1], - ]); + true, + 1, + 1.0, + "test", + [1], +]); test('has returns true if override key is present with falsy value', function ($value) { - $this->configRepo->set('feature-flags.overrides.test', $value); + $this->configRepo->set('feature-flags.overriders.config.overrides.test', $value); expect($this->configOverrides->has(TestFeatureFlag::TestFlag))->toBeTrue(); })->with([ - false, - 0, - 0.0, - "", - "0", - [[]], - ]); + false, + 0, + 0.0, + "", + "0", + [[]], +]); test('get returns true if override key is present with truthy value', function ($value) { - $this->configRepo->set('feature-flags.overrides.test', $value); + $this->configRepo->set('feature-flags.overriders.config.overrides.test', $value); expect($this->configOverrides->get(TestFeatureFlag::TestFlag))->toBeTrue(); })->with([ - true, - 1, - 1.0, - 'test', - [1], - ]); + true, + 1, + 1.0, + 'test', + [1], +]); test('get returns false if override key is present with falsy value', function ($value) { - $this->configRepo->set('feature-flags.overrides.test', $value); + $this->configRepo->set('feature-flags.overriders.config.overrides.test', $value); expect($this->configOverrides->get(TestFeatureFlag::TestFlag))->toBeFalse(); })->with([ - null, - false, - 0, - 0.0, - '', - '0', - [[]], - ]); + null, + false, + 0, + 0.0, + '', + '0', + [[]], +]); test('get returns false if override key is not present', function () { expect($this->configOverrides->get(TestFeatureFlag::TestFlag))->toBeFalse(); }); test('getAll returns true if override key is present with truthy value', function ($value) { - $this->configRepo->set('feature-flags.overrides.test', $value); + $this->configRepo->set('feature-flags.overriders.config.overrides.test', $value); expect($this->configOverrides->get(TestFeatureFlag::TestFlag))->toBeTrue(); })->with([ - true, - 1, - 1.0, - 'test', - [1], - ]); + true, + 1, + 1.0, + 'test', + [1], +]); test('getAll returns false if override key is present with falsy value', function ($value) { - $this->configRepo->set('feature-flags.overrides.test', $value); + $this->configRepo->set('feature-flags.overriders.config.overrides.test', $value); expect($this->configOverrides->get(TestFeatureFlag::TestFlag))->toBeFalse(); })->with([ - null, - false, - 0, - 0.0, - '', - '0', - [[]], - ]); + null, + false, + 0, + 0.0, + '', + '0', + [[]], +]); test('getAll returns false if override key is not present', function () { expect($this->configOverrides->get(TestFeatureFlag::TestFlag))->toBeFalse(); }); + +it('sets override value for a single feature flag', function ($value) { + $overrider = $this->app->make(ConfigOverrider::class); + $overrider->set(TestFeatureFlag::TestFlag, $value); + expect($this->configRepo->get('feature-flags.overriders.config.overrides.test'))->toBe($value); +})->with([ + true, + false, + null +]); + +it('sets overriderAll value', function ($value) { + $overrider = $this->app->make(ConfigOverrider::class); + $overrider->setAll($value); + expect($this->configRepo->get('feature-flags.overriders.config.override_all'))->toBe($value); +})->with([ + true, + false, + null +]); diff --git a/tests/Feature/FeatureFlagProviderTest.php b/tests/Feature/FeatureFlagProviderTest.php index ad38490..169b326 100644 --- a/tests/Feature/FeatureFlagProviderTest.php +++ b/tests/Feature/FeatureFlagProviderTest.php @@ -2,19 +2,18 @@ declare(strict_types=1); -use Illuminate\Config\Repository; use Illuminate\Support\Facades\Blade; -use Illuminate\Support\Facades\Config; use Worksome\FeatureFlags\FeatureFlagsOverrideProvider; -use Worksome\FeatureFlags\Overriders\ConfigOverrider; +use Worksome\FeatureFlags\Overriders\InMemoryOverrider; use Worksome\FeatureFlags\Providers\FakeProvider; use Worksome\FeatureFlags\Tests\Enums\TestFeatureFlag; beforeEach(function () { $this->fakeProvider = new FakeProvider(); + $this->overrider = new InMemoryOverrider(); $this->provider = new FeatureFlagsOverrideProvider( $this->fakeProvider, - new ConfigOverrider($this->app->get(Repository::class)) + $this->overrider, ); }); @@ -34,15 +33,16 @@ expect(Blade::compileString($bladeSnippet))->toBe($expectedCode); }); -it('should succesfully follow the override for a feature flag', function () { - expect(Config::get('feature-flags.overrides.amazing-feature')) - ->toBe(null) +it('should successfully follow the override for a feature flag', function () { + expect($this->overrider->get(TestFeatureFlag::AmazingFeature)) + ->toBeFalse() ->and($this->provider->flag(TestFeatureFlag::AmazingFeature)) ->toBeFalse(); - Config::set('feature-flags.overrides.amazing-feature', true); - expect(Config::get('feature-flags.overrides.amazing-feature')) + $this->overrider->set(TestFeatureFlag::AmazingFeature, true); + + expect($this->overrider->get(TestFeatureFlag::AmazingFeature)) ->toBeTrue() ->and($this->provider->flag(TestFeatureFlag::AmazingFeature)) ->toBeTrue(); @@ -61,7 +61,7 @@ ->and($this->provider->flag(TestFeatureFlag::FlagThree)) ->toBeFalse(); - Config::set('feature-flags.override-all', true); + $this->overrider->setAll(true); expect($this->provider->flag(TestFeatureFlag::FlagOne)) ->toBeTrue()