Skip to content

Commit aca815c

Browse files
authored
Feat: Add event system (#77)
1 parent 5086d29 commit aca815c

18 files changed

+942
-7
lines changed

.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/.github export-ignore
2+
/doc export-ignore
23
/examples export-ignore
34
/tests export-ignore
45
/.gitattributes export-ignore
@@ -7,6 +8,7 @@
78
/.php-cs-fixer.dist.php export-ignore
89
/phpstan.neon.dist export-ignore
910
/phpunit.xml export-ignore
11+
/README.md export-ignore
1012
/rector.72.php export-ignore
1113
/rector.73.php export-ignore
1214
/rector.74.php export-ignore

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ or
3232

3333
`composer require unleash/client symfony/http-client nyholm/psr7 symfony/cache`
3434

35+
If you want to make use of events you also need to install `symfony/event-dispatcher`.
36+
See [event documentation here](doc/events.md).
37+
3538
## Usage
3639

3740
The basic usage is getting the `Unleash` object and checking for a feature:

composer.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"phpunit/phpunit": "^9.5",
3838
"symfony/http-client": "^5.0 | ^6.0",
3939
"nyholm/psr7": "^1.0",
40-
"symfony/cache": "^5.0 | ^6.0"
40+
"symfony/cache": "^5.0 | ^6.0",
41+
"symfony/event-dispatcher": "^5.0 | ^6.0"
4142
},
4243
"autoload-dev": {
4344
"psr-4": {
@@ -48,7 +49,8 @@
4849
"guzzlehttp/guzzle": "A http client implementation (PSR-17 and PSR-18)",
4950
"symfony/http-client": "A http client implementation (PSR-17 and PSR-18)",
5051
"nyholm/psr7": "Needed when you use symfony/http-client",
51-
"unleash/symfony-client-bundle": "The Symfony bundle for this library"
52+
"unleash/symfony-client-bundle": "The Symfony bundle for this library",
53+
"symfony/event-dispatcher": "Needed when you want to react to events from Unleash"
5254
},
5355
"scripts": {
5456
"fixer": "php-cs-fixer fix --verbose --allow-risky=yes",

doc/events.md

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Event system in Unleash PHP SDK
2+
3+
This SDK supports events using [`symfony/event-dispatcher`](https://packagist.org/packages/symfony/event-dispatcher).
4+
5+
## Installation
6+
7+
The event dispatcher is not a mandatory component of this SDK, so you need to install it by running:
8+
9+
`composer require symfony/event-dispatcher`
10+
11+
## Usage
12+
13+
You can add event subscribers to the builder object:
14+
15+
```php
16+
<?php
17+
18+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
19+
use Unleash\Client\Event\FeatureToggleDisabledEvent;
20+
use Unleash\Client\Event\FeatureToggleMissingStrategyHandlerEvent;
21+
use Unleash\Client\Event\FeatureToggleNotFoundEvent;
22+
use Unleash\Client\Event\FeatureVariantBeforeFallbackReturnedEvent;
23+
use Unleash\Client\UnleashBuilder;
24+
use Unleash\Client\Event\UnleashEvents;
25+
26+
class MyEventSubscriber implements EventSubscriberInterface
27+
{
28+
public static function getSubscribedEvents(): array
29+
{
30+
return [
31+
UnleashEvents::FEATURE_TOGGLE_DISABLED => 'onFeatureDisabled',
32+
UnleashEvents::FEATURE_TOGGLE_MISSING_STRATEGY_HANDLER => 'onNoStrategyHandler',
33+
UnleashEvents::FEATURE_TOGGLE_NOT_FOUND => 'onFeatureNotFound',
34+
];
35+
}
36+
37+
public function onFeatureDisabled(FeatureToggleDisabledEvent $event)
38+
{
39+
// todo
40+
}
41+
42+
public function onNoStrategyHandler(FeatureToggleMissingStrategyHandlerEvent $event)
43+
{
44+
// todo
45+
}
46+
47+
public function onFeatureNotFound(FeatureToggleNotFoundEvent $event)
48+
{
49+
// todo
50+
}
51+
}
52+
53+
$unleash = UnleashBuilder::create()
54+
->withAppName('My App')
55+
->withAppUrl('http://localhost:4242')
56+
->withInstanceId('test')
57+
->withEventSubscriber(new MyEventSubscriber())
58+
->build();
59+
60+
$unleash->isEnabled('test');
61+
```
62+
63+
The relevant methods will be called in the above example when their respective event occurs.
64+
65+
### List of events:
66+
67+
- `\Unleash\Client\Event\UnleashEvents::FEATURE_TOGGLE_NOT_FOUND` - when a feature with the name isn't found on the
68+
unleash server (or in the bootstrap if it's used). Event object: `Unleash\Client\Event\FeatureToggleNotFoundEvent`
69+
- `\Unleash\Client\Event\UnleashEvents::FEATURE_TOGGLE_DISABLED` - when a feature is found but it's disabled.
70+
Event object: Unleash\Client\Event\FeatureToggleDisabledEvent
71+
- `\Unleash\Client\Event\UnleashEvents::FEATURE_TOGGLE_MISSING_STRATEGY_HANDLER` - when there is no suitable strategy handler
72+
implemented for any of the feature's strategies. Event object: `Unleash\Client\Event\FeatureToggleMissingStrategyHandlerEvent`
73+
74+
## FEATURE_TOGGLE_NOT_FOUND event
75+
76+
Example:
77+
78+
```php
79+
<?php
80+
81+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
82+
use Unleash\Client\Event\FeatureToggleNotFoundEvent;
83+
use Unleash\Client\Event\UnleashEvents;
84+
use Unleash\Client\DTO\DefaultFeature;
85+
use Unleash\Client\UnleashBuilder;
86+
87+
final class MyEventSubscriber implements EventSubscriberInterface
88+
{
89+
public static function getSubscribedEvents()
90+
{
91+
return [
92+
UnleashEvents::FEATURE_TOGGLE_NOT_FOUND => 'onNotFound',
93+
];
94+
}
95+
96+
public function onNotFound(FeatureToggleNotFoundEvent $event): void
97+
{
98+
// methods:
99+
$event->getFeatureName(); // string
100+
$event->getContext(); // instance of Context
101+
}
102+
}
103+
104+
$unleash = UnleashBuilder::create()
105+
->withEventSubscriber(new MyEventSubscriber())
106+
->build();
107+
```
108+
109+
## FEATURE_TOGGLE_DISABLED event
110+
111+
Example:
112+
113+
```php
114+
<?php
115+
116+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
117+
use Unleash\Client\Event\FeatureToggleDisabledEvent;
118+
use Unleash\Client\Event\UnleashEvents;
119+
use Unleash\Client\DTO\DefaultFeature;
120+
use Unleash\Client\DTO\DefaultStrategy;
121+
122+
final class MyEventSubscriber implements EventSubscriberInterface
123+
{
124+
public static function getSubscribedEvents()
125+
{
126+
return [
127+
UnleashEvents::FEATURE_TOGGLE_DISABLED => 'onFeatureDisabled',
128+
];
129+
}
130+
131+
public function onFeatureDisabled(FeatureToggleDisabledEvent $event): void
132+
{
133+
// methods:
134+
$event->getContext(); // instance of Context
135+
$event->getFeature(); // instance of Feature
136+
}
137+
}
138+
```
139+
140+
## FEATURE_TOGGLE_MISSING_STRATEGY_HANDLER event
141+
142+
Triggered when no strategy handler can be found for any of the strategies.
143+
144+
Example:
145+
146+
```php
147+
<?php
148+
149+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
150+
use Unleash\Client\Event\UnleashEvents;
151+
use Unleash\Client\UnleashBuilder;
152+
use Unleash\Client\Event\FeatureToggleMissingStrategyHandlerEvent;
153+
use Unleash\Client\Strategy\DefaultStrategyHandler;
154+
155+
final class MyEventSubscriber implements EventSubscriberInterface
156+
{
157+
public static function getSubscribedEvents()
158+
{
159+
return [
160+
UnleashEvents::FEATURE_TOGGLE_MISSING_STRATEGY_HANDLER => 'onMissingStrategyHandler',
161+
];
162+
}
163+
164+
public function onMissingStrategyHandler(FeatureToggleMissingStrategyHandlerEvent $event): void
165+
{
166+
// methods:
167+
$event->getContext(); // instance of Context
168+
$event->getFeature(); // instance of Feature
169+
// get strategies
170+
$event->getFeature()->getStrategies(); // iterable of Strategy instances
171+
}
172+
}
173+
```
174+
175+
## Customizing event dispatcher
176+
177+
If you already use event dispatcher in your app, you can provide it to the builder:
178+
179+
```php
180+
<?php
181+
182+
use Symfony\Component\EventDispatcher\EventDispatcher;
183+
use Unleash\Client\UnleashBuilder;
184+
185+
$eventDispatcher = new EventDispatcher();
186+
187+
// do something with event dispatcher
188+
189+
$unleash = UnleashBuilder::create()
190+
->withEventDispatcher($eventDispatcher)
191+
// add other unleash configurations
192+
->build();
193+
```
194+
195+
All event subscribers/listeners registered directly in the event dispatcher work as usual:
196+
197+
```php
198+
<?php
199+
200+
use Symfony\Component\EventDispatcher\EventDispatcher;
201+
use Unleash\Client\UnleashBuilder;
202+
use Unleash\Client\Event\UnleashEvents;
203+
use Unleash\Client\Event\FeatureToggleDisabledEvent;
204+
205+
$eventDispatcher = new EventDispatcher();
206+
207+
$eventDispatcher->addSubscriber(new MyEventSubscriber());
208+
$eventDispatcher->addListener(UnleashEvents::FEATURE_TOGGLE_DISABLED, function (FeatureToggleDisabledEvent $event) {
209+
// todo
210+
});
211+
212+
213+
$unleash = UnleashBuilder::create()
214+
->withEventDispatcher($eventDispatcher)
215+
// add other unleash configurations
216+
->build();
217+
```
218+
219+
> Tip for PhpStorm users: Use the [Symfony plugin](https://plugins.jetbrains.com/plugin/7219-symfony-support)
220+
> for help with autocompletion of events, afterwards it looks like this:
221+
222+
![Symfony plugin events autocompletion](symfony-plugin-events.gif)
223+

doc/symfony-plugin-events.gif

936 KB
Loading

phpstan.neon.dist

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
parameters:
22
ignoreErrors:
3-
- '#@throws with type .+ is not subtype of Throwable#'
3+
- '#@throws with type .+ is not subtype of Throwable#'
4+
treatPhpDocTypesAsCertain: false

src/Configuration/UnleashConfiguration.php

+17
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Unleash\Client\ContextProvider\DefaultUnleashContextProvider;
1414
use Unleash\Client\ContextProvider\SettableUnleashContextProvider;
1515
use Unleash\Client\ContextProvider\UnleashContextProvider;
16+
use Unleash\Client\Helper\EventDispatcher;
1617

1718
final class UnleashConfiguration
1819
{
@@ -38,8 +39,11 @@ public function __construct(
3839
// todo remove nullability in next major version
3940
private ?BootstrapProvider $bootstrapProvider = null,
4041
private bool $fetchingEnabled = true,
42+
// todo remove nullability in next major version
43+
private ?EventDispatcher $eventDispatcher = null,
4144
) {
4245
$this->contextProvider ??= new DefaultUnleashContextProvider();
46+
$this->eventDispatcher ??= new EventDispatcher(null);
4347
if ($defaultContext !== null) {
4448
$this->setDefaultContext($defaultContext);
4549
}
@@ -245,4 +249,17 @@ public function setFetchingEnabled(bool $fetchingEnabled): self
245249

246250
return $this;
247251
}
252+
253+
public function getEventDispatcher(): EventDispatcher
254+
{
255+
return $this->eventDispatcher ?? new EventDispatcher(null);
256+
}
257+
258+
public function setEventDispatcher(?EventDispatcher $eventDispatcher): self
259+
{
260+
$eventDispatcher ??= new EventDispatcher(null);
261+
$this->eventDispatcher = $eventDispatcher;
262+
263+
return $this;
264+
}
248265
}

src/DefaultUnleash.php

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
use Unleash\Client\Configuration\UnleashConfiguration;
88
use Unleash\Client\DTO\Strategy;
99
use Unleash\Client\DTO\Variant;
10+
use Unleash\Client\Event\FeatureToggleDisabledEvent;
11+
use Unleash\Client\Event\FeatureToggleMissingStrategyHandlerEvent;
12+
use Unleash\Client\Event\FeatureToggleNotFoundEvent;
13+
use Unleash\Client\Event\UnleashEvents;
1014
use Unleash\Client\Metrics\MetricsHandler;
1115
use Unleash\Client\Repository\UnleashRepository;
1216
use Unleash\Client\Strategy\StrategyHandler;
@@ -36,10 +40,22 @@ public function isEnabled(string $featureName, ?Context $context = null, bool $d
3640

3741
$feature = $this->repository->findFeature($featureName);
3842
if ($feature === null) {
43+
$event = new FeatureToggleNotFoundEvent($context, $featureName);
44+
$this->configuration->getEventDispatcher()->dispatch(
45+
$event,
46+
UnleashEvents::FEATURE_TOGGLE_NOT_FOUND,
47+
);
48+
3949
return $default;
4050
}
4151

4252
if (!$feature->isEnabled()) {
53+
$event = new FeatureToggleDisabledEvent($feature, $context);
54+
$this->configuration->getEventDispatcher()->dispatch(
55+
$event,
56+
UnleashEvents::FEATURE_TOGGLE_DISABLED,
57+
);
58+
4359
$this->metricsHandler->handleMetrics($feature, false);
4460

4561
return false;
@@ -55,11 +71,13 @@ public function isEnabled(string $featureName, ?Context $context = null, bool $d
5571
return true;
5672
}
5773

74+
$handlersFound = false;
5875
foreach ($strategies as $strategy) {
5976
$handlers = $this->findStrategyHandlers($strategy);
6077
if (!count($handlers)) {
6178
continue;
6279
}
80+
$handlersFound = true;
6381
foreach ($handlers as $handler) {
6482
if ($handler->isEnabled($strategy, $context)) {
6583
$this->metricsHandler->handleMetrics($feature, true);
@@ -69,6 +87,14 @@ public function isEnabled(string $featureName, ?Context $context = null, bool $d
6987
}
7088
}
7189

90+
if (!$handlersFound) {
91+
$event = new FeatureToggleMissingStrategyHandlerEvent($context, $feature);
92+
$this->configuration->getEventDispatcher()->dispatch(
93+
$event,
94+
UnleashEvents::FEATURE_TOGGLE_MISSING_STRATEGY_HANDLER,
95+
);
96+
}
97+
7298
$this->metricsHandler->handleMetrics($feature, false);
7399

74100
return false;
@@ -89,7 +115,7 @@ public function getVariant(string $featureName, ?Context $context = null, ?Varia
89115
$this->metricsHandler->handleMetrics($feature, true, $variant);
90116
}
91117

92-
return $variant ?? $fallbackVariant;
118+
return $variant ?? $fallbackVariant;
93119
}
94120

95121
public function register(): bool

0 commit comments

Comments
 (0)