How to make Laravel Pennant features that apply globally
Tags: laravel, php, feature, flags
Published: 2023-05-05 11:54:43

---
title: How to make Laravel Pennant features that apply globally
published: true
description: 
tags: laravel, php, feature, flags
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6iugo2unb2ru9j6kagn5.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-04-19 11:47 +0000
canonical_url: https://elliotderhay.com/blog/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](https://laravel.com/docs/10.x/pennant#defining-features "‌") 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
<?php
Feature::define('new-feature', fn (User $user) => match (true) {
  $user->isBetaTester() => true,
  default => false,
});
```

_(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:

```text
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
```

You can also define features in a service provider, or as dedicated [feature classes](https://laravel.com/docs/10.x/pennant#class-based-features "‌").

```php
<?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,
        };
    }
}
```

_(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:

```text
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
```

## 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](https://laravel.com/docs/10.x/pennant#specifying-the-scope "‌") 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
<?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);
```

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

```php
<?php
Feature::for(null)->active();
```

That technically `resolve`s 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](https://laravel.com/docs/10.x/pennant#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
<?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);
    }
}
```

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

Now features will have entries like this in the DB:

```text
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
```

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”](https://laravel.com/docs/10.x/pennant#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!**