From 8ccf2ffb2622852fdf2218f5da94dbf1d5cf7818 Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Wed, 27 Apr 2022 18:33:34 +0100 Subject: [PATCH 01/11] Added new Columns to Features Table Added DBal to allow appending columns to SQLite --- composer.json | 1 + ...2_04_27_090000_add_feature_flag_expiry.php | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 database/migrations/2022_04_27_090000_add_feature_flag_expiry.php diff --git a/composer.json b/composer.json index b6d0ee3..0d1eabd 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "illuminate/support": "^9.0.1" }, "require-dev": { + "doctrine/dbal": "^3.3", "nunomaduro/collision": "^6.0", "orchestra/testbench": "^7.0.0", "pestphp/pest": "^1.21.1", diff --git a/database/migrations/2022_04_27_090000_add_feature_flag_expiry.php b/database/migrations/2022_04_27_090000_add_feature_flag_expiry.php new file mode 100644 index 0000000..7beda9f --- /dev/null +++ b/database/migrations/2022_04_27_090000_add_feature_flag_expiry.php @@ -0,0 +1,26 @@ +datetime('expires_at')->nullable(); + }); + } + } + + public function down() + { + if (Schema::hasTable('features')) { + Schema::table('features', function (Blueprint $table) { + $table->dropColumn('expires_at'); + }); + } + } +}; From 6bfe87310213e6845fed42815c60ce51addc23cc Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Wed, 27 Apr 2022 18:33:56 +0100 Subject: [PATCH 02/11] Added Timebomb Feature --- config/feature-flags.php | 12 ++++ src/Console/AddFeature.php | 5 ++ src/Console/ExtendFeature.php | 33 +++++++++++ src/Console/ViewFeatures.php | 6 +- src/FeatureFlagsServiceProvider.php | 2 + src/Models/Feature.php | 16 +++++- tests/Unit/FeatureArtisanTest.php | 73 +++++++++++++++++++++++- tests/Unit/FeatureTimeBombTest.php | 87 +++++++++++++++++++++++++++++ 8 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 src/Console/ExtendFeature.php create mode 100644 tests/Unit/FeatureTimeBombTest.php diff --git a/config/feature-flags.php b/config/feature-flags.php index aacedeb..489fb02 100644 --- a/config/feature-flags.php +++ b/config/feature-flags.php @@ -36,4 +36,16 @@ */ 'status_code' => 404, ], + + /* + |-------------------------------------------------------------------------- + | Enabling Time bombs for Features + |-------------------------------------------------------------------------- + | + | This option controls whether an exception will be thrown if a feature + | has expired. See Martin Fowler's blog post on this: + | https://martinfowler.com/articles/feature-toggles.html#WorkingWithFeature-flaggedSystems + | + */ + 'enable_time_bombs' => false, ]; diff --git a/src/Console/AddFeature.php b/src/Console/AddFeature.php index fcbf073..1df1d52 100644 --- a/src/Console/AddFeature.php +++ b/src/Console/AddFeature.php @@ -27,10 +27,15 @@ public function handle() $description = $this->ask('Feature Description'); $active = $this->choice('Is the feature active', ['no', 'yes'], 'yes'); + if(config('feature-flags.enable_time_bombs')) { + $expires_at = $this->ask('When do you want your feature to expire? (Number of Days)', 0); + } + Feature::create([ 'name' => $featureName, 'description' => $description, 'active' => $active == 'yes', + 'expires_at' => isset($expires_at) ? \Carbon\Carbon::now()->addDays($expires_at) : null ]); $this->info("Created '{$featureName}' feature"); diff --git a/src/Console/ExtendFeature.php b/src/Console/ExtendFeature.php new file mode 100644 index 0000000..316ee32 --- /dev/null +++ b/src/Console/ExtendFeature.php @@ -0,0 +1,33 @@ +info("Time bombs are not enabled!"); + + $featureName = $this->ask('Feature Name to Extend'); + $feature = Feature::name($featureName)->first(); + + $extendBy = $this->ask('When do you want your feature to expire? (Number of Days)', 0); + + $feature->expires_at = $feature->expires_at->addDays($extendBy); + $feature->save(); + + $this->info("Updated '{$featureName}' feature expiry date"); + + return 0; + } +} diff --git a/src/Console/ViewFeatures.php b/src/Console/ViewFeatures.php index d91022b..c566c98 100644 --- a/src/Console/ViewFeatures.php +++ b/src/Console/ViewFeatures.php @@ -15,9 +15,11 @@ class ViewFeatures extends Command public function handle() { - $features = Feature::all(['name', 'description', 'active'])->toArray(); + $features = Feature::withoutEvents(function() { + return Feature::all(['name', 'description', 'active', 'expires_at'])->toArray(); + }); - $headers = ['Name', 'Description', 'Active']; + $headers = ['Name', 'Description', 'Active', 'Expires At']; $this->table($headers, $features); } diff --git a/src/FeatureFlagsServiceProvider.php b/src/FeatureFlagsServiceProvider.php index a244bca..4a4b483 100644 --- a/src/FeatureFlagsServiceProvider.php +++ b/src/FeatureFlagsServiceProvider.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; use JustSteveKing\Laravel\FeatureFlags\Console\AddFeature; +use JustSteveKing\Laravel\FeatureFlags\Console\ExtendFeature; use JustSteveKing\Laravel\FeatureFlags\Console\ViewFeatures; use JustSteveKing\Laravel\FeatureFlags\Console\ActivateFeature; use JustSteveKing\Laravel\FeatureFlags\Console\AddFeatureGroup; @@ -59,6 +60,7 @@ public function boot() ViewFeatures::class, ActivateFeature::class, AddFeatureGroup::class, + ExtendFeature::class, ViewFeatureGroups::class, AddFeatureToGroup::class, DeactivateFeature::class, diff --git a/src/Models/Feature.php b/src/Models/Feature.php index cdbc27c..ebf11b1 100644 --- a/src/Models/Feature.php +++ b/src/Models/Feature.php @@ -4,6 +4,8 @@ namespace JustSteveKing\Laravel\FeatureFlags\Models; +use Carbon\Carbon; +use Exception; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use JustSteveKing\Laravel\FeatureFlags\Models\Concerns\NormaliseName; @@ -12,17 +14,29 @@ class Feature extends Model { use NormaliseName; - + protected $fillable = [ 'name', 'description', 'active', + 'expires_at' ]; protected $casts = [ 'active' => 'boolean', + 'expires_at' => 'datetime' ]; + public static function booted() + { + static::retrieved(function(Feature $feature) { + if(config('feature-flags.enable_time_bombs')) { + return (! is_null($feature->expires_at) && Carbon::now() >= $feature->expires_at) ? throw new Exception(sprintf('The Feature has expired - %s', $feature->name)) : true; + } + return true; + }); + } + public function groups(): BelongsToMany { return $this->belongsToMany( diff --git a/tests/Unit/FeatureArtisanTest.php b/tests/Unit/FeatureArtisanTest.php index 2b7da0e..e3b5d14 100644 --- a/tests/Unit/FeatureArtisanTest.php +++ b/tests/Unit/FeatureArtisanTest.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole; use JustSteveKing\Laravel\FeatureFlags\Models\Feature; +use Illuminate\Support\Facades\Config; uses(InteractsWithConsole::class); @@ -118,7 +119,7 @@ it('can display a table of empty features', function(): void { $this->artisan('feature-flags:view-features') - ->expectsTable(['Name', 'Description', 'Active'], []); + ->expectsTable(['Name', 'Description', 'Active', 'Expires At'], []); }); it('can display a table of all features', function(): void { @@ -136,8 +137,74 @@ 'active' => false, ]); - $expected_rows = Feature::all(['name', 'description', 'active'])->toArray(); + $expected_rows = Feature::all(['name', 'description', 'active', 'expires_at'])->toArray(); $this->artisan('feature-flags:view-features') - ->expectsTable(['Name', 'Description', 'Active'], $expected_rows); + ->expectsTable(['Name', 'Description', 'Active', 'Expires At'], $expected_rows); +}); + +it('can display a table of all features with Expire At', function(): void { + Feature::create([ + 'name' => 'first feature', + ]); + + Feature::create([ + 'name' => 'second feature', + 'description' => 'A description' + ]); + + Feature::create([ + 'name' => 'third feature', + 'active' => false, + 'expires_at' => \Carbon\Carbon::now()->addDays(7) + ]); + + $expected_rows = Feature::all(['name', 'description', 'active', 'expires_at'])->toArray(); + + $this->artisan('feature-flags:view-features') + ->expectsTable(['Name', 'Description', 'Active', 'Expires At'], $expected_rows); +}); + +it('can display a table of all features with Expire At when TimeBombs are enabled', function(): void { + Config::set('feature-flags.enable_time_bombs', true); + + Feature::create([ + 'name' => 'first feature', + ]); + + Feature::create([ + 'name' => 'second feature', + 'description' => 'A description' + ]); + + Feature::create([ + 'name' => 'third feature', + 'active' => false, + 'expires_at' => \Carbon\Carbon::now()->subDays(7) + ]); + + $expected_rows = Feature::withoutEvents(function() { + return Feature::all(['name', 'description', 'active', 'expires_at'])->toArray(); + }); + + $this->artisan('feature-flags:view-features') + ->expectsTable(['Name', 'Description', 'Active', 'Expires At'], $expected_rows); +}); + +it('can update an expiry date', function(): void { + \Carbon\Carbon::setTestNow(); + + Feature::create([ + 'name' => 'test feature', + 'active' => true, + 'expires_at' => \Carbon\Carbon::now() + ]); + + $this->artisan('feature-flags:extend-feature') + ->expectsQuestion('Feature Name to Extend', 'test feature') + ->expectsQuestion('When do you want your feature to expire? (Number of Days)', 7) + ->expectsOutput("Updated 'test feature' feature expiry date") + ->assertExitCode(0); + + expect(Feature::first()->expires_at)->toEqual(\Carbon\Carbon::now()->addDays(7)); }); diff --git a/tests/Unit/FeatureTimeBombTest.php b/tests/Unit/FeatureTimeBombTest.php new file mode 100644 index 0000000..a4a4e1b --- /dev/null +++ b/tests/Unit/FeatureTimeBombTest.php @@ -0,0 +1,87 @@ + 'Expiring Feature', + 'expires_at' => Carbon::now()->addDay() + ]); + + expect(Feature::all()) + ->toHaveCount(1); +}); + +it('Does not throw an Exception when time bombs are disabled', function(): void { + Config::set('feature-flags.enable_time_bombs', false); + + Feature::create([ + 'name' => 'Expired Feature', + 'expires_at' => Carbon::now()->subDay() + ]); + + expect(Feature::all())->toHaveCount(1); + // Assert No Exception is thrown + $this->assertTrue(true); +}); + +it('throws an Exception when time bombs are enabled', function(): void { + Feature::create([ + 'name' => 'Expired Feature', + 'expires_at' => Carbon::now()->subDay() + ]); + + Feature::all(); +})->throws(Exception::class, 'The Feature has expired - expired feature'); + +it('casts the Expiry date to Carbon', function(): void { + Feature::create([ + 'name' => 'Expired Feature', + 'expires_at' => Carbon::now()->addDay() + ]); + + expect(Feature::first()->expires_at)->toBeInstanceOf(Carbon::class); +}); + +it('throws an Exception when 1 second past an expiry date', function(): void { + Feature::create([ + 'name' => 'Expired Feature', + 'expires_at' => Carbon::now()->subSecond() + ]); + + Feature::all(); +})->throws(Exception::class, 'The Feature has expired - expired feature'); + +it('Does not throw an Exception when an expiry date is 1 second in the future', function(): void { + Feature::create([ + 'name' => 'Expired Feature', + 'expires_at' => Carbon::now()->addSecond() + ]); + + Feature::all(); + // Assert No Exception is thrown + $this->assertTrue(true); +}); + +it('Does not throw an Exception when Expiry date is Null', function(): void { + Feature::create([ + 'name' => 'Null Expiry Feature', + 'expires_at' => null + ]); + + Feature::all(); + // Assert No Exception is thrown + $this->assertTrue(true); +}); From fc0728b809a7b1ba07dd52a5ebf27a213c2a4aa1 Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Wed, 27 Apr 2022 18:38:01 +0100 Subject: [PATCH 03/11] Resolved failing test --- tests/Unit/FeatureArtisanTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Unit/FeatureArtisanTest.php b/tests/Unit/FeatureArtisanTest.php index e3b5d14..c00f94f 100644 --- a/tests/Unit/FeatureArtisanTest.php +++ b/tests/Unit/FeatureArtisanTest.php @@ -200,11 +200,13 @@ 'expires_at' => \Carbon\Carbon::now() ]); + $feature = Feature::first()->expires_at; + $this->artisan('feature-flags:extend-feature') ->expectsQuestion('Feature Name to Extend', 'test feature') ->expectsQuestion('When do you want your feature to expire? (Number of Days)', 7) ->expectsOutput("Updated 'test feature' feature expiry date") ->assertExitCode(0); - expect(Feature::first()->expires_at)->toEqual(\Carbon\Carbon::now()->addDays(7)); + expect(Feature::first()->expires_at)->toEqual($feature->addDays(7)); }); From dfe25963e62c5b06c69cec922effe58a5f40e436 Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Fri, 6 May 2022 09:44:28 +0100 Subject: [PATCH 04/11] Added Time bomb Environments to prevent affecting production --- config/feature-flags.php | 12 ++++++++ src/Models/Feature.php | 6 +++- tests/Unit/FeatureTimeBombTest.php | 44 +++++++++++++++++------------- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/config/feature-flags.php b/config/feature-flags.php index 489fb02..b0b1020 100644 --- a/config/feature-flags.php +++ b/config/feature-flags.php @@ -48,4 +48,16 @@ | */ 'enable_time_bombs' => false, + + /* + |-------------------------------------------------------------------------- + | Environments that will NOT trigger Time Bombs + |-------------------------------------------------------------------------- + | + | This option controls which environment settings will prevent time bomb + | exceptions from being thrown. To trigger in all environments, leave + | the array as empty. + | + */ + 'time_bomb_environments' => ['production'] ]; diff --git a/src/Models/Feature.php b/src/Models/Feature.php index ebf11b1..5124e6b 100644 --- a/src/Models/Feature.php +++ b/src/Models/Feature.php @@ -8,6 +8,7 @@ use Exception; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Support\Facades\App; use JustSteveKing\Laravel\FeatureFlags\Models\Concerns\NormaliseName; use JustSteveKing\Laravel\FeatureFlags\Models\Builders\FeatureBuilder; @@ -30,7 +31,10 @@ class Feature extends Model public static function booted() { static::retrieved(function(Feature $feature) { - if(config('feature-flags.enable_time_bombs')) { + if( + config('feature-flags.enable_time_bombs') + && ! App::environment(config('feature-flags.time_bomb_environments')) + ) { return (! is_null($feature->expires_at) && Carbon::now() >= $feature->expires_at) ? throw new Exception(sprintf('The Feature has expired - %s', $feature->name)) : true; } return true; diff --git a/tests/Unit/FeatureTimeBombTest.php b/tests/Unit/FeatureTimeBombTest.php index a4a4e1b..abd30b9 100644 --- a/tests/Unit/FeatureTimeBombTest.php +++ b/tests/Unit/FeatureTimeBombTest.php @@ -8,12 +8,24 @@ beforeEach(function(): void { Config::set('feature-flags.enable_time_bombs', true); + Config::set('feature-flags.time_bomb_environments', ['production']); }); afterAll(function(): void { Config::set('feature-flags.enable_time_bombs', false); }); +function tryException() { + $exception = null; + + try { + Feature::all(); + } catch(Throwable $exception) {} + + // Assert No Exception is thrown + test()->assertNull($exception, 'Unexpected Exception was thrown'); +} + it('can create a feature with an expiry date', function(): void { Feature::create([ 'name' => 'Expiring Feature', @@ -24,19 +36,6 @@ ->toHaveCount(1); }); -it('Does not throw an Exception when time bombs are disabled', function(): void { - Config::set('feature-flags.enable_time_bombs', false); - - Feature::create([ - 'name' => 'Expired Feature', - 'expires_at' => Carbon::now()->subDay() - ]); - - expect(Feature::all())->toHaveCount(1); - // Assert No Exception is thrown - $this->assertTrue(true); -}); - it('throws an Exception when time bombs are enabled', function(): void { Feature::create([ 'name' => 'Expired Feature', @@ -70,9 +69,7 @@ 'expires_at' => Carbon::now()->addSecond() ]); - Feature::all(); - // Assert No Exception is thrown - $this->assertTrue(true); + test()->tryException(); }); it('Does not throw an Exception when Expiry date is Null', function(): void { @@ -81,7 +78,16 @@ 'expires_at' => null ]); - Feature::all(); - // Assert No Exception is thrown - $this->assertTrue(true); + test()->tryException(); }); + +it('Throws an exception when the environments array is empty', function() : void { + Config::set('feature-flags.time_bomb_environments', []); + + Feature::create([ + 'name' => 'Expired Feature', + 'expires_at' => Carbon::now()->subSecond() + ]); + + Feature::all(); +})->throws(Exception::class, 'The Feature has expired - expired feature'); From 6325b1fbe8ca740d375b8499a6baf594cd5fe286 Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Mon, 9 May 2022 11:33:47 +0100 Subject: [PATCH 05/11] Refactored exception check for whether a feature has expired. --- src/Models/Feature.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Models/Feature.php b/src/Models/Feature.php index 5124e6b..9d8a208 100644 --- a/src/Models/Feature.php +++ b/src/Models/Feature.php @@ -35,7 +35,13 @@ public static function booted() config('feature-flags.enable_time_bombs') && ! App::environment(config('feature-flags.time_bomb_environments')) ) { - return (! is_null($feature->expires_at) && Carbon::now() >= $feature->expires_at) ? throw new Exception(sprintf('The Feature has expired - %s', $feature->name)) : true; + $featureHasExpired = Carbon::now()->isAfter($feature->expires_at); + + if ($featureHasExpired) { + throw new Exception(sprintf('The Feature has expired - %s', $feature->name)); + } + + return true; } return true; }); From 48602d038948e18f9037fd52331eef9edf6d38ce Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Mon, 9 May 2022 11:36:57 +0100 Subject: [PATCH 06/11] Refactored nested conditionals into intermediate variables for checking Timebombs are enabled. --- src/Models/Feature.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Models/Feature.php b/src/Models/Feature.php index 9d8a208..9d7dfa9 100644 --- a/src/Models/Feature.php +++ b/src/Models/Feature.php @@ -31,16 +31,15 @@ class Feature extends Model public static function booted() { static::retrieved(function(Feature $feature) { - if( - config('feature-flags.enable_time_bombs') - && ! App::environment(config('feature-flags.time_bomb_environments')) - ) { + $timeBombsAreEnabled = config('feature-flags.enable_time_bombs'); + $environmentAllowsTimeBombs = ! App::environment(config('feature-flags.time_bomb_environments')); + + if($timeBombsAreEnabled && $environmentAllowsTimeBombs) { $featureHasExpired = Carbon::now()->isAfter($feature->expires_at); if ($featureHasExpired) { throw new Exception(sprintf('The Feature has expired - %s', $feature->name)); } - return true; } return true; From a7c1e6e32381d68c3f1d2de6f00f735908680c52 Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Mon, 9 May 2022 11:39:48 +0100 Subject: [PATCH 07/11] Readded test to check that no Exception is thrown when time bombs are disabled. --- tests/Unit/FeatureTimeBombTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Unit/FeatureTimeBombTest.php b/tests/Unit/FeatureTimeBombTest.php index abd30b9..97e6230 100644 --- a/tests/Unit/FeatureTimeBombTest.php +++ b/tests/Unit/FeatureTimeBombTest.php @@ -36,6 +36,17 @@ function tryException() { ->toHaveCount(1); }); +it('Does not throw an Exception when time bombs are disabled', function(): void { + Config::set('feature-flags.enable_time_bombs', false); + + Feature::create([ + 'name' => 'Expired Feature', + 'expires_at' => Carbon::now()->subDay() + ]); + + test()->tryException(); +}); + it('throws an Exception when time bombs are enabled', function(): void { Feature::create([ 'name' => 'Expired Feature', From 907fbcef688e9d27ceb340aa3b1f3b8a77797fee Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Mon, 27 Jun 2022 20:50:01 +0100 Subject: [PATCH 08/11] Added Pest Plugins --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0d1eabd..af742b1 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,10 @@ }, "config": { "sort-packages": true, - "optimize-autoloader": true + "optimize-autoloader": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } }, "extra": { "laravel": { From df3a22c2b8b7c937b1cb79adb3ec293c7a820db3 Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Mon, 27 Jun 2022 20:50:30 +0100 Subject: [PATCH 09/11] Removed unneeded comment. And minor refactor to simplify null check. --- src/Concerns/HasFeatures.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Concerns/HasFeatures.php b/src/Concerns/HasFeatures.php index 9d05430..af857ec 100644 --- a/src/Concerns/HasFeatures.php +++ b/src/Concerns/HasFeatures.php @@ -112,9 +112,6 @@ public function joinGroup(...$groups): self groups: Arr::flatten($groups) ); - // TODO? Fix groups? Maybe? - // dd($groups); - if (is_null($groups)) { return $this; } @@ -160,7 +157,7 @@ protected function featureExists(string $featureName): bool { $exists = Feature::name($featureName)->first(); - return (is_null($exists)) ? false : true; + return !is_null($exists); } public function features(): BelongsToMany From 55662324b1829d4825345b80c596e02279d17032 Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Mon, 27 Jun 2022 20:51:04 +0100 Subject: [PATCH 10/11] Implemented missing return types. --- src/Console/ActivateFeature.php | 2 +- src/Console/ActivateFeatureGroup.php | 2 +- src/Console/AddFeature.php | 2 +- src/Console/AddFeatureGroup.php | 2 +- src/Console/AddFeatureToGroup.php | 2 +- src/Console/DeactivateFeature.php | 2 +- src/Console/DeactivateFeatureGroup.php | 2 +- src/Console/ExtendFeature.php | 3 +-- src/Console/ViewFeatureGroups.php | 2 +- src/Console/ViewFeatures.php | 2 +- src/Console/ViewGroupsWithFeatures.php | 2 +- src/Models/Feature.php | 2 +- tests/Stubs/User.php | 2 +- 13 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Console/ActivateFeature.php b/src/Console/ActivateFeature.php index 77beb2c..aa734b2 100644 --- a/src/Console/ActivateFeature.php +++ b/src/Console/ActivateFeature.php @@ -13,7 +13,7 @@ class ActivateFeature extends Command protected $description = 'Activates a feature'; - public function handle() + public function handle(): int { $featureName = $this->ask('Feature name to activate'); $feature = Feature::name($featureName)->first(); diff --git a/src/Console/ActivateFeatureGroup.php b/src/Console/ActivateFeatureGroup.php index b04240b..c7ae093 100644 --- a/src/Console/ActivateFeatureGroup.php +++ b/src/Console/ActivateFeatureGroup.php @@ -13,7 +13,7 @@ class ActivateFeatureGroup extends Command protected $description = 'Activates a feature group'; - public function handle() + public function handle(): int { $groupName = $this->ask('Group name to activate'); $group = FeatureGroup::name($groupName)->first(); diff --git a/src/Console/AddFeature.php b/src/Console/AddFeature.php index 1df1d52..3416712 100644 --- a/src/Console/AddFeature.php +++ b/src/Console/AddFeature.php @@ -13,7 +13,7 @@ class AddFeature extends Command protected $description = 'Add a new feature'; - public function handle() + public function handle(): int { $featureName = $this->ask('Feature Name'); $existingFeature = Feature::name($featureName)->first(); diff --git a/src/Console/AddFeatureGroup.php b/src/Console/AddFeatureGroup.php index 4ff6688..07d37a3 100644 --- a/src/Console/AddFeatureGroup.php +++ b/src/Console/AddFeatureGroup.php @@ -13,7 +13,7 @@ class AddFeatureGroup extends Command protected $description = 'Add a new feature group'; - public function handle() + public function handle(): int { $groupName = $this->ask('Group Name'); $existingGroup = FeatureGroup::name($groupName)->first(); diff --git a/src/Console/AddFeatureToGroup.php b/src/Console/AddFeatureToGroup.php index 1514b39..9e40a4f 100644 --- a/src/Console/AddFeatureToGroup.php +++ b/src/Console/AddFeatureToGroup.php @@ -14,7 +14,7 @@ class AddFeatureToGroup extends Command protected $description = 'Add a feature to a group'; - public function handle() + public function handle(): int { $featureName = $this->ask('Feature Name'); $feature = Feature::name($featureName)->first(); diff --git a/src/Console/DeactivateFeature.php b/src/Console/DeactivateFeature.php index 5201a7d..2521161 100644 --- a/src/Console/DeactivateFeature.php +++ b/src/Console/DeactivateFeature.php @@ -13,7 +13,7 @@ class DeactivateFeature extends Command protected $description = 'Deactivates a feature'; - public function handle() + public function handle(): int { $featureName = $this->ask('Feature name to deactivate'); $feature = Feature::name($featureName)->first(); diff --git a/src/Console/DeactivateFeatureGroup.php b/src/Console/DeactivateFeatureGroup.php index d05700d..83742dc 100644 --- a/src/Console/DeactivateFeatureGroup.php +++ b/src/Console/DeactivateFeatureGroup.php @@ -13,7 +13,7 @@ class DeactivateFeatureGroup extends Command protected $description = 'Deactivates a feature group'; - public function handle() + public function handle(): int { $groupName = $this->ask('Group name to deactivate'); $group = FeatureGroup::name($groupName)->first(); diff --git a/src/Console/ExtendFeature.php b/src/Console/ExtendFeature.php index 316ee32..cd01bbd 100644 --- a/src/Console/ExtendFeature.php +++ b/src/Console/ExtendFeature.php @@ -4,7 +4,6 @@ namespace JustSteveKing\Laravel\FeatureFlags\Console; -use Carbon\Carbon; use Illuminate\Console\Command; use JustSteveKing\Laravel\FeatureFlags\Models\Feature; @@ -14,7 +13,7 @@ class ExtendFeature extends Command protected $description = 'Extend a features expiry date'; - public function handle() + public function handle(): int { if(! config('feature-flags.enable_time_bombs')) $this->info("Time bombs are not enabled!"); diff --git a/src/Console/ViewFeatureGroups.php b/src/Console/ViewFeatureGroups.php index 0d38369..1cbff09 100644 --- a/src/Console/ViewFeatureGroups.php +++ b/src/Console/ViewFeatureGroups.php @@ -13,7 +13,7 @@ class ViewFeatureGroups extends Command protected $description = 'View feature groups'; - public function handle() + public function handle(): void { $groups = FeatureGroup::all(['name', 'description', 'active'])->toArray(); diff --git a/src/Console/ViewFeatures.php b/src/Console/ViewFeatures.php index c566c98..bf9ec20 100644 --- a/src/Console/ViewFeatures.php +++ b/src/Console/ViewFeatures.php @@ -13,7 +13,7 @@ class ViewFeatures extends Command protected $description = 'View features'; - public function handle() + public function handle(): void { $features = Feature::withoutEvents(function() { return Feature::all(['name', 'description', 'active', 'expires_at'])->toArray(); diff --git a/src/Console/ViewGroupsWithFeatures.php b/src/Console/ViewGroupsWithFeatures.php index cd0aa42..17b9997 100644 --- a/src/Console/ViewGroupsWithFeatures.php +++ b/src/Console/ViewGroupsWithFeatures.php @@ -13,7 +13,7 @@ class ViewGroupsWithFeatures extends Command protected $description = 'View groups with features'; - public function handle() + public function handle(): void { $groups = FeatureGroup::all(); $headers = ['Group', 'Features']; diff --git a/src/Models/Feature.php b/src/Models/Feature.php index 9d7dfa9..9185bc6 100644 --- a/src/Models/Feature.php +++ b/src/Models/Feature.php @@ -28,7 +28,7 @@ class Feature extends Model 'expires_at' => 'datetime' ]; - public static function booted() + public static function booted(): void { static::retrieved(function(Feature $feature) { $timeBombsAreEnabled = config('feature-flags.enable_time_bombs'); diff --git a/tests/Stubs/User.php b/tests/Stubs/User.php index 40e94fc..db24ecc 100644 --- a/tests/Stubs/User.php +++ b/tests/Stubs/User.php @@ -10,7 +10,7 @@ class User extends AuthUser { use HasFeatures; - + public $guarded = []; public $table = 'users'; From f4b2fdb6a45bcc90fdd4cb92c77ba3705cd4267b Mon Sep 17 00:00:00 2001 From: Aidan Laycock Date: Mon, 27 Jun 2022 21:04:56 +0100 Subject: [PATCH 11/11] Updated ReadMe to include how to use Timebombs functionality --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 227834c..e6d17e1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ return [ 'status_code' => 404, ], + + 'enable_time_bombs' => false, + + 'time_bomb_environments' => ['production'] ]; ``` @@ -152,6 +156,36 @@ if (auth()->user()->hasFeature('user level feature')) { } ``` +## Timebombs for Features + +A common use case for Feature Flags is to allow developers to add new functionality without breaking existing code. + +This process is great when paired with a solid CI/CD pipeline. But the biggest drawback to this is residual technical debt that can +occur when developers forget about removing implemented flags across a code base. + +To handle this, users of this package can utilise Timebombs! Timebombs are used to cause Feature Flags to throw an exception +when a flag should have been removed from the code base. + +To use Timebombs, you will need to explicitly enable them within the config ('enable_time_bombs' => true). +And define which environments you do not want exceptions to be thrown. (This is particularly useful with CI/CD, as you will want to throw exceptions locally, in CI and on staging environments but NOT on production). + +### Defining when a timebomb should throw an exception + +Once Timebombs are enabled, when creating a new Flag, you will be asked when you want your flag to expire (This is number of days). +When the current time surpasses that expiration date, then your feature flag will throw an exception. + +To extend a flag, you can use the handy command + +```php +php artisan feature-flags:extend-feature +``` + +Where you will be prompted to define how many more days are required before the flag should throw an exception again. + +### Further reading + +To learn more on Feature flags and Timebombs, there is a great article by Martin Fowler [Here](https://martinfowler.com/articles/feature-toggles.html). + ## Template Usage There are some Blade Directives to help control access to features in your UI: