PHPackages                             henzeb/laravel-pennant-unleash - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. henzeb/laravel-pennant-unleash

ActiveLibrary

henzeb/laravel-pennant-unleash
==============================

An Unleash driver for Laravel Pennant

v0.4-beta(1mo ago)00MITPHPPHP ^8.3CI passing

Since Jul 2Pushed 1mo agoCompare

[ Source](https://github.com/henzeb/laravel-pennant-unleash)[ Packagist](https://packagist.org/packages/henzeb/laravel-pennant-unleash)[ Docs](https://github.com/henzeb/laravel-pennant-unleash)[ RSS](/packages/henzeb-laravel-pennant-unleash/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (4)Dependencies (14)Versions (5)Used By (0)

Unleash driver for Laravel Pennant
==================================

[](#unleash-driver-for-laravel-pennant)

[![Build Status](https://github.com/henzeb/laravel-pennant-unleash/workflows/tests/badge.svg)](https://github.com/henzeb/laravel-pennant-unleash/actions)[![Latest Version on Packagist](https://camo.githubusercontent.com/a8b2bdc9177b0969be5eb2c4ac90f86fd094e2e107bb03ca697ddb631a16191e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f68656e7a65622f6c61726176656c2d70656e6e616e742d756e6c656173682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/henzeb/laravel-pennant-unleash)[![Total Downloads](https://camo.githubusercontent.com/ca2aaeffdc590bcd87b8f22f9a7cfe99ce6b64d7190186ba0a1371ddfaa51496/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f68656e7a65622f6c61726176656c2d70656e6e616e742d756e6c656173682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/henzeb/laravel-pennant-unleash)[![License](https://camo.githubusercontent.com/ba0e39878a05cc9e458fe2157dd91b8464a88c8c6cc4f7d763bf2799216dfa56/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f68656e7a65622f6c61726176656c2d70656e6e616e742d756e6c65617368)](https://packagist.org/packages/henzeb/laravel-pennant-unleash)

[Laravel Pennant](https://laravel.com/docs/master/pennant) is a lightweight feature flag package, but its built-in drivers store state locally. [Unleash](https://www.getunleash.io/) is a feature management platform that evaluates flags server-side with rich targeting strategies.

This package bridges the two: it registers an `unleash` Pennant driver so you can use the full Pennant API while Unleash handles all flag evaluation.

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
    - [Custom client builder](#custom-client-builder)
- [Configuration](#configuration)
    - [Caching](#caching)
    - [Custom strategies](#custom-strategies)
    - [Events](#events)
    - [Metrics reporting](#metrics-reporting)
    - [Variant handler](#variant-handler)
- [Context](#context)
    - [Authenticated users](#authenticated-users)
    - [Eloquent models](#eloquent-models)
    - [Plain strings](#plain-strings)
    - [UnleashContext](#unleashcontext)
    - [FeatureScopeable](#featurescopeable)
    - [Custom context resolver](#custom-context-resolver)
- [Variants](#variants)
- [Defining local defaults](#defining-local-defaults)
- [Development mode](#development-mode)
- [Testing this package](#testing-this-package)
- [Security](#security)
- [Credits](#credits)
- [License](#license)

Installation
------------

[](#installation)

```
composer require henzeb/laravel-pennant-unleash
```

Publish the config file:

```
php artisan vendor:publish --tag=laravel-pennant-unleash
```

To use Unleash with Pennant, you must add an `unleash` entry in the `stores`section of `config/pennant.php`, equivalent to adding this yourself:

```
'stores' => [
    'unleash' => [
        'driver' => 'unleash',
    ],

    // Pennant's built-in stores, e.g.:
    'array' => [
        'driver' => 'array',
    ],
],
```

In your `.env`, add the following. Use your own credentials, of course.

```
UNLEASH_URL=https://your-unleash-instance/api
UNLEASH_API_KEY=your-client-api-key
UNLEASH_INSTANCE_ID=your-instance-id
UNLEASH_APP_NAME=your-app-name        # defaults to APP_NAME
UNLEASH_CACHE_DRIVER=array            # defaults to CACHE_DRIVER
```

### Custom client builder

[](#custom-client-builder)

If you have specific needs for the client builder, you can use `Feature::buildUnleashClientUsing()` to customize the UnleashBuilder.

```
use Unleash\Client\UnleashBuilder;

Feature::buildUnleashClientUsing(function (UnleashBuilder $builder): UnleashBuilder {
    return $builder->withStrategy(new MyCustomStrategy());
});
```

Or return a completely new `UnleashBuilder` instance to bypass the package's own configuration:

```
use Unleash\Client\UnleashBuilder;

Feature::buildUnleashClientUsing(function (UnleashBuilder $builder): UnleashBuilder {
    return UnleashBuilder::create()
        ->withAppUrl(config('unleash.app_url'))
        ->withInstanceId(config('unleash.instance_id'))
        ->withAppName(config('unleash.app_name'));
});
```

Configuration
-------------

[](#configuration)

### Caching

[](#caching)

Toggle state fetched from Unleash is cached, and refreshed once the cache expires. Configure this in `config/unleash.php`:

```
'cache' => [
    'driver' => env('UNLEASH_CACHE_DRIVER', env('CACHE_DRIVER')),
    'ttl' => env('UNLEASH_CACHE_TTL', 15), // seconds

    'stale_driver' => env('UNLEASH_STALE_CACHE_DRIVER'),
    'stale_ttl' => env('UNLEASH_STALE_CACHE_TTL', 30 * 60), // seconds
],
```

`stale_driver` is optional and only used as a fallback when Unleash can't be reached and the main cache has already expired; it defaults to the same store as `driver`. `stale_ttl` controls how long that stale cache remains acceptable to serve.

### Custom strategies

[](#custom-strategies)

If you have [custom activation strategies](https://docs.getunleash.io/reference/custom-activation-strategies), list their handler classes in `config/unleash.php`:

```
'strategies' => [
    App\Unleash\Strategies\MyCustomStrategy::class,
],
```

Each class is resolved through Laravel's container, so constructor dependencies are injected automatically, and is added alongside the client's built-in strategy handlers.

### Events

[](#events)

The underlying Unleash client fires its own events for things Pennant itself doesn't tell you about — a feature being queried that doesn't exist in Unleash, a feature evaluating as disabled, a strategy Unleash sent that no registered handler understands, a background fetch failing, or impression data being recorded. This package maps each of those to a plain Laravel event, so you can listen for them the normal way:

```
use Henzeb\Pennant\Unleash\Events\FeatureToggleNotFound;
use Illuminate\Support\Facades\Event;

Event::listen(function (FeatureToggleNotFound $event) {
    Log::warning("Feature [{$event->featureName}] was checked but does not exist in Unleash.");
});
```

EventFired whenProperties`FeatureToggleNotFound`A queried feature doesn't exist in Unleash`context`, `featureName``FeatureToggleDisabled`A feature exists but evaluated to disabled`feature`, `context``FeatureToggleMissingStrategyHandler`A feature has a strategy no registered handler supports`context`, `feature``FetchingDataFailed`The background fetch from the Unleash server failed`exception``ImpressionDataReceived`A feature with [impression data](https://docs.getunleash.io/reference/impression-data) enabled was evaluated`eventType`, `eventId`, `context`, `enabled`, `featureName`, `variant`All classes live under `Henzeb\Pennant\Unleash\Events`. This is off by default; enable it in `config/unleash.php`:

```
'events' => env('UNLEASH_EVENTS_ENABLED', false),
```

### Metrics reporting

[](#metrics-reporting)

By default, the client reports feature usage back to Unleash every 60 seconds, which drives the "last seen" and usage metrics in the Unleash admin UI. Configure this in `config/unleash.php`:

```
'metrics' => [
    'enabled' => env('UNLEASH_METRICS_ENABLED', true),
    'interval' => env('UNLEASH_METRICS_INTERVAL', 60_000), // milliseconds
    'handler' => DefaultMetricsHandler::class,
],
```

`handler` must be a class implementing `Unleash\Client\Metrics\MetricsHandler`. It's resolved through Laravel's container, so constructor dependencies are injected automatically. Swap it for your own class to replace the client's default metrics handler.

### Variant handler

[](#variant-handler)

Variant assignment (which variant a user falls into for a feature with variants configured) is delegated to a handler. Configure it in `config/unleash.php`:

```
'variant_handler' => DefaultVariantHandler::class,
```

`variant_handler` must be a class implementing `Unleash\Client\Variant\VariantHandler`. It's resolved through Laravel's container, so constructor dependencies are injected automatically. Swap it for your own class to replace the client's default variant handler.

Context
-------

[](#context)

The scope passed to Pennant is turned into an `UnleashContext`, so it can be matched by strategy constraints in the Unleash admin UI.

### Authenticated users

[](#authenticated-users)

Pass a user (or anything implementing `Illuminate\Contracts\Auth\Authenticatable`) and the driver sends the auth identifier as the Unleash `currentUserId`. In Unleash, target these with a **userId** constraint.

```
Feature::for($user)->active('my-feature');
```

### Eloquent models

[](#eloquent-models)

Pass an Eloquent model and the driver sends its class (or morph map alias, if configured) and key as custom context properties. In Unleash, target these with **model** and **key** constraints.

```
Feature::for($tenant)->active('my-feature');
```

For example, `$tenant = App\Models\Tenant::find(42)` sends:

```
[
    'model' => 'App\Models\Tenant', // or the morph map alias, e.g. 'tenant'
    'key' => '42',
]
```

so an Unleash strategy constraint on `model` with value `App\Models\Tenant` (or your morph map alias) combined with a `key` constraint on `42` will match this scope.

### Plain strings

[](#plain-strings)

Pass a plain string and the driver sends it as a custom context property. In Unleash, target these with a **scope** constraint.

```
Feature::for('some-identifier')->active('my-feature');
```

For example, `Feature::for('some-identifier')` sends:

```
[
    'scope' => 'some-identifier',
]
```

so an Unleash strategy constraint on `scope` with value `some-identifier` will match this scope.

### UnleashContext

[](#unleashcontext)

`Henzeb\Pennant\Unleash\Configuration\UnleashContext` is a Pennant-aware context object you can construct and pass as the scope directly, to set any Unleash context field (`userId`, `sessionId`, `ipAddress`, `environment`, `currentTime`, custom properties) yourself.

```
use Henzeb\Pennant\Unleash\Configuration\UnleashContext;

$context = new UnleashContext(
    currentUserId: '42',
    ipAddress: '1.2.3.4',
    sessionId: 'abc123',
    customContext: ['region' => 'eu-west'],
);

Feature::for($context)->active('my-feature');
```

Custom properties can also be set in bulk, from an array or anything implementing `Illuminate\Contracts\Support\Arrayable`, using `setCustomProperties()` (replacing them) or `withCustomProperties()`(merging into the existing ones):

```
$context = UnleashContext::make()->withCustomProperties(['plan' => 'enterprise', 'region' => 'eu-west']);
```

It also supports Laravel's `Conditionable` trait:

```
$context = UnleashContext::make(currentUserId: '42')
    ->when($request->has('region'), fn($ctx) => $ctx->setCustomProperty('region', $request->region));
```

### FeatureScopeable

[](#featurescopeable)

Any object can become scopable by implementing `Laravel\Pennant\Contracts\FeatureScopeable`. Pennant calls `toFeatureIdentifier` before passing the scope to the driver, so the driver receives whatever you return from it. Return an `UnleashContext` (or any `Unleash\Client\Configuration\Context`) to have it used for flag evaluation.

This is the right approach when you have domain objects — such as a `Tenant` or `Team` — that you want to pass directly as a scope without registering a global context resolver.

```
use Henzeb\Pennant\Unleash\Configuration\UnleashContext;
use Laravel\Pennant\Contracts\FeatureScopeable;
use Unleash\Client\Configuration\Context;

class Tenant implements FeatureScopeable
{
    public function toFeatureIdentifier(string $driver): mixed
    {
        return match ($driver) {
            'unleash' => UnleashContext::make(customContext: ['tenantId' => (string) $this->id]),
            default   => $this->id,
        };
    }
}

Feature::for($tenant)->active('my-feature');
```

### Custom context resolver

[](#custom-context-resolver)

If you need to map an arbitrary scope to an Unleash context, register a resolver in a service provider:

```
use Henzeb\Pennant\Unleash\Configuration\UnleashContext;
use Unleash\Client\Configuration\Context;

Feature::resolveUnleashContextUsing(function (mixed $scope): ?Context {
    if ($scope instanceof Tenant) {
        return UnleashContext::make(customContext: ['tenantId' => $scope->id]);
    }

    return null;
});
```

Returning `null` sends no context to Unleash.

Variants
--------

[](#variants)

`Feature::value('my-feature')` resolves the [Unleash variant](https://docs.getunleash.io/reference/feature-toggle-variants)for the given feature and scope:

- If the feature has no variants configured, it's evaluated as a plain boolean instead.
- If the feature (or the matched variant) is disabled, it returns `false`.
- If the variant has no payload, it returns the variant's name.
- If the variant has a `string` or `csv` payload, it returns the raw payload value as a string.
- If the variant has a `json` payload, it returns the decoded value (array).

```
Feature::value('my-feature');
// false                    — feature/variant disabled
// 'my-variant'             — variant with no payload
// 'hello'                  — variant with a string/csv payload
// ['foo' => 'bar']         — variant with a json payload
```

Defining local defaults
-----------------------

[](#defining-local-defaults)

Because this driver reads flags from Unleash, `Feature::define()` doesn't register an initial value the way it does for Pennant's built-in drivers. Instead, it registers a **default** resolver that only runs when the feature doesn't exist in Unleash yet — for example, before the toggle has been created there, or in an environment where it hasn't been rolled out. Once the toggle exists in Unleash, the default is ignored and Unleash's evaluation is used instead.

Without a default, a feature that doesn't exist in Unleash simply resolves to `false`. A default is only useful when you need something other than that:

- a **fail-open** default (e.g. `true`) for a toggle that should behave as enabled until it's deliberately created and configured in Unleash;
- a **non-boolean default**, since without one an undefined feature resolves to the boolean `false`, which won't match code expecting a string or decoded JSON payload from `Feature::value()`;
- an **environment-specific default**, e.g. `true` locally while every environment that matters defaults to `false`in production until someone enables the toggle there.

```
Feature::define('my-feature', fn (mixed $scope) => true);
```

Return a `string` or `array` instead of a `bool` and it's treated as a variant default, so `Feature::value()` gets that string or array back instead of `false`. You can also return any object implementing Unleash's own `Variant`interface directly — this package ships a fluent `UnleashVariant` for building one, which also uses Laravel's `Conditionable` trait so you can use `when()`/`unless()` while building it:

```
use Henzeb\Pennant\Unleash\DTO\UnleashVariant;

Feature::define(
    'my-variant-feature',
    fn (mixed $scope) => UnleashVariant::make('trial')
        ->payload(['plan' => 'trial'])
        ->when($scope?->isAdmin(), fn (UnleashVariant $variant) => $variant->payload(['plan' => 'enterprise'])),
);
```

Register this in a service provider's `boot()` method, same as any other `Feature::define()` call.

A feature that only has a local default (and doesn't exist in Unleash) won't show up in `Feature::for($scope)->all()`— it's only used when the feature is checked directly, e.g. via `Feature::active()` or `Feature::value()`.

Development mode
----------------

[](#development-mode)

If you don't have an Unleash server available for local development at all, you may enable development mode instead, which makes the driver evaluate features from a local JSON file and never contact Unleash:

```
UNLEASH_DEVELOPMENT=true
UNLEASH_BOOTSTRAP_FILE=/path/to/unleash-features.json
```

The file must contain feature definitions in the same shape Unleash's own [client feature API](https://docs.getunleash.io/reference/api/unleash/features) returns them — this is passed directly to the underlying SDK's own [bootstrap](https://docs.getunleash.io/sdks/php#bootstrap) support, so strategies, constraints, and variants are evaluated exactly as they would be against a real server:

```
{
    "features": [
        {
            "name": "my-feature",
            "enabled": true,
            "strategies": [
                {
                    "name": "default",
                    "parameters": {},
                    "constraints": [
                        { "contextName": "scope", "operator": "IN", "values": ["42"] }
                    ]
                }
            ]
        }
    ]
}
```

While development mode is enabled, no `app_url`, `api_key`, or `instance_id` configuration is required. A feature not present in the file still falls back to any resolver you registered with `Feature::define()`, exactly as it would against a real server; if neither applies, it simply resolves to `false`.

If development mode is enabled without `unleash.bootstrap_file` configured, or the configured file doesn't exist, an exception is thrown — this is a misconfiguration, not a state the driver silently works around.

Testing this package
--------------------

[](#testing-this-package)

```
composer test
```

Security
--------

[](#security)

If you discover any security related issues, please email  instead of using the issue tracker.

Credits
-------

[](#credits)

- [Henze Berkheij](https://github.com/henzeb)

License
-------

[](#license)

The MIT License. Please see [License File](LICENSE.md) for more information.

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance91

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

4

Last Release

45d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/15928532?v=4)[henzeb](/maintainers/henzeb)[@henzeb](https://github.com/henzeb)

---

Top Contributors

[![henzeb](https://avatars.githubusercontent.com/u/15928532?v=4)](https://github.com/henzeb "henzeb (5 commits)")

---

Tags

laravelpennantfeature-flagshenzebunleash

###  Code Quality

TestsPest

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/henzeb-laravel-pennant-unleash/health.svg)

```
[![Health](https://phpackages.com/badges/henzeb-laravel-pennant-unleash/health.svg)](https://phpackages.com/packages/henzeb-laravel-pennant-unleash)
```

###  Alternatives

[stephenjude/filament-feature-flags

Filament implementation of feature flags and segmentation with Laravel Pennant.

123204.9k1](/packages/stephenjude-filament-feature-flags)[qodenl/laravel-posthog

Laravel implementation for Posthog

35109.2k](/packages/qodenl-laravel-posthog)[stogon/unleash-bundle

Unleash SDK implementation for Symfony framework

1281.3k](/packages/stogon-unleash-bundle)[gabrielmoura/laravel-pennant-redis

A Redis Driver for Laravel Pennant

165.0k](/packages/gabrielmoura-laravel-pennant-redis)[defstudio/enum-features

A simple trait to enable a feature system using Enums and Laravel Pennant

162.3k](/packages/defstudio-enum-features)[henzeb/laravel-cache-index

Flexible replacement for tags

1217.0k](/packages/henzeb-laravel-cache-index)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
