Looking to hire Laravel developers? Try LaraJobs

laravel-pennant-unleash maintained by henzeb

Description
An Unleash driver for Laravel Pennant
Last update
2026/07/02 07:49 (dev-main)
License
Downloads
0

Comments
comments powered by Disqus

Unleash driver for Laravel Pennant

Build Status Latest Version on Packagist Total Downloads License

Laravel Pennant is a lightweight feature flag package, but its built-in drivers store state locally. Unleash 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

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

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'));
});

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

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

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 class and id constraints.

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

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

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

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

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

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');

It also has a static make factory and supports Laravel's Conditionable trait:

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

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

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

Feature::value('my-feature') resolves the Unleash variant for the given feature and scope:

  • 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');
// 'my-variant'             — variant with no payload
// 'hello'                  — variant with a string/csv payload
// ['foo' => 'bar']         — variant with a json payload

Testing this package

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

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

Credits

License

The MIT License. Please see License File for more information.