Skip to content

Commit a266f3a

Browse files
authored
Merge pull request #4 from codinglabsau/st/sc-7287-move-syncfeaturesaction-over-to-package-with
St/sc 7287 move syncfeaturesaction over to package with
2 parents 5153222 + c48931d commit a266f3a

8 files changed

+403
-49
lines changed

config/feature-flags.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22

33
return [
44

5+
/*
6+
|--------------------------------------------------------------------------
7+
| Features
8+
|--------------------------------------------------------------------------
9+
|
10+
| Declare features that are managed by the app with the Feature
11+
| Flag package. The format is ['name' => FeatureState::on()].
12+
*/
13+
14+
'features' => [],
15+
16+
/*
17+
|--------------------------------------------------------------------------
18+
| Always On
19+
|--------------------------------------------------------------------------
20+
|
21+
| Declare the environments where features will be synced to the on
22+
| state. This is useful if features should always be on locally.
23+
| Note this only impacts the behaviour of the sync action.
24+
*/
25+
26+
'always_on' => [],
27+
528
/*
629
|--------------------------------------------------------------------------
730
| Cache
@@ -20,8 +43,8 @@
2043
| Models
2144
|--------------------------------------------------------------------------
2245
|
23-
| If you need to customise any models used then you can swap them out by
24-
| replacing the default models defined here.
46+
| If you need to customise any models used then you can swap
47+
| them out by replacing the default models defined here.
2548
*/
2649

2750
'feature_model' => \Codinglabs\FeatureFlags\Models\Feature::class,

src/Actions/SyncFeaturesAction.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Codinglabs\FeatureFlags\Actions;
4+
5+
use Codinglabs\FeatureFlags\Models\Feature;
6+
use Codinglabs\FeatureFlags\Enums\FeatureState;
7+
8+
class SyncFeaturesAction
9+
{
10+
public function __invoke(): void
11+
{
12+
$features = collect(config('feature-flags.features'))
13+
->map(fn ($state, $name) => [
14+
'name' => $name,
15+
'state' => app()->environment(config('feature-flags.always_on', []))
16+
? FeatureState::on()
17+
: $state
18+
]);
19+
20+
$featureModels = Feature::all();
21+
22+
$featureModels->whereNotIn('name', $features->pluck('name'))
23+
->each(fn (Feature $feature) => $feature->delete());
24+
25+
$features->whereNotIn('name', $featureModels->pluck('name'))
26+
->each(fn (array $feature) => Feature::create($feature));
27+
}
28+
}

src/Casts/FeatureStateCast.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
class FeatureStateCast implements CastsAttributes
1010
{
11-
public function get($model, string $key, $value, array $attributes)
11+
public function get($model, string $key, $value, array $attributes): FeatureState
1212
{
1313
return FeatureState::from($attributes['state']);
1414
}
1515

16-
public function set($model, string $key, $value, array $attributes)
16+
public function set($model, string $key, $value, array $attributes): array
1717
{
1818
if (! $value instanceof FeatureState) {
1919
throw new InvalidArgumentException('The given value is not an instance of FeatureState.');

src/Models/Feature.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
namespace Codinglabs\FeatureFlags\Models;
44

5+
use Illuminate\Database\Eloquent\Model;
56
use Codinglabs\FeatureFlags\Casts\FeatureStateCast;
67
use Illuminate\Database\Eloquent\Factories\HasFactory;
78

8-
class Feature extends \Illuminate\Database\Eloquent\Model
9+
class Feature extends Model
910
{
1011
use HasFactory;
1112

tests/BladeTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
use Codinglabs\FeatureFlags\Models\Feature;
4+
use Codinglabs\FeatureFlags\Enums\FeatureState;
5+
use Codinglabs\FeatureFlags\Facades\FeatureFlag;
6+
use Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
7+
8+
uses(InteractsWithViews::class);
9+
10+
beforeEach(function () {
11+
config([
12+
'feature-flags.cache_store' => 'array',
13+
'feature-flags.cache_prefix' => 'testing',
14+
]);
15+
16+
cache()->store('array')->clear();
17+
});
18+
19+
afterEach(function () {
20+
FeatureFlag::reset();
21+
});
22+
23+
it('does not reveal things when feature is off', function () {
24+
Feature::factory()->create([
25+
'name' => 'some-feature',
26+
'state' => FeatureState::off()
27+
]);
28+
29+
$view = $this->blade("@feature('some-feature') secret things @endfeature");
30+
31+
$view->assertDontSee('secret things');
32+
});
33+
34+
it('reveals things when feature is on ', function () {
35+
Feature::factory()->create([
36+
'name' => 'some-feature',
37+
'state' => FeatureState::on()
38+
]);
39+
40+
$view = $this->blade("@feature('some-feature') secret things @endfeature");
41+
42+
$view->assertSee('secret things');
43+
});

tests/FeaturesTest.php renamed to tests/FeatureFlagTest.php

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
<?php
22

3-
use Illuminate\Support\Facades\Route;
43
use Codinglabs\FeatureFlags\Models\Feature;
54
use Codinglabs\FeatureFlags\Enums\FeatureState;
65
use Codinglabs\FeatureFlags\Facades\FeatureFlag;
7-
use Codinglabs\FeatureFlags\Middleware\VerifyFeatureIsOn;
6+
use Codinglabs\FeatureFlags\Events\FeatureUpdatedEvent;
87
use Codinglabs\FeatureFlags\Exceptions\MissingFeatureException;
98

109
beforeEach(function () {
@@ -13,17 +12,22 @@
1312
'feature-flags.cache_prefix' => 'testing',
1413
]);
1514

16-
Route::get('test-middleware', function () {
17-
return 'ok';
18-
})->middleware(VerifyFeatureIsOn::class . ':some-feature');
19-
2015
cache()->store('array')->clear();
2116
});
2217

2318
afterEach(function () {
2419
FeatureFlag::reset();
2520
});
2621

22+
it('throws an exception if casting to a feature state that does not exist', function () {
23+
$this->expectException(\InvalidArgumentException::class);
24+
25+
Feature::factory()->create([
26+
'name' => 'some-feature',
27+
'state' => 'foo',
28+
]);
29+
});
30+
2731
it('throws an exception if calling isOn on a feature that does not exist', function () {
2832
$this->expectException(MissingFeatureException::class);
2933

@@ -146,7 +150,7 @@
146150
->and(FeatureFlag::getState('some-on-feature'))->toBe(FeatureState::on());
147151
});
148152

149-
it('can update a features state', function () {
153+
it('can turn on a feature', function () {
150154
Event::fake();
151155

152156
Feature::factory()->create([
@@ -156,69 +160,72 @@
156160

157161
cache()->store('array')->set('testing.some-feature', 'off');
158162

159-
FeatureFlag::updateFeatureState('some-feature', FeatureState::on());
163+
FeatureFlag::turnOn('some-feature');
160164

161-
Event::assertDispatched(\Codinglabs\FeatureFlags\Events\FeatureUpdatedEvent::class);
165+
Event::assertDispatched(FeatureUpdatedEvent::class);
162166
expect(FeatureFlag::isOn('some-feature'))->toBeTrue()
163167
->and(FeatureFlag::isOff('some-feature'))->toBeFalse()
164168
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::on()->value);
165169
});
166170

167-
it('uses the default cache store when cache store has not been set', function () {
168-
config(['cache.default' => 'file']);
169-
170-
config(['feature-flags.cache_store' => env('FEATURES_CACHE_STORE', config('cache.default'))]);
171+
it('can turn off a feature', function () {
172+
Event::fake();
171173

172-
expect(config('feature-flags.cache_store'))->toBe('file');
173-
});
174+
Feature::factory()->create([
175+
'name' => 'some-feature',
176+
'state' => FeatureState::on()
177+
]);
174178

175-
it('returns a 500 status when a feature does not exist', function () {
176-
$this->withoutExceptionHandling();
179+
cache()->store('array')->set('testing.some-feature', 'on');
177180

178-
$this->expectException(MissingFeatureException::class);
181+
FeatureFlag::turnOff('some-feature');
179182

180-
$this->get('test-middleware')
181-
->assertStatus(500);
183+
Event::assertDispatched(FeatureUpdatedEvent::class);
184+
expect(FeatureFlag::isOn('some-feature'))->toBeFalse()
185+
->and(FeatureFlag::isOff('some-feature'))->toBeTrue()
186+
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::off()->value);
182187
});
183188

184-
it('returns a 404 status when a feature is off', function () {
189+
it('can make a feature dynamic', function () {
190+
Event::fake();
191+
185192
Feature::factory()->create([
186193
'name' => 'some-feature',
187-
'state' => FeatureState::off()
194+
'state' => FeatureState::on()
188195
]);
189196

190-
$this->get('test-middleware')
191-
->assertStatus(404);
192-
});
197+
cache()->store('array')->set('testing.some-feature', 'on');
193198

194-
it('returns a 404 status when a feature is dynamic', function () {
195-
Feature::factory()->create([
196-
'name' => 'some-feature',
197-
'state' => FeatureState::dynamic()
198-
]);
199+
FeatureFlag::makeDynamic('some-feature');
199200

200-
$this->get('test-middleware')
201-
->assertStatus(404);
201+
Event::assertDispatched(FeatureUpdatedEvent::class);
202+
expect(FeatureFlag::isOn('some-feature'))->toBeFalse()
203+
->and(FeatureFlag::isOff('some-feature'))->toBeTrue()
204+
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::dynamic()->value);
202205
});
203206

204-
it('returns an ok status when a feature is dynamic and enabled', function () {
207+
it('can update a features state', function () {
208+
Event::fake();
209+
205210
Feature::factory()->create([
206211
'name' => 'some-feature',
207-
'state' => FeatureState::dynamic()
212+
'state' => FeatureState::off()
208213
]);
209214

210-
FeatureFlag::registerDynamicHandler('some-feature', fn ($feature) => true);
215+
cache()->store('array')->set('testing.some-feature', 'off');
211216

212-
$this->get('test-middleware')
213-
->assertOk();
217+
FeatureFlag::updateFeatureState('some-feature', FeatureState::on());
218+
219+
Event::assertDispatched(FeatureUpdatedEvent::class);
220+
expect(FeatureFlag::isOn('some-feature'))->toBeTrue()
221+
->and(FeatureFlag::isOff('some-feature'))->toBeFalse()
222+
->and(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::on()->value);
214223
});
215224

216-
it('returns an ok status when a feature is on', function () {
217-
Feature::factory()->create([
218-
'name' => 'some-feature',
219-
'state' => FeatureState::on()
220-
]);
225+
it('uses the default cache store when cache store has not been set', function () {
226+
config(['cache.default' => 'file']);
221227

222-
$this->get('test-middleware')
223-
->assertOk();
228+
config(['feature-flags.cache_store' => env('FEATURES_CACHE_STORE', config('cache.default'))]);
229+
230+
expect(config('feature-flags.cache_store'))->toBe('file');
224231
});

tests/MiddlewareTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Route;
4+
use Codinglabs\FeatureFlags\Models\Feature;
5+
use Codinglabs\FeatureFlags\Enums\FeatureState;
6+
use Codinglabs\FeatureFlags\Facades\FeatureFlag;
7+
use Codinglabs\FeatureFlags\Middleware\VerifyFeatureIsOn;
8+
use Codinglabs\FeatureFlags\Exceptions\MissingFeatureException;
9+
10+
beforeEach(function () {
11+
config([
12+
'feature-flags.cache_store' => 'array',
13+
'feature-flags.cache_prefix' => 'testing',
14+
]);
15+
16+
Route::get('test-middleware', function () {
17+
return 'ok';
18+
})->middleware(VerifyFeatureIsOn::class . ':some-feature');
19+
20+
cache()->store('array')->clear();
21+
});
22+
23+
afterEach(function () {
24+
FeatureFlag::reset();
25+
});
26+
27+
it('returns a 500 status when a feature does not exist', function () {
28+
$this->withoutExceptionHandling();
29+
30+
$this->expectException(MissingFeatureException::class);
31+
32+
$this->get('test-middleware')
33+
->assertStatus(500);
34+
});
35+
36+
it('returns a 404 status when a feature is off', function () {
37+
Feature::factory()->create([
38+
'name' => 'some-feature',
39+
'state' => FeatureState::off()
40+
]);
41+
42+
$this->get('test-middleware')
43+
->assertStatus(404);
44+
});
45+
46+
it('returns a 404 status when a feature is dynamic', function () {
47+
Feature::factory()->create([
48+
'name' => 'some-feature',
49+
'state' => FeatureState::dynamic()
50+
]);
51+
52+
$this->get('test-middleware')
53+
->assertStatus(404);
54+
});
55+
56+
it('returns an ok status when a feature is dynamic and enabled', function () {
57+
Feature::factory()->create([
58+
'name' => 'some-feature',
59+
'state' => FeatureState::dynamic()
60+
]);
61+
62+
FeatureFlag::registerDynamicHandler('some-feature', fn ($feature) => true);
63+
64+
$this->get('test-middleware')
65+
->assertOk();
66+
});
67+
68+
it('returns an ok status when a feature is on', function () {
69+
Feature::factory()->create([
70+
'name' => 'some-feature',
71+
'state' => FeatureState::on()
72+
]);
73+
74+
$this->get('test-middleware')
75+
->assertOk();
76+
});

0 commit comments

Comments
 (0)