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 12, 2024
1 parent 9eeb0bc commit 4ab637e
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 45 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 any default [ key => value ] for the InMemoryOverrider to be populated with on instantiation
*/
'overrides' => [
//
],
/**
* Specify default override all value for the InMemoryOverrider to be populated with on instantiation
*/
'overrider-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' => [],
];
34 changes: 34 additions & 0 deletions src/FeatureFlagsOverriderManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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
{
/** @var bool|null $overrideAll */
$overrideAll = $this->config->get('feature-flags.overriders.array.overrider-all');
/** @var array<string, bool|null> $overrideAll */
$overrides = $this->config->get('feature-flags.overriders.array.overrides', []);

return new InMemoryOverrider($overrides, $overrideAll);

Check failure on line 27 in src/FeatureFlagsOverriderManager.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $overrides of class Worksome\FeatureFlags\Overriders\InMemoryOverrider constructor expects array<string, bool|null>, mixed given.

Check failure on line 27 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, array<string, bool|null> given.
}

public function getDefaultDriver(): string
{
return strval($this->config->get('feature-flags.overrider'));

Check failure on line 32 in src/FeatureFlagsOverriderManager.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $value of function strval expects bool|float|int|resource|string|null, mixed given.
}
}
9 changes: 3 additions & 6 deletions src/FeatureFlagsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,10 @@ function (Container $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 4ab637e

Please sign in to comment.