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

v1.0.0 2026-01-10 06:16 UTC

This package is auto-updated.

Last update: 2026-01-10 06:27:10 UTC


README

Latest Version on Packagist GitHub Tests Action Status Total Downloads

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 ChecksFeatures to 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, @featurevalue for cleaner templates
  • Caching - Built-in caching for global features and in-memory resolution cache
  • Mandate Integration - Implements FeatureAccessHandler for feature-scoped roles and permissions
  • Feature Generator - Generate feature classes via mandate:make:feature command
  • Feature Syncing - Sync feature classes to database with flagged:sync command
  • Feature Attributes - Support for Label and Description attributes 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 label and description columns 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.