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 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),
};
}
}
container, so you may inject dependencies into the feature class's constructor when needed.
Note: Feature classes are resolved via the
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']);
Note:
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),
);
}
// ...
}
The unless
method serves as the inverse of
the when
method, executing the first
closure if the feature is inactive:
return Feature::unless(NewApi::class,
fn () => $this->resolveLegacyApiResponse($request),
fn () => $this->resolveNewApiResponse($request),
);
The HasFeatures
Trait
Pennant's HasFeatures
trait may be added to
your application's User
model (or any other
model that has features) to provide a fluent, convenient
way to check features directly from the model:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;
class User extends Authenticatable
{
use HasFeatures;
// ...
}
Once the trait has been added to your model, you may
easily check features by invoking the
features
method:
if ($user->features()->active('new-api')) {
// ...
}
Of course, the features
method provides
access to many other convenient methods for interacting
with features:
// Values...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);
// State...
$user->features()->active('new-api');
$user->features()->allAreActive(['new-api', 'server-api']);
$user->features()->someAreActive(['new-api', 'server-api']);
$user->features()->inactive('new-api');
$user->features()->allAreInactive(['new-api', 'server-api']);
$user->features()->someAreInactive(['new-api', 'server-api']);
// Conditional execution...
$user->features()->when('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
$user->features()->unless('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
Blade Directive
To make checking features in Blade a seamless experience,
Pennant offers a @feature
directive:
@feature('site-redesign')
<!-- 'site-redesign' is active -->
@else
<!-- 'site-redesign' is inactive -->
@endfeature
Middleware
Pennant also includes a middleware that may be
used to verify the currently authenticated user has
access to a feature before a route is even invoked. You
may assign the middleware to a route and specify the
features that are required to access the route. If any
of the specified features are inactive for the currently
authenticated user, a 400 Bad Request
HTTP
response will be returned by the route. Multiple
features may be passed to the static using
method.
use Illuminate\Support\Facades\Route;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
Route::get('/api/servers', function () {
// ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));
Customizing the Response
If you would like to customize the response that is
returned by the middleware when one of the listed
features is inactive, you may use the
whenInactive
method provided by the
EnsureFeaturesAreActive
middleware.
Typically, this method should be invoked within the
boot
method of one of your application's
service providers:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
EnsureFeaturesAreActive::whenInactive(
function (Request $request, array $features) {
return new Response(status: 403);
}
);
// ...
}
In-Memory Cache
When checking a feature, Pennant will create an in-memory
cache of the result. If you are using the
database
driver, this means that
re-checking the same feature flag within a single
request will not trigger additional database queries.
This also ensures that the feature has a consistent
result for the duration of the request.
If you need to manually flush the in-memory cache, you
may use the flushCache
method offered by
the Feature
facade:
Feature::flushCache();
Scope
Specifying the Scope
As discussed, features are typically checked against the
currently authenticated user. However, this may not
always suit your needs. Therefore, it is possible to
specify the scope you would like to check a given
feature against via the Feature
facade's
for
method:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
Of course, feature scopes are not limited to "users". Imagine you have built a new billing experience that you are rolling out to entire teams rather than individual users. Perhaps you would like the oldest teams to have a slower rollout than the newer teams. Your feature resolution closure might look something like the following:
use App\Models\Team;
use Carbon\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
Feature::define('billing-v2', function (Team $team) {
if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
return true;
}
if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
return Lottery::odds(1 / 100);
}
return Lottery::odds(1 / 1000);
});
You will notice that the closure we have defined is not
expecting a User
, but is instead expecting
a Team
model. To determine if this feature
is active for a user's team, you should pass the team to
the for
method offered by the
Feature
facade:
if (Feature::for($user->team)->active('billing-v2')) {
return redirect()->to('/billing/v2');
}
// ...
Default Scope
It is also possible to customize the default scope
Pennant uses to check features. For example, maybe all
of your features are checked against the currently
authenticated user's team instead of the user. Instead
of having to call
Feature::for($user->team)
every time you
check a feature, you may instead specify the team as the
default scope. Typically, this should be done in one of
your application's service providers:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
// ...
}
}
If no scope is explicitly provided via the
for
method, the feature check will now use
the currently authenticated user's team as the default
scope:
Feature::active('billing-v2');
// Is now equivalent to...
Feature::for($user->team)->active('billing-v2');
Nullable Scope
If the scope you provide when checking a feature is
null
and the feature's definition does not
support null
via a nullable type or by
including null
in a union type, Pennant
will automatically return false
as the
feature's result value.
So, if the scope you are passing to a feature is
potentially null
and you want the feature's
value resolver to be invoked, you should account for
that in your feature's definition. A null
scope may occur if you check a feature within an Artisan
command, queued job, or unauthenticated route. Since
there is usually not an authenticated user in these
contexts, the default scope will be
null
.
If you do not always explicitly specify your
feature scope then you should ensure the scope's
type is "nullable" and handle the
null
scope value within your feature
definition logic:
use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
Feature::define('new-api', fn (User $user) => match (true) {// [tl! remove]
Feature::define('new-api', fn (User|null $user) => match (true) {// [tl! add]
$user === null => true,// [tl! add]
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});
Identifying Scope
Pennant's built-in array
and
database
storage drivers know how to
properly store scope identifiers for all PHP data types
as well as Eloquent models. However, if your application
utilizes a third-party Pennant driver, that driver may
not know how to properly store an identifier for an
Eloquent model or other custom types in your
application.
In light of this, Pennant allows you to format scope
values for storage by implementing the
FeatureScopeable
contract on the objects in
your application that are used as Pennant scopes.
For example, imagine you are using two different feature
drivers in a single application: the built-in
database
driver and a third-party
"Flag Rocket" driver. The "Flag
Rocket" driver does not know how to properly store
an Eloquent model. Instead, it requires a
FlagRocketUser
instance. By implementing
the toFeatureIdentifier
defined by the
FeatureScopeable
contract, we can customize
the storable scope value provided to each driver used by
our application:
<?php
namespace App\Models;
use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;
class User extends Model implements FeatureScopeable
{
/**
* Cast the object to a feature scope identifier for the given driver.
*/
public function toFeatureIdentifier(string $driver): mixed
{
return match($driver) {
'database' => $this,
'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
};
}
}
Serializing Scope
By default, Pennant will use a fully qualified class name when storing a feature associated with an Eloquent model. If you are already using an Eloquent morph map, you may choose to have Pennant also use the morph map to decouple the stored feature from your application structure.
To achieve this, after defining your Eloquent morph map
in a service provider, you may invoke the
Feature
facade's useMorphMap
method:
use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
Feature::useMorphMap();
Rich Feature Values
Until now, we have primarily shown features as being in a binary state, meaning they are either "active" or "inactive", but Pennant also allows you to store rich values as well.
For example, imagine you are testing three new colors for
the "Buy now" button of your application.
Instead of returning true
or
false
from the feature definition, you may
instead return a string:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn (User $user) => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));
You may retrieve the value of the
purchase-button
feature using the
value
method:
$color = Feature::value('purchase-button');
Pennant's included Blade directive also makes it easy to conditionally render content based on the current value of the feature:
@feature('purchase-button', 'blue-sapphire')
<!-- 'blue-sapphire' is active -->
@elsefeature('purchase-button', 'seafoam-green')
<!-- 'seafoam-green' is active -->
@elsefeature('purchase-button', 'tart-orange')
<!-- 'tart-orange' is active -->
@endfeature
Note: When using rich values, it is important to know that a feature is considered "active" when it has any value other than
false
.
When calling the conditional
when
method, the feature's rich
value will be provided to the first closure:
Feature::when('purchase-button',
fn ($color) => /* ... */,
fn () => /* ... */,
);
Likewise, when calling the conditional
unless
method, the feature's rich value
will be provided to the optional second closure:
Feature::unless('purchase-button',
fn () => /* ... */,
fn ($color) => /* ... */,
);
Retrieving Multiple Features
The values
method allows the retrieval of
multiple features for a given scope:
Feature::values(['billing-v2', 'purchase-button']);
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// ]
Or, you may use the all
method to retrieve
the values of all defined features for a given
scope:
Feature::all();
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]
However, class based features are dynamically registered
and are not known by Pennant until they are explicitly
checked. This means your application's class based
features may not appear in the results returned by the
all
method if they have not already been
checked during the current request.
If you would like to ensure that feature classes are
always included when using the all
method,
you may use Pennant's feature discovery capabilities. To
get started, invoke the discover
method in
one of your application's service providers:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::discover();
// ...
}
}
The discover
method will register all of the
feature classes in your application's
app/Features
directory. The
all
method will now include these classes
in its results, regardless of whether they have been
checked during the current request:
Feature::all();
// [
// 'App\Features\NewApi' => true,
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]
Eager Loading
Although Pennant keeps an in-memory cache of all resolved features for a single request, it is still possible to encounter performance issues. To alleviate this, Pennant offers the ability to eager load feature values.
To illustrate this, imagine that we are checking if a feature is active within a loop:
use Laravel\Pennant\Feature;
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
Assuming we are using the database driver, this code will
execute a database query for every user in the loop -
executing potentially hundreds of queries. However,
using Pennant's load
method, we can remove
this potential performance bottleneck by eager loading
the feature values for a collection of users or
scopes:
Feature::for($users)->load(['notifications-beta']);
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
To load feature values only when they have not already
been loaded, you may use the loadMissing
method:
Feature::for($users)->loadMissing([
'new-api',
'purchase-button',
'notifications-beta',
]);
Updating Values
When a feature's value is resolved for the first time, the underlying driver will store the result in storage. This is often necessary to ensure a consistent experience for your users across requests. However, at times, you may want to manually update the feature's stored value.
To accomplish this, you may use the activate
and deactivate
methods to toggle a feature
"on" or "off":
use Laravel\Pennant\Feature;
// Activate the feature for the default scope...
Feature::activate('new-api');
// Deactivate the feature for the given scope...
Feature::for($user->team)->deactivate('billing-v2');
It is also possible to manually set a rich value for a
feature by providing a second argument to the
activate
method:
Feature::activate('purchase-button', 'seafoam-green');
To instruct Pennant to forget the stored value for a
feature, you may use the forget
method.
When the feature is checked again, Pennant will resolve
the feature's value from its feature definition:
Feature::forget('purchase-button');
Bulk Updates
To update stored feature values in bulk, you may use the
activateForEveryone
and
deactivateForEveryone
methods.
For example, imagine you are now confident in the
new-api
feature's stability and have landed
on the best 'purchase-button'
color for
your checkout flow - you can update the stored value for
all users accordingly:
use Laravel\Pennant\Feature;
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
Alternatively, you may deactivate the feature for all users:
Feature::deactivateForEveryone('new-api');
Note: This will only update the resolved feature values that have been stored by Pennant's storage driver. You will also need to update the feature definition in your application.
Purging Features
Sometimes, it can be useful to purge an entire feature from storage. This is typically necessary if you have removed the feature from your application or you have made adjustments to the feature's definition that you would like to rollout to all users.
You may remove all stored values for a feature using the
purge
method:
// Purging a single feature...
Feature::purge('new-api');
// Purging multiple features...
Feature::purge(['new-api', 'purchase-button']);
If you would like to purge all features from
storage, you may invoke the purge
method
without any arguments:
Feature::purge();
As it can be useful to purge features as part of your
application's deployment pipeline, Pennant includes a
pennant:purge
Artisan command which will
purge the provided features from storage:
php artisan pennant:purge new-api
php artisan pennant:purge new-api purchase-button
It is also possible to purge all features except
those in a given feature list. For example, imagine you
wanted to purge all features but keep the values for the
"new-api" and "purchase-button"
features in storage. To accomplish this, you can pass
those feature names to the --except
option:
php artisan pennant:purge --except=new-api --except=purchase-button
For convenience, the pennant:purge
command
also supports an --except-registered
flag.
This flag indicates that all features except those
explicitly registered in a service provider should be
purged:
php artisan pennant:purge --except-registered
Testing
When testing code that interacts with feature flags, the easiest way to control the feature flag's returned value in your tests is to simply re-define the feature. For example, imagine you have the following feature defined in one of your application's service provider:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn () => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));
To modify the feature's returned value in your tests, you
may re-define the feature at the beginning of the test.
The following test will always pass, even though the
Arr::random()
implementation is still
present in the service provider:
use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define('purchase-button', 'seafoam-green');
$this->assertSame('seafoam-green', Feature::value('purchase-button'));
}
The same approach may be used for class based features:
use App\Features\NewApi;
use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define(NewApi::class, true);
$this->assertTrue(Feature::value(NewApi::class));
}
If your feature is returning a Lottery
instance, there are a handful of useful testing
helpers available.
Store Configuration
You may configure the store that Pennant will use during
testing by defining the PENNANT_STORE
environment variable in your application's
phpunit.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<!-- ... -->
<php>
<env name="PENNANT_STORE" value="array"/>
<!-- ... -->
</php>
</phpunit>
Adding Custom Pennant Drivers
Implementing the Driver
If none of Pennant's existing storage drivers fit your
application's needs, you may write your own storage
driver. Your custom driver should implement the
Laravel\Pennant\Contracts\Driver
interface:
<?php
namespace App\Extensions;
use Laravel\Pennant\Contracts\Driver;
class RedisFeatureDriver implements Driver
{
public function define(string $feature, callable $resolver): void {}
public function defined(): array {}
public function getAll(array $features): array {}
public function get(string $feature, mixed $scope): mixed {}
public function set(string $feature, mixed $scope, mixed $value): void {}
public function setForAllScopes(string $feature, mixed $value): void {}
public function delete(string $feature, mixed $scope): void {}
public function purge(array|null $features): void {}
}
Now, we just need to implement each of these methods
using a Redis connection. For an example of how to
implement each of these methods, take a look at the
Laravel\Pennant\Drivers\DatabaseDriver
in
the Pennant
source code
Note:
Laravel does not ship with a directory to contain your extensions. You are free to place them anywhere you like. In this example, we have created anExtensions
directory to house theRedisFeatureDriver
.
Registering the Driver
Once your driver has been implemented, you are ready to
register it with Laravel. To add additional drivers to
Pennant, you may use the extend
method
provided by the Feature
facade. You should
call the extend
method from the
boot
method of one of your application's service provider:
<?php
namespace App\Providers;
use App\Extensions\RedisFeatureDriver;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// ...
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::extend('redis', function (Application $app) {
return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
}
}
Once the driver has been registered, you may use the
redis
driver in your application's
config/pennant.php
configuration file:
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => null,
],
// ...
],
Events
Pennant dispatches a variety of events that can be useful when tracking feature flags throughout your application.
Laravel\Pennant\Events\RetrievingKnownFeature
This event is dispatched the first time a known feature is retrieved during a request for a specific scope. This event can be useful to create and track metrics against the feature flags that are being used throughout your application.
Laravel\Pennant\Events\RetrievingUnknownFeature
This event is dispatched the first time an unknown feature is retrieved during a request for a specific scope. This event can be useful if you have intended to remove a feature flag, but may have accidentally left some stray references to it throughout your application.
For example, you may find it useful to listen for this
event and report
or throw an exception when
it occurs:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
use Laravel\Pennant\Events\RetrievingUnknownFeature;
class EventServiceProvider extends ServiceProvider
{
/**
* Register any other events for your application.
*/
public function boot(): void
{
Event::listen(function (RetrievingUnknownFeature $event) {
report("Resolving unknown feature [{$event->feature}].");
});
}
}
Laravel\Pennant\Events\DynamicallyDefiningFeature
This event is dispatched when a class based feature is being dynamically checked for the first time during a request.