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.
Architecture fit
unleash/client is the official server-side PHP SDK for Unleash: a polling client that fetches a feature-toggle snapshot from the Unleash /client/features API (default ~30s interval), evaluates strategies locally against a context object (userId, sessionId, IP, environment, custom properties), and reports usage metrics back on a separate cadence. That model — local evaluation, no per-request network hop in the hot path — is the correct one for a Laravel request lifecycle. Evaluation is cheap and deterministic once the snapshot is cached.
The friction is that this is a library, not a Laravel package. There is no service provider, no publishable config, no facade, no Blade directive, no testing fake. Every one of those is glue you own and must maintain across SDK upgrades. For a TPM, that means the integration is a small internal package with its own semver and test suite, not a composer require and a config file.
Integration feasibility
Feasible in a single sprint for a senior Laravel engineer. The SDK is PSR-compliant and unopinionated: it wants a PSR-18 HTTP client, a PSR-16 cache, and a PSR-3 logger — all of which Laravel's container can supply (Guzzle, Illuminate\Cache\Repository via a PSR-16 adapter or a thin bridge, Monolog). Binding it as a singleton in a service provider is ~40 lines.
Two real constraints:
Psr\SimpleCache\CacheInterface, so you either add an adapter or use a dedicated cache store. Choose deliberately: the cache is the only thing keeping a fleet of PHP-FPM workers from each hammering Unleash every 30 seconds.laravel/framework's HTTP client is worth verifying in a scratch branch before committing.Technical risk
semver gte/lte constraint operators, which materially closes the gap on the most common constraint class (version-gated rollouts, "only new clients", "kill switch for versions below X"). Streaming/SSE updates, newer constraint operators beyond the semver family, and impact-metrics surfaces still lag Node/Go/Java. The parity objection downgrades from "we can't do version targeting" to "we can't do streaming yet" — a much cheaper gap to live with at 30s polling.semver constraint against version strings that carry build metadata (e.g. 2.4.1+build.77) can change evaluation result after upgrade: previously distinct versions may now compare equal. This is low-probability for most apps but high-impact for a rollout flag, and it fails silently as a wrong boolean rather than an error. Treat the upgrade as behavior-affecting, not cosmetic.v2.10.184 to v2.11.084 shows an unusual numbering scheme and a high release frequency. That reinforces the pinning advice below — you cannot audit a moving target, and a floating constraint means an unplanned behavior change ships on a Tuesday.default is false on an authorization-relevant flag, you have an outage; if true, you have a security hole. This is a per-flag product decision that must be encoded, not inherited from a default argument.Key questions
semver constraints, and is our application version string — including build metadata — normalized and correct at every call site? This is now a load-bearing input, not a decorative header.Stack fit
Good fit, with a wrapper. Recommend an internal acme/feature-flags package that depends on unleash/client and exposes a narrow, framework-native API: Feature::isEnabled(string $flag, ?Context $ctx): bool, Feature::variant(...), and a Feature::fake() test double. The public surface should be ~5 methods, so the SDK never appears in application code. This is the single highest-leverage decision in the integration.
Wire it as:
config/feature_flags.php) with app_name, instance_id, api_url, api_token (from env/Secrets Manager, never committed), cache_store, cache_ttl, metrics_interval, and a bootstrap_file fallback path.booted()/terminating() hooks, or a scheduled feature-flags:sync command, so polling is explicit rather than a side effect of the first request.app_version source (build pipeline → env var → SDK context) so semver constraints read one truthful value rather than config('app.version') drift.Migration path
Assume the app has accumulated env()/config-based booleans, if (config('features.new_checkout')) branches, and possibly hardcoded rollout percentages. Migrate in three passes:
Do not do a big-bang migration; the shadow-read phase is what produces the evidence needed to defend the change. If we adopt semver targeting, the shadow-read phase must additionally log the normalized version string used in evaluation — otherwise the first semver flag is debugged in production.
Compatibility
laravel/framework and any existing Guzzle pins (guzzlehttp/guzzle 6 vs 7 conflicts are a classic time sink).^2.11. Given the cadence and the semver-normalization change in this release, upgrade the SDK as a deliberate, reviewed change with a changelog diff on every bump.1.2.3, 1.2.3+abc, 1.2.4) before anyone authors a production flag on them.Sequencing
fake() for tests, canonical app-version plumbing, one integration test that boots the container.Maintenance
Ongoing cost is modest but non-zero and it is yours: the wrapper, its tests, and the version pin. Budget roughly 2–4 engineer-days per quarter for SDK bumps plus a small allowance for the first Octane incident. Track SDK releases deliberately rather than floating ^2.11 — the low dependents count means regressions are less likely to be caught by the community before they reach you, and this release is a working example: a one-line semver normalization change is easy to skim past in a changelog. Review release notes for evaluation semantics changes, not just new features. The dominant long-run maintenance burden is not the SDK; it is flag hygiene. Without a lifecycle policy (owner, expiry date, ticket, cleanup SLA on every flag), you will accumulate hundreds of stale toggles and the codebase becomes unreadable. Semver-targeted flags deserve an extra rule: they must declare the version window they were authored for, because they expire on their own as versions advance and silently become dead code.
Support Two tiers: Unleash's own team for SDK and server behavior (GitHub issues, plus contractual support and SLA only on a commercial plan), and internal platform engineering for the wrapper and the config. There is no meaningful third-party knowledge base at 65 stars, so the wrapper's documentation is the actual onboarding artifact — write it or every new engineer re-derives the same PSR-16 adapter. Document the semver constraint syntax and its build-metadata behavior explicitly in that one-pager; it is the part of the API most likely to be used incorrectly and least likely to be answered by a search engine.
Scaling Evaluation is in-process and cheap — CPU cost is negligible next to a typical Laravel request, and semver comparison does not change that. The scaling concern is entirely about polling fan-out: without a shared cache, N nodes each poll independently, producing N× the request volume and N slightly divergent snapshots. With a shared Redis cache, the load on Unleash is roughly constant. Metrics submission is fire-and-forget and batched. Queue workers and Octane processes need the same shared cache for consistency, or a flag flipped to 50% rollout can behave differently across a horizontally scaled fleet — an outcome that is very difficult to debug.
Failure modes
2.4.1+a from 2.4.1+b no longer does. Gate SDK bumps behind a test that asserts current production flag outcomes are unchanged.Ramp-up
A mid-level Laravel engineer can consume the wrapper productively in under a day if the API is narrow and the fake() helper exists. The platform engineer building the wrapper needs ~3–5 days including Unleash setup, PSR adapters, Octane verification, semver constraint testing, and observability. Product and non-engineering stakeholders need a short primer on why flags exist, the difference between release/kill-switch/experiment/permission flags, and the expectation that every flag has an owner and an expiry — otherwise the tool's success becomes the reason the codebase degrades. Version-targeted flags add one more concept worth teaching: the flag's audience is defined by deployed versions, so a flag can quietly stop matching anyone after a release. Recommend a 30-minute brown-bag plus a written one-pager, and gate the first production flag on that having happened.
How can I help you explore Laravel packages today?