Laravel Pennant
Introduction
Laravel Pennant is a simple and light-weight feature flag package - without the cruft. Feature flags enable you to incrementally roll out new application features with confidence, A/B test new interface designs, complement a trunk-based development strategy, and much more.
Installation
First, install Pennant into your project using the Composer package manager:
composer require laravel/pennant
Next, you should publish the Pennant configuration and migration files using the vendor:publish Artisan command:
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
Finally, you should run your application's database migrations. This will create a features table that Pennant uses to power its database driver:
php artisan migrate
Configuration
After publishing Pennant's assets, its configuration file will be located at config/pennant.php. This configuration file allows you to specify the default storage mechanism that will be used by Pennant to store resolved feature flag values.
Pennant includes support for storing resolved feature flag values in an in-memory array via the array driver. Or, Pennant can store resolved feature flag values persistently in a relational database via the database driver, which is the default storage mechanism used by Pennant.
Defining Features
To define a feature, you may use the define method offered by the Feature facade. You will need to provide a name for the feature, as well as a closure that will be invoked to resolve the feature's initial value.
Typically, features are defined in a service provider using the Feature facade. The closure will receive the "scope" for the feature check. Most commonly, the scope is the currently authenticated user. In this example, we will define a feature for incrementally rolling out a new API to our application's users:
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::define('new-api', fn (User $user) => match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});
}
}
As you can see, we have the following rules for our feature:
- All internal team members should be using the new API.
- Any high traffic customers should not be using the new API.
- Otherwise, the feature should be randomly assigned to users with a 1 in 100 chance of being active.
The first time the new-api feature is checked for a given user, the result of the closure will be stored by the storage driver. The next time the feature is checked against the same user, the value will be retrieved from storage and the closure will not be invoked.
For convenience, if a feature definition only returns a lottery, you may omit the closure completely:
Feature::define('site-redesign', Lottery::odds(1, 1000));
Class Based Features
Pennant also allows you to define class based features. Unlike closure based feature definitions, there is no need to register a class based feature in a service provider. To create a class based feature, you may invoke the pennant:feature Artisan command. By default the feature class will be placed in your application's app/Features directory:
php artisan pennant:feature NewApi
When writing a feature class, you only need to define a resolve method, which will be invoked to resolve the feature's initial value for a given scope. Again, the scope will typically be the currently authenticated user:
<?php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* Resolve the feature's initial value.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}
If you would like to manually resolve an instance of a class based feature, you may invoke the instance method on the Feature facade:
use Illuminate\Support\Facades\Feature;
$instance = Feature::instance(NewApi::class);
Feature classes are resolved via the container, so you may inject dependencies into the feature class's constructor when needed.
Customizing the Stored Feature Name
By default, Pennant will store the feature class's fully qualified class name. If you would like to decouple the stored feature name from the application's internal structure, you may specify a $name property on the feature class. The value of this property will be stored in place of the class name:
<?php
namespace App\Features;
class NewApi
{
/**
* The stored name of the feature.
*
* @var string
*/
public $name = 'new-api';
// ...
}
Checking Features
To determine if a feature is active, you may use the active method on the Feature facade. By default, features are checked against the currently authenticated user:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
return Feature::active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}
Although features are checked against the currently authenticated user by default, you may easily check the feature against another user or scope. To accomplish this, use the for method offered by the Feature facade:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
Pennant also offers some additional convenience methods that may prove useful when determining if a feature is active or not:
// Determine if all of the given features are active...
Feature::allAreActive(['new-api', 'site-redesign']);
// Determine if any of the given features are active...
Feature::someAreActive(['new-api', 'site-redesign']);
// Determine if a feature is inactive...
Feature::inactive('new-api');
// Determine if all of the given features are inactive...
Feature::allAreInactive(['new-api', 'site-redesign']);
// Determine if any of the given features are inactive...
Feature::someAreInactive(['new-api', 'site-redesign']);
When using Pennant outside of an HTTP context, such as in an Artisan command or a queued job, you should typically explicitly specify the feature's scope. Alternatively, you may define a default scope that accounts for both authenticated HTTP contexts and unauthenticated contexts.
Checking Class Based Features
For class based features, you should provide the class name when checking the feature:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
return Feature::active(NewApi::class)
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}
Conditional Execution
The when method may be used to fluently execute a given closure if a feature is active. Additionally, a second closure may be provided and will be executed if the feature is inactive:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
return Feature::when(NewApi::class,
fn () => $this->resolveNewApiResponse($request),
fn () => $this->resolveLegacyApiResponse($request),
);
}
// ...
}