DEV Community

Cover image for How to make Laravel Pennant features that apply globally
Elliot Derhay
Elliot Derhay

Posted on • Originally published at elliotderhay.com

How to make Laravel Pennant features that apply globally

If you use feature flags, Laravel Pennant probably caught your attention in the Laravel 10 announcement. This was the part of upgrading to Laravel 10 that I was looking forward to the most.

What is Laravel Pennant?

Pennant is a package that provides standardized feature flags out of the box. Features are saved in a dedicated table features, along with their scope(s) and status.

Here’s a simple example of how to define a Pennant feature:

<?php
Feature::define('new-feature', fn (User $user) => match (true) {
  $user->isBetaTester() => true,
  default => false,
});
Enter fullscreen mode Exit fullscreen mode

(This is a simpler version of a snippet from the Pennant docs.)

Now if you were to check ‘new-feature' like Feature::active('new-feature'), you might find an entry like this in the database:

id | name        | scope             | value | created_at          | updated_at
 1 | new-feature | App\Models\User:1 | true  | 2023-01-01 00:00:00 | 2023-01-01 00:00:00
Enter fullscreen mode Exit fullscreen mode

You can also define features in a service provider, or as dedicated feature classes.

<?php

namespace App\Features;

class NewFeature
{
    /**
      * Resolve the feature's initial value.
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isBetaTester() => true,
            default => false,
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

(This is a simpler version of a snippet from the Pennant docs.)

If you checked or updated a class-based feature, you might find an entry like this in the features table instead:

id | name                    | scope             | value | created_at          | updated_at
 1 | App\Features\NewFeature | App\Models\User:1 | true  | 2023-01-01 00:00:00 | 2023-01-01 00:00:00
Enter fullscreen mode Exit fullscreen mode

How it works

The value returned in both examples is the default value. You decide the default based on whatever conditions you want. After it’s initialized, the table is checked instead.

As you can see, this example feature is based on a User class. Pennant’s doc explains that Pennant, by default, scopes features to the authenticated user.

This means if you check a feature like Feature::active('new-feature'), Pennant will automatically use a User object to check or act on a feature. If no matching entry for that feature and User scope is found in the DB, a new one will be saved based on the feature’s defaults.

Changing a feature’s scope

The Pennant docs give an example on how to change the scope. Basically, you type the parameter for default resolution to something else (say Team instead of User), and then use Feature::for() to pass the scope value.

If there were no default scope, you’d do Feature::for($user)->active('new-feature') or something similar every time.

So how do we make a feature global?

By “global”, I mean on or off for everyone.

You probably don’t want to do this too much on larger projects. But if you have a small public project, you’ll probably use it more.

The simplest way is to make your feature expect null.

<?php
// I use `false` as an example only. You can default your
// features to `true` (active) if you want.
Feature::define('new-feature', fn (null $scope) => false);
Enter fullscreen mode Exit fullscreen mode

Then when you check features, you will pass null explicitly:

<?php
Feature::for(null)->active();
Enter fullscreen mode Exit fullscreen mode

That technically resolves this question, but what if most of your features will be “global”?

Defaulting to “global scope”

Pennant’s docs have a section on setting the default scope. This is something you call in a service provider, like in the AppServiceProvider.

In our case, we can write something like this:

<?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) => null);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now anytime we don’t call Feature::for(), null will be passed automatically.

Now features will have entries like this in the DB:

id | name        | scope          | value | created_at          | updated_at
 1 | new-feature | __laravel_null | true  | 2023-01-01 00:00:00 | 2023-01-01 00:00:00
Enter fullscreen mode Exit fullscreen mode

When null is used in the scope, Pennant saves __laravel_null as the scope.

Incidentally, if you were to test or update a feature using something like a command or queued job, this is also the scope. The docs aren’t explicit about this, but at the bottom of the “Checking Features” section of the doc, it implies this type of behavior is because it’s running in an unauthenticated context—meaning, there’s no auth’d user interacting with the application.

Summary

With a little digging, we were able to create features that apply globally instead of being scoped to specific contexts and even learn a little extra about Pennant that’s not spelled out in the docs.

I hope you found this post useful.

Thanks for reading!

Top comments (0)