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 3fb39eb
Show file tree
Hide file tree
Showing 10 changed files with 347 additions and 117 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' => [],
];
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
43 changes: 43 additions & 0 deletions src/FeatureFlagsOverriderManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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', []);

assert(
$overrideAll !== null || is_bool($overrideAll),

Check failure on line 28 in src/FeatureFlagsOverriderManager.php

View workflow job for this annotation

GitHub Actions / phpstan

Call to function is_bool() with null will always evaluate to false.
new InvalidArgumentException('Config key feature-flags.overriders.in-memory.override-all should either be a boolean or null.')
);
assert(
is_array($overrides),
new InvalidArgumentException('Config key feature-flags.overriders.in-memory.overrides should either be a boolean or null.')
);

return new InMemoryOverrider($overrides, $overrideAll);

Check failure on line 36 in src/FeatureFlagsOverriderManager.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #2 $overrideAll of class Worksome\FeatureFlags\Overriders\InMemoryOverrider constructor expects bool|null, mixed given.
}

public function getDefaultDriver(): string
{
return strval($this->config->get('feature-flags.overrider')); // @phpstan-ignore-line
}
}
16 changes: 6 additions & 10 deletions src/FeatureFlagsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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<FeatureFlagOverrider> $convertor */
$convertor = $config->get('feature-flags.overrider');
/** @var FeatureFlagsOverriderManager */
$manager = $app->get(FeatureFlagsOverriderManager::class);

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;
}
}
100 changes: 100 additions & 0 deletions tests/Feature/ArrayOverriderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?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 3fb39eb

Please sign in to comment.