Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Client Laravel Package

unleash/client

PHP client for Unleash, the open-source feature flag service. Evaluate toggles and activation strategies in PHP applications to support gradual rollouts, A/B tests, and kill switches, with framework-friendly integration for Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

unleash/client is the framework-agnostic PHP SDK for Unleash, so a Laravel install needs three things wired up before the first flag check: the SDK itself, a PSR-18 HTTP client with PSR-17 factories, and a PSR-16 cache that survives between requests.

composer require unleash/client:^2.11 guzzlehttp/guzzle guzzlehttp/psr7 symfony/cache

Pin at least ^2.11 if you intend to use semver constraints: SEMVER_GTE and SEMVER_LTE were only added in v2.11.084, and older SDKs silently treat those operators as unsatisfied.

Add a config file (config/unleash.php) so nothing is hardcoded in the provider:

return [
    'url'         => env('UNLEASH_URL', 'https://unleash.example.com/api'),
    'token'       => env('UNLEASH_API_TOKEN'),
    'app_name'    => env('UNLEASH_APP_NAME', env('APP_NAME')),
    'instance_id' => env('UNLEASH_INSTANCE_ID', gethostname()),
    'environment' => env('UNLEASH_ENVIRONMENT', env('APP_ENV')),
];

Bind the client as a singleton in AppServiceProvider::register():

use Unleash\Client\Unleash;
use Unleash\Client\UnleashBuilder;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\Psr16Cache;

$this->app->singleton(Unleash::class, function ($app) {
    return UnleashBuilder::create()
        ->withAppName(config('unleash.app_name'))
        ->withInstanceId(config('unleash.instance_id'))
        ->withApiUrl(config('unleash.url'))
        ->withApiToken(config('unleash.token'))
        ->withHttpClient(new \GuzzleHttp\Client(['timeout' => 1.5]))
        ->withHttpClientRequestFactory(new \GuzzleHttp\Psr7\HttpFactory())
        ->withCache(new Psr16Cache(
            new RedisAdapter(Redis::connection('cache')->client(), 'unleash', 30)
        ))
        ->build();
});

First useful case: protect a risky change behind a toggle instead of a deploy.

public function __construct(private Unleash $unleash) {}

public function checkout(Request $request)
{
    if ($this->unleash->isEnabled('new-checkout-flow')) {
        return $this->newCheckout($request);
    }

    return $this->legacyCheckout($request);
}

Keep the default you pass as the second argument (or the false return) aligned with safe behavior — a toggle check should never be the thing that breaks checkout when Unleash is unreachable.

Implementation Patterns

Context-aware checks. Unleash evaluates strategies locally, so the context you pass decides the result. Build it from the request and inject it once:

use Unleash\Client\Configuration\Context;

$context = (new Context())
    ->setUserId((string) $request->user()?->id)
    ->setSessionId($request->session()->getId())
    ->setIpAddress($request->ip())
    ->addCustomProperty('tenant', (string) $request->user()?->tenant_id)
    ->addCustomProperty('appVersion', config('app.version'))
    ->setCurrentTime(now());

A middleware that stashes this in the container (or a ContextProvider bound in the container and passed via withContextProvider()) keeps controllers free of context assembly. For guest requests, still set sessionId — gradual rollout needs a stable identifier.

Version-gated rollouts with semver constraints (v2.11+). The SDK now evaluates the full semver operator set in strategy constraints, including SEMVER_GTE and SEMVER_LTE. That turns "keep API v1 alive for old mobile clients" into a flag instead of a version_compare() buried in a controller:

// Constraint in Unleash: appVersion SEMVER_GTE 4.2.0
// and a second strategy: appVersion SEMVER_LTE 4.2.x handled by your rollout plan
if ($this->unleash->isEnabled('mobile.new-payload-format', $context)) {
    return $this->newPayload($data);
}

return $this->legacyPayload($data);

The custom property must carry a valid semver string (4.2.0, 4.2.0-rc.1). Feed it from your app version constant or the client's User-Agent, not from config('app.env')-style values.

Variants as remote configuration. Instead of a boolean plus a database row, use variant payloads for things like layout choice, model names, or timeout values:

$variant = $this->unleash->getVariant('search-provider', $context);

$driver = $variant->isEnabled()
    ? ($variant->getPayload()?->getValue() ?? 'algolia')
    : 'database';

Blade and helper sugar. Register a small helper or Blade directive that pulls the container-bound context; avoid calling app(Unleash::class) from views directly so you can swap in a fake in tests.

Testing. Mock the interface in feature tests ($this->mock(Unleash::class)) and assert isEnabled was consulted rather than asserting on the real client. For unit tests of the resolution logic itself, build a client with an in-memory bootstrap provider (or an in-memory PSR-16 cache) so no network call happens. Never let the real client hit Unleash from the test suite — set registration and metrics off in the testing environment. If you test semver constraints, cover prerelease strings (1.0.0-beta.2 vs 1.0.0) explicitly; the SDK follows semver precedence, which surprises people who expect version_compare semantics.

Queues and console commands. Jobs and Artisan commands have no HTTP context. Build a context explicitly from the job payload (setUserId($this->userId)), and remember the singleton resolved in a queue worker is reused across jobs — don't stash a per-user context in a service that outlives the job.

Gradual rollout. For percentage rollouts, always supply a consistent identifier (userId for authenticated users, sessionId otherwise). Without one, the SDK has nothing stable to hash, and users flip on and off between requests.

Cache sharing. Point the PSR-16 cache at the same Redis instance your app already uses. This makes all web pods and queue workers share a single feature snapshot and avoids a cold fetch per PHP-FPM worker.

Gotchas and Tips

  • Boot-time network call. Building the client fetches /api/client/features synchronously. Set a short Guzzle timeout (1–2s) and rely on the PSR-16 cache; otherwise the first request after each cache expiry pays the latency, and a Unleash outage becomes your outage.
  • Laravel's cache is not PSR-16. Illuminate\Contracts\Cache\Repository does not implement Psr\SimpleCache\CacheInterface. Either use symfony/cache's Psr16Cache directly or write a ~40-line adapter delegating get/set/delete to the Laravel repository. Skipping the cache entirely means a feature fetch on every request.
  • Semver version skew is silent. SEMVER_GTE / SEMVER_LTE landed in v2.11.084. On an older SDK, a strategy using those operators simply never matches — no exception, no log line, just a flag that stays off. Audit composer.lock across services before rolling out a version-gated strategy, and verify with a throwaway flag that 1.2.3 matches SEMVER_GTE 1.2.0 on each pinned version.
  • Build metadata is ignored in semver comparison (v2.11+). Per the semver spec, 1.4.0+sha.abc123 compares equal to 1.4.0. Don't try to gate on a commit hash or build number embedded after +; use a dedicated numeric custom property (e.g. buildNumber with a numeric constraint) instead. Prereleases still sort below their release (2.0.0-rc.1 < 2.0.0).
  • Metrics and registration are side effects. The SDK registers the instance and periodically posts metrics. That's great for adoption dashboards, noise in tests and local dev. Disable registration/metrics in testing and local, or point at a local Unleash instance.
  • Long-running workers. Under Octane or Horizon, the singleton lives for the process lifetime, so feature state and accumulated metrics can go stale. Verify the refresh behavior of your cache TTL and consider rebuilding the client (or refreshing its repository) on a schedule, or scoping it per request if your traffic profile is low.
  • Missing identifiers make rollouts non-deterministic. The most common "the flag is flaky" report is a gradual rollout with no userId/sessionId in the context. Log the context alongside decisions when debugging.
  • URL and token shape. The API URL usually ends in /api, and the token is a client token, not an admin/personal token. A 401 at boot throws rather than silently returning false — wrap the binding in a try/catch that falls back to a no-op implementation if you need hard availability.
  • Defaults are your kill switch. Treat isEnabled('flag') with no fallback as "off when broken." If the feature is load-bearing (e.g., a new payment path), pass an explicit default that reflects the safe path, and consider a second flag for the fallback.
  • Naming and hygiene. Prefix toggles by domain (checkout.new-flow), record the owning team in Unleash, and delete flags after rollout. An SDK check is cheap, but dead flags accumulate and mislead.
  • Debugging. When a flag behaves unexpectedly, inspect the cached feature definition (the raw JSON in Redis) before blaming the SDK — strategy constraints, environment, and project scoping are evaluated there. Then confirm the exact context keys match the constraint names; they are case-sensitive. For semver specifically, dump the context property you're sending: a trailing space or a leading v (v1.2.3) will not parse as semver and the constraint will fail to match.
  • Extension points. ContextProvider, custom bootstrap providers, and a decorator around the Unleash interface (for logging decisions or hard-coding overrides in tests) are the cleanest seams. Prefer a decorator over editing the service provider when you need per-request behavior.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle