This is a port of Featurevisor Javascript SDK v3.x to PHP, providing a way to evaluate feature flags, variations, and variables in your PHP applications.
This SDK is compatible with Featurevisor v3.0 projects and v2 datafiles.
- Installation
- Public API
- Initialization
- Evaluation types
- Context
- Check if enabled
- Getting variation
- Getting variables
- Getting all evaluations
- Sticky
- Setting datafile
- Evaluation details
- Diagnostics
- Events
- Modules
- Child instance
- Close
- OpenFeature
- CLI usage
- Development of this package
- License
The Featurevisor PHP SDK requires PHP 8.0 or newer. Install it using Composer:
$ composer require featurevisor/featurevisor-php
The main runtime API is Featurevisor::createFeaturevisor():
use Featurevisor\Featurevisor;
$f = Featurevisor::createFeaturevisor([
"datafile" => $datafileContent,
]);Most applications only need this factory and the returned Featurevisor instance. Public extension and observability APIs include modules, diagnostics, events, and the datafile arrays accepted by the factory.
Treat an instance as request-owned in normal PHP applications. If a long-running parallel runtime shares an instance, serialize calls that mutate or close it. Module, event, and diagnostic callbacks are responsible for synchronizing mutable state that they capture.
The SDK can be initialized by passing datafile content directly:
<?php
use Featurevisor\Featurevisor;
$datafileUrl = "https://cdn.yoursite.com/datafile.json";
$datafileContent = file_get_contents($datafileUrl);
$datafileContent = json_decode($datafileContent, true);
$f = Featurevisor::createFeaturevisor([
"datafile" => $datafileContent
]);We can evaluate 3 types of values against a particular feature:
- Flag (
boolean): whether the feature is enabled or not - Variation (
string): the variation of the feature (if any) - Variables: variable values of the feature (if any)
These evaluations are run against the provided context.
Contexts are attribute values that we pass to SDK for evaluating features against.
Think of the conditions that you define in your segments, which are used in your feature's rules.
They are plain objects:
$context = [
"userId" => "123",
"country" => "nl",
// ...other attributes
];Context can be passed to SDK instance in various different ways, depending on your needs:
You can set context at the time of initialization:
use Featurevisor\Featurevisor;
$f = Featurevisor::createFeaturevisor([
"context" => [
"deviceId" => "123",
"country" => "nl",
],
]);This is useful for values that don't change too frequently and available at the time of application startup.
You can also set more context after the SDK has been initialized:
$f->setContext([
"userId" => "123",
"country" => "nl",
]);This will merge the new context with the existing one (if already set).
If you wish to fully replace the existing context, you can pass true in second argument:
$f->setContext(
[
"deviceId" => "123",
"userId" => "234",
"country" => "nl",
"browser" => "chrome",
],
true // replace existing context
);You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations:
$context = [
"userId" => "123",
"country" => "nl",
];
$isEnabled = $f->isEnabled('my_feature', $context);
$variation = $f->getVariation('my_feature', $context);
$variableValue = $f->getVariable('my_feature', 'my_variable', $context);When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
Further details for each evaluation types are described below.
Once the SDK is initialized, you can check if a feature is enabled or not:
$featureKey = 'my_feature';
$isEnabled = $f->isEnabled($featureKey);
if ($isEnabled) {
// do something
}You can also pass additional context per evaluation:
$isEnabled = $f->isEnabled($featureKey, [
// ...additional context
]);If your feature has any variations defined, you can evaluate them as follows:
$featureKey = 'my_feature';
$variation = $f->getVariation($featureKey);
if ($variation === "treatment") {
// do something for treatment variation
} else {
// handle default/control variation
}Additional context per evaluation can also be passed:
$variation = $f->getVariation($featureKey, [
// ...additional context
]);Your features may also include variables, which can be evaluated as follows:
$variableKey = 'bgColor';
$bgColorValue = $f->getVariable($featureKey, $variableKey);Additional context per evaluation can also be passed:
$bgColorValue = $f->getVariable($featureKey, $variableKey, [
// ...additional context
]);Next to generic getVariable() methods, there are also type specific methods available for convenience:
$f->getVariableBoolean($featureKey, $variableKey, $context = []);
$f->getVariableString($featureKey, $variableKey, $context = []);
$f->getVariableInteger($featureKey, $variableKey, $context = []);
$f->getVariableDouble($featureKey, $variableKey, $context = []);
$f->getVariableArray($featureKey, $variableKey, $context = []);
$f->getVariableObject($featureKey, $variableKey, $context = []);
$f->getVariableJSON($featureKey, $variableKey, $context = []);Type specific methods do not coerce values. getVariableInteger() returns null for the string "1", and boolean getters return null for non-boolean values.
You can get evaluations of all features available in the SDK instance:
$allEvaluations = $f->getAllEvaluations($context = []);
print_r($allEvaluations);
// [
// myFeature: [
// enabled: true,
// variation: "control",
// variables: [
// myVariableKey: "myVariableValue",
// ],
// ],
//
// anotherFeature: [
// enabled: true,
// variation: "treatment",
// ]
// ]This is handy especially when you want to pass all evaluations from a backend application to the frontend.
For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched datafile:
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use spawn($context, ['sticky' => ...]) when a child needs its own sticky state.
use Featurevisor\Featurevisor;
$f = Featurevisor::createFeaturevisor([
"sticky" => [
"myFeatureKey" => [
"enabled" => true,
// optional
"variation" => 'treatment',
"variables" => [
"myVariableKey" => 'myVariableValue',
],
],
"anotherFeatureKey" => [
"enabled" => false,
],
],
]);Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
You can also set sticky features after the SDK is initialized:
$f->setSticky(
[
"myFeatureKey" => [
"enabled" => true,
"variation" => 'treatment',
"variables" => [
"myVariableKey" => 'myVariableValue',
],
],
"anotherFeatureKey" => [
"enabled" => false,
],
],
// replace existing sticky features (false by default)
true
]);You may also initialize the SDK without passing datafile, and set it later on:
$f->setDatafile($datafileContent);By default, setDatafile($datafileContent) merges the incoming datafile with the SDK instance's existing datafile:
- incoming
featuresandsegmentsoverride matching keys - existing
featuresandsegmentsthat are missing from the incoming datafile are kept revision,schemaVersion, andfeaturevisorVersionare taken from the incoming datafile
This means you can call setDatafile more than once with different datafiles, and the SDK instance accumulates their features and segments together.
To replace the stored datafile completely, pass true as the second argument:
$f->setDatafile($datafileContent, true);Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.
This pairs well with targets, where each target produces a smaller datafile for a specific part of your application:
$f = Featurevisor::createFeaturevisor([]);
function loadDatafile($f, string $target): void {
$url = "https://cdn.yoursite.com/production/featurevisor-$target.json";
$datafile = json_decode(file_get_contents($url), true);
// merges into whatever was loaded before
$f->setDatafile($datafile);
}
loadDatafile($f, 'products');
// later, when the user reaches checkout
loadDatafile($f, 'checkout');You can set the datafile as many times as you want in your application, which will result in emitting a datafile_set event that you can listen and react to accordingly.
The triggers for setting the datafile again can be:
- periodic updates based on an interval (like every 5 minutes), or
- reacting to:
- a specific event in your application (like a user action), or
- an event served via websocket or server-sent events (SSE)
By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.
Available diagnostic levels are fatal, error, warn, info, and debug.
Set the level during initialization or update it afterwards:
$f = Featurevisor::createFeaturevisor([
"logLevel" => "debug",
]);
$f->setLogLevel("info");Use onDiagnostic to send structured diagnostics to your observability system:
$f = Featurevisor::createFeaturevisor([
"logLevel" => "info",
"onDiagnostic" => function (array $diagnostic) {
// send $diagnostic to your observability system
},
]);Every diagnostic has level, code, message, and an object-shaped details value. Optional module, moduleName, and originalError fields describe provenance. Evaluation metadata belongs in details.
Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
You can listen to these events that can occur at various stages in your application:
$unsubscribe = $f->on('datafile_set', function ($event) {
$revision = $event['revision']; // new revision
$previousRevision = $event['previousRevision'];
$revisionChanged = $event['revisionChanged']; // true if revision has changed
$replaced = $event['replaced']; // true if datafile was replaced instead of merged
// list of feature keys that have new updates,
// and you should re-evaluate them
$features = $event['features'];
// handle here
});
// stop listening to the event
$unsubscribe();The features array will contain keys of features that have either been:
- added, or
- updated, or
- removed
compared to the previous datafile content that existed in the SDK instance.
$unsubscribe = $f->on('context_set', function ($event) {
$replaced = $event['replaced']; // true if context was replaced
$context = $event['context']; // the new context
echo "Context set";
});$unsubscribe = $f->on('sticky_set', function ($event) {
$replaced = $event['replaced']; // true if sticky features got replaced
$features = $event['features']; // list of all affected feature keys
echo "Sticky features set";
});$unsubscribe = $f->on('error', function ($event) {
echo $event['diagnostic']['message'];
});The error event is emitted for diagnostics whose level is error.
Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context:
// flag
$evaluation = $f->evaluateFlag($featureKey, $context = []);
// variation
$evaluation = $f->evaluateVariation($featureKey, $context = []);
// variable
$evaluation = $f->evaluateVariable($featureKey, $variableKey, $context = []);The returned object will always contain the following properties:
featureKey: the feature keyreason: the reason how the value was evaluated
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
bucketValue: the bucket value between 0 and 100,000ruleKey: the rule keyerror: the error objectenabled: if feature itself is enabled or notvariation: the variation objectvariationValue: the variation valuevariableKey: the variable keyvariableValue: the variable valuevariableSchema: the variable schema
Modules allow you to intercept the evaluation process, report diagnostics, and customize behavior further as per your needs.
A module is a simple array with optional lifecycle callbacks. A name is optional, but when provided it must be unique:
If setup throws, the module is not registered. Featurevisor removes subscriptions created during setup, reports module_setup_error, and calls close when present.
$myCustomModule = [
// only required property
'name' => 'my-custom-module',
// rest of the properties below are all optional per module
// setup receives a module API
'setup' => function ($api) {
$revision = $api['getRevision']();
$unsubscribe = $api['onDiagnostic'](function (array $diagnostic) {
// observe diagnostics reported by other modules or the SDK
});
$api['reportDiagnostic']([
'level' => 'info',
'code' => 'custom_module_ready',
'message' => 'Custom module is ready',
]);
},
// before evaluation
'before' => function ($options) {
$type = $options['type']; // `flag` | `variation` | `variable`
$featureKey = $options['featureKey'];
$variableKey = $options['variableKey']; // if type is `variable`
$context = $options['context'];
// update context before evaluation
$options['context'] = array_merge($options['context'], [
'someAdditionalAttribute' => 'value',
]);
return $options;
},
// after evaluation
'after' => function ($evaluation, $options) {
$reason = $evaluation['reason']; // `error` | `feature_not_found` | `variable_not_found` | ...
if ($reason === "error") {
// log error
return;
}
},
// configure bucket key
'bucketKey' => function ($options) {
$featureKey = $options['featureKey'];
$context = $options['context'];
$bucketBy = $options['bucketBy'];
$bucketKey = $options['bucketKey']; // default bucket key
// return custom bucket key
return $bucketKey;
},
// configure bucket value (between 0 and 100,000)
'bucketValue' => function ($options) {
$featureKey = $options['featureKey'];
$context = $options['context'];
$bucketKey = $options['bucketKey'];
$bucketValue = $options['bucketValue']; // default bucket value
// return custom bucket value
return $bucketValue;
},
// cleanup
'close' => function () {
// release module resources
},
];You can register modules at the time of SDK initialization:
use Featurevisor\Featurevisor;
$f = Featurevisor::createFeaturevisor([
'modules' => [
$myCustomModule
],
]);Or after initialization:
$removeModule = $f->addModule($myCustomModule);
// $removeModule()
// or remove later by name
$f->removeModule('my-custom-module');A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling close() removes both child-owned listeners and subscriptions delegated to the parent.
When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.
That's where child instances come in handy:
$childF = $f->spawn([
// user or request specific context
'userId' => '123',
]);Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone:
$isEnabled = $childF->isEnabled('my_feature');
$variation = $childF->getVariation('my_feature');
$variableValue = $childF->getVariable('my_feature', 'my_variable');Similar to parent SDK, child instances also support several additional methods:
setContextsetStickyevaluateFlagisEnabledevaluateVariationgetVariationevaluateVariablegetVariablegetVariableBooleangetVariableStringgetVariableIntegergetVariableDoublegetVariableArraygetVariableObjectgetVariableJSONgetAllEvaluationsonclose
Both primary and child instances support a .close() method. The primary instance also closes registered modules and removes diagnostic subscriptions.
$f->close();This package also provides a CLI tool for running your Featurevisor project's test specs and benchmarking against this PHP SDK:
All three commands accept repeatable --target=<target> options. test builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. benchmark and assess-distribution run independently against every selected Target datafile. Without --target, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
Learn more about testing here.
$ vendor/bin/featurevisor test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project"
Additional options that are available:
$ vendor/bin/featurevisor test \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--quiet|verbose \
--onlyFailures \
--keyPattern="myFeatureKey" \
--assertionPattern="#1"
If assertions include target, the runner builds and selects the corresponding Target datafile automatically via npx featurevisor build --target=<target> --environment=<env> --json.
Learn more about benchmarking here.
$ vendor/bin/featurevisor benchmark \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--environment="production" \
--feature="myFeatureKey" \
--context='{"country": "nl"}' \
--n=1000
Learn more about assessing distribution here.
$ vendor/bin/featurevisor assess-distribution \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--environment=production \
--feature=foo \
--variation \
--context='{"country": "nl"}' \
--populateUuid=userId \
--populateUuid=deviceId \
--n=1000
The provider targets OpenFeature PHP SDK 2.x. OpenFeature remains optional and is not installed or loaded by the base Featurevisor SDK.
composer require featurevisor/featurevisor-php open-feature/sdk:^2.2use Featurevisor\OpenFeatureProvider;
use OpenFeature\OpenFeatureAPI;
use OpenFeature\implementation\flags\Attributes;
use OpenFeature\implementation\flags\EvaluationContext;
$provider = new OpenFeatureProvider([
'datafile' => $datafileContent,
]);
$api = OpenFeatureAPI::getInstance();
$api->setProvider($provider);
$client = $api->getClient();
$enabled = $client->getBooleanValue(
'checkout',
false,
new EvaluationContext('user-123', new Attributes(['country' => 'nl']))
);The current OpenFeature PHP SDK does not expose provider shutdown through its API. Call $provider->shutdown() when your application shuts down. This closes a Featurevisor instance created by the provider and releases provider subscriptions.
| OpenFeature key | Featurevisor evaluation |
|---|---|
checkout |
Boolean flag for checkout |
checkout:variation |
Variation value for checkout |
checkout:title |
Variable title for checkout |
Boolean variables use the boolean resolver. Integer and double variables use their matching numeric resolvers. Arrays, objects, and JSON variables use the object resolver.
The first separator divides the feature key from the selector. Use keySeparator and variationKey when project keys require a different grammar:
$provider = new OpenFeatureProvider(
options: ['datafile' => $datafileContent],
keySeparator: '/',
variationKey: '$variation'
);This makes checkout/$variation the variation key and checkout/title a variable key.
OpenFeature's targeting key maps to userId by default. Use targetingKeyField to map it to another Featurevisor context field:
$provider = new OpenFeatureProvider(
options: ['datafile' => $datafileContent],
targetingKeyField: 'accountId'
);OpenFeature context attributes are copied without mutating the incoming context. Nested arrays are preserved. Dates are normalized to UTC ISO strings with millisecond precision, matching the JavaScript provider.
The provider maps Featurevisor evaluation results to OpenFeature details:
| Featurevisor result | OpenFeature result |
|---|---|
| Required, forced, sticky, or rule match | TARGETING_MATCH |
| Traffic allocation | SPLIT |
| Disabled variation or variable | DISABLED |
| No match or variable default | DEFAULT |
| Missing feature, variable, or variations | ERROR with FLAG_NOT_FOUND |
| Wrong resolver type | ERROR with TYPE_MISMATCH |
| Invalid datafile | ERROR with PARSE_ERROR |
| Evaluation failure | ERROR with GENERAL |
Errors return the default value supplied to OpenFeature. A malformed datafile uses the stable message Could not parse datafile. A later successful setDatafile() call clears the parse error.
Selected Featurevisor variations are exposed as the OpenFeature variant when available. OpenFeature PHP SDK 2.x does not expose flag metadata in resolution details, so Featurevisor metadata such as revision, rule key, and bucket value cannot currently be returned by this provider.
Tracking is a no-op unless onTrack is configured:
$provider = new OpenFeatureProvider(
options: ['datafile' => $datafileContent],
onTrack: function ($name, $context, $details) {
echo $name;
}
);You can reuse an existing Featurevisor instance:
$featurevisor = Featurevisor::createFeaturevisor(['datafile' => $datafileContent]);
$provider = new OpenFeatureProvider(featurevisor: $featurevisor);The caller owns an instance passed this way. Calling $provider->shutdown() does not close it. Call $featurevisor->close() when every consumer is finished with it. When the provider creates the instance from options, the provider owns and closes it. If both are supplied, $featurevisor takes precedence over $options. Shutdown is safe to call more than once.
See the OpenFeature provider guide for resolution reasons, errors, lifecycle, and providers for other languages.
Clone the repository, and install the dependencies using Composer:
$ composer install
$ composer test
Run the complete local release check with:
$ make check
The OpenFeature and base SDK tests can also be run separately with make test-openfeature and make test-base.
To run the SDK against Featurevisor example-1 from the local monorepo checkout:
$ make test-example-1
- Manually create a new release on GitHub
- Tag it with a prefix of
v, likev2.0.0 - The Packagist workflow notifies Packagist after the tag is pushed
MIT © Fahad Heylaal