PHP SDK Reference
TL;DR: Run
composer require featureflip/featureflip-php, call$client->boolVariation('flag-key', ['user_id' => 'u-123'], false), and you’re evaluating flags from PHP.
The featureflip/featureflip-php package is a server-side PHP SDK for evaluating feature flags locally — no per-request callouts to Featureflip. The SDK requires PHP 8.2 or later and is built around PSR standards: bring your own PSR-18 HTTP client, PSR-17 request factories, and PSR-16 simple cache, and the SDK plugs into them. This means it works cleanly with Laravel, Symfony, Slim, Mezzio, WordPress, Drupal, or any framework that already has Guzzle/HTTPlug and psr/cache in the autoloader.
PHP’s request-per-process model makes long-lived in-memory clients impractical, so the SDK is built around a cache-first strategy. On first use it fetches the full flag configuration once and writes it through to your PSR-16 cache; subsequent requests in the same minute (or however long your TTL is) read from cache without a network round trip. PSR-16 reserved characters ({}()/\@:) are escaped automatically in cache keys so the SDK plays nicely with Redis, Memcached, APCu, and filesystem cache backends. PascalCase enum values from the API (operators, evaluation reasons) are normalized via regex on the client side, so you compare against snake_case literals in your code.
The SDK’s classes are final, so don’t try to mock them — use the built-in forTesting() factory instead, which returns a client backed by flag values you supply and makes no network calls. Custom evaluation context is a plain associative array, the same shape used everywhere else in PHP. Targeting rules and percentage rollouts work identically to the other server SDKs, using the user_id key as the rollout bucket anchor.
Installation
Section titled “Installation”composer require featureflip/featureflip-phpThe package requires PSR-18 (HTTP client), PSR-17 (HTTP factories), and PSR-16 (simple cache) implementations. For example, with Guzzle and Symfony Cache:
composer require guzzlehttp/guzzle symfony/cacheConfiguration
Section titled “Configuration”The Config class accepts the following options:
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | "https://eval.featureflip.io" | Base URL of the Evaluation API |
pollInterval | int | 30 | How often the SDK refreshes its flag configuration. Not a cache lifetime — a fetched configuration is kept and served until a fetch succeeds in replacing it |
flushInterval | int | 30 | Ship queued events at least this often, even if the batch is not full |
flushBatchSize | int | 100 | Ship queued events once this many have accumulated |
cache | CacheInterface | null | PSR-16 simple cache implementation (required) |
httpClient | ClientInterface | null | PSR-18 HTTP client (required) |
requestFactory | RequestFactoryInterface | null | PSR-17 request factory (required) |
streamFactory | StreamFactoryInterface | null | PSR-17 stream factory (required) |
inspectors | array | [] | Callables invoked with an EvaluationEvent on every variation call |
logger | LoggerInterface | null | PSR-3 logger for config-load failures — an unreachable Evaluation API, a rejected SDK key, a skipped flag. Defaults to PHP’s error log |
Quick example with Guzzle and Symfony Cache
Section titled “Quick example with Guzzle and Symfony Cache”use Featureflip\FeatureflipClient;use Featureflip\Config;use GuzzleHttp\Client as Guzzle;use GuzzleHttp\Psr7\HttpFactory;use Symfony\Component\Cache\Psr16Cache;use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$cache = new Psr16Cache(new FilesystemAdapter());$guzzle = new Guzzle();$factory = new HttpFactory();
$config = new Config( baseUrl: 'https://eval.featureflip.io', cache: $cache, httpClient: $guzzle, requestFactory: $factory, streamFactory: $factory,);
$client = FeatureflipClient::get('your-sdk-key', $config);Initialization
Section titled “Initialization”Create a client by calling FeatureflipClient::get() with an SDK key and configuration:
$client = FeatureflipClient::get('sdk-dev-abc123', $config);The factory fetches the initial flag configuration synchronously if the cached configuration is due a refresh and a recent failure is not still backing off. The same check runs on the evaluation path, so a client held across many requests — Laravel Octane, RoadRunner, FrankenPHP, Swoole — keeps picking up flag changes rather than freezing at the configuration it booted with. A shutdown function is registered to flush any events still queued when the PHP process exits.
Required PSR implementations
Section titled “Required PSR implementations”The SDK requires all four PSR dependencies to be provided:
CacheInterface(PSR-16) — for caching flag configurations between requestsClientInterface(PSR-18) — for making HTTP requests to the Evaluation APIRequestFactoryInterface(PSR-17) — for creating HTTP request objectsStreamFactoryInterface(PSR-17) — for creating HTTP request body streams
An \InvalidArgumentException is thrown if any of these are missing.
Lifetime
Section titled “Lifetime”FeatureflipClient::get() is a singleton-by-construction factory. Calling it multiple times with the same SDK key returns handles that share one underlying connection and flag store. This design makes the SDK safe to use with any DI container lifetime (singleton, scoped, or transient).
Each close() call decrements an internal reference count. The underlying resources (HTTP client, cache, event processor) are only cleaned up when the last handle is closed. Double-closing a handle is a safe no-op.
$h1 = FeatureflipClient::get('sdk-key', $config);$h2 = FeatureflipClient::get('sdk-key'); // Shares the same core — no config needed
$h1->close(); // Core stays alive (h2 still holds a reference)$h2->close(); // Last handle — core shuts downEvaluating Flags
Section titled “Evaluating Flags”All evaluation methods accept a flag key, an associative array context, and a typed default value. If the flag is not found or evaluation fails, the default value is returned. These methods never throw exceptions.
boolVariation()
Section titled “boolVariation()”$enabled = $client->boolVariation('dark-mode', ['user_id' => 'u-123'], false);Signature:
public function boolVariation(string $key, array $context, bool $default): boolstringVariation()
Section titled “stringVariation()”$plan = $client->stringVariation('pricing-tier', ['user_id' => 'u-123'], 'free');Signature:
public function stringVariation(string $key, array $context, string $default): stringnumberVariation()
Section titled “numberVariation()”$limit = $client->numberVariation('rate-limit', ['user_id' => 'u-123'], 100);Signature:
public function numberVariation(string $key, array $context, int|float $default): int|floatjsonVariation()
Section titled “jsonVariation()”Returns a decoded associative array for JSON flags:
$config = $client->jsonVariation('banner-config', ['user_id' => 'u-123'], [ 'text' => 'Welcome', 'color' => '#000',]);Signature:
public function jsonVariation(string $key, array $context, array $default): arrayEvaluation Details
Section titled “Evaluation Details”variationDetail()
Section titled “variationDetail()”Returns an EvaluationDetail object with the evaluated value and metadata about the decision.
$detail = $client->variationDetail('dark-mode', ['user_id' => 'u-123'], false);
echo $detail->value; // trueecho $detail->reason; // "RULE_MATCH"echo $detail->ruleId; // "rule-abc"echo $detail->variationKey; // "true-variation"Signature:
public function variationDetail(string $key, array $context, mixed $default): EvaluationDetailEvaluationDetail
Section titled “EvaluationDetail”A value object containing the evaluation result:
| Property | Type | Description |
|---|---|---|
value | mixed | The evaluated flag value |
reason | string | Why this value was returned |
ruleId | string|null | The ID of the matched targeting rule, if applicable |
variationKey | string|null | The key of the matched variation |
prerequisiteKey | string|null | The prerequisite that failed, set only when reason is PREREQUISITE_FAILED |
Evaluation reasons
Section titled “Evaluation reasons”| Value | Description |
|---|---|
FALLTHROUGH | No rules matched; the fallthrough variation was served |
RULE_MATCH | A targeting rule matched the context |
FLAG_DISABLED | The flag is disabled; the off variation was served |
FLAG_NOT_FOUND | The flag key does not exist |
PREREQUISITE_FAILED | A prerequisite flag was unsatisfied; the off variation was served. prerequisiteKey names it |
ERROR | Evaluation could not complete, or the handle is closed; the value is the default you passed |
Event Tracking
Section titled “Event Tracking”track()
Section titled “track()”Records a custom analytics event.
$client->track('purchase-completed', ['user_id' => 'u-123'], [ 'plan' => 'pro', 'amount' => 29.99,]);Signature:
public function track(string $eventKey, array $context, array $metadata = []): voididentify()
Section titled “identify()”Records an Identify event for the given user, counted in your evaluation analytics.
This does not affect flag evaluation. Targeting rules and segments are matched against the context you pass to each variation call. identify() does not set a context for subsequent calls, and it does not persist user attributes for later targeting.
$client->identify([ 'user_id' => 'u-123', 'email' => 'user@example.com', 'plan' => 'pro',]);Signature:
public function identify(array $context): voidLifecycle
Section titled “Lifecycle”flush()
Section titled “flush()”Forces all buffered events to be sent to the server immediately.
$client->flush();isInitialized()
Section titled “isInitialized()”Returns whether the SDK holds a flag configuration, and false after close() — a closed handle serves the caller’s default from every accessor.
if (!$client->isInitialized()) { // Nothing was ever loaded — every evaluation is returning the default you // pass. Usually a rejected SDK key, or an unreachable Evaluation API with // no cached snapshot to fall back on.}It answers “was anything ever loaded”, not “is it current”: a configuration
retained through an outage still returns true, because the SDK is out of date
rather than uninitialised. An environment with no flags in it also returns
true — it loaded successfully, there was simply nothing to load. A closed
handle always returns false.
refresh()
Section titled “refresh()”Refreshes the flag configuration if it is due one. Optional — evaluation refreshes by itself — but a persistent worker can call this from a tick hook so the fetch lands between requests instead of inside one:
Octane::tick('featureflip', fn () => $client->refresh())->seconds(10);It honours pollInterval and the failure backoff, so calling it when nothing is due does no work.
close()
Section titled “close()”Releases this handle and, once the last one closes, flushes whatever events are still queued. A closed handle is inert: variation calls return the default you pass, track()/identify()/flush() do nothing, and no inspectors fire. Closing one handle never affects a sibling still holding the same core. Under PHP-FPM the SDK calls fastcgi_finish_request() first when there is a non-empty queue, so the response reaches the user before the analytics POST goes out.
$client->close();A shutdown function is registered automatically when the client is created, so close() is called when the process ends. You can call it explicitly if you need to flush events before the process exits.
Testing
Section titled “Testing”FeatureflipClient::forTesting()
Section titled “FeatureflipClient::forTesting()”Creates a test client with fixed flag values. No network calls are made and no PSR implementations are required.
$client = FeatureflipClient::forTesting([ 'dark-mode' => true, 'pricing-tier' => 'enterprise', 'rate-limit' => 500,]);
$client->boolVariation('dark-mode', [], false); // true$client->stringVariation('pricing-tier', [], 'free'); // "enterprise"Signature:
public static function forTesting(array $flags, array $inspectors = []): selfThe client returns the override value for known flags, or the default value with a FLAG_NOT_FOUND reason for unknown flags.
PSR Compatibility
Section titled “PSR Compatibility”The SDK is designed to work with any PSR-compatible libraries. Common combinations:
| Library | PSR-18 | PSR-17 | PSR-16 |
|---|---|---|---|
| Guzzle + Symfony Cache | GuzzleHttp\Client | GuzzleHttp\Psr7\HttpFactory | Symfony\Component\Cache\Psr16Cache |
| Symfony HttpClient + Cache | Symfony\Component\HttpClient\Psr18Client | Nyholm\Psr7\Factory\Psr17Factory | Symfony\Component\Cache\Psr16Cache |
| PHP-HTTP Discovery | Auto-discovered | Auto-discovered | Your choice |
The SDK does not include any HTTP client or cache implementation to keep it lightweight and avoid dependency conflicts.
See also
Section titled “See also”- PHP Quickstart — get started in under 5 minutes
- Targeting & Segments — control which users see which variation
- Rollout Strategies — gradual percentage-based rollouts
- Evaluation API — REST API reference
- SDK Overview — compare server-side and client-side SDKs