Skip to content
This repository was archived by the owner on Jun 23, 2025. It is now read-only.

Commit 059a22f

Browse files
committed
Add variant and mocking support. Refactor to use value objects.
[NEW] Mocking can now be achieved easily using `Unleash::fake()` similar to Laravel's `Event::fake()` or `Bus::fake()`. This is achieved using `UnleashFake`. [NEW] Variant support. Get the correct variant based on the flag configuration using `Unleash::variant('flag-name', 'default-value');` [CHANGE] To better support mocking and variants, the package has been updated to use value objects. This should be backwards compatible for the most part, with the implementation fo `ArrayAccess`, except for checks using `is_array()`. [CHANGE] The Laravel-like `enabled()`, `disabled()`, `get()` and `all()` methods are now the primary, with the standard Unleash methods as aliases [CHANGE] Tests have been updated to better handle Config mocking, simplifying some of them dramatically.
1 parent ed1a1b8 commit 059a22f

37 files changed

+3388
-611
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/vendor
22
/.idea
3+
/build
34

45
.phpunit.result.cache
56
phpunit.xml

README.md

Lines changed: 163 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,84 +23,96 @@ Documentation for configuration can be found in [config/unleash.php](https://git
2323

2424
## Usage
2525

26-
```php
27-
use \MikeFrancis\LaravelUnleash\Unleash;
28-
29-
$unleash = app(Unleash::class);
26+
This package provides a `Unleash` facade for quick and easy usage. Alternatively, you can use the aliased `Feature` facade instead.
3027

31-
if ($unleash->isFeatureEnabled('myAwesomeFeature')) {
28+
```php
29+
if (\Unleash::enabled('myAwesomeFeature')) {
3230
// Congratulations, you can see this awesome feature!
3331
}
3432

35-
if ($unleash->isFeatureDisabled('myAwesomeFeature')) {
33+
if (\Unleash::disabled('myAwesomeFeature')) {
3634
// Check back later for more features!
3735
}
3836

39-
$feature = $unleash->getFeature('myAwesomeFeature');
37+
$feature = \Unleash::get('myAwesomeFeature');
4038

41-
$allFeatures = $unleash->getFeatures();
39+
$allFeatures = \Unleash::all();
4240
```
4341

44-
### Facades
42+
### Unleash Client Consistency
4543

46-
You can use the `Unleash` facade:
44+
Both the `Unleash` and `Feature` facade support methods consistent with standard Unleash clients:
4745

4846
```php
49-
use Unleash;
50-
51-
if (Unleash::isFeatureEnabled('myAwesomeFeature')) {
47+
if (\Unleash::isFeatureEnabled('myAwesomeFeature')) {
5248
// Congratulations, you can see this awesome feature!
5349
}
5450

55-
if (Unleash::isFeatureDisabled('myAwesomeFeature')) {
51+
if (\Unleash::isFeatureDisabled('myAwesomeFeature')) {
5652
// Check back later for more features!
5753
}
5854

59-
$feature = Unleash::getFeature('myAwesomeFeature');
55+
$feature = \Unleash::getFeature('myAwesomeFeature');
6056

61-
$allFeatures = Unleash::getFeatures();
57+
$allFeatures = \Unleash::getFeatures();
6258
```
6359

64-
or use the generically named `Feature` facade:
60+
## Strategies
6561

66-
```php
67-
use Feature;
62+
To enable or disable strategies, add or remove from `unleash.strategies` config in your `unleash.php` config file.
6863

69-
if (Feature::enabled('myAwesomeFeature')) {
70-
// Congratulations, you can see this awesome feature!
71-
}
64+
### Custom Strategies
7265

73-
if (Feature::disabled('myAwesomeFeature')) {
74-
// Check back later for more features!
75-
}
66+
Custom strategies must implement `\MikeFrancis\LaravelUnleash\Strategies\Contracts\Strategy` or if your strategy relies on dynamic data at runtime it should implement `\MikeFrancis\LaravelUnleash\Strategies\Contracts\DynamicStrategy`.
7667

77-
$feature = Feature::get('myAwesomeFeature');
68+
```php
69+
use \MikeFrancis\LaravelUnleash\Strategies\Contracts\Strategy;
70+
use \Illuminate\Http\Request;
7871

79-
$allFeatures = Feature::all();
72+
class CustomStrategy implements Strategy {
73+
public function isEnabled(array $params, Request $request) : bool {
74+
// logic here
75+
return true || false;
76+
}
77+
}
8078
```
8179

82-
### Dynamic Arguments
80+
### Dynamic Strategies
8381

84-
If your strategy relies on dynamic data at runtime, you can pass additional arguments to the feature check functions:
82+
When implementing `DynamicStrategy` you can pass additional arguments to the feature check functions which will be passed as extra arguments to the `isEnabled()` method:
8583

8684
```php
87-
use \MikeFrancis\LaravelUnleash\Unleash;
88-
use Config;
89-
90-
$unleash = app(Unleash::class);
91-
9285
$allowList = config('app.allow_list');
9386

94-
if ($unleash->isFeatureEnabled('myAwesomeFeature', $allowList)) {
87+
if (Unleash::enabled('myAwesomeFeature', $allowList)) {
9588
// Congratulations, you can see this awesome feature!
9689
}
9790

98-
if ($unleash->isFeatureDisabled('myAwesomeFeature', $allowList)) {
91+
if (Unleash::disabled('myAwesomeFeature', $allowList)) {
9992
// Check back later for more features!
10093
}
10194
```
10295

103-
### Blade
96+
## Variants
97+
98+
To use variant support, define your variants on the feature and use:
99+
100+
```php
101+
$color = \Unleash::variant('title-color', '#000')->payload->value;
102+
```
103+
104+
This will return the correct variant for the user, or the default if the feature flag is disabled or no valid variant is found.
105+
106+
The variant payload will be one of the following, depending on the variant type:
107+
108+
- `\MikeFrancis\LaravelUnleash\Values\Variant\PayloadCSV`
109+
- `\MikeFrancis\LaravelUnleash\Values\Variant\PayloadJSON`
110+
- `\MikeFrancis\LaravelUnleash\Values\Variant\PayloadString`
111+
- `\MikeFrancis\LaravelUnleash\Values\Variant\PayloadDefault` — when no variant is found and the default is used instead
112+
113+
> **Note:** You _can_ combine variants with strategies.
114+
115+
## Blade Templates
104116

105117
Blade directive for checking if a feature is **enabled**:
106118

@@ -118,9 +130,9 @@ Check back later for more features!
118130
@endfeatureDisabled
119131
```
120132

121-
You cannot currently use dynamic strategy arguments with Blade template directives.
133+
> **Note:** You cannot currently use dynamic strategy arguments with Blade template directives.
122134
123-
### Middleware
135+
## Middleware
124136

125137
This package includes middleware that will deny routes depending on whether a feature is enabled or not.
126138

@@ -160,4 +172,115 @@ class ExampleController extends Controller
160172
}
161173
```
162174

163-
You cannot currently use dynamic strategy arguments with Middleware.
175+
> **Note:** You cannot currently use dynamic strategy arguments with Middleware.
176+
177+
## Mocking
178+
179+
If you are writing tests against code utilizing feature flags you can mock feature flags using the `Unleash::fake()` method.
180+
181+
As with Unleash itself, the default behavior is for all flags to be considered disabled:
182+
183+
```php
184+
\Unleash::fake();
185+
186+
$this->assertFalse(\Unleash::enabled('any-flag'));
187+
```
188+
189+
To consider all flags enabled, you can set the default to `true` using `withDefaultStatus()`:
190+
191+
```php
192+
\Unleash::fake()->withDefaultStatus(true);
193+
194+
$this->assertTrue(\Unleash::enabled('any-flag'));
195+
```
196+
197+
You may also dynamically return the default status using `withDefaultUsing()`:
198+
199+
```php
200+
\Unleash::fake()->withDefaultStatusUsing(function($flagName, $flagStatus, ... $args) {
201+
return !$args[0];
202+
});
203+
204+
$this->assertTrue(\Unleash::enabled('any-flag', false));
205+
206+
$this->assertFalse(\Unleash::enabled('any-flag', true));
207+
```
208+
209+
Additionaly, there are several ways to set specific Feature Flags to enabled.
210+
211+
To always enable one or more feature flags, you can pass in an array of flag names:
212+
213+
```php
214+
\Unleash::fake(['flag-name-to-enable', 'another-enabled-flag']);
215+
216+
$this->assertTrue(\Unleash::enabled('flag-name-to-enable'));
217+
$this->assertTrue(\Unleash::enabled('another-enabled-flag'));
218+
219+
$this->assertFalse(\Unleash::enabled('an-unknown-flag'));
220+
```
221+
222+
> **Note:** You can call `Unleash::fake()` multiple times in a single test to set additional flags
223+
224+
Alternatively, for more advanced scenarios, you can pass in a variable number of `\MikeFrancis\LaravelUnleash\Values\FeatureFlag` instances.
225+
226+
If you wish to only enable the feature with the correct `DynamicStrategy` arguments without executing the strategy, you can use `withTestArgs()`:
227+
228+
```php
229+
use \MikeFrancis\LaravelUnleash\Values\FeatureFlag;
230+
231+
\Unleash::fake(
232+
(new FeatureFlag('flag-name', true))->withTestArgs(1, 2, 3);
233+
)
234+
235+
$this->assertTrue(\Unleash::enabled('flag-name', 1, 2, 3));
236+
237+
$this->assertFalse(\Unleash::enabled('flag-name'));
238+
$this->assertFalse(\Unleash::enabled('flag-name', 2, 4, 6));
239+
```
240+
241+
If you need to validate the arguments dynamically, you can instead use `withTestArgsUsing()` which takes a callback that returns a boolean on whether the arguments are accepted or not:
242+
243+
```php
244+
use \MikeFrancis\LaravelUnleash\Values\FeatureFlag;
245+
246+
\Unleash::fake(
247+
(new FeatureFlag('flag-name', true))->withTestArgsUsing(function(int $int) {
248+
return $int % 2 === 0;
249+
});
250+
)
251+
252+
$this->assertTrue(\Unleash::enabled('flag-name', 2));
253+
$this->assertTrue(\Unleash::enabled('flag-name', 4));
254+
255+
$this->assertFalse(\Unleash::enabled('flag-name'));
256+
$this->assertFalse(\Unleash::enabled('flag-name', 1));
257+
$this->assertFalse(\Unleash::enabled('flag-name', 3));
258+
```
259+
260+
One final option is the `withTestArgsAny()` which will allow any arguments. This is an alias for the following:
261+
262+
```php
263+
use \MikeFrancis\LaravelUnleash\Values\FeatureFlag;
264+
265+
\Unleash::fake(
266+
(new FeatureFlag('flag-name', true))->withTestArgsUsing(function() {
267+
return true;
268+
});
269+
)
270+
```
271+
272+
We recommend using the `Unleash::fake(['flag-name'])` option instead.
273+
274+
Lastly, you may pass in both Strategies and Variants to the `FeatureFlag` and both will execute as normal:
275+
276+
```php
277+
use \MikeFrancis\LaravelUnleash\Values\FeatureFlag;
278+
279+
\Unleash::fake(
280+
(new FeatureFlag('flag-name', true, '', 'default', false, 'release', [
281+
'myStrategy' => MyStrategyClass::class,
282+
]))->withTestArgsUsing(function() {
283+
return true;
284+
});
285+
)
286+
```

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
"description": "An Unleash client for Laravel",
44
"type": "library",
55
"require": {
6+
"ext-json": "*",
67
"guzzlehttp/guzzle": "^6.3|^7.0",
78
"illuminate/support": "^5.8|^6|^7|^8",
89
"illuminate/http": "^5.8|^6|^7|^8",
9-
"illuminate/contracts": "^5.8|^6|^7|^8"
10+
"illuminate/contracts": "^5.8|^6|^7|^8",
11+
"lastguest/murmurhash": "^2.1"
1012
},
1113
"require-dev": {
1214
"phpunit/phpunit": "^8.3",
13-
"squizlabs/php_codesniffer": "^3.5"
15+
"squizlabs/php_codesniffer": "^3.5",
16+
"orchestra/testbench": "^6.0"
1417
},
1518
"autoload": {
1619
"psr-4": {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace MikeFrancis\LaravelUnleash\Exception;
4+
5+
class UnknownVariantTypeException extends \Exception
6+
{
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace MikeFrancis\LaravelUnleash\Exception;
4+
5+
class VariantNotFoundException extends \Exception
6+
{
7+
8+
}

src/Facades/Feature.php

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,9 @@
11
<?php
22
namespace MikeFrancis\LaravelUnleash\Facades;
33

4-
use Illuminate\Support\Facades\Facade;
5-
6-
class Feature extends Facade
4+
/**
5+
* @inheritDoc
6+
*/
7+
class Feature extends Unleash
78
{
8-
public static function enabled(string $feature, ...$args): bool
9-
{
10-
return static::isFeatureEnabled($feature, ...$args);
11-
}
12-
13-
public static function disabled(string $feature, ...$args): bool
14-
{
15-
return static::isFeatureDisabled($feature, ...$args);
16-
}
17-
18-
public static function all(): array
19-
{
20-
return static::getFeatures();
21-
}
22-
23-
public static function get(string $name)
24-
{
25-
return static::getFeature($name);
26-
}
27-
28-
protected static function getFacadeAccessor()
29-
{
30-
return 'unleash';
31-
}
329
}

src/Facades/Unleash.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,36 @@
22
namespace MikeFrancis\LaravelUnleash\Facades;
33

44
use Illuminate\Support\Facades\Facade;
5+
use MikeFrancis\LaravelUnleash\Testing\Fakes\UnleashFake;
6+
use MikeFrancis\LaravelUnleash\Values\FeatureFlag;
7+
use MikeFrancis\LaravelUnleash\Values\FeatureFlagCollection;
58

9+
/**
10+
* @method static FeatureFlagCollection all()
11+
* @method static FeatureFlag get(string $name)
12+
* @method static bool enabled(string $feature, ... $args)
13+
* @method static bool disabled(string $feature, ... $args)
14+
* @method static FeatureFlagCollection getFeatures()
15+
* @method static FeatureFlag getFeature(string $name)
16+
* @method static bool isFeatureEnabled(string $feature, ... $args)
17+
* @method static bool isFeatureDisabled(string $feature, ... $args)
18+
*/
619
class Unleash extends Facade
720
{
21+
static protected $fake;
22+
23+
public static function fake(...$features): UnleashFake
24+
{
25+
if (static::getFacadeRoot() instanceof UnleashFake) {
26+
static::getFacadeRoot()->fake(... $features);
27+
return static::getFacadeRoot();
28+
}
29+
30+
$fake = new UnleashFake(static::getFacadeRoot(), ... $features);
31+
static::swap($fake);
32+
return $fake;
33+
}
34+
835
protected static function getFacadeAccessor()
936
{
1037
return 'unleash';

0 commit comments

Comments
 (0)