Skip to content

Commit 233e763

Browse files
committed
Adds v2.1 functionality
1 parent 88d7ca4 commit 233e763

23 files changed

+935
-35
lines changed

README.md

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ Features::noScheduling();
234234
Features::noValidations();
235235
Features::noCommands();
236236
Features::noMiddlewares();
237+
Features::noQueryBuilderMixin();
237238
```
238239

239240
## Usage
@@ -301,6 +302,22 @@ $schedule->command('emails:send Peter --force')
301302
->skipWithoutFeature('my-other-feature')
302303
```
303304

305+
### Query Builder
306+
307+
A useful extension of this package is also in being able to decide if part of a query should occur if a feature is
308+
enabled or disabled.
309+
310+
```php
311+
$results = DB::table('users')
312+
->whenFeatureIsAccessible('my-feature', function (Builder $query) {
313+
return $query->where('type', 'new');
314+
})
315+
->whenFeatureIsNotAccessible('my-feature', function (Builder $query) {
316+
return $query->where('type', 'old');
317+
})
318+
->get();
319+
```
320+
304321
### Artisan Commands
305322

306323
You may run the following commands to toggle the on or off state of the feature.
@@ -311,6 +328,36 @@ php artisan feature:on <gateway> <feature>
311328
php artisan feature:off <gateway> <feature>
312329
```
313330

331+
### Cleaning up Features
332+
333+
Often when working with feature flags you will want to remove flags frequently but aren't clear where
334+
such flags are referenced within the application you're developing. To help with this you can then
335+
add a list of features that you have expired. When these features are accessed, an exception will be thrown.
336+
337+
This is useful when used in conjunction with a test suit.
338+
339+
```php
340+
Features::callOnExpiredFeatures([
341+
'my-feature',
342+
])
343+
```
344+
345+
You may also customise this and provide your own callback if you wish to.
346+
347+
```php
348+
Features::callOnExpiredFeatures([
349+
'my-feature',
350+
], function (string $feature): void {
351+
logger()->debug('Expired Feature!', ['feature' => $feature]);
352+
})
353+
```
354+
355+
You can even implement your own `ExpiredFeaturesHandler` which decides how a feature is expired etc.
356+
357+
```php
358+
Features::applyOnExpiredHandler(new CustomExpiredFeaturesHandler));
359+
```
360+
314361
### Implementing your Own Gateway Drivers
315362

316363
You can create your own gateway drivers. To do so you will need to make your own class
@@ -361,7 +408,73 @@ Then you only need use it in your `features.php` config.
361408

362409
You may also make your driver be `Toggleable` and `Cacheable`
363410

364-
## Testing
411+
### Writing tests with Flags
412+
413+
There is a simple Features Fake that can be used when writing tests. You can do so by simply listing the
414+
feature you wish to be faked.
415+
416+
```php
417+
Features::fake(['my-feature' => true])
418+
```
419+
420+
If you know a feature will be called multiple times that you wish to change the state of during the test you
421+
can supply and array of values which will be used.
422+
423+
```php
424+
Features::fake(['my-feature' => [true, false, true]])
425+
```
426+
427+
There are then also assertions that can be used to check if a feature was or was not accessed and how many
428+
times it was accessed during the test.
429+
430+
```php
431+
Features::assertAccessed('my-feature');
432+
Features::assertAccessedCount('my-feature', 2);
433+
Features::assertNotAccessed('my-feature');
434+
```
435+
436+
If you are using the service container to resolve the `Features` class you must inject the service using the
437+
`Accessibles` contract.
438+
439+
```php
440+
public function get(\YlsIdeas\FeatureFlags\Contracts\Features $features)
441+
{
442+
$features->accessible('my-feature');
443+
}
444+
```
445+
446+
### Debugging Flag access
447+
448+
If you wish to see what features are being accessed during a request you can enable the debug mode.
449+
450+
```php
451+
Features::configureDebugging();
452+
```
453+
454+
Then using an event listener you can use the ActionDebugLog to inspect how the decision was made, such as
455+
which gateway responded or if it came from the cache.
456+
457+
```php
458+
\Illuminate\Support\Facades\Event::listen(
459+
\YlsIdeas\FeatureFlags\Events\FeatureAccessed::class,
460+
function (\YlsIdeas\FeatureFlags\Events\FeatureAccessed $event) {
461+
$event->log->file; // the file that accessed the feature
462+
$event->log->line; // the line of the file that accessed the feature
463+
// the decisions made by each gateway in order of access
464+
// e.g. [
465+
// ['pipe' => 'redis', 'reason' => ActionDebugLog::REASON_NO_RESULT, 'result' => false],
466+
// ['pipe' => 'database', 'reason' => ActionDebugLog::REASON_RESULT, 'result' => true],
467+
// ]
468+
$event->log->decisions;
469+
}
470+
);
471+
```
472+
473+
Logging this information can then help you if you're finding that a feature is not behaving as expected.
474+
475+
## Package Testing
476+
477+
If you wish to develop new features for this package you may run the tests using the following command.
365478

366479
``` bash
367480
composer test

src/ActionableFlag.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace YlsIdeas\FeatureFlags;
44

5-
use YlsIdeas\FeatureFlags\Contracts\ActionableFlag as ActionableFlagContract;
5+
use YlsIdeas\FeatureFlags\Contracts\DebuggableFlag as ActionableFlagContract;
6+
use YlsIdeas\FeatureFlags\Support\ActionDebugLog;
67

78
class ActionableFlag implements ActionableFlagContract
89
{
910
public string $feature;
1011
public ?bool $result = null;
12+
public ?Support\ActionDebugLog $debug = null;
1113

1214
public function feature(): string
1315
{
@@ -28,4 +30,19 @@ public function hasResult(): bool
2830
{
2931
return ! is_null($this->result);
3032
}
33+
34+
public function isDebuggable(): bool
35+
{
36+
return (bool) $this->debug;
37+
}
38+
39+
public function storeInspectionInformation(string $pipe, string $reason, ?bool $result = null)
40+
{
41+
$this->debug->addDecision($pipe, $reason, $result);
42+
}
43+
44+
public function log(): ?ActionDebugLog
45+
{
46+
return $this->debug;
47+
}
3148
}

src/Contracts/DebuggableFlag.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Contracts;
4+
5+
use YlsIdeas\FeatureFlags\Support\ActionDebugLog;
6+
7+
interface DebuggableFlag extends ActionableFlag
8+
{
9+
public function isDebuggable(): bool;
10+
11+
public function storeInspectionInformation(string $pipe, string $reason, ?bool $result = null);
12+
13+
public function log(): ?ActionDebugLog;
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Contracts;
4+
5+
interface ExpiredFeaturesHandler
6+
{
7+
public function isExpired(string $feature): void;
8+
}

src/Contracts/Features.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Contracts;
4+
5+
interface Features
6+
{
7+
public function accessible(string $feature): bool;
8+
}

src/Events/FeatureAccessed.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace YlsIdeas\FeatureFlags\Events;
44

5+
use YlsIdeas\FeatureFlags\Support\ActionDebugLog;
6+
57
/**
68
* @see \YlsIdeas\FeatureFlags\Tests\Events\FeatureAccessedTest
79
*/
810
class FeatureAccessed
911
{
10-
public function __construct(public string $feature, public ?bool $result)
12+
public function __construct(public string $feature, public ?bool $result, public ?ActionDebugLog $log = null)
1113
{
1214
}
1315
}

src/Events/FeatureAccessing.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace YlsIdeas\FeatureFlags\Events;
44

5+
use YlsIdeas\FeatureFlags\Support\ActionDebugLog;
6+
57
/**
68
* @see \YlsIdeas\FeatureFlags\Tests\Events\FeatureAccessingTest
79
*/
810
class FeatureAccessing
911
{
10-
public function __construct(public string $feature)
12+
public function __construct(public string $feature, public ?ActionDebugLog $log = null)
1113
{
1214
}
1315
}

src/Exceptions/FeatureExpired.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Exceptions;
4+
5+
class FeatureExpired extends \RuntimeException
6+
{
7+
public function __construct(
8+
protected string $feature,
9+
string $message = "",
10+
int $code = 0,
11+
?\Throwable $previous = null
12+
) {
13+
parent::__construct($message, $code, $previous);
14+
}
15+
16+
protected function feature(): string
17+
{
18+
return $this->feature;
19+
}
20+
}

src/ExpiredFeaturesHandler.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags;
4+
5+
use YlsIdeas\FeatureFlags\Contracts\ExpiredFeaturesHandler as ExpiredFeaturesHandlerContract;
6+
7+
/**
8+
* @see \YlsIdeas\FeatureFlags\Tests\ExpiredFeaturesHandlerTest
9+
*/
10+
class ExpiredFeaturesHandler implements ExpiredFeaturesHandlerContract
11+
{
12+
/**
13+
* @var callable
14+
*/
15+
protected mixed $handler;
16+
17+
public function __construct(protected array $features, callable $handler)
18+
{
19+
$this->handler = $handler;
20+
}
21+
22+
public function isExpired(string $feature): void
23+
{
24+
if (in_array($feature, $this->features)) {
25+
call_user_func($this->handler, $feature);
26+
}
27+
}
28+
}

src/Facades/Features.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,47 @@
33
namespace YlsIdeas\FeatureFlags\Facades;
44

55
use Illuminate\Support\Facades\Facade;
6+
use YlsIdeas\FeatureFlags\Contracts\ExpiredFeaturesHandler;
7+
use YlsIdeas\FeatureFlags\Contracts\Features as FeaturesContract;
68
use YlsIdeas\FeatureFlags\Manager;
9+
use YlsIdeas\FeatureFlags\Support\FeatureFake;
710

811
/**
912
* @see \YlsIdeas\FeatureFlags\Manager
1013
*
1114
* @method static bool accessible(string $feature)
12-
* @method static turnOn(string $gateway, string $feature)
13-
* @method static turnOff(string $gateway, string $feature)
15+
* @method static void turnOn(string $gateway, string $feature)
16+
* @method static void turnOff(string $gateway, string $feature)
1417
* @method static bool usesValidations()
1518
* @method static bool usesScheduling()
1619
* @method static bool usesBlade()
1720
* @method static bool usesCommands()
1821
* @method static bool usesMiddlewares()
22+
* @method static bool usesQueryBuilderMixin()
1923
* @method static Manager noValidations()
2024
* @method static Manager noScheduling()
2125
* @method static Manager noBlade()
2226
* @method static Manager noCommands()
2327
* @method static Manager noMiddleware()
28+
* @method static Manager noQueryBuilderMixin()
29+
* @method static Manager callOnExpiredFeatures(array $features, callable $handler)
30+
* @method static Manager applyOnExpiredHandler(ExpiredFeaturesHandler $handler)
2431
*/
2532
class Features extends Facade
2633
{
34+
/**
35+
* Replace the bound instance with a fake.
36+
* @param array<string, bool|array> $flagsToFake
37+
*/
38+
public static function fake(array $flagsToFake): FeatureFake
39+
{
40+
static::swap($fake = new FeatureFake(static::getFacadeRoot(), $flagsToFake));
41+
42+
return $fake;
43+
}
44+
2745
protected static function getFacadeAccessor(): string
2846
{
29-
return Manager::class;
47+
return FeaturesContract::class;
3048
}
3149
}

src/FeatureFlagsServiceProvider.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
namespace YlsIdeas\FeatureFlags;
44

55
use Illuminate\Console\Scheduling\Event;
6+
use Illuminate\Database\Query\Builder;
67
use Illuminate\Routing\Router;
78
use Illuminate\Support\Facades\Blade;
89
use Illuminate\Support\Facades\Validator;
910
use Illuminate\Support\ServiceProvider;
10-
use YlsIdeas\FeatureFlags\Contracts\Gateway;
11+
use YlsIdeas\FeatureFlags\Contracts\Features as FeaturesContract;
1112
use YlsIdeas\FeatureFlags\Facades\Features;
1213
use YlsIdeas\FeatureFlags\Middlewares\GuardFeature;
1314
use YlsIdeas\FeatureFlags\Rules\FeatureOnRule;
15+
use YlsIdeas\FeatureFlags\Support\QueryBuilderMixin;
1416

1517
/**
1618
* @see \YlsIdeas\FeatureFlags\Tests\FeatureFlagsServiceProviderTest
@@ -62,6 +64,10 @@ public function boot()
6264
$this->app->make(Router::class)
6365
->aliasMiddleware('feature', GuardFeature::class);
6466
}
67+
68+
if (Features::usesQueryBuilderMixin()) {
69+
$this->queryBuilder();
70+
}
6571
}
6672

6773
/**
@@ -73,9 +79,9 @@ public function register()
7379
$this->mergeConfigFrom(__DIR__.'/../config/features.php', 'features');
7480

7581
if (method_exists($this->app, 'scoped')) {
76-
$this->app->scoped(Gateway::class, Manager::class);
82+
$this->app->scoped(FeaturesContract::class, Manager::class);
7783
} else {
78-
$this->app->singleton(Gateway::class, Manager::class);
84+
$this->app->singleton(FeaturesContract::class, Manager::class);
7985
}
8086
}
8187

@@ -109,4 +115,9 @@ protected function validator()
109115
{
110116
Validator::extendImplicit('requiredWithFeature', FeatureOnRule::class);
111117
}
118+
119+
protected function queryBuilder()
120+
{
121+
Builder::mixin(new QueryBuilderMixin());
122+
}
112123
}

0 commit comments

Comments
 (0)