From 76cba0a725edc18eda5f388c354ec7d4fefda552 Mon Sep 17 00:00:00 2001 From: Michel Chowanski Date: Fri, 4 Oct 2019 20:09:14 +0200 Subject: [PATCH] #13 Support new "match all" chain strategy --- docs/activator/chain.md | 46 +++++++++++ src/Activator/ChainActivator.php | 54 ++++++++++++- tests/Activator/ChainActivatorTest.php | 106 +++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) diff --git a/docs/activator/chain.md b/docs/activator/chain.md index 31d90c5..5ddef42 100644 --- a/docs/activator/chain.md +++ b/docs/activator/chain.md @@ -24,3 +24,49 @@ class MyClass } } ``` + +The `ChainActivator` use the "first match" strategy. At least one activator must return `true` for activating the feature. +You can change the strategy to "match all". Then **all** activators must return `true` for activating the feature. + +Pass your needed strategy via constructor argument ... + +```php +// MyClass.php +class MyClass +{ + public function doSomething() + { + $activator = new ChainActivator(ChainActivator::STRATEGY_ALL_MATCH); + // ... + } +} +``` + +... or by context: + +```php +// MyClass.php +class MyClass +{ + public function doSomething() + { + $activator = new ChainActivator(); + $activator->add(new ArrayActivator([ + 'feature_foo' + ])); + $activator->add(new YourCustomActivator()); + + $manager = new FeatureManager($activator); + + $context = new Context(); + $context->add('chain_strategy', ChainActivator::STRATEGY_ALL_MATCH); + + // The array activator returns false, so it will request YourCustomActivator. + if ($manager->isActive('feature_def', $context)) { + // do something + } + } +} +``` + +The strategy defined in context will override the strategy defined in constructor. diff --git a/src/Activator/ChainActivator.php b/src/Activator/ChainActivator.php index 10eb24b..0f7ae26 100644 --- a/src/Activator/ChainActivator.php +++ b/src/Activator/ChainActivator.php @@ -12,6 +12,21 @@ */ class ChainActivator implements FeatureActivatorInterface { + /** + * At least one activator must return true to activating the feature (default) + */ + const STRATEGY_FIRST_MATCH = 1; + + /** + * All activators must return true to activating the feature + */ + const STRATEGY_ALL_MATCH = 2; + + /** + * The reserved name for strategy override via context + */ + const CONTEXT_STRATEGY_NAME = 'chain_strategy'; + /** * Ordered array of feature activators * @@ -19,6 +34,23 @@ class ChainActivator implements FeatureActivatorInterface */ private $bag = []; + /** + * The used strategy + * + * @var int + */ + private $strategy; + + /** + * ChainActivator constructor. + * + * @param int $strategy + */ + public function __construct($strategy = self::STRATEGY_FIRST_MATCH) + { + $this->strategy = $strategy; + } + /** * Add activator * @@ -54,12 +86,26 @@ public function getName() */ public function isActive($name, Context $context) { - foreach ($this->bag as $activator) { - if ($activator->isActive($name, $context) === true) { - return true; + $strategy = $context->get(self::CONTEXT_STRATEGY_NAME, $this->strategy); + + if ($strategy === self::STRATEGY_ALL_MATCH) { + $result = true; + foreach ($this->bag as $activator) { + if ($activator->isActive($name, $context) === false) { + $result = false; + break; + } + } + } else { + $result = false; + foreach ($this->bag as $activator) { + if ($activator->isActive($name, $context) === true) { + $result = true; + break; + } } } - return false; + return $result; } } diff --git a/tests/Activator/ChainActivatorTest.php b/tests/Activator/ChainActivatorTest.php index de9061b..6b78859 100644 --- a/tests/Activator/ChainActivatorTest.php +++ b/tests/Activator/ChainActivatorTest.php @@ -5,6 +5,7 @@ use Flagception\Activator\ArrayActivator; use Flagception\Activator\ChainActivator; use Flagception\Activator\FeatureActivatorInterface; +use Flagception\Exception\AlreadyDefinedException; use Flagception\Model\Context; use PHPUnit\Framework\TestCase; @@ -117,4 +118,109 @@ public function testAddAndGet() static::assertSame($fakeActivator1, $decorator->getActivators()[0]); static::assertSame($fakeActivator2, $decorator->getActivators()[1]); } + + /** + * Test match all strategy + * + * @return void + */ + public function testMatchAllStrategy() + { + $activator = new ChainActivator(ChainActivator::STRATEGY_ALL_MATCH); + $activator->add($firstActivator = $this->createMock(FeatureActivatorInterface::class)); + $activator->add($secondActivator = $this->createMock(FeatureActivatorInterface::class)); + $activator->add($thirdActivator = $this->createMock(FeatureActivatorInterface::class)); + + $firstActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(true); + + $secondActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(true); + + $thirdActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(true); + + static::assertTrue($activator->isActive('feature_abc', new Context())); + } + + /** + * Test match all strategy break as soon as possible + * + * @return void + */ + public function testMatchAllStrategyBreakChain() + { + $activator = new ChainActivator(ChainActivator::STRATEGY_ALL_MATCH); + $activator->add($firstActivator = $this->createMock(FeatureActivatorInterface::class)); + $activator->add($secondActivator = $this->createMock(FeatureActivatorInterface::class)); + $activator->add($thirdActivator = $this->createMock(FeatureActivatorInterface::class)); + + $firstActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(true); + + $secondActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(false); + + $thirdActivator + ->expects(static::never()) + ->method('isActive'); + + static::assertFalse($activator->isActive('feature_abc', new Context())); + } + + /** + * Test match all strategy + * + * @return void + * + * @throws AlreadyDefinedException + */ + public function testContextOverrideStrategy() + { + $activator = new ChainActivator(ChainActivator::STRATEGY_FIRST_MATCH); + $activator->add($firstActivator = $this->createMock(FeatureActivatorInterface::class)); + $activator->add($secondActivator = $this->createMock(FeatureActivatorInterface::class)); + $activator->add($thirdActivator = $this->createMock(FeatureActivatorInterface::class)); + + $firstActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(true); + + $secondActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(true); + + $thirdActivator + ->expects(static::once()) + ->method('isActive') + ->willReturn(true); + + $context = new Context(); + $context->add('chain_strategy', ChainActivator::STRATEGY_ALL_MATCH); + static::assertTrue($activator->isActive('feature_abc', $context)); + } + + /** + * Test public constants + * + * @return void + */ + public function testConstants() + { + static::assertEquals(1, ChainActivator::STRATEGY_FIRST_MATCH); + static::assertEquals(2, ChainActivator::STRATEGY_ALL_MATCH); + static::assertEquals('chain_strategy', ChainActivator::CONTEXT_STRATEGY_NAME); + } }