Skip to content

Commit 12005f8

Browse files
authored
Merge pull request #1 from byjg/add-featureflagselectorset
Add FeatureFlagSelectorSet.php
2 parents 18c8f8d + 4172250 commit 12005f8

12 files changed

+405
-30
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"psalm": "vendor/bin/psalm"
1717
},
1818
"require": {
19-
"php": ">=8.1 <8.5"
19+
"php": ">=8.1 <8.5",
20+
"psr/container": "^1.0|^2.0|^3.0"
2021
},
2122
"require-dev": {
2223
"phpunit/phpunit": "^10|^11",

docs/callable-dispatchers.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ $dispatcher->dispatch();
2020

2121
## Types of FeatureFlagSelector
2222

23-
- `FeatureFlagSelector::whenFlagIs($flag, $value, $callable)`: Execute the callable when the flag is equal to the value
24-
- `FeatureFlagSelector::whenFlagIsSet($flag, $callable)`: Execute the callable when the flag is set with any value
23+
See:
24+
- [FeatureFlagSelector](featureflag-selector.md)
25+
- [FeatureFlagSelectorSet](featureflag-selectorset.md)

docs/featureflag-selector.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# FeatureFlagSelector
2+
3+
The `FeatureFlagSelector` class defines a way to filter the feature flags and if the condition is met
4+
when the dispatcher is called, it will execute a function.
5+
6+
## Basic Usage
7+
8+
```php
9+
<?php
10+
// Create a Dispatcher
11+
$dispatcher = new FeatureFlagDispatcher();
12+
13+
// Add a feature flag handler
14+
$dispatcher->add(FeatureFlagSelector::whenFlagIs('flag2', 'value1', function () {/** function1 */}));
15+
$dispatcher->add(FeatureFlagSelector::whenFlagIs('flag2', 'value2', function () {/** function2 */}));
16+
$dispatcher->add(FeatureFlagSelector::whenFlagIs('flag2', 'value3', [Someclass::class, 'method1']));
17+
18+
// Dispatch the request
19+
$dispatcher->dispatch();
20+
```
21+
22+
## Reference
23+
24+
### whenFlagIs(string \$flag, string \$value, callable|array \$function)
25+
26+
This static method creates a new instance of the `FeatureFlagSelector` class with the condition that the flag is
27+
equal to the value. If the condition is met, the function will be executed.
28+
29+
### whenFlagIsSet(string \$flag, callable|array \$function)
30+
31+
This static method creates a new instance of the `FeatureFlagSelector` class with the condition that the flag is set
32+
with any value. If the condition is met, the function will be executed.
33+
34+
### stopPropagation()
35+
36+
When the condition is met, the dispatcher will stop the propagation and will not execute the next condition.
37+
The default behavior is to continue the propagation.
38+
39+

docs/featureflag-selectorset.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# FeatureFlagSelectorSet
2+
3+
The `FeatureFlagSelectorSet` class defines a way to filter the feature flags and if **all** the conditions met
4+
when the dispatcher is called, it will execute a function.
5+
6+
## Basic Usage
7+
8+
```php
9+
<?php
10+
// Create a Dispatcher
11+
$dispatcher = new FeatureFlagDispatcher();
12+
13+
// Set the feature flags
14+
$set = FeatureFlagSelectorSet::instance(/* callable */)
15+
->whenFlagIs(/* flag1 */, /* value1 */)
16+
->whenFlagIsSet(/* flag2 */)
17+
18+
19+
// Add a feature flag handler
20+
$dispatcher->add($set);
21+
22+
// Dispatch the request
23+
$dispatcher->dispatch();
24+
```
25+
26+
## Reference
27+
28+
The `FeatureFlagSelectorSet` class has the same methods as the `FeatureFlagSelector`.
29+
30+
The main difference is that the `FeatureFlagSelectorSet` will execute the function only if **all** the conditions are met.
31+

src/FeatureFlagDispatcher.php

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use ByJG\FeatureFlag\Attributes\FeatureFlagAttribute;
66
use ReflectionAttribute;
77
use ReflectionClass;
8+
use ReflectionException;
89
use ReflectionMethod;
910

1011
class FeatureFlagDispatcher
@@ -14,17 +15,42 @@ class FeatureFlagDispatcher
1415
protected SearchOrder $searchOrder = SearchOrder::Selector;
1516
protected array $limitFlags = [];
1617

17-
public function add(FeatureFlagSelector $selector): static
18+
public function add(FeatureFlagSelector|FeatureFlagSelectorSet $selector): static
19+
{
20+
if ($selector instanceof FeatureFlagSelector) {
21+
$this->addSelector($selector);
22+
} else {
23+
$this->addSelectorSet($selector);
24+
}
25+
26+
return $this;
27+
}
28+
29+
protected function addSelector(FeatureFlagSelector $selector): void
1830
{
1931
if (!isset($this->selectors[$selector->getFlagName()])) {
2032
$this->selectors[$selector->getFlagName()] = [];
2133
}
2234

2335
$this->selectors[$selector->getFlagName()][] = $selector;
36+
}
2437

25-
return $this;
38+
protected function addSelectorSet(FeatureFlagSelectorSet $selectorSet): void
39+
{
40+
$list = $selectorSet->get();
41+
42+
$keys = array_keys($list);
43+
44+
if (!isset($this->selectors[$keys[0]])) {
45+
$this->selectors[$keys[0]] = [];
46+
}
47+
48+
$this->selectors[$keys[0]][] = $list;
2649
}
2750

51+
/**
52+
* @throws ReflectionException
53+
*/
2854
public function addClass(string $className): void
2955
{
3056
$reflection = new ReflectionClass($className);
@@ -52,75 +78,122 @@ public function addClass(string $className): void
5278
public function withSearchOrder(SearchOrder $searchOrder, array $flags = []): static
5379
{
5480
$this->searchOrder = $searchOrder;
55-
$this->limitFlags = $flags;
5681
if ($searchOrder == SearchOrder::Custom && empty($flags)) {
5782
throw new \InvalidArgumentException("You must provide the flags when using Custom search order");
5883
}
5984
if ($searchOrder != SearchOrder::Custom && !empty($flags)) {
6085
throw new \InvalidArgumentException("You must not provide the flags when not using Custom search order");
6186
}
87+
88+
if (!empty($flags)) {
89+
$this->limitFlags = array_filter(
90+
FeatureFlags::getFlags(),
91+
fn($key) => in_array($key, $flags),
92+
ARRAY_FILTER_USE_KEY
93+
);
94+
}
95+
6296
return $this;
6397
}
6498

6599
public function dispatch(...$args): int
66100
{
67101
return match ($this->searchOrder) {
68-
SearchOrder::Selector => $this->dispatchQuerySelector(...$args),
69-
SearchOrder::FeatureFlags => $this->dispatchQueryingFlags(...$args),
70-
SearchOrder::Custom => $this->dispatchQueryCustom(...$args),
102+
SearchOrder::Selector => $this->dispatchQuerySelector($this->selectors, true, ...$args),
103+
SearchOrder::FeatureFlags => $this->dispatchQueryingFlags(FeatureFlags::getFlags(), $this->selectors, true, ...$args),
104+
SearchOrder::Custom => $this->dispatchQueryingFlags($this->limitFlags, $this->selectors, true, ...$args),
71105
default => 0,
72106
};
73107
}
74108

75-
protected function dispatchQueryingFlags(...$args): int
109+
protected function dispatchQueryingFlags(array $flagList, array $selectorList, bool $invoke, ...$args): int
76110
{
77111
$count = 0;
78-
foreach (FeatureFlags::getFlags() as $flagName => $flagValue) {
79-
if (!isset($this->selectors[$flagName])) {
112+
foreach ($flagList as $flagName => $flagValue) {
113+
if (!isset($selectorList[$flagName])) {
80114
continue;
81115
}
82116

83117
if (!FeatureFlags::hasFlag($flagName)) {
84118
continue;
85119
}
86120

87-
foreach ($this->selectors[$flagName] as $selector) {
88-
if ($selector->isMatch($flagName, $flagValue)) {
89-
$selector->invoke($args);
90-
$count++;
91-
if (!$selector->isContinueProcessing()) {
92-
break;
93-
}
121+
foreach ($selectorList[$flagName] as $selector) {
122+
$result = $this->match($selector, $flagName, $flagValue, $flagList, $invoke, ...$args);
123+
$count += ($result < 0 ? -$result : $result);
124+
if ($result < 0) {
125+
break;
94126
}
95127
}
96128
}
97129

98130
return $count;
99131
}
100132

101-
protected function dispatchQuerySelector(...$args): int
133+
protected function dispatchQuerySelector(array $selectorList, bool $invoke, ...$args): int
102134
{
103135
$count = 0;
104-
foreach ($this->selectors as $flagName => $selectors) {
136+
foreach ($selectorList as $flagName => $selectors) {
105137
/** @var FeatureFlagSelector $selector */
106138
foreach ($selectors as $selector) {
107139
if (!FeatureFlags::hasFlag($flagName)) {
108140
continue;
109141
}
110142

111-
if ($selector->isMatch($flagName, FeatureFlags::getFlag($flagName))) {
112-
$selector->invoke($args);
113-
$count++;
114-
if (!$selector->isContinueProcessing()) {
115-
break;
116-
}
143+
$result = $this->match($selector, $flagName, FeatureFlags::getFlag($flagName), [], $invoke, ...$args);
144+
$count += ($result < 0 ? -$result : $result);
145+
if ($result < 0) {
146+
break;
117147
}
118148
}
119149
}
120150

121151
return $count;
122152
}
123153

154+
protected function match(FeatureFlagSelector|array $selector, string $flagName, mixed $flagValue, array $flagList, bool $invoke, mixed ...$args): int
155+
{
156+
if (is_array($selector)) {
157+
return $this->matchSelectorArray($selector, $flagName, $flagValue, $flagList, $invoke, ...$args);
158+
}
159+
160+
return $this->matchSelector($selector, $flagName, $flagValue, $invoke, ...$args);
161+
}
162+
163+
protected function matchSelector(FeatureFlagSelector $selector, string $flagName, mixed $flagValue, bool $invoke, mixed ...$args): int
164+
{
165+
if ($selector->isMatch($flagName, $flagValue)) {
166+
if ($invoke) {
167+
$selector->invoke($args);
168+
}
169+
if (!$selector->isContinueProcessing()) {
170+
return -1;
171+
}
172+
return 1;
173+
}
174+
175+
return 0;
176+
}
177+
178+
protected function matchSelectorArray(array $selector, string $flagName, mixed $flagValue, array $flagList, bool $invoke, mixed ...$args): int
179+
{
180+
$first = array_shift($selector)[0];
181+
if (!$first->isMatch($flagName, $flagValue)) {
182+
return 0;
183+
}
184+
if (empty($flagList)) {
185+
$subCount = $this->dispatchQuerySelector($selector, false);
186+
} else {
187+
$subCount = $this->dispatchQueryingFlags($flagList, $selector, false);
188+
}
189+
190+
if ($subCount === count($selector)) {
191+
return $this->matchSelector($first, $flagName, $flagValue, $invoke, ...$args);
192+
}
193+
194+
return 0;
195+
}
196+
124197
protected function dispatchQueryCustom(...$args): int
125198
{
126199
$count = 0;

src/FeatureFlagSelector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ public static function whenFlagIs(string $flagName, string $flagValue, \Closure|
4141
return new static($callable, $flagName, $flagValue);
4242
}
4343

44-
public function stopPropagation(): void
44+
public function stopPropagation(): static
4545
{
4646
$this->continueProcessing = false;
47+
return $this;
4748
}
4849

4950
public function isContinueProcessing(): bool

src/FeatureFlagSelectorSet.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace ByJG\FeatureFlag;
4+
5+
class FeatureFlagSelectorSet
6+
{
7+
protected array $list = [];
8+
protected \Closure|array $callable;
9+
10+
public function __construct(\Closure|array $callable)
11+
{
12+
$this->callable = $callable;
13+
}
14+
15+
public function whenFlagIsSet(string $flagName): static
16+
{
17+
if (!isset($this->list[$flagName])) {
18+
$this->list[$flagName] = [];
19+
}
20+
$this->list[$flagName][] = FeatureFlagSelector::whenFlagIsSet($flagName, $this->callable);
21+
return $this;
22+
}
23+
24+
public function whenFlagIs(string $flagName, string $flagValue): static
25+
{
26+
if (!isset($this->list[$flagName])) {
27+
$this->list[$flagName] = [];
28+
}
29+
$this->list[$flagName][] = FeatureFlagSelector::whenFlagIs($flagName, $flagValue, $this->callable);
30+
return $this;
31+
}
32+
33+
public static function instance(\Closure|array $callable): static
34+
{
35+
return new FeatureFlagSelectorSet($callable);
36+
}
37+
38+
public function get(): array
39+
{
40+
return $this->list;
41+
}
42+
}

src/FeatureFlags.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace ByJG\FeatureFlag;
44

5+
use Psr\Container\ContainerInterface;
6+
57
class FeatureFlags
68
{
79
protected static array $flags = [];
@@ -11,6 +13,13 @@ public static function addFlag(string $flagName, ?string $flagValue = null): voi
1113
self::$flags[$flagName] = $flagValue;
1214
}
1315

16+
public static function addFromContainer(string $flagName, ContainerInterface $container): void
17+
{
18+
if ($container->has($flagName)) {
19+
self::$flags[$flagName] = $container->get($flagName);
20+
}
21+
}
22+
1423
public static function hasFlag(string $flagName): bool
1524
{
1625
return array_key_exists($flagName, self::$flags);

0 commit comments

Comments
 (0)