Mastering Feature Flags with Laravel Pennant

· 3 min read

In modern development environments, deploying code to production frequently (CI/CD) is the standard. But how do you merge new feature code into the main branch without affecting users when that feature isn't ready? Or how do you enable a new feature only for a team of Beta Testers?

The answer is Feature Flags.

Laravel Pennant is a first-party package that makes managing Feature Flags extremely simple, lightweight, and "Laravel-way".

Why Feature Flags?

  1. Trunk-based Development: Developers merge code daily, hiding unfinished code behind a FALSE flag.
  2. Canary Releases: Enable new features for 1% of users to monitor for errors before releasing to 100%.
  3. A/B Testing: Show UI A to this group, B to that group to measure effectiveness.
  4. Kill Switch: Instantly turn off a buggy feature without needing to rollback code.

Installing Pennant

composer require laravel/pennant

php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate

By default, Pennant stores flags in the database. It also supports redis or array (for testing).

Defining Features

Typically, you define features in AppServiceProvider:

use Laravel\Pennant\Feature;
use App\Models\User;

public function boot(): void
{
    // Simple flag: on or off for everyone
    Feature::define('new-dashboard', fn (User $user) => match ($user->role) {
        'admin', 'beta-tester' => true,
        default => false,
    });

    // Lottery: Randomly for 10% of users
    Feature::define('site-redesign', fn (User $user) => \Laravel\Pennant\Feature::lottery(1 in: 10));
}

These features can also be defined using separate Classes if the logic is complex.

Usage in Code

In Blade View

The @feature helper makes view code very clean:

@feature('new-dashboard')
    <!-- New Interface -->
    <x-dashboard-v2 />
@else
    <!-- Old Interface -->
    <x-dashboard-v1 />
@endfeature

In Controller

public function index()
{
    if (Feature::active('new-dashboard')) {
        return view('dashboard.v2');
    }

    return view('dashboard.v1');
}

Middleware

Pennant provides middleware to protect routes:

Route::get('/analytics', AnalyticsController::class)
    ->middleware('features:view-analytics');

Class Based Features

For large features, it's better to use Classes to manage state.

php artisan pennant:feature NewBillingSystem

This Class will have a resolve method. The return value doesn't strictly have to be boolean. It can be string, number, or object (Rich Feature).

// app/Features/NewBillingSystem.php
public function resolve(User $user)
{
    return $user->isPremium() ? 'stripe' : 'paypal';
}

Usage:

$gateway = Feature::value(NewBillingSystem::class); // returns 'stripe' or 'paypal'

Testing with Pennant

A strong point of official packages is comprehensive test support.

public function test_it_shows_new_dashboard_when_enabled()
{
    Feature::activate('new-dashboard');

    $response = $this->get('/dashboard');

    $response->assertSee('Welcome to Dashboard V2');
}

Rollout Strategy

Pennant is extremely powerful when combined with business logic.

For example, you want to enable the AI Chat feature for:

  1. Internal Admins.
  2. "Enterprise" customers.
  3. Old users registered before 2024.

Instead of writing messy if/else checks everywhere, you gather it all in one place within Feature::define. Your codebase remains clean, maintainable, and most importantly, you can toggle that entire logic with a single line of code (or via UI if using Database Driver).

Conclusion

Laravel Pennant transforms Feature Flags management from a manual, error-prone task into a standardized process. If you are building a product with real users, consider adopting Pennant immediately to make your release process more confident.

Comments