offload-project / laravel-flagged
Feature flags with custom resolution logic for Laravel. Define how feature access is determined rather than storing per-user state.
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
pkg:composer/offload-project/laravel-flagged
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0
- illuminate/contracts: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- captainhook/captainhook-phar: ^5.27
- larastan/larastan: ^3.8.1
- laravel/pint: ^1.26.0
- orchestra/testbench: ^9.0|^10.8.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- ramsey/conventional-commits: ^1.6
README
Laravel Flagged
Feature flags with custom resolution logic for Laravel. Define how feature access is determined rather than storing per-user state.
Flagged integrates with Laravel Mandate to enable feature-scoped roles and permissions.
Features
- Custom Resolution Logic - Define features with closures or classes that accept any typed parameter (User, Team, Plan, etc.)
- Rich Return Values - Return booleans or complex values (rate limits, config arrays, tier levels)
- Class-Based Features - Encapsulate complex logic in dedicated feature classes
- Global On/Off Flags - Simple database-stored flags for maintenance mode, kill switches, etc.
- Scoped Feature Checks - Resolve features differently based on the model passed
- Model Trait - Add
ChecksFeaturesto any model for$user->hasFeature()syntax - Batch Checking - Check multiple features at once with
activeMany() - Query Helpers - List all defined features, active features for a scope, and more
- Blade Directives -
@feature,@unlessfeature,@featurevaluefor cleaner templates - Caching - Built-in caching for global features and in-memory resolution cache
- Mandate Integration - Implements
FeatureAccessHandlerfor feature-scoped roles and permissions - Feature Generator - Generate feature classes via
mandate:make:featurecommand - Feature Syncing - Sync feature classes to database with
flagged:synccommand - Feature Attributes - Support for
LabelandDescriptionattributes on feature classes
Installation
composer require offload-project/laravel-flagged
Publish the configuration and migrations:
php artisan vendor:publish --tag=flagged-config php artisan vendor:publish --tag=flagged-migrations php artisan migrate
Quick Start
Defining Features
Define features in a service provider's boot() method:
use OffloadProject\Flagged\Facades\Flagged; public function boot(): void { // Closure-based feature with scope Flagged::define('premium-dashboard', function (?User $user) { return $user?->plan === 'pro'; }); // Feature returning a rich value Flagged::define('api-rate-limit', function (?User $user) { return match ($user?->plan) { 'enterprise' => 10000, 'pro' => 1000, default => 100, }; }); // Global on/off feature (stored in database) Flagged::defineGlobal('maintenance-mode'); }
Checking Features
use OffloadProject\Flagged\Facades\Flagged; // Check if feature is active for a user if (Flagged::active('premium-dashboard', $user)) { // Show premium dashboard } // Get a feature's resolved value $rateLimit = Flagged::value('api-rate-limit', $user); // Check without scope (global features) if (Flagged::globalActive('maintenance-mode')) { // Show maintenance page } // Check multiple features at once $features = Flagged::activeMany(['feature-a', 'feature-b'], $user); // ['feature-a' => true, 'feature-b' => false]
Managing Global Features
// Activate a global feature Flagged::activate('maintenance-mode'); // Deactivate a global feature Flagged::deactivate('maintenance-mode'); // Check global state Flagged::globalActive('maintenance-mode');
Class-Based Features
For complex features, create a dedicated class:
use Illuminate\Database\Eloquent\Model; use OffloadProject\Flagged\Contracts\Feature; class BetaAccessFeature implements Feature { public function name(): string { return 'beta-access'; } public function resolve(?Model $scope = null): mixed { if (!$scope instanceof User) { return false; } return $scope->is_beta_tester || $scope->created_at->isAfter('2024-01-01'); } }
Register in your service provider:
Flagged::defineClass(new BetaAccessFeature());
Model Trait
Add the ChecksFeatures trait to your models for convenient feature checking:
use OffloadProject\Flagged\Concerns\ChecksFeatures; class User extends Authenticatable { use ChecksFeatures; }
Then use it directly on the model:
$user->hasFeature('premium-dashboard'); $user->missingFeature('premium-dashboard'); $user->featureValue('api-rate-limit'); $user->activeFeatures(); // All active features for this user // Conditional execution $user->whenFeature('beta-access', function () { // Only runs if feature is active }); $user->whenFeatureOr('premium', fn () => 'Premium content', fn () => 'Free content' );
Blade Directives
Flagged provides Blade directives for cleaner templates:
{{-- Check if feature is active --}} @feature("premium-dashboard") <x-premium-dashboard /> @else <x-basic-dashboard /> @endfeature {{-- Check with scope --}} @feature("premium", $user) Welcome, premium user! @endfeature {{-- Inverse check --}} @unlessfeature("maintenance-mode") <x-main-content /> @endfeature {{-- Output feature value --}} <p>Your API limit: @featurevalue("api-rate-limit", $user) requests/hour</p>
Available directives:
@feature('name')/@feature('name', $scope)- Check if feature is active@elsefeature('name')- Check alternate feature in else-if chain@unlessfeature('name')- Check if feature is inactive@endfeature- Close any feature block@featurevalue('name')/@featurevalue('name', $scope)- Output feature value
Query Helpers
// Get all defined feature names Flagged::defined(); // Get all active features for a scope Flagged::activeFor($user); // Check if a feature is defined Flagged::isDefined('some-feature'); // Get a feature definition Flagged::getDefinition('premium-dashboard'); // Get all stored features from database Flagged::all(); // Get all active stored features Flagged::allActive(); // Get all inactive stored features Flagged::allInactive();
Generating Features
Flagged provides a feature generator that integrates with Mandate's mandate:make:feature command:
php artisan mandate:make:feature ExportData
This creates a feature class at app/Features/ExportDataFeature.php:
<?php namespace App\Features; use Illuminate\Database\Eloquent\Model; use OffloadProject\Flagged\Contracts\Feature; use OffloadProject\Mandate\Attributes\Description; use OffloadProject\Mandate\Attributes\Label; #[Label('Export Data')] #[Description('Controls access to the export-data feature.')] class ExportDataFeature implements Feature { public function name(): string { return 'export-data'; } public function resolve(?Model $scope = null): mixed { // TODO: Implement your feature resolution logic return false; } }
Feature Attributes
Feature classes support Mandate's Label and Description attributes for UI display:
use OffloadProject\Mandate\Attributes\Label; use OffloadProject\Mandate\Attributes\Description; #[Label('Premium Export')] #[Description('Allows exporting data in premium formats like PDF and Excel')] class PremiumExportFeature implements Feature { // ... }
Syncing Features
Use the sync command to create database records from your feature classes:
# Sync all features in app/Features php artisan flagged:sync # Preview changes without modifying the database php artisan flagged:sync --dry-run # Sync from a custom path php artisan flagged:sync --path=app/CustomFeatures
The sync command:
- Creates database records for new feature classes
- Updates
labelanddescriptioncolumns from attributes (if columns exist) - Never deletes existing records (additive only)
To enable label/description syncing, run the optional metadata migration:
php artisan vendor:publish --tag=flagged-migrations php artisan migrate
Customizing the Stub
Publish the stub to customize generated feature classes:
php artisan vendor:publish --tag=flagged-stubs
This creates stubs/flagged.feature.stub which will be used for future feature generation.
Mandate Integration
Flagged implements Mandate's FeatureAccessHandler contract, enabling feature-scoped roles and permissions. When you
use a Feature model as a context in Mandate, Flagged handles the access checks.
// In Mandate, when checking permissions within a feature context: $user->hasPermission('edit', context: $feature); // Flagged will verify: // 1. The feature is globally active // 2. The user has access to the feature (via your resolution logic) // 3. Then Mandate checks the permission within that feature context
Configure the integration in config/flagged.php:
'mandate' => [ 'enabled' => true, 'context_model' => \OffloadProject\Flagged\Models\Feature::class, ],
Configuration
return [ // Enable or disable Flagged entirely 'enabled' => env('FLAGGED_ENABLED', true), // Default value for undefined features 'default' => false, // Cache settings 'cache' => [ 'enabled' => env('FLAGGED_CACHE_ENABLED', true), 'store' => env('FLAGGED_CACHE_STORE'), 'ttl' => env('FLAGGED_CACHE_TTL', 3600), 'prefix' => 'flagged_', ], // Global feature storage: 'database' or 'cache' 'global_storage' => env('FLAGGED_GLOBAL_STORAGE', 'database'), // Feature model for global flags 'model' => \OffloadProject\Flagged\Models\Feature::class, // Mandate integration 'mandate' => [ 'enabled' => true, 'context_model' => \OffloadProject\Flagged\Models\Feature::class, ], // Database table names 'tables' => [ 'features' => 'features', ], ];
Testing
composer test
License
The MIT License (MIT). Please see License File for more information.