├── banner.png ├── src ├── Contracts │ ├── InMemoryLoader.php │ ├── Features.php │ ├── Gateway.php │ ├── Cacheable.php │ ├── ExpiredFeaturesHandler.php │ ├── Toggleable.php │ ├── ActionableFlag.php │ ├── Maintenance.php │ └── DebuggableFlag.php ├── Events │ ├── FeatureSwitchedOff.php │ ├── FeatureSwitchedOn.php │ ├── FeatureAccessing.php │ └── FeatureAccessed.php ├── Middlewares │ ├── PreventRequestsDuringMaintenance.php │ └── GuardFeature.php ├── Exceptions │ └── FeatureExpired.php ├── Support │ ├── StateChecking.php │ ├── QueryBuilderMixin.php │ ├── ActionDebugLog.php │ ├── FileLoader.php │ ├── MaintenanceDriver.php │ ├── FeaturesFileDiscoverer.php │ ├── FeatureFilter.php │ ├── GatewayCache.php │ ├── MaintenanceScenario.php │ ├── FeatureFake.php │ ├── GatewayInspector.php │ └── MaintenanceRepository.php ├── Commands │ ├── SwitchOnFeature.php │ └── SwitchOffFeature.php ├── ExpiredFeaturesHandler.php ├── Gateways │ ├── InMemoryGateway.php │ ├── GateGateway.php │ ├── RedisGateway.php │ └── DatabaseGateway.php ├── Rules │ └── FeatureOnRule.php ├── ActionableFlag.php ├── Facades │ └── Features.php ├── FeatureFlagsServiceProvider.php └── Manager.php ├── stubs └── PreventRequestsDuringMaintenance.php ├── config ├── .features.php └── features.php ├── rector.php ├── migrations └── create_features_table.php ├── LICENSE.md ├── pint.json ├── composer.json ├── CHANGELOG.md ├── README.md └── UPGRADE.md /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ylsideas/feature-flags/HEAD/banner.png -------------------------------------------------------------------------------- /src/Contracts/InMemoryLoader.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | return static function (Application $app): array { 9 | return [ 10 | // 'my.feature.flag' => true, 11 | ]; 12 | }; 13 | -------------------------------------------------------------------------------- /src/Events/FeatureSwitchedOff.php: -------------------------------------------------------------------------------- 1 | app->maintenanceMode()->data()['except'] ?? []; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/config', 10 | __DIR__ . '/src', 11 | __DIR__ . '/tests', 12 | ]) 13 | // uncomment to reach your current PHP version 14 | ->withPhpSets(php82: true) 15 | ->withImportNames() 16 | ->withTypeCoverageLevel(PHP_INT_MAX) 17 | ->withDeadCodeLevel(PHP_INT_MAX) 18 | ->withCodeQualityLevel(PHP_INT_MAX); 19 | -------------------------------------------------------------------------------- /src/Exceptions/FeatureExpired.php: -------------------------------------------------------------------------------- 1 | feature; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Support/StateChecking.php: -------------------------------------------------------------------------------- 1 | $this->when(Features::accessible($feature), $action); 16 | } 17 | 18 | public function whenFeatureIsNotAccessible(): callable 19 | { 20 | return fn (string $feature, callable $action): \Illuminate\Contracts\Database\Query\Builder => $this->when(! Features::accessible($feature), $action); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Commands/SwitchOnFeature.php: -------------------------------------------------------------------------------- 1 | argument('gateway'), $this->argument('feature')); 17 | 18 | $this->line( 19 | sprintf( 20 | 'Feature `%s` has been turned on', 21 | $this->argument('feature') 22 | ) 23 | ); 24 | 25 | return self::SUCCESS; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Commands/SwitchOffFeature.php: -------------------------------------------------------------------------------- 1 | argument('gateway'), $this->argument('feature')); 17 | 18 | $this->line( 19 | sprintf( 20 | 'Feature `%s` has been turned off', 21 | $this->argument('feature') 22 | ) 23 | ); 24 | 25 | return self::SUCCESS; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ExpiredFeaturesHandler.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 20 | } 21 | 22 | public function isExpired(string $feature): void 23 | { 24 | if (in_array($feature, $this->features)) { 25 | call_user_func($this->handler, $feature); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/ActionDebugLog.php: -------------------------------------------------------------------------------- 1 | decisions[] = ['pipe' => $pipe, 'reason' => $reason, 'result' => $result]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/FileLoader.php: -------------------------------------------------------------------------------- 1 | discoverer->find()); 21 | if (! is_callable($callable)) { 22 | throw new RuntimeException(sprintf('File `%s` does not return a callable', $file)); 23 | } 24 | 25 | return $this->container->call($callable); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Support/MaintenanceDriver.php: -------------------------------------------------------------------------------- 1 | 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 | } 34 | -------------------------------------------------------------------------------- /src/Gateways/InMemoryGateway.php: -------------------------------------------------------------------------------- 1 | flags = $loader->load(); 19 | } 20 | 21 | public function accessible(string $feature): ?bool 22 | { 23 | if (($result = ($this->flags[$feature] ?? null)) !== null) { 24 | return (bool) $result; 25 | } 26 | 27 | return null; 28 | } 29 | 30 | public function generateKey(string $feature): string 31 | { 32 | return md5($feature); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /migrations/create_features_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('title')->nullable(); 19 | $table->string('feature')->unique(); 20 | $table->text('description')->nullable(); 21 | $table->timestamp('active_at')->nullable(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('features'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Rules/FeatureOnRule.php: -------------------------------------------------------------------------------- 1 | check($parameters[1] ?? 'on') 25 | ? ! Features::accessible($parameters[0]) 26 | : Features::accessible($parameters[0]); 27 | 28 | if (! $featureState) { 29 | return $this->validateRequired($attribute, $value); 30 | } 31 | 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Support/FeaturesFileDiscoverer.php: -------------------------------------------------------------------------------- 1 | application->make('files'); 21 | 22 | if (Str::startsWith($this->file, '/') && $files->exists($this->file)) { 23 | return $this->file; 24 | } 25 | 26 | if ($files->exists($path = $this->application->basePath($this->file))) { 27 | return $path; 28 | } 29 | 30 | if ($files->exists($path = $this->application->basePath($this->file . '.dist'))) { 31 | return $path; 32 | } 33 | 34 | throw new RuntimeException(sprintf('`%s` file could not be found.', $this->file)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Peter Fox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/Support/FeatureFilter.php: -------------------------------------------------------------------------------- 1 | passes($feature); 22 | } 23 | 24 | public function passes(string $feature): bool 25 | { 26 | foreach ($this->rules as $rule) { 27 | if ($this->checkPattern($rule, $feature)) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | } 34 | 35 | protected function checkPattern(string $rule, string $feature): bool 36 | { 37 | $negative = Str::startsWith($rule, '!'); 38 | 39 | if ($negative) { 40 | $rule = Str::after($rule, '!'); 41 | } 42 | 43 | $rule = Str::beforeLast($rule, '*'); 44 | 45 | return $negative xor Str::startsWith($feature, $rule); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Gateways/GateGateway.php: -------------------------------------------------------------------------------- 1 | gate->forUser($this->guard->user())->allows($this->gateName, [$feature]); 26 | } 27 | 28 | public function generateKey(string $feature): string 29 | { 30 | /** @var Model|null $model */ 31 | $model = $this->guard->user(); 32 | 33 | if (is_null($model)) { 34 | return md5($feature); 35 | } 36 | 37 | return implode(':', [md5($feature), $model::class, $model->getKey()]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ActionableFlag.php: -------------------------------------------------------------------------------- 1 | feature; 17 | } 18 | 19 | public function setResult(bool $value): void 20 | { 21 | $this->result = $value; 22 | } 23 | 24 | public function getResult(): ?bool 25 | { 26 | return $this->result; 27 | } 28 | 29 | public function hasResult(): bool 30 | { 31 | return ! is_null($this->result); 32 | } 33 | 34 | public function isDebuggable(): bool 35 | { 36 | return (bool) $this->debug; 37 | } 38 | 39 | public function storeInspectionInformation(string $pipe, string $reason, ?bool $result = null): void 40 | { 41 | $this->debug->addDecision($pipe, $reason, $result); 42 | } 43 | 44 | public function log(): ?ActionDebugLog 45 | { 46 | return $this->debug; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Gateways/RedisGateway.php: -------------------------------------------------------------------------------- 1 | connection->get($this->key($feature)); 22 | } 23 | 24 | public function turnOn(string $feature): void 25 | { 26 | $this->connection->set($this->key($feature), true); 27 | } 28 | 29 | public function turnOff(string $feature): void 30 | { 31 | $this->connection->set($this->key($feature), false); 32 | } 33 | 34 | protected function key(string $key): string 35 | { 36 | return implode(':', array_filter([$this->prefix, $key])); 37 | } 38 | 39 | public function generateKey(string $feature): string 40 | { 41 | return md5($feature); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "psr12", 3 | "exclude": [ 4 | "bin" 5 | ], 6 | "rules": { 7 | "array_syntax": { 8 | "syntax": "short" 9 | }, 10 | "ordered_imports": { 11 | "sort_algorithm": "alpha" 12 | }, 13 | "no_unused_imports": true, 14 | "not_operator_with_successor_space": true, 15 | "trailing_comma_in_multiline": true, 16 | "phpdoc_scalar": true, 17 | "unary_operator_spaces": true, 18 | "binary_operator_spaces": true, 19 | "blank_line_before_statement": { 20 | "statements": [ 21 | "break", 22 | "continue", 23 | "declare", 24 | "return", 25 | "throw", 26 | "try" 27 | ] 28 | }, 29 | "phpdoc_single_line_var_spacing": true, 30 | "phpdoc_var_without_name": true, 31 | "class_attributes_separation": { 32 | "elements": { 33 | "method": "one" 34 | } 35 | }, 36 | "method_argument_space": { 37 | "on_multiline": "ensure_fully_multiline", 38 | "keep_multiple_spaces_after_comma": true 39 | }, 40 | "single_trait_insert_per_statement": true, 41 | "php_unit_method_casing": { 42 | "case": "snake_case" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Middlewares/GuardFeature.php: -------------------------------------------------------------------------------- 1 | check($state) 40 | ? ! $this->features->accessible($feature) 41 | : $this->features->accessible($feature)) 42 | ) { 43 | $this->application->abort($abort, $this->translator->get($message)); 44 | } 45 | 46 | return $next($request); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Support/GatewayCache.php: -------------------------------------------------------------------------------- 1 | repository->has($this->generateKey($feature)); 23 | } 24 | 25 | /** 26 | * @throws InvalidArgumentException 27 | */ 28 | public function result(string $feature): bool 29 | { 30 | return (bool) $this->repository->get($this->generateKey($feature)); 31 | } 32 | 33 | public function store(string $feature, ?bool $result): void 34 | { 35 | $this->repository->put($this->generateKey($feature), $result, $this->ttl); 36 | } 37 | 38 | public function delete(string $feature): void 39 | { 40 | $this->repository->delete($this->generateKey($feature)); 41 | } 42 | 43 | public function generateKey(string $feature): string 44 | { 45 | return $this->namespace . ':' . $this->cacheable->generateKey($feature); 46 | } 47 | 48 | public function configureTtl(?int $seconds): self 49 | { 50 | $this->ttl = $seconds; 51 | 52 | return $this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Gateways/DatabaseGateway.php: -------------------------------------------------------------------------------- 1 | getTable()->where('feature', $feature)->first(); 23 | 24 | if ($row) { 25 | return (bool) ($row->{$this->field} ?? false); 26 | } 27 | 28 | return null; 29 | } 30 | 31 | public function turnOn(string $feature): void 32 | { 33 | $this->getTable()->updateOrInsert([ 34 | 'feature' => $feature, 35 | ], [ 36 | $this->field => now(), 37 | ]); 38 | } 39 | 40 | public function turnOff(string $feature): void 41 | { 42 | $this->getTable()->updateOrInsert([ 43 | 'feature' => $feature, 44 | ], [ 45 | $this->field => null, 46 | ]); 47 | } 48 | 49 | protected function getTable(): Builder 50 | { 51 | return $this->connection->table($this->table); 52 | } 53 | 54 | public function generateKey(string $feature): string 55 | { 56 | return md5($feature); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /config/features.php: -------------------------------------------------------------------------------- 1 | ['database', 'in_memory'], 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Gateways 18 | |-------------------------------------------------------------------------- 19 | | 20 | | Configures the different gateway options 21 | | 22 | */ 23 | 24 | 'gateways' => [ 25 | 'in_memory' => [ 26 | 'file' => env('FEATURE_FLAG_IN_MEMORY_FILE', '.features.php'), 27 | 'driver' => 'in_memory', 28 | ], 29 | 'database' => [ 30 | 'driver' => 'database', 31 | 'cache' => [ 32 | 'ttl' => 600, 33 | ], 34 | 'connection' => env('FEATURE_FLAG_DATABASE_CONNECTION'), 35 | 'table' => env('FEATURE_FLAG_DATABASE_TABLE', 'features'), 36 | ], 37 | 'gate' => [ 38 | 'driver' => 'gate', 39 | 'gate' => env('FEATURE_FLAG_GATE_GATE', 'feature'), 40 | 'guard' => env('FEATURE_FLAG_GATE_GUARD'), 41 | 'cache' => [ 42 | 'ttl' => 600, 43 | ], 44 | ], 45 | 'redis' => [ 46 | 'driver' => 'redis', 47 | 'prefix' => env('FEATURE_FLAG_REDIS_PREFIX', 'features'), 48 | 'connection' => env('FEATURE_FLAG_REDIS_CONNECTION', 'default'), 49 | ], 50 | ], 51 | ]; 52 | -------------------------------------------------------------------------------- /src/Support/MaintenanceScenario.php: -------------------------------------------------------------------------------- 1 | feature = $feature; 20 | $this->onEnabled = true; 21 | 22 | return $this; 23 | } 24 | 25 | public function whenDisabled(string $feature): static 26 | { 27 | $this->feature = $feature; 28 | $this->onEnabled = false; 29 | 30 | return $this; 31 | } 32 | 33 | public function refresh(int $seconds): static 34 | { 35 | $this->attributes['refresh'] = (string) $seconds; 36 | 37 | return $this; 38 | } 39 | 40 | public function statusCode(int $status): static 41 | { 42 | $this->attributes['status'] = $status; 43 | 44 | return $this; 45 | } 46 | 47 | public function retry(int $seconds): static 48 | { 49 | $this->attributes['retry'] = $seconds; 50 | 51 | return $this; 52 | } 53 | 54 | public function secret(string $secret): static 55 | { 56 | $this->attributes['secret'] = $secret; 57 | 58 | return $this; 59 | } 60 | 61 | public function redirect(string $url): static 62 | { 63 | $this->attributes['redirect'] = $url; 64 | 65 | return $this; 66 | } 67 | 68 | public function template(string $html): static 69 | { 70 | $this->attributes['template'] = $html; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @param string[] $urls 77 | */ 78 | public function exceptPaths(array $urls): static 79 | { 80 | $this->attributes['except'] = $urls; 81 | 82 | return $this; 83 | } 84 | 85 | public function toArray(): array 86 | { 87 | return $this->attributes; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ylsideas/feature-flags", 3 | "description": "A Laravel package for handling feature flags", 4 | "keywords": [ 5 | "ylsideas", 6 | "feature-flags" 7 | ], 8 | "homepage": "https://github.com/ylsideas/feature-flags", 9 | "license": "MIT", 10 | "type": "library", 11 | "authors": [ 12 | { 13 | "name": "Peter Fox", 14 | "email": "peter.fox@ylsideas.co", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "illuminate/contracts": "12.*|11.*|10.*" 21 | }, 22 | "require-dev": { 23 | "composer/semver": "^3.0", 24 | "larastan/larastan": "^3.0|^2.0|^1.0", 25 | "laravel/pint": "^1.0", 26 | "mockery/mockery": "^1.6.12", 27 | "nunomaduro/collision": "^8.0|^7.8|^6.0", 28 | "orchestra/testbench": "^10.0|^9.0|^8.0", 29 | "pestphp/pest": "^1.21|^2.34|^3.0", 30 | "pestphp/pest-plugin-arch": "^2.6|^3.0", 31 | "pestphp/pest-plugin-laravel": "^1.1|^2.3|^3.0", 32 | "phpstan/extension-installer": "^1.4", 33 | "phpstan/phpdoc-parser": "^2.0", 34 | "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", 35 | "rector/rector": "^1.0|^2.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "YlsIdeas\\FeatureFlags\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "YlsIdeas\\FeatureFlags\\Tests\\": "tests" 45 | } 46 | }, 47 | "scripts": { 48 | "analyse": "vendor/bin/phpstan analyse", 49 | "lint": "vendor/bin/pint", 50 | "test": "vendor/bin/pest", 51 | "test-coverage": "vendor/bin/pest coverage", 52 | "fix": "vendor/bin/rector", 53 | "ci": [ 54 | "@fix", 55 | "@lint", 56 | "@test", 57 | "@analyse" 58 | ] 59 | }, 60 | "config": { 61 | "sort-packages": true, 62 | "allow-plugins": { 63 | "pestphp/pest-plugin": true, 64 | "phpstan/extension-installer": true 65 | } 66 | }, 67 | "extra": { 68 | "laravel": { 69 | "providers": [ 70 | "YlsIdeas\\FeatureFlags\\FeatureFlagsServiceProvider" 71 | ], 72 | "aliases": { 73 | "Features": "YlsIdeas\\FeatureFlags\\Facades\\Features" 74 | } 75 | } 76 | }, 77 | "minimum-stability": "dev", 78 | "prefer-stable": true 79 | } 80 | -------------------------------------------------------------------------------- /src/Facades/Features.php: -------------------------------------------------------------------------------- 1 | $flagsToFake 49 | */ 50 | public static function fake(array $flagsToFake = []): FeatureFake 51 | { 52 | $manager = static::isFake() 53 | ? static::getFacadeRoot()->manager 54 | : static::getFacadeRoot(); 55 | 56 | static::swap($fake = new FeatureFake($manager, $flagsToFake)); 57 | 58 | return $fake; 59 | } 60 | 61 | protected static function getFacadeAccessor(): string 62 | { 63 | return FeaturesContract::class; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Support/FeatureFake.php: -------------------------------------------------------------------------------- 1 | $featureFlags 26 | */ 27 | public function __construct(public Manager $manager, protected array $featureFlags = []) 28 | { 29 | } 30 | 31 | public function accessible(string $feature): bool 32 | { 33 | Event::dispatch(new FeatureAccessing($feature)); 34 | 35 | $featureValue = $this->featureFlags[$feature] ?? false; 36 | 37 | if (is_array($featureValue)) { 38 | $execution = $this->getCount($feature); 39 | 40 | // if the array has run out of values, then use the last position 41 | $featureValue = ($featureValue[$execution] ?? null) !== null ? 42 | $featureValue[$execution] : 43 | Arr::last($featureValue); 44 | } 45 | 46 | Arr::set($this->flagCounts, $feature, $this->getCount($feature) + 1); 47 | 48 | Event::dispatch(new FeatureAccessed($feature, $featureValue)); 49 | 50 | return $featureValue; 51 | } 52 | 53 | public function assertAccessed(string $feature, ?int $count = null, string $message = ''): void 54 | { 55 | if ($count === null) { 56 | Assert::assertGreaterThan(0, $this->getCount($feature), $message); 57 | } else { 58 | Assert::assertSame($count, $this->getCount($feature), $message); 59 | } 60 | } 61 | 62 | public function assertNotAccessed(string $feature, string $message = ''): void 63 | { 64 | Assert::assertLessThan(1, $this->getCount($feature), $message); 65 | } 66 | 67 | public function assertAccessedCount(string $feature, int $count = 0, string $message = ''): void 68 | { 69 | $this->assertAccessed($feature, $count, $message); 70 | } 71 | 72 | public function __call(string $method, array $args) 73 | { 74 | return $this->forwardCallTo($this->manager, $method, $args); 75 | } 76 | 77 | protected function getCount(string $feature) 78 | { 79 | return Arr::get($this->flagCounts, $feature, 0); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Support/GatewayInspector.php: -------------------------------------------------------------------------------- 1 | gateway; 25 | } 26 | 27 | public function filter(): ?FeatureFilter 28 | { 29 | return $this->filter; 30 | } 31 | 32 | public function cache(): ?GatewayCache 33 | { 34 | return $this->cache; 35 | } 36 | 37 | public function handle(ActionableFlag $action, callable $next): ActionableFlag 38 | { 39 | if ($action->hasResult()) { 40 | $this->handleDebug($action, ActionDebugLog::REASON_RESULT_ALREADY_FOUND, $action->getResult()); 41 | 42 | return $next($action); 43 | } 44 | 45 | if ($this->filter && $this->filter->fails($action->feature())) { 46 | $this->handleDebug($action, ActionDebugLog::REASON_FILTER, $action->getResult()); 47 | 48 | 49 | return $next($action); 50 | } 51 | 52 | if ($this->cache && $this->cache->hits($action->feature())) { 53 | $action->setResult($this->cache->result($action->feature())); 54 | $this->handleDebug( 55 | $action, 56 | ActionDebugLog::REASON_CACHE, 57 | $action->getResult() 58 | ); 59 | 60 | return $next($action); 61 | } 62 | 63 | if (($result = $this->gateway->accessible($action->feature())) !== null) { 64 | $this->cache?->store($action->feature(), $result); 65 | $action->setResult($result); 66 | 67 | $this->handleDebug( 68 | $action, 69 | ActionDebugLog::REASON_RESULT, 70 | $action->getResult() 71 | ); 72 | } else { 73 | $this->handleDebug( 74 | $action, 75 | ActionDebugLog::REASON_NO_RESULT, 76 | $action->getResult() 77 | ); 78 | } 79 | 80 | return $next($action); 81 | } 82 | 83 | protected function handleDebug(mixed $action, string $reason, ?bool $result = null): void 84 | { 85 | if ($action instanceof DebuggableFlag && $action->isDebuggable()) { 86 | $action->storeInspectionInformation($this->name, $reason, $result); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Support/MaintenanceRepository.php: -------------------------------------------------------------------------------- 1 | uponActivation = Closure::fromCallable($callable); 25 | 26 | return $this; 27 | } 28 | 29 | public function uponDeactivation(callable $callable): static 30 | { 31 | $this->uponDeactivation = Closure::fromCallable($callable); 32 | 33 | return $this; 34 | } 35 | 36 | public function callActivation(array $properties): void 37 | { 38 | $this->container->call($this->uponActivation, [ 39 | 'properties' => $properties, 'features' => $this->features, 40 | ]); 41 | } 42 | 43 | public function callDeactivation(): void 44 | { 45 | $this->container->call($this->uponDeactivation, ['features' => $this->features]); 46 | } 47 | 48 | public function onEnabled(string $feature): MaintenanceScenario 49 | { 50 | return tap((new MaintenanceScenario())->whenEnabled($feature), function (MaintenanceScenario $scenario): void { 51 | $this->scenarios[] = $scenario; 52 | }); 53 | } 54 | 55 | public function onDisabled(string $feature): MaintenanceScenario 56 | { 57 | return tap((new MaintenanceScenario())->whenDisabled($feature), function (MaintenanceScenario $scenario): void { 58 | $this->scenarios[] = $scenario; 59 | }); 60 | } 61 | 62 | public function active(): bool 63 | { 64 | return (bool) $this->findScenario(); 65 | } 66 | 67 | public function parameters(): ?array 68 | { 69 | if ($this->foundScenario === false) { 70 | $this->findScenario(); 71 | } 72 | 73 | return $this->foundScenario?->toArray(); 74 | } 75 | 76 | protected function findScenario(): ?MaintenanceScenario 77 | { 78 | return $this->foundScenario = collect($this->scenarios) 79 | ->first(function (MaintenanceScenario $scenario): bool { 80 | if ($scenario->onEnabled && $this->features->accessible($scenario->feature)) { 81 | return true; 82 | } 83 | 84 | return ! $scenario->onEnabled && ! $this->features->accessible($scenario->feature); 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `feature-flags` will be documented in this file 4 | 5 | ## v3.0.2 - 2025-05-11 6 | 7 | ### What's Changed 8 | 9 | * Fix deprecated implicit nullable parameter by @brunodevel in https://github.com/ylsideas/feature-flags/pull/74 10 | 11 | ### New Contributors 12 | 13 | * @brunodevel made their first contribution in https://github.com/ylsideas/feature-flags/pull/74 14 | 15 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v3.0.1...v3.0.2 16 | 17 | ## v3.0.1 - 2025-03-01 18 | 19 | ### What's Fixed 20 | 21 | * Force casting cache result to boolean by @peterfox in https://github.com/ylsideas/feature-flags/pull/73 22 | 23 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v3.0.0...v3.0.1 24 | 25 | ## v3.0.0 - 2025-03-01 26 | 27 | ### What's Changed 28 | 29 | * Update workflow for 8.4 by @peterfox in https://github.com/ylsideas/feature-flags/pull/68 30 | * change instance of manager by @uesley in https://github.com/ylsideas/feature-flags/pull/71 31 | * Upgrade static analysis tools & Laravel 12 compatibility by @peterfox in https://github.com/ylsideas/feature-flags/pull/72 32 | 33 | ### New Contributors 34 | 35 | * @uesley made their first contribution in https://github.com/ylsideas/feature-flags/pull/71 36 | 37 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.6.0...v3.0.0 38 | 39 | ## v2.6.0 - 2024-05-29 40 | 41 | ### What's Changed 42 | 43 | * Allows for multiple fakes to occur by @peterfox @brunodevel in https://github.com/ylsideas/feature-flags/pull/67 44 | 45 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.5.0...v2.6.0 46 | 47 | ## v2.5.0 - 2024-03-19 48 | 49 | ### What's Changed 50 | 51 | * Laravel 11 by @peterfox in https://github.com/ylsideas/feature-flags/pull/65 52 | 53 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.4.2...v2.5.0 54 | 55 | ## v2.4.2 - 2023-12-02 56 | 57 | ### What's Changed 58 | 59 | * Fix maintenance mode by @peterfox in https://github.com/ylsideas/feature-flags/pull/62 60 | * Fix a typo in the about command by @peterfox in https://github.com/ylsideas/feature-flags/pull/63 61 | 62 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.4.1...v2.4.2 63 | 64 | ## v2.4.1 - 2023-02-09 65 | 66 | ### What's Changed 67 | 68 | - Fixes a typo in the error message for missing in memory config files #55 69 | 70 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.4.0...v2.4.1 71 | 72 | ## 2.4.0 - 2023-02-02 73 | 74 | ### What's Changed 75 | 76 | - Maintenance mode with flags by @peterfox in https://github.com/ylsideas/feature-flags/pull/54 77 | 78 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.3.1...v2.4.0 79 | 80 | ## 2.3.1 - 2023-01-21 81 | 82 | ### What's Changed 83 | 84 | - Automates updating Facade DocBlock by @peterfox in https://github.com/ylsideas/feature-flags/pull/52 85 | - Fix facade docs to contain fake methods by @peterfox in https://github.com/ylsideas/feature-flags/pull/53 86 | 87 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.3.0...v2.3.1 88 | 89 | ## 2.3.0 - 2023-01-15 90 | 91 | ### What's Changed 92 | 93 | - Implement a middleware message by @peterfox in https://github.com/ylsideas/feature-flags/pull/49 94 | - Default config changes by @peterfox in https://github.com/ylsideas/feature-flags/pull/50 95 | - Support Laravel 10 by @peterfox in https://github.com/ylsideas/feature-flags/pull/51 96 | 97 | **Full Changelog**: https://github.com/ylsideas/feature-flags/compare/v2.2.0...v2.3.0 98 | 99 | ## 2.1.0 - 2022-10-22 100 | 101 | - Adds a new system for debugging features that are accessed. 102 | - Testing a feature now is easy to do via the Features facade. 103 | - You can now add a handler for when features should be expired. 104 | - Adds a mixin for the Eloquent Query Builder allowing you to use the methods whenFeatureIsAccessible() and whenFeatureIsNotAccessible(). 105 | 106 | ## 2.0.0 - 2022-09-18 107 | 108 | - Breaking Changes. Adds a pipeline and gateway system over the old repository system. Allows for multiple use 109 | - of the same driver within the pipeline. Changes config to in_memory driver. Adds a gate based driver. 110 | 111 | ## 1.2.1 - 2019-12-08 112 | 113 | - Fixes a mistake with the `update_on_resolve` config option not being used for the Chain repository. 114 | 115 | ## 1.2.0 - 2019-10-25 116 | 117 | - Adds a new console command `feature:state` to report the current state of a feature flag. 118 | 119 | ## 1.1.0 - 2019-09-06 120 | 121 | - Fixes incorrect logic for handling features that are off and being check via the middleware or validations. 122 | 123 | ## 1.0.1 - 2019-09-06 124 | 125 | - Tested to work with Laravel 6.0 release 126 | 127 | ## 1.0.0 - 2019-07-10 128 | 129 | - initial release 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Feature Flags - The extendable and adaptable Laravel Feature Flags package for managing flags within code](banner.png "Feature Flags") 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ylsideas/feature-flags.svg?style=flat-square)](https://packagist.org/packages/ylsideas/feature-flags) 4 | [![Tests](https://github.com/ylsideas/feature-flags/actions/workflows/run-tests.yml/badge.svg)](https://github.com/ylsideas/feature-flags/actions/workflows/run-tests.yml) 5 | [![Check & fix styling](https://github.com/ylsideas/feature-flags/actions/workflows/pint.yml/badge.svg)](https://github.com/ylsideas/feature-flags/actions/workflows/pint.yml) 6 | [![codecov](https://codecov.io/github/ylsideas/feature-flags/branch/main/graph/badge.svg)](https://codecov.io/github/ylsideas/feature-flags) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/ylsideas/feature-flags.svg?style=flat-square)](https://packagist.org/packages/ylsideas/feature-flags) 8 | [![Help Fund](https://img.shields.io/github/sponsors/peterfox?style=flat-square)](https://github.com/sponsors/peterfox) 9 | 10 | A Feature flag is at times referred to as a feature toggle or feature switch. Ultimately it's a coding strategy 11 | to be used along with source control to make it easier to continuously integrate and deploy. The idea of 12 | the flags works by essentially safe guarding sections of code from executing if a feature flag isn't in a switched 13 | on state. 14 | 15 | This package aims to make implementing such flags across your application a great deal easier by providing solutions 16 | that work with not only your code but your routes, blade files, task scheduling and validations. 17 | 18 | ## The Feature flagging dashboard for Laravel 19 | 20 | [![flagfox - the power to deploy features form your own dashboard via a single Laravel package](https://www.flagfox.dev/img/github-readme-image.png "Flagfox")](https://www.flagfox.dev/?utm_campaign=waitlist&utm_source=github&utm_content=featureflags) 21 | 22 | In late 2022 we decided to start work on a dashboard that will work on top of all the awesomeness that [Feature flags 23 | for Laravel](https://github.com/ylsideas/feature-flags) gives you. Right now you can 24 | [join the waiting list](https://www.flagfox.dev/?utm_campaign=waitlist&utm_source=github&utm_content=featureflags#waitlist). 25 | 26 | ## How adding feature flags looks with this package 27 | 28 | It's pretty simple, you can start of with just simple calls to check if a flag's state is on or off. 29 | 30 | ```php 31 | Features::accessible('my-feature') // returns true or false 32 | ``` 33 | 34 | One of the unique features of this package is that it integrates heavily into Laravel by allowing you to 35 | configure different things such as access to route, schedule tasks or modifying the query builder. 36 | 37 | To get a full understanding, it's best to [read the docs](https://feature-flags.docs.ylsideas.co/). 38 | 39 | ## Upgrading 40 | 41 | Version 3 and 2 are mostly the same. Some additonal type hinting was adding but not no major 42 | upgrade work should be required. 43 | 44 | Version 2 and is somewhat different to version 1. If you are using Laravel 9 and PHP8 45 | you should aim to use version 2. Version 1 is no longer supported. There is an [upgrade guide for moving 46 | from version 1 to version 2](UPGRADE.md). 47 | 48 | ## Installation 49 | 50 | You can install the package via composer: 51 | 52 | ```bash 53 | composer require ylsideas/feature-flags:^3.0 54 | ``` 55 | 56 | Once installed you should publish the config with the following command. 57 | 58 | ```bash 59 | php artisan vendor:publish --provider="YlsIdeas\FeatureFlags\FeatureFlagsServiceProvider" --tag=config 60 | ``` 61 | 62 | You can customise the `features.php` config in a number of ways. 63 | 64 | ## Documentation 65 | 66 | For the complete documentation, visit [https://feature-flags.docs.ylsideas.co/](https://feature-flags.docs.ylsideas.co/). 67 | 68 | ## Package Development 69 | 70 | If you wish to develop new features for this package you may run the tests using the following command. 71 | 72 | ``` bash 73 | composer test 74 | ``` 75 | 76 | Make sure any code you work on is linted as well. 77 | 78 | ```bash 79 | composer lint 80 | ``` 81 | 82 | and that the code doesn't introduce errors with PHPStan. 83 | 84 | ```bash 85 | composer analyse 86 | ``` 87 | 88 | Please make sure you *follow the Pull Request template* for all proposed changes. Ignoring it 89 | will mean the PR will be ignored. 90 | 91 | ### Changelog 92 | 93 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 94 | 95 | ## Contributing 96 | 97 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 98 | 99 | ### Security 100 | 101 | If you discover any security related issues, please email peter.fox@ylsideas.co instead of using the issue tracker. 102 | 103 | ## Credits 104 | 105 | - [Peter Fox](https://github.com/ylsideas) 106 | - [All Contributors](../../contributors) 107 | 108 | ## License 109 | 110 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 111 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | ## Version 1 to Version 2 4 | 5 | There's been a number of changes with the Feature Flag package. Some of them may cause difficulties for those using 6 | the version 1 to be able to migrate to version to as these will be breaking changes. 7 | 8 | The most important thing to state is the Facade itself and the API to check if a feature is accessible has not been 9 | changed. All 10 | the points where you have put flags within your app should not be affected in this upgrade. 11 | 12 | The database schema for the database driver has not been changed either. 13 | 14 | ### Philosophy of Version 2 15 | 16 | The original version 1 package was made in 2019. While it's been fairly popular, it never quite lived up to what I 17 | wanted it to be. There's also been a few support issues where people have requested features which I felt the previous 18 | version didn't work well with. 19 | 20 | Version 2 ditches repositories of features for gateways instead. 21 | 22 | A gateway is a single stop of many along the journey to assessing a feature flag. Each gateway has a driver behind it allowing for 23 | a gateway to use the same technologies but with a different configuration. For example, you can now implement multiple 24 | database gateways that look at different tables or even on different connections. Want to access a local SQLite DB 25 | and then your MySQL database if you get nothing from the SQLite connection? Sure, why not. 26 | 27 | There's also no longer a chain repository, because the whole package works like a chain. You now have a `pipeline` 28 | option in the which specifies the order of the gateways. 29 | 30 | Previously in version one, to implement caching you had to use the Chain repository, or implement your own system. 31 | Now you can just specify your own per gateway and how to store it. 32 | 33 | ### Updating your config 34 | 35 | You are likely best off removing your original `features.php` config and performing a vendor publish of the new config. 36 | From there you can set the `pipeline`. If you were previously only using the database, redis or config drivers then 37 | you just need to set the `pipleine` to be `['database']` etc. 38 | 39 | If you were using the chain repository the default configuration for it as a pipeline would look like `['in_memory', 40 | 'redis', 'database']`. 41 | 42 | You'll notice there is now an option for a cache per gateway, this allows for you to customise how results from one 43 | gateway are cached and for how long, the cache keys will be namespaced by the name of the gateway even if you use 44 | the same store, there will not be any conflicts. 45 | 46 | Another feature is if you only want some gates to handle features with a particular prefix, you may provide a pattern 47 | in the gateway configuration or a set of patterns that will be used to block the gateway from being inspected and 48 | then continue onto the next pipe. 49 | 50 | ### The difference between the Config Repository and the InMemory Gateway 51 | 52 | The database and redis drivers have no real difference. The Config Repository now gets its values from a different 53 | source. There is no `feature` key in the `features.php` config. Instead, you should use the vendor publish command to 54 | create a `.features.php` file at the base of your project with which you can store the values. It's simple enough to 55 | copy and paste your original values. You can have the advantage that you can inject any services from the container 56 | into your `.features.php` config whereas before you were limited to just .ENV values. 57 | 58 | ```php 59 | 65 | */ 66 | return static function (Application $app): array { 67 | return [ 68 | 'my.feature.flag' => true, 69 | ]; 70 | }; 71 | ``` 72 | 73 | ### Package Options & Middleware 74 | 75 | The package options to turn off functionality that is typically on by default can be changed by setting. 76 | 77 | ### Implementing your own Gateways vs. Repositories 78 | 79 | The biggest change will be to those who have implemented their own `YlsIdeas\FeatureFlags\Contracts\Repository` 80 | in version 1. Originally you only 81 | needed to implement one interface. You now have multiple interfaces although the only interface you need is 82 | `YlsIdeas\FeatureFlags\Contracts\Gateway`. Once this is implemented you may also implement a 83 | `YlsIdeas\FeatureFlags\Contracts\Toggleable` and/or `YlsIdeas\FeatureFlags\Contracts\Cacheable` interface. The 84 | first allows for the Gateway to be used be the console commands for turning on and off features. The second is for 85 | allowing Gateways to be cached. 86 | 87 | ### Removal of the Features Controller/Route 88 | 89 | In version 1 a route and controller existed that could allow for HTTP clients (like from a frontend) to fetch all the 90 | features and all the 91 | states they were in. This has now been removed as the Gate driver does not behave in this manner because the results 92 | are contextual to the feature you are using. A feature like this may return in future versions but for now if you use 93 | this you will need to either not upgrade or implement your own controller. 94 | 95 | # Issues with Upgrading 96 | 97 | If you feel there is a mistake in these docs on upgrading or something that needs clarifying 98 | please open an Issue. 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/FeatureFlagsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 36 | $this->publishes([ 37 | __DIR__.'/../config/features.php' => config_path('features.php'), 38 | ], 'config'); 39 | 40 | $this->publishes([ 41 | __DIR__.'/../config/.features.php' => base_path('.features.php'), 42 | ], 'inmemory-config'); 43 | 44 | // Publishing the migrations. 45 | $migration = date('Y_m_d_His').'_create_features_table.php'; 46 | $this->publishes([ 47 | __DIR__.'/../migrations/create_features_table.php' => database_path('migrations/'.$migration), 48 | ], 'features-migration'); 49 | 50 | $this->publishes([ 51 | __DIR__.'/../stubs/PreventRequestsDuringMaintenance.php' => app_path('Http/Middleware/PreventRequestsDuringMaintenance.php'), 52 | ], 'maintenance-middleware'); 53 | 54 | // Registering package commands. 55 | if (Features::usesCommands()) { 56 | $this->commands([ 57 | SwitchOnFeature::class, 58 | SwitchOffFeature::class, 59 | ]); 60 | } 61 | 62 | $this->aboutCommandInfo(); 63 | } 64 | 65 | if (Features::usesValidations()) { 66 | $this->validator(); 67 | } 68 | 69 | if (Features::usesScheduling()) { 70 | $this->schedulingMacros(); 71 | } 72 | 73 | if (Features::usesBlade()) { 74 | $this->bladeDirectives(); 75 | } 76 | 77 | if (Features::usesMiddlewares()) { 78 | $this->app->make(Router::class) 79 | ->aliasMiddleware('feature', GuardFeature::class); 80 | } 81 | 82 | if (Features::usesQueryBuilderMixin()) { 83 | $this->queryBuilder(); 84 | } 85 | } 86 | 87 | /** 88 | * Register the application services. 89 | */ 90 | public function register(): void 91 | { 92 | // Automatically apply the package configuration 93 | $this->mergeConfigFrom(__DIR__.'/../config/features.php', 'features'); 94 | 95 | if (method_exists($this->app, 'scoped')) { 96 | $this->app->scoped(FeaturesContract::class, Manager::class); 97 | } else { 98 | $this->app->singleton(FeaturesContract::class, Manager::class); 99 | } 100 | 101 | $this->app->scoped(MaintenanceRepository::class, fn (Container $app): MaintenanceRepository => new MaintenanceRepository($app->make(FeaturesContract::class), $app)); 102 | 103 | $this->app->extend(MaintenanceModeManager::class, fn (MaintenanceModeManager $manager) => $manager->extend('features', fn (): MaintenanceMode => new MaintenanceDriver( 104 | $this->app->make(MaintenanceRepository::class) 105 | ))); 106 | } 107 | 108 | protected function schedulingMacros() 109 | { 110 | if (! Event::hasMacro('skipWithoutFeature')) { 111 | /** @noRector \Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector */ 112 | Event::macro('skipWithoutFeature', fn (string $feature): Event => 113 | /** @var Event $this */ 114 | $this->skip(fn (): bool => ! Features::accessible($feature))); 115 | } 116 | 117 | if (! Event::hasMacro('skipWithFeature')) { 118 | /** @noRector \Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector */ 119 | Event::macro('skipWithFeature', fn ($feature): Event => 120 | /** @var Event $this */ 121 | $this->skip(fn () => Features::accessible($feature))); 122 | } 123 | } 124 | 125 | protected function bladeDirectives() 126 | { 127 | Blade::if('feature', fn (string $feature, $applyIfOn = true) => $applyIfOn 128 | ? Features::accessible($feature) 129 | : ! Features::accessible($feature)); 130 | } 131 | 132 | protected function validator() 133 | { 134 | Validator::extendImplicit('requiredWithFeature', FeatureOnRule::class); 135 | } 136 | 137 | protected function queryBuilder() 138 | { 139 | Builder::mixin(new QueryBuilderMixin()); 140 | } 141 | 142 | protected function aboutCommandInfo(): void 143 | { 144 | if (class_exists(AboutCommand::class)) { 145 | AboutCommand::add('Feature Flags', [ 146 | 'Pipeline' => fn (): string => implode(', ', config('features.pipeline')), 147 | ]); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | container)) 67 | ->through($this->pipes()); 68 | } 69 | 70 | /** 71 | * @throws BindingResolutionException 72 | */ 73 | public function accessible(string $feature): bool 74 | { 75 | if ($this->expiredFeaturesHandler instanceof \YlsIdeas\FeatureFlags\Contracts\ExpiredFeaturesHandler) { 76 | $this->expiredFeaturesHandler->isExpired($feature); 77 | } 78 | 79 | $flagAction = new ActionableFlag(); 80 | $flagAction->feature = $feature; 81 | if ($this->usesDebugging()) { 82 | $this->configureDebug($flagAction, debug_backtrace()); 83 | } 84 | 85 | $this->dispatcher->dispatch(new FeatureAccessing($feature, $flagAction->debug)); 86 | 87 | /** @var Contracts\DebuggableFlag $flagAction */ 88 | $flagAction = $this->pipeline()->send($flagAction)->thenReturn(); 89 | 90 | $this->dispatcher->dispatch(new FeatureAccessed($feature, $flagAction->getResult(), $flagAction->log())); 91 | 92 | return (bool) $flagAction->getResult(); 93 | } 94 | 95 | public function turnOn(string $gateway, string $feature): void 96 | { 97 | $toggleable = $this->resolve($gateway)->gateway(); 98 | 99 | if (! $toggleable instanceof Toggleable) { 100 | throw new InvalidArgumentException(sprintf( 101 | 'Gateway `%s` is not a toggleable gateway.', 102 | $gateway 103 | )); 104 | } 105 | 106 | $toggleable->turnOn($feature); 107 | 108 | $this->dispatcher->dispatch(new FeatureSwitchedOn($feature, $gateway)); 109 | } 110 | 111 | public function turnOff(string $gateway, string $feature): void 112 | { 113 | $toggleable = $this->resolve($gateway)->gateway(); 114 | 115 | if (! $toggleable instanceof Toggleable) { 116 | throw new InvalidArgumentException(sprintf( 117 | 'Gateway `%s` is not a toggleable gateway.', 118 | $gateway 119 | )); 120 | } 121 | 122 | $toggleable->turnOff($feature); 123 | 124 | $this->dispatcher->dispatch(new FeatureSwitchedOff($feature, $gateway)); 125 | } 126 | 127 | public function noBlade(): static 128 | { 129 | $this->useBlade = false; 130 | 131 | return $this; 132 | } 133 | 134 | public function noScheduling(): static 135 | { 136 | $this->useScheduling = false; 137 | 138 | return $this; 139 | } 140 | 141 | public function noValidations(): static 142 | { 143 | $this->useValidations = false; 144 | 145 | return $this; 146 | } 147 | 148 | public function noCommands(): static 149 | { 150 | $this->useCommands = false; 151 | 152 | return $this; 153 | } 154 | 155 | public function noMiddlewares(): static 156 | { 157 | $this->useMiddlewares = false; 158 | 159 | return $this; 160 | } 161 | 162 | public function noQueryBuilderMixin(): static 163 | { 164 | $this->useQueryBuilderMixin = false; 165 | 166 | return $this; 167 | } 168 | 169 | public function configureDebugging(bool $value = true): static 170 | { 171 | $this->useDebugging = $value; 172 | 173 | return $this; 174 | } 175 | 176 | public function usesBlade(): bool 177 | { 178 | return $this->useBlade; 179 | } 180 | 181 | public function usesValidations(): bool 182 | { 183 | return $this->useValidations; 184 | } 185 | 186 | public function usesScheduling(): bool 187 | { 188 | return $this->useScheduling; 189 | } 190 | 191 | public function usesCommands(): bool 192 | { 193 | return $this->useCommands; 194 | } 195 | 196 | public function usesMiddlewares(): bool 197 | { 198 | return $this->useMiddlewares; 199 | } 200 | 201 | public function usesDebugging(): bool 202 | { 203 | return $this->useDebugging; 204 | } 205 | 206 | public function usesQueryBuilderMixin(): bool 207 | { 208 | return $this->useQueryBuilderMixin; 209 | } 210 | 211 | public function callOnExpiredFeatures(array $expiredFeatures, ?callable $handler = null): static 212 | { 213 | $handler ??= static function ($feature): void { 214 | throw new FeatureExpired($feature); 215 | }; 216 | 217 | $this->expiredFeaturesHandler = new ExpiredFeaturesHandler($expiredFeatures, $handler); 218 | 219 | return $this; 220 | } 221 | 222 | public function applyOnExpiredHandler(Contracts\ExpiredFeaturesHandler $handler): static 223 | { 224 | $this->expiredFeaturesHandler = $handler; 225 | 226 | return $this; 227 | } 228 | 229 | public function extend(string $driver, callable $builder): static 230 | { 231 | $this->gatewayDrivers[$driver] = $builder; 232 | 233 | return $this; 234 | } 235 | 236 | public function maintenanceMode(): MaintenanceRepository 237 | { 238 | return $this->container->make(MaintenanceRepository::class); 239 | } 240 | 241 | protected function getContainer(): Container 242 | { 243 | return $this->container; 244 | } 245 | 246 | protected function resolve($name): GatewayInspector 247 | { 248 | $config = $this->getConfig($name); 249 | 250 | if (is_null($config)) { 251 | throw new InvalidArgumentException("The [{$name}] feature gateway has not been configured."); 252 | } 253 | 254 | $gateway = $this->getGateway($config['driver'], $config, $name); 255 | if (($config['cache'] ?? null) && $gateway instanceof Cacheable) { 256 | $cache = $this->buildCache($name, $config['cache'], $gateway) 257 | ->configureTtl($config['cache']['ttl'] ?? 300); 258 | } 259 | if (($config['filter'] ?? null)) { 260 | if (is_string($config['filter']) && Str::contains($config['filter'], '|')) { 261 | $config['filter'] = explode('|', $config['filter']); 262 | } 263 | $config['filter'] = Arr::wrap($config['filter']); 264 | $filter = new FeatureFilter($config['filter']); 265 | } 266 | 267 | return new GatewayInspector($name, $gateway, $filter ?? null, $cache ?? null); 268 | } 269 | 270 | protected function getConfig($name): ?array 271 | { 272 | return $this->container->make(Repository::class)->get("features.gateways")[$name] ?? null; 273 | } 274 | 275 | protected function getGateway(string $driver, array $config, string $name): Gateway 276 | { 277 | if (! $this->driverIsNative($driver) && 278 | ! isset($this->gatewayDrivers[$driver]) 279 | ) { 280 | throw new InvalidArgumentException("No gateway for [$driver]."); 281 | } 282 | 283 | if ($this->driverIsNative($driver)) { 284 | return call_user_func([$this, 'build' . Str::ucfirst(Str::camel($driver)) . 'Gateway'], $config, $name); 285 | } 286 | 287 | return call_user_func($this->gatewayDrivers[$driver], $config, $name); 288 | } 289 | 290 | /** 291 | * @throws BindingResolutionException 292 | */ 293 | protected function pipes(): array 294 | { 295 | $pipes = $this->container->make(Repository::class)->get('features.pipeline'); 296 | 297 | return collect($pipes) 298 | ->map(fn (string $pipe): GatewayInspector => $this->resolve($pipe)) 299 | ->all(); 300 | } 301 | 302 | protected function buildCache(string $namespace, array $config, Cacheable $cacheable): GatewayCache 303 | { 304 | $cache = $this->getContainer()->make(CacheManager::class)->driver($config['store'] ?? null); 305 | 306 | return (new GatewayCache($cache, $namespace, $cacheable)) 307 | ->configureTtl($config['ttl']); 308 | } 309 | 310 | protected function configureDebug(ActionableFlag $flagAction, array $trace): void 311 | { 312 | $call = $trace[0] ?? []; 313 | $file = $call['file'] ?? null; 314 | $line = $call['line'] ?? null; 315 | 316 | $flagAction->debug = new ActionDebugLog($file, $line); 317 | } 318 | 319 | protected function buildDatabaseGateway(array $config): DatabaseGateway 320 | { 321 | return new DatabaseGateway( 322 | connection: $this->getContainer()->make(DatabaseManager::class)->connection( 323 | $config['connection'] ?? null 324 | ), 325 | table: $config['table'] ?? 'features', 326 | field: $config['field'] ?? 'active_at', 327 | ); 328 | } 329 | 330 | protected function buildRedisGateway(array $config): RedisGateway 331 | { 332 | return new RedisGateway( 333 | connection: $this->getContainer() 334 | ->make(RedisManager::class) 335 | ->connection($config['connection'] ?? null), 336 | prefix: $config['prefix'] ?? 'features', 337 | ); 338 | } 339 | 340 | protected function buildGateGateway(array $config, string $name): GateGateway 341 | { 342 | if (! ($config['gate'] ?? false)) { 343 | throw new RuntimeException(sprintf('No gate is configured for gateway `%s`', $name)); 344 | } 345 | 346 | return new GateGateway( 347 | $this->getContainer()->make(AuthManager::class)->guard($config['guard'] ?? null), 348 | $this->getContainer()->make(Gate::class), 349 | $config['gate'], 350 | ); 351 | } 352 | 353 | protected function buildInMemoryGateway(array $config): InMemoryGateway 354 | { 355 | return new InMemoryGateway( 356 | loader: new FileLoader(new FeaturesFileDiscoverer( 357 | application: $this->getContainer()->make(Application::class), 358 | file: $config['file'] ?? null, 359 | ), container: $this->getContainer()), 360 | ); 361 | } 362 | 363 | protected function driverIsNative(string $driver): bool 364 | { 365 | return method_exists($this, 'build' . Str::ucfirst(Str::camel($driver)) . 'Gateway'); 366 | } 367 | } 368 | --------------------------------------------------------------------------------