Skip to content

Commit

Permalink
Adds v2.1 functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
peterfox committed Dec 21, 2022
1 parent 88d7ca4 commit 233e763
Show file tree
Hide file tree
Showing 23 changed files with 935 additions and 35 deletions.
115 changes: 114 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ Features::noScheduling();
Features::noValidations();
Features::noCommands();
Features::noMiddlewares();
Features::noQueryBuilderMixin();
```

## Usage
Expand Down Expand Up @@ -301,6 +302,22 @@ $schedule->command('emails:send Peter --force')
->skipWithoutFeature('my-other-feature')
```

### Query Builder

A useful extension of this package is also in being able to decide if part of a query should occur if a feature is
enabled or disabled.

```php
$results = DB::table('users')
->whenFeatureIsAccessible('my-feature', function (Builder $query) {
return $query->where('type', 'new');
})
->whenFeatureIsNotAccessible('my-feature', function (Builder $query) {
return $query->where('type', 'old');
})
->get();
```

### Artisan Commands

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

### Cleaning up Features

Often when working with feature flags you will want to remove flags frequently but aren't clear where
such flags are referenced within the application you're developing. To help with this you can then
add a list of features that you have expired. When these features are accessed, an exception will be thrown.

This is useful when used in conjunction with a test suit.

```php
Features::callOnExpiredFeatures([
'my-feature',
])
```

You may also customise this and provide your own callback if you wish to.

```php
Features::callOnExpiredFeatures([
'my-feature',
], function (string $feature): void {
logger()->debug('Expired Feature!', ['feature' => $feature]);
})
```

You can even implement your own `ExpiredFeaturesHandler` which decides how a feature is expired etc.

```php
Features::applyOnExpiredHandler(new CustomExpiredFeaturesHandler));
```

### Implementing your Own Gateway Drivers

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

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

## Testing
### Writing tests with Flags

There is a simple Features Fake that can be used when writing tests. You can do so by simply listing the
feature you wish to be faked.

```php
Features::fake(['my-feature' => true])
```

If you know a feature will be called multiple times that you wish to change the state of during the test you
can supply and array of values which will be used.

```php
Features::fake(['my-feature' => [true, false, true]])
```

There are then also assertions that can be used to check if a feature was or was not accessed and how many
times it was accessed during the test.

```php
Features::assertAccessed('my-feature');
Features::assertAccessedCount('my-feature', 2);
Features::assertNotAccessed('my-feature');
```

If you are using the service container to resolve the `Features` class you must inject the service using the
`Accessibles` contract.

```php
public function get(\YlsIdeas\FeatureFlags\Contracts\Features $features)
{
$features->accessible('my-feature');
}
```

### Debugging Flag access

If you wish to see what features are being accessed during a request you can enable the debug mode.

```php
Features::configureDebugging();
```

Then using an event listener you can use the ActionDebugLog to inspect how the decision was made, such as
which gateway responded or if it came from the cache.

```php
\Illuminate\Support\Facades\Event::listen(
\YlsIdeas\FeatureFlags\Events\FeatureAccessed::class,
function (\YlsIdeas\FeatureFlags\Events\FeatureAccessed $event) {
$event->log->file; // the file that accessed the feature
$event->log->line; // the line of the file that accessed the feature
// the decisions made by each gateway in order of access
// e.g. [
// ['pipe' => 'redis', 'reason' => ActionDebugLog::REASON_NO_RESULT, 'result' => false],
// ['pipe' => 'database', 'reason' => ActionDebugLog::REASON_RESULT, 'result' => true],
// ]
$event->log->decisions;
}
);
```

Logging this information can then help you if you're finding that a feature is not behaving as expected.

## Package Testing

If you wish to develop new features for this package you may run the tests using the following command.

``` bash
composer test
Expand Down
19 changes: 18 additions & 1 deletion src/ActionableFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace YlsIdeas\FeatureFlags;

use YlsIdeas\FeatureFlags\Contracts\ActionableFlag as ActionableFlagContract;
use YlsIdeas\FeatureFlags\Contracts\DebuggableFlag as ActionableFlagContract;
use YlsIdeas\FeatureFlags\Support\ActionDebugLog;

class ActionableFlag implements ActionableFlagContract
{
public string $feature;
public ?bool $result = null;
public ?Support\ActionDebugLog $debug = null;

public function feature(): string
{
Expand All @@ -28,4 +30,19 @@ public function hasResult(): bool
{
return ! is_null($this->result);
}

public function isDebuggable(): bool
{
return (bool) $this->debug;
}

public function storeInspectionInformation(string $pipe, string $reason, ?bool $result = null)
{
$this->debug->addDecision($pipe, $reason, $result);
}

public function log(): ?ActionDebugLog
{
return $this->debug;
}
}
14 changes: 14 additions & 0 deletions src/Contracts/DebuggableFlag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace YlsIdeas\FeatureFlags\Contracts;

use YlsIdeas\FeatureFlags\Support\ActionDebugLog;

interface DebuggableFlag extends ActionableFlag
{
public function isDebuggable(): bool;

public function storeInspectionInformation(string $pipe, string $reason, ?bool $result = null);

public function log(): ?ActionDebugLog;
}
8 changes: 8 additions & 0 deletions src/Contracts/ExpiredFeaturesHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace YlsIdeas\FeatureFlags\Contracts;

interface ExpiredFeaturesHandler
{
public function isExpired(string $feature): void;
}
8 changes: 8 additions & 0 deletions src/Contracts/Features.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace YlsIdeas\FeatureFlags\Contracts;

interface Features
{
public function accessible(string $feature): bool;
}
4 changes: 3 additions & 1 deletion src/Events/FeatureAccessed.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace YlsIdeas\FeatureFlags\Events;

use YlsIdeas\FeatureFlags\Support\ActionDebugLog;

/**
* @see \YlsIdeas\FeatureFlags\Tests\Events\FeatureAccessedTest
*/
class FeatureAccessed
{
public function __construct(public string $feature, public ?bool $result)
public function __construct(public string $feature, public ?bool $result, public ?ActionDebugLog $log = null)
{
}
}
4 changes: 3 additions & 1 deletion src/Events/FeatureAccessing.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace YlsIdeas\FeatureFlags\Events;

use YlsIdeas\FeatureFlags\Support\ActionDebugLog;

/**
* @see \YlsIdeas\FeatureFlags\Tests\Events\FeatureAccessingTest
*/
class FeatureAccessing
{
public function __construct(public string $feature)
public function __construct(public string $feature, public ?ActionDebugLog $log = null)
{
}
}
20 changes: 20 additions & 0 deletions src/Exceptions/FeatureExpired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace YlsIdeas\FeatureFlags\Exceptions;

class FeatureExpired extends \RuntimeException
{
public function __construct(
protected string $feature,
string $message = "",
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}

protected function feature(): string
{
return $this->feature;
}
}
28 changes: 28 additions & 0 deletions src/ExpiredFeaturesHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace YlsIdeas\FeatureFlags;

use YlsIdeas\FeatureFlags\Contracts\ExpiredFeaturesHandler as ExpiredFeaturesHandlerContract;

/**
* @see \YlsIdeas\FeatureFlags\Tests\ExpiredFeaturesHandlerTest
*/
class ExpiredFeaturesHandler implements ExpiredFeaturesHandlerContract
{
/**
* @var callable
*/
protected mixed $handler;

public function __construct(protected array $features, callable $handler)
{
$this->handler = $handler;
}

public function isExpired(string $feature): void
{
if (in_array($feature, $this->features)) {
call_user_func($this->handler, $feature);
}
}
}
24 changes: 21 additions & 3 deletions src/Facades/Features.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,47 @@
namespace YlsIdeas\FeatureFlags\Facades;

use Illuminate\Support\Facades\Facade;
use YlsIdeas\FeatureFlags\Contracts\ExpiredFeaturesHandler;
use YlsIdeas\FeatureFlags\Contracts\Features as FeaturesContract;
use YlsIdeas\FeatureFlags\Manager;
use YlsIdeas\FeatureFlags\Support\FeatureFake;

/**
* @see \YlsIdeas\FeatureFlags\Manager
*
* @method static bool accessible(string $feature)
* @method static turnOn(string $gateway, string $feature)
* @method static turnOff(string $gateway, string $feature)
* @method static void turnOn(string $gateway, string $feature)
* @method static void turnOff(string $gateway, string $feature)
* @method static bool usesValidations()
* @method static bool usesScheduling()
* @method static bool usesBlade()
* @method static bool usesCommands()
* @method static bool usesMiddlewares()
* @method static bool usesQueryBuilderMixin()
* @method static Manager noValidations()
* @method static Manager noScheduling()
* @method static Manager noBlade()
* @method static Manager noCommands()
* @method static Manager noMiddleware()
* @method static Manager noQueryBuilderMixin()
* @method static Manager callOnExpiredFeatures(array $features, callable $handler)
* @method static Manager applyOnExpiredHandler(ExpiredFeaturesHandler $handler)
*/
class Features extends Facade
{
/**
* Replace the bound instance with a fake.
* @param array<string, bool|array> $flagsToFake
*/
public static function fake(array $flagsToFake): FeatureFake
{
static::swap($fake = new FeatureFake(static::getFacadeRoot(), $flagsToFake));

return $fake;
}

protected static function getFacadeAccessor(): string
{
return Manager::class;
return FeaturesContract::class;
}
}
17 changes: 14 additions & 3 deletions src/FeatureFlagsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
namespace YlsIdeas\FeatureFlags;

use Illuminate\Console\Scheduling\Event;
use Illuminate\Database\Query\Builder;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use YlsIdeas\FeatureFlags\Contracts\Gateway;
use YlsIdeas\FeatureFlags\Contracts\Features as FeaturesContract;
use YlsIdeas\FeatureFlags\Facades\Features;
use YlsIdeas\FeatureFlags\Middlewares\GuardFeature;
use YlsIdeas\FeatureFlags\Rules\FeatureOnRule;
use YlsIdeas\FeatureFlags\Support\QueryBuilderMixin;

/**
* @see \YlsIdeas\FeatureFlags\Tests\FeatureFlagsServiceProviderTest
Expand Down Expand Up @@ -62,6 +64,10 @@ public function boot()
$this->app->make(Router::class)
->aliasMiddleware('feature', GuardFeature::class);
}

if (Features::usesQueryBuilderMixin()) {
$this->queryBuilder();
}
}

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

if (method_exists($this->app, 'scoped')) {
$this->app->scoped(Gateway::class, Manager::class);
$this->app->scoped(FeaturesContract::class, Manager::class);
} else {
$this->app->singleton(Gateway::class, Manager::class);
$this->app->singleton(FeaturesContract::class, Manager::class);
}
}

Expand Down Expand Up @@ -109,4 +115,9 @@ protected function validator()
{
Validator::extendImplicit('requiredWithFeature', FeatureOnRule::class);
}

protected function queryBuilder()
{
Builder::mixin(new QueryBuilderMixin());
}
}
Loading

0 comments on commit 233e763

Please sign in to comment.