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..de0fd85 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,46 @@ ], /** - * 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' => env('FEATURE_FLAGS_OVERRIDE_ALL'), + + /** + * 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' => [ + /** + * Specify default override all value for the InMemoryOverrider to be populated with on instantiation + */ + 'override-all' => null, + /** + * Specify any default [ key => value ] for the InMemoryOverrider to be populated with on instantiation + */ + 'overrides' => [ + // + ], + ] + ], - /** - * 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/FeatureFlagsOverriderManager.php b/src/FeatureFlagsOverriderManager.php new file mode 100644 index 0000000..bfe2b15 --- /dev/null +++ b/src/FeatureFlagsOverriderManager.php @@ -0,0 +1,41 @@ +config, + ); + } + + public function createInMemoryDriver(): InMemoryOverrider + { + $overrideAll = $this->config->get('feature-flags.overriders.in-memory.override-all'); + /** @var array $overrides */ + $overrides = $this->config->get('feature-flags.overriders.in-memory.overrides', []); + + if ($overrideAll !== null && !is_bool($overrideAll)) { + throw new InvalidArgumentException("Config key feature-flags.overriders.in-memory.override-all should either be a boolean or null."); + } + if ( !is_array($overrides)) { + throw new InvalidArgumentException("Config key feature-flags.overriders.in-memory.overrides should either be a boolean or null."); + } + + return new InMemoryOverrider($overrides, $overrideAll); + } + + 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..873709f 100644 --- a/src/FeatureFlagsServiceProvider.php +++ b/src/FeatureFlagsServiceProvider.php @@ -50,10 +50,9 @@ public function register(): void ]); }); - $this->app->singleton( - FeatureFlagsManager::class, - static fn (Container $container) => new FeatureFlagsManager($container) - ); + $this->app->singleton(FeatureFlagsManager::class); + + $this->app->singleton(FeatureFlagsOverriderManager::class); $this->app->singleton(FeatureFlagsProviderContract::class, function (Container $app) { /** @var FeatureFlagsManager $manager */ @@ -84,13 +83,10 @@ function (Container $app) { $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'); + /** @var FeatureFlagsOverriderManager */ + $manager = $app->get(FeatureFlagsOverriderManager::class); - return $app->get($convertor); + return $manager->driver(); } ); } diff --git a/src/Overriders/ConfigOverrider.php b/src/Overriders/ConfigOverrider.php index 82056fd..9263c69 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,12 @@ 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'); } } diff --git a/src/Overriders/InMemoryOverrider.php b/src/Overriders/InMemoryOverrider.php new file mode 100644 index 0000000..ee1169d --- /dev/null +++ b/src/Overriders/InMemoryOverrider.php @@ -0,0 +1,69 @@ + $overrides + * @param bool|null $overrideAll + */ + public function __construct(private array $overrides = [], 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 setOverrideAll(bool|null $overrideAll = null): self + { + $this->overrideAll = $overrideAll; + return $this; + } + + public function setKey(FeatureFlagEnum $key, mixed $value): self + { + 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/tests/Feature/ArrayOverriderTest.php b/tests/Feature/ArrayOverriderTest.php new file mode 100644 index 0000000..0d298fb --- /dev/null +++ b/tests/Feature/ArrayOverriderTest.php @@ -0,0 +1,101 @@ +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->setKey(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->setKey(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->setKey(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->setKey(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->setKey(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->setKey(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->setKey(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 82% rename from tests/Feature/OverridesTest.php rename to tests/Feature/ConfigOverriderTest.php index 41b8ebf..6107b7e 100644 --- a/tests/Feature/OverridesTest.php +++ b/tests/Feature/ConfigOverriderTest.php @@ -18,12 +18,12 @@ }); 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, @@ -34,7 +34,7 @@ ]); 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, @@ -46,7 +46,7 @@ ]); 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, @@ -57,7 +57,7 @@ ]); 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, @@ -74,7 +74,7 @@ }); 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, @@ -85,7 +85,7 @@ ]); 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, diff --git a/tests/Feature/FeatureFlagProviderTest.php b/tests/Feature/FeatureFlagProviderTest.php index ad38490..b1e7810 100644 --- a/tests/Feature/FeatureFlagProviderTest.php +++ b/tests/Feature/FeatureFlagProviderTest.php @@ -1,72 +1,72 @@ -fakeProvider = new FakeProvider(); - $this->provider = new FeatureFlagsOverrideProvider( - $this->fakeProvider, - new ConfigOverrider($this->app->get(Repository::class)) - ); -}); - -it('should return true if flag is set to true', function () { - $this->fakeProvider->setFlag(TestFeatureFlag::TestFlag, true); - expect($this->provider->flag(TestFeatureFlag::TestFlag))->toBeTrue(); -}); - -it('should return false if flag is set to false', function () { - $this->fakeProvider->setFlag(TestFeatureFlag::TestFlag, false); - expect($this->provider->flag(TestFeatureFlag::TestFlag))->toBeFalse(); -}); - -it('should validate the blade tags working correctly', function () { - $bladeSnippet = "@feature('test-flag') This is hidden feature @endfeature"; - $expectedCode = " This is hidden feature "; - 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) - ->and($this->provider->flag(TestFeatureFlag::AmazingFeature)) - ->toBeFalse(); - - Config::set('feature-flags.overrides.amazing-feature', true); - - expect(Config::get('feature-flags.overrides.amazing-feature')) - ->toBeTrue() - ->and($this->provider->flag(TestFeatureFlag::AmazingFeature)) - ->toBeTrue(); -}); - -it('should correctly overide all feature flags if value is set', function () { - $this->fakeProvider->setFlag(TestFeatureFlag::FlagOne, true); - $this->fakeProvider->setFlag(TestFeatureFlag::FlagTwo, false); - $this->fakeProvider->setFlag(TestFeatureFlag::FlagThree, false); - - - expect($this->provider->flag(TestFeatureFlag::FlagOne)) - ->toBeTrue() - ->and($this->provider->flag(TestFeatureFlag::FlagTwo)) - ->toBeFalse() - ->and($this->provider->flag(TestFeatureFlag::FlagThree)) - ->toBeFalse(); - - Config::set('feature-flags.override-all', true); - - expect($this->provider->flag(TestFeatureFlag::FlagOne)) - ->toBeTrue() - ->and($this->provider->flag(TestFeatureFlag::FlagTwo)) - ->toBeTrue() - ->and($this->provider->flag(TestFeatureFlag::FlagThree)) - ->toBeTrue(); -}); +fakeProvider = new FakeProvider(); + $this->overrider = new InMemoryOverrider(); + $this->provider = new FeatureFlagsOverrideProvider( + $this->fakeProvider, + $this->overrider, + ); +}); + +it('should return true if flag is set to true', function () { + $this->fakeProvider->setFlag(TestFeatureFlag::TestFlag, true); + expect($this->provider->flag(TestFeatureFlag::TestFlag))->toBeTrue(); +}); + +it('should return false if flag is set to false', function () { + $this->fakeProvider->setFlag(TestFeatureFlag::TestFlag, false); + expect($this->provider->flag(TestFeatureFlag::TestFlag))->toBeFalse(); +}); + +it('should validate the blade tags working correctly', function () { + $bladeSnippet = "@feature('test-flag') This is hidden feature @endfeature"; + $expectedCode = " This is hidden feature "; + expect(Blade::compileString($bladeSnippet))->toBe($expectedCode); +}); + +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(); + + + $this->overrider->setKey(TestFeatureFlag::AmazingFeature, true); + + expect($this->overrider->get(TestFeatureFlag::AmazingFeature)) + ->toBeTrue() + ->and($this->provider->flag(TestFeatureFlag::AmazingFeature)) + ->toBeTrue(); +}); + +it('should correctly overide all feature flags if value is set', function () { + $this->fakeProvider->setFlag(TestFeatureFlag::FlagOne, true); + $this->fakeProvider->setFlag(TestFeatureFlag::FlagTwo, false); + $this->fakeProvider->setFlag(TestFeatureFlag::FlagThree, false); + + + expect($this->provider->flag(TestFeatureFlag::FlagOne)) + ->toBeTrue() + ->and($this->provider->flag(TestFeatureFlag::FlagTwo)) + ->toBeFalse() + ->and($this->provider->flag(TestFeatureFlag::FlagThree)) + ->toBeFalse(); + + $this->overrider->setOverrideAll(true); + + expect($this->provider->flag(TestFeatureFlag::FlagOne)) + ->toBeTrue() + ->and($this->provider->flag(TestFeatureFlag::FlagTwo)) + ->toBeTrue() + ->and($this->provider->flag(TestFeatureFlag::FlagThree)) + ->toBeTrue(); +});