Laravel Pennant Feature Flags

Laravel Tutorials

Deployment and Maintenance

Laravel Pennant provides feature flags for Laravel applications. A feature flag lets you release code without making it available to every user immediately. You can test a feature with an internal team, enable it for selected accounts, run a gradual rollout, and disable it quickly if a problem appears.

This tutorial follows Laravel 13 and the current Pennant package. Pennant stores resolved feature values so each user receives a consistent result across requests.

Install Laravel Pennant

Install the package with Composer, publish its configuration and migration, and create the database table used by the default driver.

Example:

# Install the official feature flag package.
composer require laravel/pennant

# Publish config/pennant.php and the database migration.
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"

# Create the features table.
php artisan migrate

The default database driver stores resolved values persistently. The array driver stores values in memory and is useful during automated tests.

Define a Feature Flag

Define simple flags in a service provider with the Feature facade. The resolver receives the current scope, which is usually the authenticated user.

Example:

<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Feature::define('new-dashboard', function (User $user): bool {
            // Give staff members early access.
            return $user->is_staff;
        });
    }
}

When Pennant checks this flag for a user for the first time, it resolves and stores the result. Later checks reuse that stored value instead of running the resolver again.

Check a Feature in Application Code

Use Feature::active() when the flag should use the default scope.

Example:

use Laravel\Pennant\Feature;

if (Feature::active('new-dashboard')) {
    // Return the new experience for eligible users.
    return view('dashboard.new');
}

return view('dashboard.current');

Use Feature::for() when you need to check the flag for a specific user or another scope.

Example:

$enabled = Feature::for($user)->active('new-dashboard');

// The result belongs to the supplied user scope.
if ($enabled) {
    $user->notify(new DashboardPreviewAvailable());
}

Use Feature Flags in Blade

The @feature directive keeps conditional interface code readable.

Example:

@feature('new-dashboard')
    {{-- Render the new navigation for enabled users. --}}
    @include('dashboard.partials.new-navigation')
@else
    @include('dashboard.partials.current-navigation')
@endfeature

Create a Class-Based Feature

Class-based flags are useful when rollout rules need several conditions or injected dependencies. Generate a feature class with Artisan.

Example:

# Create app/Features/InvoiceExport.php.
php artisan pennant:feature InvoiceExport

Add a resolve() method that returns the initial value.

Example:

<?php

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class InvoiceExport
{
    public function resolve(User $user): bool|Lottery
    {
        // Administrators always receive the feature.
        if ($user->is_admin) {
            return true;
        }

        // Roll it out consistently to approximately 10% of users.
        return Lottery::odds(1, 10);
    }
}

Check a class-based flag by passing its class name.

Example:

use App\Features\InvoiceExport;
use Laravel\Pennant\Feature;

if (Feature::active(InvoiceExport::class)) {
    // Display the export control.
}

Store Rich Feature Values

A flag can return more than true or false. Rich values work well for controlled interface variants or configuration choices.

Example:

Feature::define('checkout-layout', function (User $user): string {
    // Assign a stable layout according to the user's account group.
    return $user->account_group === 'business' ? 'compact' : 'guided';
});

$layout = Feature::value('checkout-layout');

Keep rich values small and predictable. Store large or sensitive configuration in the application rather than inside a feature value.

Activate and Deactivate Features

You can override stored values for a specific scope without changing the feature definition.

Example:

// Enable the feature for Priya.
Feature::for($priya)->activate('new-dashboard');

// Disable it for Oliver while an issue is investigated.
Feature::for($oliver)->deactivate('new-dashboard');

// Remove the stored decision so Pennant resolves it again.
Feature::for($oliver)->forget('new-dashboard');

For a completed rollout, update the definition and then use the bulk activation or purge operations deliberately. A temporary flag should not remain in the codebase forever.

Avoid Repeated Database Queries

Pennant caches checked values in memory for the current request. When processing a collection, eager-load the required flags to avoid repeated lookups.

Example:

// Load the flag values before iterating over users.
Feature::for($users)->load(['new-dashboard']);

foreach ($users as $user) {
    if (Feature::for($user)->active('new-dashboard')) {
        // Perform work only for enabled users.
    }
}

Test Code Behind a Feature Flag

Redefine the flag in a test so the enabled and disabled paths remain deterministic.

Example:

use Laravel\Pennant\Feature;

public function test_staff_can_open_the_new_dashboard(): void
{
    // Force the flag on for this test.
    Feature::define('new-dashboard', true);

    $response = $this->actingAs(User::factory()->create())
        ->get('/dashboard');

    $response->assertOk()
        ->assertViewIs('dashboard.new');
}

Tip: Test both flag states. A safe rollout needs confidence in the current path as well as the new path.

Feature Flag Best Practices

  • Give each flag a clear, stable name that describes the capability.
  • Define who owns the rollout and when the flag should be removed.
  • Keep authorization checks separate; a feature flag is not a security boundary.
  • Monitor errors and business results while increasing exposure.
  • Avoid deeply nested flags that make application behavior difficult to predict.
  • Remove the old branch and purge stored values after the rollout is complete.

Conclusion

Laravel Pennant gives Laravel applications a consistent way to define, check, store, and test feature flags. Start with a small resolver, scope it to the correct user or account, test both outcomes, and remove temporary flags after rollout. This keeps releases controlled without turning flags into permanent complexity.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram