Skip to content

Commit

Permalink
[1.x] Allow excluding features from purge command (#89)
Browse files Browse the repository at this point in the history
* Allow excluding features from purge command

* Remove contract suffix

* Fix code styling

* Update PurgeCommand.php

---------

Co-authored-by: timacdonald <timacdonald@users.noreply.github.com>
Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
3 people authored Mar 4, 2024
1 parent c703915 commit b3783b0
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 8 deletions.
24 changes: 22 additions & 2 deletions src/Commands/PurgeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class PurgeCommand extends Command
*/
protected $signature = 'pennant:purge
{features?* : The features to purge}
{--except=* : The features that should be excluded from purging}
{--except-registered : Purge all features except those registered}
{--store= : The store to purge the features from}';

/**
Expand All @@ -37,9 +39,27 @@ class PurgeCommand extends Command
*/
public function handle(FeatureManager $manager)
{
$manager->store($this->option('store'))->purge($this->argument('features') ?: null);
$store = $manager->store($this->option('store'));

with($this->argument('features') ?: ['All features'], function ($names) {
$features = $this->argument('features') ?: null;

$except = collect($this->option('except'))
->when($this->option('except-registered'), fn ($except) => $except->merge($store->defined()))
->unique()
->all();

if ($except) {
$features = collect($features ?: $store->stored())
->flip()
->forget($except)
->flip()
->values()
->all();
}

$store->purge($features);

with($features ?: ['All features'], function ($names) {
$this->components->info(implode(', ', $names).' successfully purged from storage.');
});

Expand Down
13 changes: 13 additions & 0 deletions src/Contracts/CanListStoredFeatures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Laravel\Pennant\Contracts;

interface CanListStoredFeatures
{
/**
* Retrieve the names of all stored features.
*
* @return array<string>
*/
public function stored(): array;
}
13 changes: 12 additions & 1 deletion src/Drivers/ArrayDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Collection;
use Laravel\Pennant\Contracts\CanListStoredFeatures;
use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Events\UnknownFeatureResolved;
use Laravel\Pennant\Feature;
use stdClass;

class ArrayDriver implements Driver
class ArrayDriver implements CanListStoredFeatures, Driver
{
/**
* The event dispatcher.
Expand Down Expand Up @@ -74,6 +75,16 @@ public function defined(): array
return array_keys($this->featureStateResolvers);
}

/**
* Retrieve the names of all stored features.
*
* @return array<string>
*/
public function stored(): array
{
return array_keys($this->resolvedFeatureStates);
}

/**
* Get multiple feature flag values.
*
Expand Down
20 changes: 18 additions & 2 deletions src/Drivers/DatabaseDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Laravel\Pennant\Contracts\CanListStoredFeatures;
use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Events\UnknownFeatureResolved;
use Laravel\Pennant\Feature;
use stdClass;

class DatabaseDriver implements Driver
class DatabaseDriver implements CanListStoredFeatures, Driver
{
/**
* The database connection.
Expand Down Expand Up @@ -100,7 +101,7 @@ public function define($feature, $resolver): void
}

/**
* Define the names of all defined features.
* Retrieve the names of all defined features.
*
* @return array<string>
*/
Expand All @@ -109,6 +110,21 @@ public function defined(): array
return array_keys($this->featureStateResolvers);
}

/**
* Retrieve the names of all stored features.
*
* @return array<string>
*/
public function stored(): array
{
return $this->newQuery()
->select('name')
->distinct()
->get()
->pluck('name')
->all();
}

/**
* Get multiple feature flag values.
*
Expand Down
22 changes: 19 additions & 3 deletions src/Drivers/Decorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
use Illuminate\Support\Lottery;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Laravel\Pennant\Contracts\Driver as DriverContract;
use Laravel\Pennant\Contracts\CanListStoredFeatures;
use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Contracts\FeatureScopeable;
use Laravel\Pennant\Events\AllFeaturesPurged;
use Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass;
Expand All @@ -24,12 +25,13 @@
use Laravel\Pennant\LazilyResolvedFeature;
use Laravel\Pennant\PendingScopedFeatureInteraction;
use ReflectionFunction;
use RuntimeException;
use Symfony\Component\Finder\Finder;

/**
* @mixin \Laravel\Pennant\PendingScopedFeatureInteraction
*/
class Decorator implements DriverContract
class Decorator implements CanListStoredFeatures, Driver
{
use Macroable {
__call as macroCall;
Expand Down Expand Up @@ -191,6 +193,20 @@ public function defined(): array
return $this->driver->defined();
}

/**
* Retrieve the names of all stored features.
*
* @return array<string>
*/
public function stored(): array
{
if (! $this->driver instanceof CanListStoredFeatures) {
throw new RuntimeException("The [{$this->name}] driver does not support listing stored features.");
}

return $this->driver->stored();
}

/**
* Get multiple feature flag values.
*
Expand Down Expand Up @@ -391,7 +407,7 @@ public function purge($features = null): void
Collection::wrap($features)
->map($this->resolveFeature(...))
->pipe(function ($features) {
$this->driver->purge($features);
$this->driver->purge($features->all());

$this->cache->forget(
$this->cache->whereInStrict('feature', $features)->keys()->all()
Expand Down
10 changes: 10 additions & 0 deletions tests/Feature/ArrayDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,16 @@ public function test_caching_of_features(): void
$this->assertEquals(4, Feature::for($user1)->value('myflag'));
$this->assertEquals(4, Feature::for($user2)->value('myflag'));
}

public function test_it_can_list_stored_features()
{
Feature::define('foo', fn () => true);
Feature::define('bar', fn () => true);

Feature::for('Tim')->active('bar');

$this->assertSame(Feature::stored(), ['bar']);
}
}

class MyFeature
Expand Down
17 changes: 17 additions & 0 deletions tests/Feature/DatabaseDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,23 @@ public function test_it_respects_updated_connection_configuration()
$this->expectExceptionMessage('Database connection [xxxx] not configured.');
Feature::store('database')->active('feature-name');
}

public function test_it_can_list_stored_features()
{
Feature::define('foo', fn () => true);
Feature::define('bar', fn () => true);

Feature::for('tim')->active('bar');
DB::table('features')->insert([
'name' => 'baz',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);

$this->assertSame(Feature::stored(), ['bar', 'baz']);
}
}

class UnregisteredFeature
Expand Down
88 changes: 88 additions & 0 deletions tests/Feature/PurgeCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,92 @@ public function purge()
$this->expectExceptionMessage('Pennant store [foo] is not defined.');
$this->artisan('pennant:purge --store=foo');
}

public function test_it_can_exclude_features_to_purge_from_storage()
{
Feature::define('foo', true);
Feature::define('bar', false);

Feature::for('tim')->active('foo');
Feature::for('taylor')->active('foo');

Feature::for('taylor')->active('bar');

DB::table('features')->insert([
'name' => 'baz',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);

$this->assertCount(3, DB::table('features')->get()->unique('name'));

$this->artisan('pennant:purge --except=foo')->expectsOutputToContain('bar, baz successfully purged from storage.');

$this->assertCount(1, DB::table('features')->get()->unique('name'));

$this->artisan('pennant:purge foo')->expectsOutputToContain('foo successfully purged from storage.');

$this->assertSame(0, DB::table('features')->count());
}

public function test_it_can_combine_except_and_features_as_arguments()
{
DB::table('features')->insert([
'name' => 'foo',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);
DB::table('features')->insert([
'name' => 'bar',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);
DB::table('features')->insert([
'name' => 'baz',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);

$this->artisan('pennant:purge foo bar --except=bar')->expectsOutputToContain('foo successfully purged from storage.');

$this->assertSame(['bar', 'baz'], DB::table('features')->pluck('name')->all());
}

public function test_it_can_purge_features_except_those_registered()
{
Feature::define('foo', fn () => true);
DB::table('features')->insert([
'name' => 'foo',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);
DB::table('features')->insert([
'name' => 'bar',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);
DB::table('features')->insert([
'name' => 'baz',
'scope' => 'Tim',
'value' => true,
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
]);

$this->artisan('pennant:purge --except-registered')->expectsOutputToContain('bar, baz successfully purged from storage.');

$this->assertSame(['foo'], DB::table('features')->pluck('name')->all());
}
}

0 comments on commit b3783b0

Please sign in to comment.