Skip to content

Commit b36ac69

Browse files
authored
Maintenance mode with flags (#54)
1 parent 485ecdc commit b36ac69

14 files changed

+532
-3
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
],
1818
"require": {
1919
"php": "^8.0",
20-
"illuminate/contracts": "10.*|9.*"
20+
"illuminate/contracts": "10.*|^9.6"
2121
},
2222
"require-dev": {
2323
"laravel/pint": "^1.2",

src/Contracts/Maintenance.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+
interface Maintenance
6+
{
7+
public function active(): bool;
8+
9+
public function parameters(): ?array;
10+
11+
public function callActivation(array $properties): void;
12+
13+
public function callDeactivation(): void;
14+
}

src/Facades/Features.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* @method static \static callOnExpiredFeatures(array $expiredFeatures, callable|null $handler = null)
2929
* @method static \static applyOnExpiredHandler(\YlsIdeas\FeatureFlags\Contracts\ExpiredFeaturesHandler $handler)
3030
* @method static \static extend(string $driver, callable $builder)
31+
* @method static \YlsIdeas\FeatureFlags\Support\MaintenanceRepository maintenanceMode()
3132
* @method static void assertAccessed(string $feature, int|null $count = null, string $message = '')
3233
* @method static void assertNotAccessed(string $feature, string $message = '')
3334
* @method static void assertAccessedCount(string $feature, int $count = 0, string $message = '')

src/FeatureFlagsServiceProvider.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
namespace YlsIdeas\FeatureFlags;
44

55
use Illuminate\Console\Scheduling\Event;
6+
use Illuminate\Contracts\Container\Container;
7+
use Illuminate\Contracts\Foundation\MaintenanceMode;
68
use Illuminate\Database\Query\Builder;
79
use Illuminate\Foundation\Console\AboutCommand;
10+
use Illuminate\Foundation\MaintenanceModeManager;
811
use Illuminate\Routing\Router;
912
use Illuminate\Support\Facades\Blade;
1013
use Illuminate\Support\Facades\Validator;
@@ -13,6 +16,8 @@
1316
use YlsIdeas\FeatureFlags\Facades\Features;
1417
use YlsIdeas\FeatureFlags\Middlewares\GuardFeature;
1518
use YlsIdeas\FeatureFlags\Rules\FeatureOnRule;
19+
use YlsIdeas\FeatureFlags\Support\MaintenanceDriver;
20+
use YlsIdeas\FeatureFlags\Support\MaintenanceRepository;
1621
use YlsIdeas\FeatureFlags\Support\QueryBuilderMixin;
1722

1823
/**
@@ -40,6 +45,10 @@ public function boot()
4045
__DIR__.'/../migrations/create_features_table.php' => database_path('migrations/'.$migration),
4146
], 'features-migration');
4247

48+
$this->publishes([
49+
__DIR__.'/../stubs/PreventRequestsDuringMaintenance.php' => app_path('Http/Middleware/PreventRequestsDuringMaintenance.php'),
50+
], 'maintenance-middleware');
51+
4352
// Registering package commands.
4453
if (Features::usesCommands()) {
4554
$this->commands([
@@ -76,7 +85,7 @@ public function boot()
7685
/**
7786
* Register the application services.
7887
*/
79-
public function register()
88+
public function register(): void
8089
{
8190
// Automatically apply the package configuration
8291
$this->mergeConfigFrom(__DIR__.'/../config/features.php', 'features');
@@ -86,6 +95,18 @@ public function register()
8695
} else {
8796
$this->app->singleton(FeaturesContract::class, Manager::class);
8897
}
98+
99+
$this->app->scoped(MaintenanceRepository::class, function (Container $app) {
100+
return new MaintenanceRepository($app->make(FeaturesContract::class), $app);
101+
});
102+
103+
$this->app->extend(MaintenanceModeManager::class, function (MaintenanceModeManager $manager) {
104+
return $manager->extend('features', function (): MaintenanceMode {
105+
return new MaintenanceDriver(
106+
$this->app->make(MaintenanceRepository::class)
107+
);
108+
});
109+
});
89110
}
90111

91112
protected function schedulingMacros()
@@ -126,7 +147,7 @@ protected function queryBuilder()
126147

127148
protected function aboutCommandInfo(): void
128149
{
129-
if (class_exists('Illuminate\Foundation\Console\AboutCommand')) {
150+
if (class_exists(AboutCommand::class)) {
130151
AboutCommand::add('Feature Flags', [
131152
'Pipeline' => fn () => implode(', Hello', config('features.pipeline')),
132153
]);

src/Manager.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use YlsIdeas\FeatureFlags\Support\FileLoader;
3131
use YlsIdeas\FeatureFlags\Support\GatewayCache;
3232
use YlsIdeas\FeatureFlags\Support\GatewayInspector;
33+
use YlsIdeas\FeatureFlags\Support\MaintenanceRepository;
3334

3435
/**
3536
* @see \YlsIdeas\FeatureFlags\Tests\ManagerTest
@@ -226,6 +227,11 @@ public function extend(string $driver, callable $builder): static
226227
return $this;
227228
}
228229

230+
public function maintenanceMode(): MaintenanceRepository
231+
{
232+
return $this->container->make(MaintenanceRepository::class);
233+
}
234+
229235
protected function getContainer(): Container
230236
{
231237
return $this->container;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Middlewares;
4+
5+
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as BasePreventRequestsDuringMaintenance;
6+
7+
class PreventRequestsDuringMaintenance extends BasePreventRequestsDuringMaintenance
8+
{
9+
public function getExcludedPaths()
10+
{
11+
return $this->app->maintenanceMode()->data()['except'] ?? [];
12+
}
13+
}

src/Support/MaintenanceDriver.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Support;
4+
5+
use Illuminate\Contracts\Foundation\MaintenanceMode;
6+
use YlsIdeas\FeatureFlags\Contracts\Maintenance as MaintenanceContract;
7+
8+
class MaintenanceDriver implements MaintenanceMode
9+
{
10+
public function __construct(protected MaintenanceContract $features)
11+
{
12+
}
13+
14+
public function activate(array $payload): void
15+
{
16+
$this->features->callActivation($payload);
17+
}
18+
19+
public function deactivate(): void
20+
{
21+
$this->features->callDeactivation();
22+
}
23+
24+
public function active(): bool
25+
{
26+
return $this->features->active();
27+
}
28+
29+
public function data(): array
30+
{
31+
return $this->features->parameters() ?? [];
32+
}
33+
}

src/Support/MaintenanceRepository.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Support;
4+
5+
use Illuminate\Contracts\Container\Container;
6+
use YlsIdeas\FeatureFlags\Contracts\Features;
7+
use YlsIdeas\FeatureFlags\Contracts\Maintenance;
8+
9+
class MaintenanceRepository implements Maintenance
10+
{
11+
public array $scenarios = [];
12+
13+
public ?MaintenanceScenario $foundScenario = null;
14+
protected \Closure $uponActivation;
15+
protected \Closure $uponDeactivation;
16+
17+
public function __construct(protected Features $features, protected Container $container)
18+
{
19+
}
20+
21+
public function uponActivation(callable $callable): static
22+
{
23+
$this->uponActivation = \Closure::fromCallable($callable);
24+
25+
return $this;
26+
}
27+
28+
public function uponDeactivation(callable $callable): static
29+
{
30+
$this->uponDeactivation = \Closure::fromCallable($callable);
31+
32+
return $this;
33+
}
34+
35+
public function callActivation(array $properties): void
36+
{
37+
$this->container->call($this->uponActivation, [
38+
'properties' => $properties, 'features' => $this->features,
39+
]);
40+
}
41+
42+
public function callDeactivation(): void
43+
{
44+
$this->container->call($this->uponDeactivation, ['features' => $this->features]);
45+
}
46+
47+
public function onEnabled($feature): MaintenanceScenario
48+
{
49+
return tap((new MaintenanceScenario())->whenEnabled($feature), function (MaintenanceScenario $scenario) {
50+
$this->scenarios[] = $scenario;
51+
});
52+
}
53+
54+
public function onDisabled($feature): MaintenanceScenario
55+
{
56+
return tap((new MaintenanceScenario())->whenDisabled($feature), function (MaintenanceScenario $scenario) {
57+
$this->scenarios[] = $scenario;
58+
});
59+
}
60+
61+
public function active(): bool
62+
{
63+
return (bool) $this->findScenario();
64+
}
65+
66+
public function parameters(): ?array
67+
{
68+
return $this->foundScenario?->toArray();
69+
}
70+
71+
protected function findScenario(): ?MaintenanceScenario
72+
{
73+
return $this->foundScenario = collect($this->scenarios)
74+
->first(function (MaintenanceScenario $scenario) {
75+
if ($scenario->onEnabled && $this->features->accessible($scenario->feature)) {
76+
return true;
77+
}
78+
if (! $scenario->onEnabled && ! $this->features->accessible($scenario->feature)) {
79+
return true;
80+
}
81+
82+
return false;
83+
});
84+
}
85+
}

src/Support/MaintenanceScenario.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Support;
4+
5+
use Illuminate\Contracts\Support\Arrayable;
6+
7+
class MaintenanceScenario implements Arrayable
8+
{
9+
public string $feature;
10+
public bool $onEnabled;
11+
12+
protected array $attributes = [];
13+
14+
public function whenEnabled(string $feature): static
15+
{
16+
$this->feature = $feature;
17+
$this->onEnabled = true;
18+
19+
return $this;
20+
}
21+
22+
public function whenDisabled(string $feature): static
23+
{
24+
$this->feature = $feature;
25+
$this->onEnabled = false;
26+
27+
return $this;
28+
}
29+
30+
public function refresh(int $seconds): static
31+
{
32+
$this->attributes['refresh'] = (string) $seconds;
33+
34+
return $this;
35+
}
36+
37+
public function statusCode(int $status): static
38+
{
39+
$this->attributes['status'] = $status;
40+
41+
return $this;
42+
}
43+
44+
public function retry(int $seconds): static
45+
{
46+
$this->attributes['retry'] = $seconds;
47+
48+
return $this;
49+
}
50+
51+
public function secret(string $secret): static
52+
{
53+
$this->attributes['secret'] = $secret;
54+
55+
return $this;
56+
}
57+
58+
public function redirect(string $url): static
59+
{
60+
$this->attributes['redirect'] = $url;
61+
62+
return $this;
63+
}
64+
65+
public function template(string $html): static
66+
{
67+
$this->attributes['template'] = $html;
68+
69+
return $this;
70+
}
71+
72+
/**
73+
* @param string[] $urls
74+
*/
75+
public function exceptPaths(array $urls): static
76+
{
77+
$this->attributes['except'] = $urls;
78+
79+
return $this;
80+
}
81+
82+
public function toArray(): array
83+
{
84+
return $this->attributes;
85+
}
86+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use YlsIdeas\FeatureFlags\Middlewares\PreventRequestsDuringMaintenance as Middleware;
6+
7+
class PreventRequestsDuringMaintenance extends Middleware
8+
{
9+
}

tests/FeatureFlagsServiceProviderTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ protected function cleanUp(): void
2727
File::delete(config_path('features.php'));
2828
File::delete(base_path('.features.php'));
2929

30+
File::delete(app_path('Http/Middleware/PreventRequestsDuringMaintenance.php.backup'));
31+
3032
collect(File::files(database_path('migrations')))
3133
->each(fn (\SplFileInfo $file) => File::delete($file->getPathname()));
3234
});
@@ -82,6 +84,21 @@ public function test_publishes_the_features_migration(): void
8284
$this->assertNotNull($filename);
8385
}
8486

87+
public function test_publishes_the_maintenance_middleware(): void
88+
{
89+
$this->artisan('vendor:publish', [
90+
'--tag' => 'maintenance-middleware',
91+
'--force' => true,
92+
]);
93+
94+
$this->assertTrue(File::exists(app_path('Http/Middleware/PreventRequestsDuringMaintenance.php')));
95+
96+
$this->assertStringContainsString(
97+
'use YlsIdeas\FeatureFlags\Middlewares\PreventRequestsDuringMaintenance as Middleware;',
98+
File::get(app_path('Http/Middleware/PreventRequestsDuringMaintenance.php'))
99+
);
100+
}
101+
85102
public function test_posting_about_info(): void
86103
{
87104
if (version_compare(Application::VERSION, '9.20.0', '<')) {

tests/Kernel.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace YlsIdeas\FeatureFlags\Tests;
4+
5+
class Kernel extends \Orchestra\Testbench\Foundation\Http\Kernel
6+
{
7+
protected $middleware = [
8+
\YlsIdeas\FeatureFlags\Middlewares\PreventRequestsDuringMaintenance::class,
9+
];
10+
}

0 commit comments

Comments
 (0)