From ff70411188677167f9286fb9ffbb83d0c9af9982 Mon Sep 17 00:00:00 2001 From: Joshua Estes Date: Fri, 20 Oct 2023 19:06:27 -0400 Subject: [PATCH] Feature Toggle Refactor (#24) --- docs/components/feature-toggle/index.md | 39 ++++++++++++++++ packages/feature-toggle/Context.php | 44 ++++++++++++++++--- packages/feature-toggle/ContextInterface.php | 2 +- packages/feature-toggle/Feature.php | 2 +- packages/feature-toggle/FeatureInterface.php | 7 +-- .../FeatureToggleProviderInterface.php | 5 +-- .../InMemoryFeatureToggleProvider.php | 12 +++-- .../InMemoryFeatureToggleProviderTest.php | 9 ++-- .../Toggle/AlwaysDisabledToggle.php | 2 +- .../Toggle/AlwaysEnabledToggle.php | 2 +- packages/feature-toggle/ToggleInterface.php | 2 +- 11 files changed, 97 insertions(+), 29 deletions(-) diff --git a/docs/components/feature-toggle/index.md b/docs/components/feature-toggle/index.md index e5f5758a..0b24c83c 100644 --- a/docs/components/feature-toggle/index.md +++ b/docs/components/feature-toggle/index.md @@ -7,3 +7,42 @@ title: Feature Toggle - Overview ```shell composer require sonsofphp/feature-toggle ``` + +## Usage + +```php +use SonsOfPHP\Component\FeatureToggle\Feature; +use SonsOfPHP\Component\FeatureToggle\Provider\InMemoryFeatureToggleProvider; +use SonsOfPHP\Component\FeatureToggle\Toggle\AlwaysEnabledToggle; + +// Using a feature toggle provider +$provider = new InMemoryFeatureToggleProvider(); +$provider->addFeature(new Feature('feature.example', new AlwaysEnabledToggle())); + +$feature = $provider->getFeatureToggleByKey('feature.example'); + +// Checking if the feature is enabled +$isEnabled = $feature->isEnabled(); +``` + +## Advanced Usage + +```php +// ... +use SonsOfPHP\Component\FeatureToggle\Context; + +// Different ways to build the context, the context +// is used by the Toggle +$context = new Context([ + 'user' => $user, +]); +$context->set('user', $user); +$context['user'] = $user; + +$isEnabled = $feature->isEnabled($context); +``` + +## Create your own Toggle + +Take a look at the `AlwaysEnabledToggle` and `AlwaysDisabledToggle`. This is how +more complex Toggles can be created. These two are as simple as you can get. diff --git a/packages/feature-toggle/Context.php b/packages/feature-toggle/Context.php index 321a4ffa..c00767ab 100644 --- a/packages/feature-toggle/Context.php +++ b/packages/feature-toggle/Context.php @@ -9,26 +9,56 @@ */ final class Context implements ContextInterface { - private array $data = []; + public function __construct(private array $data = []) {} - public function get(string $key) + public function offsetSet($offset, $value): void { - if ($this->has($key)) { - return $this->data[$key]; + if (null === $offset) { + throw new \Exception('Requires a key'); } - return null; + $this->data[$offset] = $value; + } + + public function offsetExists($offset): bool + { + return \array_key_exists($offset, $this->data); + } + + public function offsetUnset($offset): void + { + unset($this->data[$offset]); + } + + public function offsetGet($offset): mixed + { + return $this->data[$offset] ?? null; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->data); + } + + public function jsonSerialize(): mixed + { + return $this->data; + } + + public function get(string $key): mixed + { + return $this->offsetGet($key); } public function set(string $key, $value): ContextInterface { - $this->data[$key] = $value; + $this->offsetSet($key, $value); return $this; } public function has(string $key): bool { - return \array_key_exists($key, $this->data); + return $this->offsetExists($key); } } diff --git a/packages/feature-toggle/ContextInterface.php b/packages/feature-toggle/ContextInterface.php index 53ff0c9d..1613f015 100644 --- a/packages/feature-toggle/ContextInterface.php +++ b/packages/feature-toggle/ContextInterface.php @@ -7,7 +7,7 @@ /** * @author Joshua Estes */ -interface ContextInterface +interface ContextInterface extends \ArrayAccess, \IteratorAggregate, \JsonSerializable { public function get(string $key); diff --git a/packages/feature-toggle/Feature.php b/packages/feature-toggle/Feature.php index d3646969..c95c0e7a 100644 --- a/packages/feature-toggle/Feature.php +++ b/packages/feature-toggle/Feature.php @@ -25,7 +25,7 @@ public function getKey(): string return $this->key; } - public function isEnabled(ContextInterface $context): bool + public function isEnabled(?ContextInterface $context = null): bool { return $this->toggle->isEnabled($context); } diff --git a/packages/feature-toggle/FeatureInterface.php b/packages/feature-toggle/FeatureInterface.php index 783be0c2..c3a8ce2a 100644 --- a/packages/feature-toggle/FeatureInterface.php +++ b/packages/feature-toggle/FeatureInterface.php @@ -9,7 +9,7 @@ * * @author Joshua Estes */ -interface FeatureInterface +interface FeatureInterface extends ToggleInterface { /** * Returns the key that is used for the feature toggle. The key should be @@ -17,9 +17,4 @@ interface FeatureInterface * get them all mixed up. */ public function getKey(): string; - - /** - * Returns true or false if the toggle is enabled. - */ - public function isEnabled(ContextInterface $context): bool; } diff --git a/packages/feature-toggle/Provider/FeatureToggleProviderInterface.php b/packages/feature-toggle/Provider/FeatureToggleProviderInterface.php index bc5e7d28..afbed491 100644 --- a/packages/feature-toggle/Provider/FeatureToggleProviderInterface.php +++ b/packages/feature-toggle/Provider/FeatureToggleProviderInterface.php @@ -16,8 +16,5 @@ */ interface FeatureToggleProviderInterface { - /** - * @return FeatureInterface[] - */ - public function getFeatures(): iterable; + public function getFeatureToggleByKey(string $key): ?FeatureInterface; } diff --git a/packages/feature-toggle/Provider/InMemoryFeatureToggleProvider.php b/packages/feature-toggle/Provider/InMemoryFeatureToggleProvider.php index fad0913a..9f6a84e3 100644 --- a/packages/feature-toggle/Provider/InMemoryFeatureToggleProvider.php +++ b/packages/feature-toggle/Provider/InMemoryFeatureToggleProvider.php @@ -13,7 +13,7 @@ final class InMemoryFeatureToggleProvider implements FeatureToggleProviderInterf { private array $features = []; - public function __construct($features = []) + public function __construct(array $features = []) { foreach ($features as $feature) { $this->addFeature($feature); @@ -22,11 +22,15 @@ public function __construct($features = []) public function addFeature(FeatureInterface $feature): void { - $this->features[] = $feature; + $this->features[$feature->getKey()] = $feature; } - public function getFeatures(): iterable + public function getFeatureToggleByKey(string $key): ?FeatureInterface { - yield from $this->features; + if (isset($this->features[$key])) { + return $this->features[$key]; + } + + return null; } } diff --git a/packages/feature-toggle/Tests/Provider/InMemoryFeatureToggleProviderTest.php b/packages/feature-toggle/Tests/Provider/InMemoryFeatureToggleProviderTest.php index f4a8622b..c8e5ea81 100644 --- a/packages/feature-toggle/Tests/Provider/InMemoryFeatureToggleProviderTest.php +++ b/packages/feature-toggle/Tests/Provider/InMemoryFeatureToggleProviderTest.php @@ -9,6 +9,7 @@ use SonsOfPHP\Component\FeatureToggle\Provider\FeatureToggleProviderInterface; use SonsOfPHP\Component\FeatureToggle\Provider\InMemoryFeatureToggleProvider; use SonsOfPHP\Component\FeatureToggle\ToggleInterface; +use PHPUnit\Framework\MockObject; /** * @coversDefaultClass \SonsOfPHP\Component\FeatureToggle\Provider\InMemoryFeatureToggleProvider @@ -17,7 +18,7 @@ */ final class InMemoryFeatureToggleProviderTest extends TestCase { - private $toggle; + private MockObject|ToggleInterface $toggle; protected function setUp(): void { @@ -37,7 +38,7 @@ public function testItHasTheCorrectInterface(): void /** * @covers ::__construct * @covers ::addFeature - * @covers ::getFeatures + * @covers ::getFeatureToggleByKey */ public function testAddingFeatures(): void { @@ -48,6 +49,8 @@ public function testAddingFeatures(): void $provider = new InMemoryFeatureToggleProvider($features); - $this->assertCount(2, iterator_to_array($provider->getFeatures())); + $this->assertNotNull($provider->getFeatureToggleByKey('test.one')); + $this->assertNotNull($provider->getFeatureToggleByKey('test.two')); + $this->assertNull($provider->getFeatureToggleByKey('test.three')); } } diff --git a/packages/feature-toggle/Toggle/AlwaysDisabledToggle.php b/packages/feature-toggle/Toggle/AlwaysDisabledToggle.php index bccb91fc..77181c9e 100644 --- a/packages/feature-toggle/Toggle/AlwaysDisabledToggle.php +++ b/packages/feature-toggle/Toggle/AlwaysDisabledToggle.php @@ -14,7 +14,7 @@ */ class AlwaysDisabledToggle implements ToggleInterface { - public function isEnabled(ContextInterface $context): bool + public function isEnabled(?ContextInterface $context = null): bool { return false; } diff --git a/packages/feature-toggle/Toggle/AlwaysEnabledToggle.php b/packages/feature-toggle/Toggle/AlwaysEnabledToggle.php index cebf5d12..3e80255d 100644 --- a/packages/feature-toggle/Toggle/AlwaysEnabledToggle.php +++ b/packages/feature-toggle/Toggle/AlwaysEnabledToggle.php @@ -14,7 +14,7 @@ */ class AlwaysEnabledToggle implements ToggleInterface { - public function isEnabled(ContextInterface $context): bool + public function isEnabled(?ContextInterface $context = null): bool { return true; } diff --git a/packages/feature-toggle/ToggleInterface.php b/packages/feature-toggle/ToggleInterface.php index c5c9c232..5ca4d68f 100644 --- a/packages/feature-toggle/ToggleInterface.php +++ b/packages/feature-toggle/ToggleInterface.php @@ -12,5 +12,5 @@ interface ToggleInterface /** * Returns true if this strategy is enabled. */ - public function isEnabled(ContextInterface $context): bool; + public function isEnabled(?ContextInterface $context = null): bool; }