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 13, 2024
1 parent d2201dd commit 54e7654
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 111 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
59 changes: 40 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,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' => [],
];
41 changes: 41 additions & 0 deletions src/FeatureFlagsOverriderManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Worksome\FeatureFlags;

use Illuminate\Support\Manager;
use InvalidArgumentException;
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
{
$overrideAll = $this->config->get('feature-flags.overriders.in-memory.override-all');
/** @var array<string, bool|null> $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
}
}
16 changes: 9 additions & 7 deletions src/FeatureFlagsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function register(): void

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

$this->app->singleton(FeatureFlagsProviderContract::class, function (Container $app) {
Expand Down Expand Up @@ -81,16 +81,18 @@ function (Container $app) {
}
);

$this->app->bind(
FeatureFlagsOverriderManager::class,
fn(Container $app) => new FeatureFlagsOverriderManager($app)
);

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

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

return $app->get($convertor);
return $manager->driver();
}
);
}
Expand Down
12 changes: 6 additions & 6 deletions src/Overriders/ConfigOverrider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,26 @@ 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');
}
}
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
{

/**
* @param array<string, bool|null> $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;
}
}
101 changes: 101 additions & 0 deletions tests/Feature/ArrayOverriderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Worksome\FeatureFlags\Tests\Feature;

use Worksome\FeatureFlags\Overriders\InMemoryOverrider;
use Worksome\FeatureFlags\Tests\Enums\TestFeatureFlag;

beforeEach(function () {

$this->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();
});
Loading

0 comments on commit 54e7654

Please sign in to comment.