Powerful Feature flagging and A/B testing for PHP.
- No external dependencies (besides PSR interfaces)
- Extremely fast, all evaluation happens locally
- PHP 8.0+ with 100% test coverage and phpstan on the highest level
- Advanced user and page targeting
- Use your existing event tracking (GA, Segment, Mixpanel, custom)
- Adjust variation weights and targeting without deploying new code
GrowthBook is available on Composer:
composer require growthbook/growthbook
// Create a GrowthBook instance
$growthbook = Growthbook\Growthbook::create()
->withAttributes([
// Targeting attributes
'id' => $userId,
'someCustomAttribute' => true
]);
// Load feature flags from the GrowthBook API
// Make sure to use caching in production! (see 'Loading Features' below)
$growthbook->initialize("sdk-abc123", "https://cdn.growthbook.io");
// Feature gating
if ($growthbook->isOn("my-feature")) {
echo "It's on!";
} else {
echo "It's off :(";
}
// Remote configuration with fallback
$color = $growthbook->getValue("button-color", "blue");
echo "<button style='color:${color}'>Click Me!</button>";Some of the feature flags you evaluate might be running an A/B test behind the scenes which you'll want to track in your analytics system.
At the end of the request, you can loop through all experiments and track them however you want to:
$impressions = $growthbook->getViewedExperiments();
foreach($impressions as $impression) {
// Whatever you use for event tracking
Segment::track([
"userId" => $userId,
"event" => "Experiment Viewed",
"properties" => [
"experimentId" => $impression->experiment->key,
"variationId" => $impression->result->key
]
]);
}There are 2 ways to load features into the SDK. You can use initialize with a Client Key and API Host. Or, you can manually fetch and cache feature flags and pass them in with the withFeatures method.
The initialize method can fetch features from the GrowthBook API for you.
By default, there is no caching enabled. You can enable it by passing any PSR16-compatible instance into the withCache method.
Caching is required for production usage
// Any psr-16 library will work
use Cache\Adapter\Apcu\ApcuCachePool;
$cache = new ApcuCachePool();
$growthbook = Growthbook\Growthbook::create()
->withCache($cache);
// You can optionally pass in a TTL (default 60s)
$growthbook = Growthbook\Growthbook::create()
->withCache($cache, 120); // Cache for 120s insteadIf the GrowthBook API is unavailable and an expired cache exists, the SDK will automatically use the stale cached data as a fallback instead of failing. This means a brief API outage will not affect your users — features continue to work from the last known state.
The cached data is stored with a hard TTL of 10× the configured TTL, so stale data remains available as a fallback for that duration.
To load features, we require a PSR-17 (HttpClient) and PSR-18 (RequestFactoryInterface) compatible library like Guzzle to be installed.
We will auto-discover most HTTP libraries without any configuration required, but if you prefer to specify it explicitly, you can use the withHttpClient method. Note - you'll need to specify both an HttpClient and a RequestFactoryInterface implementation.
By default, the SDK applies a 2-second timeout for API requests made by initialize. This prevents your application from hanging if the GrowthBook API is slow or unreachable.
This works automatically when Guzzle is installed:
composer require guzzlehttp/guzzleYou can customize the timeout values:
// Via constructor options
$growthbook = new Growthbook\Growthbook([
'apiTimeout' => 5, // Total request timeout in seconds
'apiConnectTimeout' => 3, // Connection timeout in seconds
]);
// Or via fluent interface
$growthbook = Growthbook\Growthbook::create()
->withApiTimeout(5)
->withApiConnectTimeout(3);If you provide your own HTTP client via withHttpClient, timeout configuration is your responsibility:
$growthbook = Growthbook\Growthbook::create()
->withHttpClient(
new \GuzzleHttp\Client([
'timeout' => 3,
'connect_timeout' => 2,
]),
new \Http\Factory\Guzzle\RequestFactory()
);Note: If Guzzle is not installed, the SDK will use a discovered PSR-18 client which may not have timeout guarantees. A warning will be logged in this case.
The SDK automatically uses ETag-based conditional requests (HTTP Conditional GET) to reduce bandwidth. When the GrowthBook API returns an ETag header, the SDK stores it and sends If-None-Match on subsequent requests. A 304 Not Modified response reuses the existing cached data without re-downloading the full payload.
This works automatically alongside the existing PSR cache — no extra configuration needed.
You can optionally configure the in-memory ETag cache size (default: 100 entries):
$growthbook = new Growthbook\Growthbook([
'cache' => $cache,
'etagCacheSize' => 50,
]);The initialize method takes 3 arguments:
$clientKey(required) - Get this from your SDK Connection in GrowthBook.$apiHost(optional) - Defaults tohttps://cdn.growthbook.io. If self-hosting GrowthBook, set this to your API host.$decryptionKey(optional) - Only required if you've enabled encryption for your SDK Connection.
If you prefer to have full control over the fetching/caching behavior, you can use the withFeatures method instead to pass an associative array of features into the SDK.
If you're using saved groups and not using the initialize method, you'll need to call withSavedGroups to provide the saved groups data to the SDK.
// From the GrowthBook API, a custom cache layer, or somewhere else
$featuresJSON = '{"my-feature":{"defaultValue":true}}';
// Decode into an associative array
$features = json_decode($featuresJSON, true);
// Pass into the Growthbook instance
$growthbook = Growthbook\Growthbook::create()
->withFeatures($features);
// Optionally, if using saved groups
$savedGroupsJSON = '{"myGroup": [1, 2, 3]}';
$savedGroups = json_decode($savedGroupsJSON, true);
$growthbook = $growthbook->withSavedGroups($savedGroups);With remote evaluation, feature flags are evaluated on a self-hosted GrowthBook proxy/edge instead of locally in the SDK. Instead of downloading the full feature definitions, the SDK sends the current evaluation context (attributes, forced variations, forced features and URL) to the proxy via a POST request, and the proxy returns only the already-evaluated features. This keeps feature definitions and targeting logic off the client.
Enable it by setting remoteEval to true:
$growthbook = new Growthbook\Growthbook([
'remoteEval' => true,
'attributes' => ['id' => 'user-123'],
]);
// Sends a POST to https://gb-proxy.example.com/api/eval/sdk-abc123
$growthbook->initialize('sdk-abc123', 'https://gb-proxy.example.com');Remote evaluation requires a self-hosted GrowthBook proxy/edge and is not supported on GrowthBook Cloud. The following combinations throw an exception:
- A GrowthBook Cloud host (
*.growthbook.io) — you must point$apiHostat your own proxy/edge. - A
decryptionKey— encryption is handled by the proxy, not the SDK. - A
stickyBucketService— sticky bucketing is handled server-side.
When a cache is configured, remote eval responses are cached per evaluation context. By default every attribute is part of the cache key. Use cacheKeyAttributes to restrict the key to a subset of attributes, so that changes to irrelevant attributes don't cause unnecessary requests:
$growthbook = new Growthbook\Growthbook([
'remoteEval' => true,
'cacheKeyAttributes' => ['id'], // only `id` affects the cache key
'cache' => $cache,
]);Note: forced features are intentionally excluded from the cache key (they are applied client-side and the proxy does not filter on them).
Because the proxy evaluates features for a specific context, the SDK automatically re-fetches when that context changes after initialize. Calling setAttributes, setForcedVariations or setUrl (or their withX equivalents) triggers a new request. Thanks to the cache key above, repeated contexts are served from cache without a network call.
The Growthbook class has a number of properties. These can be set using a Fluent interface or can be passed into a constructor using an associative array. Every property also has a getter method if needed. Here's an example:
// Using the fluent interface
$growthbook = Growthbook\Growthbook::create()
->withFeatures($features)
->withAttributes($attributes);
// Using the constructor
$growthbook = new Growthbook\Growthbook([
'features' => $features,
'attributes' => $attributes
]);
// Getter methods
print_r($growthbook->getFeatures());
print_r($growthbook->getAttributes());Note: you can also use the fluent methods (e.g. withFeatures) at any point to update properties.
You can specify attributes about the current user and request. These are used for two things:
- Feature targeting (e.g. paid users get one value, free users get another)
- Assigning persistent variations in A/B tests (e.g. user id "123" always gets variation B)
Attributes can be any JSON data type - boolean, integer, float, string, or array.
$attributes = [
'id' => "123",
'loggedIn' => true,
'deviceId' => "abc123def456",
'age' => 21,
'tags' => ["tag1", "tag2"],
'account' => [
'age' => 90
]
];If you want to update attributes later, please note that the withAttributes method completely overwrites the attributes object. You can use array_merge if you only want to update a subset of fields:
// Only update the url attribute
$growthbook->withAttributes(array_merge(
$growthbook->getAttributes(),
[
'url' => '/checkout'
]
));Any time an experiment is run to determine the value of a feature, you want to track that event in your analytics system.
You can either do this via a callback function:
$trackingCallback = function (
Growthbook\InlineExperiment $experiment,
Growthbook\ExperimentResult $result
) {
// Segment.io example
Segment::track([
"userId" => $userId,
"event" => "Experiment Viewed",
"properties" => [
"experimentId" => $experiment->key,
"variationId" => $result->key
]
]);
};
// Fluent interface
$growthbook = Growthbook\Growthbook::create()
->withTrackingCallback($callback);
// Using the constructor
$growthbook = new Growthbook([
'trackingCallback' => $trackingCallback
]);
// Getter method
$trackingCallback = $growthbook->getTrackingCallback();You can also track every time a feature is evaluated using featureUsageCallback. Unlike trackingCallback, this fires for all feature evaluations — not just experiments.
$growthbook = Growthbook\Growthbook::create()
->withFeatureUsageCallback(function (
string $featureKey,
Growthbook\FeatureResult $result
) {
echo $featureKey . ': ' . json_encode($result->value) . ' (' . $result->source . ')';
});
// Getter method
$featureUsageCallback = $growthbook->getFeatureUsageCallback();The callback is deduplicated — it will only fire again for the same feature if the value changes.
Or track all experiment events at the end of the request by looping through an array:
$impressions = $growthbook->getViewedExperiments();
foreach($impressions as $impression) {
// Segment.io example
Segment::track([
"userId" => $userId,
"event" => "Experiment Viewed",
"properties" => [
"experimentId" => $impression->experiment->key,
"variationId" => $impression->result->key
]
]);
}Or, you can pass the impressions onto your front-end and fire analytics events from there. To do this, simply add a block to your template (shown here in plain PHP, but similar idea for Twig, Blade, etc.).
<script>
<?php foreach($growthbook->getViewedExperiments() as $impression): ?>
// tracking code goes here
<?php endforeach; ?>
</script>Below are examples for a few popular front-end tracking libraries:
ga('send', 'event', 'experiment',
"<?= $impression->experiment->key ?>",
"<?= $impression->result->variationId ?>",
{
// Custom dimension for easier analysis
'dimension1': "<?=
$impression->experiment->key.':'.$impression->result->key
?>"
}
);analytics.track("Experiment Viewed", <?=json_encode([
"experimentId" => $impression->experiment->key,
"variationId" => $impression->result->key
])?>);mixpanel.track("Experiment Viewed", <?=json_encode([
'Experiment name' => $impression->experiment->key,
'Variant name' => $impression->result->key
])?>);GrowthBook can output log messages to help you debug your feature flags and experiments.
We support any PSR-3 compatible logger. We implement a fluent interface (withLogger) as well as the standard LoggerAware interface (setLogger).
// Fluent interface
$growthbook
->withLogger($logger)
->with...;
// Setter
$growthbook->setLogger($logger);There are 3 main methods for interacting with features.
$growthbook->isOn("feature-key")returns true if the feature is on$growthbook->isOff("feature-key")returns false if the feature is on$growthbook->getValue("feature-key", "default")returns the value of the feature with a fallback
In addition, you can use $growthbook->getFeature("feature-key") to get back a FeatureResult object with the following properties:
- value - The JSON-decoded value of the feature (or
nullif not defined) - on and off - The JSON-decoded value cast to booleans
- source - Why the value was assigned to the user. One of
unknownFeature,defaultValue,force, orexperiment - experiment - Information about the experiment (if any) which was used to assign the value to the user
- experimentResult - The result of the experiment (if any) which was used to assign the value to the user
- ruleId - The identifier of the feature rule (if any) that assigned the value (or
null)
By default GrowthBook does not persist assigned experiment variations for a user. We rely on deterministic hashing to ensure that the same user attributes always map to the same experiment variation. However, there are cases where this isn't good enough. For example, if you change targeting conditions in the middle of an experiment, users may stop being shown a variation even if they were previously bucketed into it.
Sticky Bucketing is a solution to these issues. You can provide a Sticky Bucket Service to the GrowthBook instance to persist previously seen variations and ensure that the user experience remains consistent for your users.
A sample InMemoryStickyBucketService implementation is provided for reference. Note that it only persists within a single request, so it is not suitable for production on its own.
For production, the SDK ships a Psr16StickyBucketService that persists assignments in any PSR-16 (SimpleCache) backend you already use — Redis, Memcached, APCu, filesystem, etc. — without adding a dependency on a specific client:
// $cache is any \Psr\SimpleCache\CacheInterface implementation
$service = new Growthbook\Psr16StickyBucketService($cache);
// Optional: TTL in seconds (null = the cache's default), and a key prefix
$service = new Growthbook\Psr16StickyBucketService($cache, 15552000, 'gbStickyBuckets_');
$growthbook = Growthbook\Growthbook::create()
->withStickyBucketing($service, null);Alternatively you can implement your own StickyBucketService using a database, cookies, or similar.
Sticky Bucket documents contain three fields
attributeName- The name of the attribute used to identify the user (e.g.id,cookie_id, etc.)attributeValue- The value of the attribute (e.g.123)assignments- A dictionary of persisted experiment assignments. For example:{"exp1__0":"control"}
The attributeName/attributeValue combo is the primary key.
Here's an example implementation using a theoretical db object:
class InMemoryStickyBucketService extends StickyBucketService
{
/** @var array<string, array> */
public $docs = [];
/**
* @param string $attributeName
* @param string $attributeValue
* @return array<string,mixed>|null
*/
public function getAssignments(string $attributeName, string $attributeValue): ?array
{
return $this->docs[$this->getKey($attributeName, $attributeValue)] ?? null;
}
/**
* @param array<string,mixed> $doc
* @return void
*/
public function saveAssignments(array $doc): void
{
$this->docs[$this->getKey($doc['attributeName'], $doc['attributeValue'])] = $doc;
}
/**
* @return void
*/
public function destroy(): void
{
$this->docs = [];
}
}
// Fluent interface
$growthbook = Growthbook\Growthbook::create()
->withStickyBucketing(new InMemoryStickyBucketService(), null);
// Using the constructor
$growthbook = new Growthbook([
'stickyBucketService' => new InMemoryStickyBucketService()
]);Instead of declaring all features up-front and referencing them by ids in your code, you can also just run an experiment directly. This is done with the $growthbook->runInlineExperiment method:
$exp = Growthbook\InlineExperiment::create(
"my-experiment",
["red", "blue", "green"]
);
// Either "red", "blue", or "green"
echo $growthbook->runInlineExperiment($exp)->value;As you can see, there are 2 required parameters for experiments, a string key, and an array of variations. Variations can be any data type, not just strings.
There are a number of additional settings to control the experiment behavior. The methods are all chainable. Here's an example that shows all of the possible settings:
$exp = Growthbook\InlineExperiment::create("my-experiment", ["red","blue"])
// Run a 40/60 experiment instead of the default even split (50/50)
->withWeights([0.4, 0.6])
// Only include 20% of users in the experiment
->withCoverage(0.2)
// Targeting conditions using a MongoDB-like syntax
->withCondition([
'country' => 'US',
'browser' => [
'$in' => ['chrome', 'firefox']
]
])
// Use an alternate attribute for assigning variations (default is 'id')
->withHashAttribute("sessionId")
// Namespaces are used to run mutually exclusive experiments
// Another experiment in the "pricing" namespace with a non-overlapping range
// will be mutually exclusive (e.g. [0.5, 1])
->withNamespace("pricing", 0, 0.5);A call to runInlineExperiment returns an ExperimentResult object with a few useful properties:
$result = $growthbook->runInlineExperiment($exp);
// If user is part of the experiment
echo($result->inExperiment); // true or false
// The index of the assigned variation
echo($result->variationId); // e.g. 0 or 1
// The key used to identify this variation when tracking the event
echo($result->key); // e.g. "control"
// The value of the assigned variation
echo($result->value); // e.g. "A" or "B"
// If the variations was randomly assigned based on a hash
echo($result->hashUsed); // true or false
// The user attribute that was hashed
echo($result->hashAttribute); // "id"
// The value of that attribute
echo($result->hashValue); // e.g. "123"The inExperiment flag will be false if the user was excluded from being part of the experiment for any reason (e.g. failed targeting conditions).
The hashUsed flag will only be true if the user was randomly assigned a variation. If the user was forced into a specific variation instead, this flag will be false.
