Skip to content

Commit

Permalink
Maintenance mode with flags (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterfox authored Feb 2, 2023
1 parent 485ecdc commit b36ac69
Show file tree
Hide file tree
Showing 14 changed files with 532 additions and 3 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"require": {
"php": "^8.0",
"illuminate/contracts": "10.*|9.*"
"illuminate/contracts": "10.*|^9.6"
},
"require-dev": {
"laravel/pint": "^1.2",
Expand Down
14 changes: 14 additions & 0 deletions src/Contracts/Maintenance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace YlsIdeas\FeatureFlags\Contracts;

interface Maintenance
{
public function active(): bool;

public function parameters(): ?array;

public function callActivation(array $properties): void;

public function callDeactivation(): void;
}
1 change: 1 addition & 0 deletions src/Facades/Features.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* @method static \static callOnExpiredFeatures(array $expiredFeatures, callable|null $handler = null)
* @method static \static applyOnExpiredHandler(\YlsIdeas\FeatureFlags\Contracts\ExpiredFeaturesHandler $handler)
* @method static \static extend(string $driver, callable $builder)
* @method static \YlsIdeas\FeatureFlags\Support\MaintenanceRepository maintenanceMode()
* @method static void assertAccessed(string $feature, int|null $count = null, string $message = '')
* @method static void assertNotAccessed(string $feature, string $message = '')
* @method static void assertAccessedCount(string $feature, int $count = 0, string $message = '')
Expand Down
25 changes: 23 additions & 2 deletions src/FeatureFlagsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
namespace YlsIdeas\FeatureFlags;

use Illuminate\Console\Scheduling\Event;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Foundation\MaintenanceMode;
use Illuminate\Database\Query\Builder;
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Foundation\MaintenanceModeManager;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
Expand All @@ -13,6 +16,8 @@
use YlsIdeas\FeatureFlags\Facades\Features;
use YlsIdeas\FeatureFlags\Middlewares\GuardFeature;
use YlsIdeas\FeatureFlags\Rules\FeatureOnRule;
use YlsIdeas\FeatureFlags\Support\MaintenanceDriver;
use YlsIdeas\FeatureFlags\Support\MaintenanceRepository;
use YlsIdeas\FeatureFlags\Support\QueryBuilderMixin;

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

$this->publishes([
__DIR__.'/../stubs/PreventRequestsDuringMaintenance.php' => app_path('Http/Middleware/PreventRequestsDuringMaintenance.php'),
], 'maintenance-middleware');

// Registering package commands.
if (Features::usesCommands()) {
$this->commands([
Expand Down Expand Up @@ -76,7 +85,7 @@ public function boot()
/**
* Register the application services.
*/
public function register()
public function register(): void
{
// Automatically apply the package configuration
$this->mergeConfigFrom(__DIR__.'/../config/features.php', 'features');
Expand All @@ -86,6 +95,18 @@ public function register()
} else {
$this->app->singleton(FeaturesContract::class, Manager::class);
}

$this->app->scoped(MaintenanceRepository::class, function (Container $app) {
return new MaintenanceRepository($app->make(FeaturesContract::class), $app);
});

$this->app->extend(MaintenanceModeManager::class, function (MaintenanceModeManager $manager) {
return $manager->extend('features', function (): MaintenanceMode {
return new MaintenanceDriver(
$this->app->make(MaintenanceRepository::class)
);
});
});
}

protected function schedulingMacros()
Expand Down Expand Up @@ -126,7 +147,7 @@ protected function queryBuilder()

protected function aboutCommandInfo(): void
{
if (class_exists('Illuminate\Foundation\Console\AboutCommand')) {
if (class_exists(AboutCommand::class)) {
AboutCommand::add('Feature Flags', [
'Pipeline' => fn () => implode(', Hello', config('features.pipeline')),
]);
Expand Down
6 changes: 6 additions & 0 deletions src/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use YlsIdeas\FeatureFlags\Support\FileLoader;
use YlsIdeas\FeatureFlags\Support\GatewayCache;
use YlsIdeas\FeatureFlags\Support\GatewayInspector;
use YlsIdeas\FeatureFlags\Support\MaintenanceRepository;

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

public function maintenanceMode(): MaintenanceRepository
{
return $this->container->make(MaintenanceRepository::class);
}

protected function getContainer(): Container
{
return $this->container;
Expand Down
13 changes: 13 additions & 0 deletions src/Middlewares/PreventRequestsDuringMaintenance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace YlsIdeas\FeatureFlags\Middlewares;

use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as BasePreventRequestsDuringMaintenance;

class PreventRequestsDuringMaintenance extends BasePreventRequestsDuringMaintenance
{
public function getExcludedPaths()
{
return $this->app->maintenanceMode()->data()['except'] ?? [];
}
}
33 changes: 33 additions & 0 deletions src/Support/MaintenanceDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace YlsIdeas\FeatureFlags\Support;

use Illuminate\Contracts\Foundation\MaintenanceMode;
use YlsIdeas\FeatureFlags\Contracts\Maintenance as MaintenanceContract;

class MaintenanceDriver implements MaintenanceMode
{
public function __construct(protected MaintenanceContract $features)
{
}

public function activate(array $payload): void
{
$this->features->callActivation($payload);
}

public function deactivate(): void
{
$this->features->callDeactivation();
}

public function active(): bool
{
return $this->features->active();
}

public function data(): array
{
return $this->features->parameters() ?? [];
}
}
85 changes: 85 additions & 0 deletions src/Support/MaintenanceRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace YlsIdeas\FeatureFlags\Support;

use Illuminate\Contracts\Container\Container;
use YlsIdeas\FeatureFlags\Contracts\Features;
use YlsIdeas\FeatureFlags\Contracts\Maintenance;

class MaintenanceRepository implements Maintenance
{
public array $scenarios = [];

public ?MaintenanceScenario $foundScenario = null;
protected \Closure $uponActivation;
protected \Closure $uponDeactivation;

public function __construct(protected Features $features, protected Container $container)
{
}

public function uponActivation(callable $callable): static
{
$this->uponActivation = \Closure::fromCallable($callable);

return $this;
}

public function uponDeactivation(callable $callable): static
{
$this->uponDeactivation = \Closure::fromCallable($callable);

return $this;
}

public function callActivation(array $properties): void
{
$this->container->call($this->uponActivation, [
'properties' => $properties, 'features' => $this->features,
]);
}

public function callDeactivation(): void
{
$this->container->call($this->uponDeactivation, ['features' => $this->features]);
}

public function onEnabled($feature): MaintenanceScenario
{
return tap((new MaintenanceScenario())->whenEnabled($feature), function (MaintenanceScenario $scenario) {
$this->scenarios[] = $scenario;
});
}

public function onDisabled($feature): MaintenanceScenario
{
return tap((new MaintenanceScenario())->whenDisabled($feature), function (MaintenanceScenario $scenario) {
$this->scenarios[] = $scenario;
});
}

public function active(): bool
{
return (bool) $this->findScenario();
}

public function parameters(): ?array
{
return $this->foundScenario?->toArray();
}

protected function findScenario(): ?MaintenanceScenario
{
return $this->foundScenario = collect($this->scenarios)
->first(function (MaintenanceScenario $scenario) {
if ($scenario->onEnabled && $this->features->accessible($scenario->feature)) {
return true;
}
if (! $scenario->onEnabled && ! $this->features->accessible($scenario->feature)) {
return true;
}

return false;
});
}
}
86 changes: 86 additions & 0 deletions src/Support/MaintenanceScenario.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace YlsIdeas\FeatureFlags\Support;

use Illuminate\Contracts\Support\Arrayable;

class MaintenanceScenario implements Arrayable
{
public string $feature;
public bool $onEnabled;

protected array $attributes = [];

public function whenEnabled(string $feature): static
{
$this->feature = $feature;
$this->onEnabled = true;

return $this;
}

public function whenDisabled(string $feature): static
{
$this->feature = $feature;
$this->onEnabled = false;

return $this;
}

public function refresh(int $seconds): static
{
$this->attributes['refresh'] = (string) $seconds;

return $this;
}

public function statusCode(int $status): static
{
$this->attributes['status'] = $status;

return $this;
}

public function retry(int $seconds): static
{
$this->attributes['retry'] = $seconds;

return $this;
}

public function secret(string $secret): static
{
$this->attributes['secret'] = $secret;

return $this;
}

public function redirect(string $url): static
{
$this->attributes['redirect'] = $url;

return $this;
}

public function template(string $html): static
{
$this->attributes['template'] = $html;

return $this;
}

/**
* @param string[] $urls
*/
public function exceptPaths(array $urls): static
{
$this->attributes['except'] = $urls;

return $this;
}

public function toArray(): array
{
return $this->attributes;
}
}
9 changes: 9 additions & 0 deletions stubs/PreventRequestsDuringMaintenance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Http\Middleware;

use YlsIdeas\FeatureFlags\Middlewares\PreventRequestsDuringMaintenance as Middleware;

class PreventRequestsDuringMaintenance extends Middleware
{
}
17 changes: 17 additions & 0 deletions tests/FeatureFlagsServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ protected function cleanUp(): void
File::delete(config_path('features.php'));
File::delete(base_path('.features.php'));

File::delete(app_path('Http/Middleware/PreventRequestsDuringMaintenance.php.backup'));

collect(File::files(database_path('migrations')))
->each(fn (\SplFileInfo $file) => File::delete($file->getPathname()));
});
Expand Down Expand Up @@ -82,6 +84,21 @@ public function test_publishes_the_features_migration(): void
$this->assertNotNull($filename);
}

public function test_publishes_the_maintenance_middleware(): void
{
$this->artisan('vendor:publish', [
'--tag' => 'maintenance-middleware',
'--force' => true,
]);

$this->assertTrue(File::exists(app_path('Http/Middleware/PreventRequestsDuringMaintenance.php')));

$this->assertStringContainsString(
'use YlsIdeas\FeatureFlags\Middlewares\PreventRequestsDuringMaintenance as Middleware;',
File::get(app_path('Http/Middleware/PreventRequestsDuringMaintenance.php'))
);
}

public function test_posting_about_info(): void
{
if (version_compare(Application::VERSION, '9.20.0', '<')) {
Expand Down
10 changes: 10 additions & 0 deletions tests/Kernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace YlsIdeas\FeatureFlags\Tests;

class Kernel extends \Orchestra\Testbench\Foundation\Http\Kernel
{
protected $middleware = [
\YlsIdeas\FeatureFlags\Middlewares\PreventRequestsDuringMaintenance::class,
];
}
Loading

0 comments on commit b36ac69

Please sign in to comment.