Skip to content

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.

View source on GitHub

Terminal window
composer require featureflip/featureflip-php

The package requires PSR-18 (HTTP client), PSR-17 (HTTP factories), and PSR-16 (simple cache) implementations. For example, with Guzzle and Symfony Cache:

Terminal window
composer require guzzlehttp/guzzle symfony/cache

The Config class accepts the following options:

OptionTypeDefaultDescription
baseUrlstring"https://eval.featureflip.io"Base URL of the Evaluation API
pollIntervalint30How 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
flushIntervalint30Ship queued events at least this often, even if the batch is not full
flushBatchSizeint100Ship queued events once this many have accumulated
cacheCacheInterfacenullPSR-16 simple cache implementation (required)
httpClientClientInterfacenullPSR-18 HTTP client (required)
requestFactoryRequestFactoryInterfacenullPSR-17 request factory (required)
streamFactoryStreamFactoryInterfacenullPSR-17 stream factory (required)
inspectorsarray[]Callables invoked with an EvaluationEvent on every variation call
loggerLoggerInterfacenullPSR-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);

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.

The SDK requires all four PSR dependencies to be provided:

  • CacheInterface (PSR-16) — for caching flag configurations between requests
  • ClientInterface (PSR-18) — for making HTTP requests to the Evaluation API
  • RequestFactoryInterface (PSR-17) — for creating HTTP request objects
  • StreamFactoryInterface (PSR-17) — for creating HTTP request body streams

An \InvalidArgumentException is thrown if any of these are missing.

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 down

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.

$enabled = $client->boolVariation('dark-mode', ['user_id' => 'u-123'], false);

Signature:

public function boolVariation(string $key, array $context, bool $default): bool
$plan = $client->stringVariation('pricing-tier', ['user_id' => 'u-123'], 'free');

Signature:

public function stringVariation(string $key, array $context, string $default): string
$limit = $client->numberVariation('rate-limit', ['user_id' => 'u-123'], 100);

Signature:

public function numberVariation(string $key, array $context, int|float $default): int|float

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): array

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; // true
echo $detail->reason; // "RULE_MATCH"
echo $detail->ruleId; // "rule-abc"
echo $detail->variationKey; // "true-variation"

Signature:

public function variationDetail(string $key, array $context, mixed $default): EvaluationDetail

A value object containing the evaluation result:

PropertyTypeDescription
valuemixedThe evaluated flag value
reasonstringWhy this value was returned
ruleIdstring|nullThe ID of the matched targeting rule, if applicable
variationKeystring|nullThe key of the matched variation
prerequisiteKeystring|nullThe prerequisite that failed, set only when reason is PREREQUISITE_FAILED
ValueDescription
FALLTHROUGHNo rules matched; the fallthrough variation was served
RULE_MATCHA targeting rule matched the context
FLAG_DISABLEDThe flag is disabled; the off variation was served
FLAG_NOT_FOUNDThe flag key does not exist
PREREQUISITE_FAILEDA prerequisite flag was unsatisfied; the off variation was served. prerequisiteKey names it
ERROREvaluation could not complete, or the handle is closed; the value is the default you passed

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 = []): void

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): void

Forces all buffered events to be sent to the server immediately.

$client->flush();

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.

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.

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.

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 = []): self

The client returns the override value for known flags, or the default value with a FLAG_NOT_FOUND reason for unknown flags.

The SDK is designed to work with any PSR-compatible libraries. Common combinations:

LibraryPSR-18PSR-17PSR-16
Guzzle + Symfony CacheGuzzleHttp\ClientGuzzleHttp\Psr7\HttpFactorySymfony\Component\Cache\Psr16Cache
Symfony HttpClient + CacheSymfony\Component\HttpClient\Psr18ClientNyholm\Psr7\Factory\Psr17FactorySymfony\Component\Cache\Psr16Cache
PHP-HTTP DiscoveryAuto-discoveredAuto-discoveredYour choice

The SDK does not include any HTTP client or cache implementation to keep it lightweight and avoid dependency conflicts.