Skip to content

Latest commit

 

History

History
1106 lines (790 loc) · 42 KB

README.md

File metadata and controls

1106 lines (790 loc) · 42 KB

Featurevisor PHP SDK

Use the Featurevisor PHP SDK to evaluate flags, variations and variables in PHP applications.

Table of contents

Installation

Upgrading an existing application? Follow the PHP migration guide.

The Featurevisor PHP SDK requires PHP 8.0 or newer. Install it using Composer:

composer require featurevisor/featurevisor-php:^4.0

Initialization

The SDK can be initialized by passing datafile content directly:

<?php

use Featurevisor\Featurevisor;

require 'vendor/autoload.php';

$datafileUrl = "https://cdn.yoursite.com/featurevisor-all.json";

$datafileContent = @file_get_contents($datafileUrl);
if ($datafileContent === false) {
  throw new RuntimeException("Could not load datafile: " . $datafileUrl);
}

// Validate JSON before initialization, keeping the original string for the SDK.
json_decode($datafileContent, false, 512, JSON_THROW_ON_ERROR);

$f = Featurevisor::createFeaturevisor([
  "datafile" => $datafileContent
]);
try {
  echo $f->isEnabled("checkout", ["userId" => "123"]) ? "true\n" : "false\n";
} finally {
  $f->close();
}

The HTTP stream requires allow_url_fopen and rejects HTTP error responses by default. The explicit check turns a failed read into an exception. Invalid JSON also throws. Handle these exceptions at your application's startup boundary. For a local datafile, pass its path to file_get_contents instead.

Passing the raw JSON string is recommended. It preserves the distinction between JSON objects and arrays, including empty objects and objects whose only key is numeric. This lets compiled mutation and variable condition paths follow JavaScript container semantics exactly. Detailed evaluations retain preserved JSON objects as stdClass values. Public getters and the OpenFeature provider recursively adapt them to PHP arrays for compatibility.

The SDK also accepts decoded associative arrays. Non-list associative arrays are treated as objects and lists are treated as arrays, regardless of how a partial datafile was loaded. PHP cannot distinguish {} from [], or {"0": "value"} from ["value"], after json_decode($json, true). The same limitation applies to sticky values and other runtime values supplied as PHP arrays. Pass the raw JSON datafile or use stdClass, such as (object) [], for ambiguous native object values. This avoids evaluation behaviour changing when JSON and array datafile patches are mixed.

Public API

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.

Evaluation types

Featurevisor evaluates four kinds of values:

  • Flag (bool): whether the feature is enabled or not
  • Variation (string): the variation of the feature (if any)
  • Feature variable: a configuration value owned by a feature
  • Global variable: a configuration value evaluated independently of a feature

These evaluations are run against the provided context.

Context

Contexts are attribute values you pass to the SDK for evaluating features against.

Conditions compare these attributes using the operators, including empty and notEmpty.

Date comparisons accept portable timestamp strings and DateTimeInterface values. Offsets must stay within 14 hours and both local and UTC years within 0001 to 9999. Fractions are truncated to milliseconds. Invalid values make both before and after return false.

They are plain objects:

$context = [
  "userId" => "123",
  "country" => "nl",
  // ...other attributes
];

Setting initial context

Set context when you create the instance:

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.

Setting after initialization

Add context later:

$f->setContext([
  "userId" => "123",
  "country" => "nl",
]);

This will merge the new context with the existing one (if already set).

Replacing existing context

Replace the context by passing true as the second argument:

$f->setContext(
  [
    "deviceId" => "123",
    "userId" => "234",
    "country" => "nl",
    "browser" => "chrome",
  ],
  true // replace existing context
);

Manually passing context

Pass context to one evaluation without changing the instance context:

$context = [
  "userId" => "123",
  "country" => "nl",
];

$isEnabled = $f->isEnabled('checkout', $context);
$variation = $f->getVariation('checkout', $context);
$featureVariable = $f->getVariable('checkout', 'bgColor', $context);
$globalVariable = $f->getVariable('supportEmail', $context);

When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.

Check if enabled

Check whether a feature is enabled:

$featureKey = 'checkout';

$isEnabled = $f->isEnabled($featureKey);

if ($isEnabled) {
  // do something
}

Pass additional context to one evaluation:

$isEnabled = $f->isEnabled($featureKey, [
  // ...additional context
]);

Getting variation

Evaluate a feature's variations:

$featureKey = 'checkout';

$variation = $f->getVariation($featureKey);

if ($variation === "treatment") {
  // do something for treatment variation
} else {
  // handle default/control variation
}

Pass additional context to one evaluation:

$variation = $f->getVariation($featureKey, [
  // ...additional context
]);

Getting variables

Getting feature variables

Evaluate a feature's variables:

$variableKey = 'bgColor';

$bgColorValue = $f->getVariable($featureKey, $variableKey);

Pass additional context to one evaluation:

$bgColorValue = $f->getVariable($featureKey, $variableKey, [
  // ...additional context
]);

Type specific methods

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->getVariableNumber($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. Object and JSON type specific getters recursively adapt preserved JSON objects to associative PHP arrays.

getVariableNumber() reads a variable of type number as a float, whether it holds 3 or 4.5. Datafiles built before v4 call that type double, and it reads the same way. The v3 name getVariableDouble() was removed in v4.

Getting global variables

Global variables use the same methods without a feature key:

$email = $f->getVariable('supportEmail', ['country' => 'nl']);
$evaluation = $f->evaluateVariable('supportEmail', ['country' => 'nl']);
$settings = $f->getVariableObject('checkoutSettings');

Conditions, composition and overrides are applied for you. See Variables, Conditions and Composition for authoring guidance.

When an override uses continuation, the SDK applies its mutations in order without changing the stored default. Supported operations are set, append, prepend, appendUnique, removeValue, before, after, and remove. Detailed evaluations expose every matched path in variableOverridePaths:

$evaluation = $f->evaluateVariable('paymentPolicy', ['country' => 'nl']);

$value = $evaluation['variableValue'];
$matchedPaths = $evaluation['variableOverridePaths'] ?? [];

Module callbacks wrap the complete accumulated evaluation once. Sticky values and caller defaults continue to bypass or follow normal evaluation precedence.

Treat sticky feature and variable maps, including nested values passed to the SDK or returned by modules, as read only during evaluation. Use the SDK setters to change owned sticky state. PHP module callbacks compare sticky arrays by value: a newly built array equal to the incoming one is treated as unchanged. If a setter then changes the owner's sticky state during that evaluation, nested prerequisites read the new owner state, not the equal array returned by the module. A different valued module replacement remains authoritative.

Modules should return the same result for the same input during one evaluation. Pass values that change through context.

Conditions and composition

Schema version "4" uses one condition language for segments, feature and global variable root gates, force entries, traffic and variable overrides. Leaves compare attributes, reference a literal segment key, check a feature's enabled state and optional variation, or compare an ordinary variable getter result. Variable leaves may name their feature owner and a compiled path such as ['items', 0, 'enabled'].

Arrays imply AND. Groups use and, or and not. NOT negates the AND of its direct children. Empty expressions are false. Authored order, short circuiting and explicit null values are preserved.

Features evaluate sticky state, force, the root gate, traffic and variable overrides in that order. Global variables evaluate sticky state, the root gate, composition sources when present, then overrides. Unmet root gates report conditions_unmet and expose the normalized expression in conditions. Variation and feature variable disabled reasons are unchanged.

A compiled global variable may assemble ordinary getter results or alias one source:

$datafileContent = [
    'schemaVersion' => '4',
    'revision' => 'composition',
    'segments' => [],
    'features' => [],
    'variables' => [
        'policy' => ['type' => 'object', 'defaultValue' => (object) ['limit' => 50]],
        'screen' => ['type' => 'object', 'compose' => [
            ['key' => 'policy', 'variable' => 'policy'],
        ]],
        'policyAlias' => ['type' => 'object', 'compose' => 'policy'],
    ],
];
$f = Featurevisor::createFeaturevisor(['datafile' => $datafileContent]);
try {
    $screen = $f->getVariable('screen');
    $policy = $f->getVariable('policyAlias');
} finally {
    $f->close();
}

The list form returns a fresh outer object with literal property names. References can read global variables, feature variables, flags or variations. The string form aliases one literal global variable key without wrapping its value. False, zero, empty strings, arrays and null remain source values. JSON is decoded exactly once, including alias chains and strings resembling JSON.

Compositions have no defaultValue or useDefaultWhenDisabled. Optional root conditions and disabledValue are supported. A missed gate skips every source and override, returning the disabled value or null. Whole sticky state also bypasses source reads. Otherwise all sources are read before the existing override program runs. Evaluations report variable_composed, or variable_override_rule when an override matches.

Sources use effective context, modules and child sticky state without inheriting an outer explicit default. The SDK preserves JSON object and array containers internally, including empty and numeric property stdClass objects. Detailed composition evaluations retain those objects. Ordinary public getters and the provider adapt them to PHP arrays as usual. Mutations copy changed paths without changing source, sibling or sticky values.

Features, segments, variables and composition share request scoped cycle detection and a combined bound of 256 dependency handoffs. Dependency errors fail the whole evaluation, never publishing a partial composition. Completed ordinary getter null and false fallbacks remain ordinary values.

Schema version "2" selectors normalize eagerly before merging, retaining informational version metadata and leaving caller data unchanged. Schema version "4" entities normalize lazily on their first read. Partial updates invalidate only incoming entities. Replacement clears all normalized entity caches.

Malformed stringified selector JSON fails the whole selector, including through NOT and segment aliases, without rejecting the accepted datafile. The conditions_parse_error error diagnostic is reported once per selector per incoming entity load. It is eager for schema version "2" and lazy for schema version "4". Details contain exactly conditions, entityType and entityKey. Accepted data is stored before eager diagnostics, and lazy results are cached before diagnostic callbacks, allowing safe reentry.

Getting evaluations

Evaluate all features available in the instance:

$allEvaluations = $f->getFeatureEvaluations($context = []);
$globalVariables = $f->getVariableEvaluations($context = []);

print_r($allEvaluations);
// [
//   checkout: [
//     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.

Sticky

Set sticky values to take precedence over feature evaluation against the datafile:

Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides. Pass stickyFeatures or stickyVariables when a child needs its own state.

Initialize with sticky

use Featurevisor\Featurevisor;

$f = Featurevisor::createFeaturevisor([
  "stickyFeatures" => [
    "checkout" => [
      "enabled" => true,

      // optional
      "variation" => 'treatment',
      "variables" => [
        "myVariableKey" => 'myVariableValue',
      ],
    ],

    "anotherFeatureKey" => [
      "enabled" => false,
    ],
  ],
  "stickyVariables" => [
    "supportEmail" => "sticky@example.com",
  ],
]);

Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.

Set sticky afterwards

Set sticky features after initialization:

$f->setStickyFeatures(
  [
    "checkout" => [
      "enabled" => true,
      "variation" => 'treatment',
      "variables" => [
        "myVariableKey" => 'myVariableValue',
      ],
    ],
    "anotherFeatureKey" => [
      "enabled" => false,
    ],
  ],

  // replace existing sticky features (false by default)
  true
]);

Setting datafile

You may also initialize the SDK without passing datafile, and set it later on:

$f->setDatafile($datafileContent);

setDatafile returns true when the datafile is accepted, including unchanged content. It returns false when the datafile is rejected or the instance is closed. Rejected datafiles leave the last accepted datafile in place. Diagnostics and events keep their existing behaviour.

Merging by default

By default, setDatafile($datafileContent) merges the incoming datafile with the SDK instance's existing datafile:

  • incoming features, segments, and global variables override matching keys
  • existing features, segments, and global variables that are missing from the incoming datafile are kept
  • revision, schemaVersion, and featurevisorVersion are taken from the incoming datafile

Call setDatafile with different datafiles to merge their features, segments, and global variables.

Replacing

To replace the stored datafile completely, pass true as the second argument:

$f->setDatafile($datafileContent, true); // replace

Loading datafiles on demand

Merge datafiles for different targets into the same instance. Here, the two datafile values have already been loaded and validated by your application from the same published release:

$f->setDatafile($productsDatafile);
$f->setDatafile($checkoutDatafile);

Use immutable release URLs, check HTTP responses and keep shared dependencies consistent. See Loading datafiles on demand for the complete loading and refresh flow.

Updating datafile

Each accepted datafile update emits a datafile_set event. Subscribe to react to updates.

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)

Diagnostics

By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.

Levels

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");

Handler

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 behaviour. An exception in a handler does not stop other handlers or evaluations.

Events

Subscribe to SDK events to respond to context, sticky state and datafile changes.

Subscribe to instance lifecycle events:

datafile_set

$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.

context_set

$unsubscribe = $f->on('context_set', function ($event) {
  $replaced = $event['replaced']; // true if context was replaced
  $context = $event['context']; // the new context

  echo "Context set";
});

sticky_features_set and sticky_variables_set

$unsubscribe = $f->on('sticky_features_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";
});

$unsubscribeVariables = $f->on('sticky_variables_set', function ($event) {
  $variables = $event['variables'];
});

error

$unsubscribe = $f->on('error', function ($event) {
  echo $event['diagnostic']['message'];
});

The error event is emitted for diagnostics whose level is error.

Evaluation details

Inspect how variations and variables are evaluated against a context:

// flag
$evaluation = $f->evaluateFlag($featureKey, $context = []);

// variation
$evaluation = $f->evaluateVariation($featureKey, $context = []);

// variable
$evaluation = $f->evaluateVariable($featureKey, $variableKey, $context = []);

For global variables, omit the feature key:

$evaluation = $f->evaluateVariable('supportEmail', $context = []);

The returned object always contains reason. Feature evaluations also contain featureKey. Global variable evaluations contain variableKey without a featureKey.

  • featureKey: the feature key
  • reason: 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,000
  • ruleKey: the rule key
  • error: the error object
  • enabled: if feature itself is enabled or not
  • variation: the variation object
  • variationValue: the variation value
  • variableKey: the variable key
  • variableValue: the variable value
  • variableSchema: the variable schema
  • variableOverrideKey: the matched override key
  • variableOverridePath: the full path of a matched nested override
  • variableOverridePaths: all matched paths in an accumulating global variable program
  • variableOverrideIndex: the flattened override index
  • conditions: the normalized root expression when its gate prevents evaluation
  • variable: the global variable definition when available

Modules

The before evaluation and after evaluation phases each use the module membership at the start of that phase. Where an evaluation uses bucketing, the bucket key and bucket value phases each use their own starting membership. Adding or removing a module during a phase does not change the iteration already in progress. The next phase uses the membership then installed. A module added before the after phase can receive after callbacks for evaluations already in progress, even if it did not receive their before callbacks. A module removed before the after phase receives no after callback.

Module options expose dependencyOf on prerequisite reads. It identifies the immediate requesting evaluation with type, optional featureKey and optional variableKey. Application calls have no requester marker.

Modules allow you to intercept the evaluation process, report diagnostics, and customize behaviour further as per your needs.

Defining a module

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 any feature or global variable evaluation
  'beforeEvaluation' => function ($options) {
    $type = $options['type']; // `flag` | `variation` | `variable`
    $featureKey = $options['featureKey'] ?? null;
    $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 any feature or global variable evaluation
  'afterEvaluation' => 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
  },
];

All beforeEvaluation callbacks run in registration order. After evaluation and caller defaults, all afterEvaluation callbacks run in registration order. Condition and composition dependencies use the same module pipeline. A global accumulation program still invokes each callback only once for its final result.

Internal reads carry dependencyOf identifying the immediate requester and dependencyPurpose set to condition or composition. Direct application calls carry neither. Memoization separates purposes, requesters, context and state changes. Condition snapshots are owned. Composition memoization follows ordinary getter ownership without an additional deep copy. Tracking callbacks should normally skip condition reads, while composition reads consume real values. Private request state is never exposed to callbacks.

Registering modules

Register modules when you create the instance:

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');

Child instance

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',
]);

Pass the child instance to the request handler to evaluate features for that user:

$isEnabled = $childF->isEnabled('checkout');
$variation = $childF->getVariation('checkout');
$variableValue = $childF->getVariable('checkout', 'bgColor');
$globalVariableValue = $childF->getVariable('supportEmail');

Similar to parent SDK, child instances also support several additional methods:

  • setContext
  • setStickyFeatures
  • setStickyVariables
  • evaluateFlag
  • isEnabled
  • evaluateVariation
  • getVariation
  • evaluateVariable
  • getVariable
  • getVariableBoolean
  • getVariableString
  • getVariableInteger
  • getVariableNumber
  • getVariableArray
  • getVariableObject
  • getVariableJSON
  • getFeatureEvaluations
  • getVariableEvaluations
  • on
  • close

Close

Both primary and child instances support a .close() method. The primary instance also closes registered modules and removes diagnostic subscriptions.

$f->close();

OpenFeature

The provider is compatible with OpenFeature PHP SDK 2.x, tested with 2.0.0 and 2.3.0. OpenFeature remains optional and is not installed or loaded by the base Featurevisor SDK.

Installation

composer require featurevisor/featurevisor-php:^4.0 open-feature/sdk:^2.3

Provider setup

use 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']))
);

$f->setStickyVariables(['supportEmail' => 'new@example.com']);

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.

Flag key mapping

OpenFeature key Featurevisor evaluation
checkout Boolean flag for checkout
checkout:variation Variation value for checkout
checkout:title Variable title for checkout
variable:supportEmail Global variable supportEmail

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.

Global variables use the variable prefix by default. Configure globalVariablePrefix when that word is already meaningful in your project:

$provider = new OpenFeatureProvider(
    options: ['datafile' => $datafileContent],
    globalVariablePrefix: 'global'
);

This maps global:supportEmail to the global variable supportEmail.

Context mapping

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 distinguishes an absent targeting key from an explicitly empty one when called directly. The upstream OpenFeature PHP client merges contexts using only nonempty targeting keys, so it drops an empty key before the provider receives it. To override an existing targeting field with an empty string through that client, set the configured field itself as an attribute, such as ['userId' => '']. Clear any inherited nonempty targeting key at its source first, since it would overwrite that attribute again. A narrower empty key may not clear it.

Resolution details

The provider maps Featurevisor evaluation results to OpenFeature details:

Featurevisor result OpenFeature result
Composed variable, forced, sticky or rule match TARGETING_MATCH
Traffic allocation SPLIT
Unmet root conditions, disabled variation or disabled 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

Tracking is a no-op unless onTrack is configured:

$provider = new OpenFeatureProvider(
    options: ['datafile' => $datafileContent],
    onTrack: function ($name, $context, $details) {
        echo $name;
    }
);

Using an existing Featurevisor instance

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.

An invalid initial datafile returns PARSE_ERROR during evaluation, independently of logLevel. The upstream PHP provider interface has no registration lifecycle to fail. A valid datafile update clears the error. Shutdown removes provider subscriptions and closes only an owned instance.

See the OpenFeature provider guide for resolution reasons, errors, lifecycle, and providers for other languages.

CLI usage

Install the package with Composer:

composer require featurevisor/featurevisor-php:^4.0

Run the commands below from the directory containing vendor.

The native test runner requires @featurevisor/cli 4.0.0 or later and Node.js 24 or later. Install the Node.js CLI in your Featurevisor project:

npm install --save-dev @featurevisor/cli@^4.0.0

Project parsing and datafile generation use npx featurevisor. Evaluations run through this PHP SDK. The SDK runtime has no Node.js dependency. All three commands accept repeated --target=<target> options.

Use the Node.js CLI directly for authoring commands such as init, lint, build and list. Native commands delegate project parsing, test expansion and datafile generation to Node.js, then evaluate the results with this SDK.

Options use kebab case, such as --project-directory-path. The camelCase forms from earlier releases still work, and --root-directory-path is an alias for --project-directory-path. Pass values as --name=value or --name value. Repeat --target to select several targets in order. Run <command> --help to list every option.

--n and -n accept both value forms. Repeat --populate-uuid for multiple context attributes. Quote JSON and paths containing spaces.

Test

vendor/bin/featurevisor test \
  --project-directory-path=/path/to/featurevisor-project \
  --only-failures

See Testing for authoring specs and Checks for check syntax and operators.

Useful test options:

  • --key-pattern=<pattern> and --assertion-pattern=<pattern> filter tests.
  • --entity-type=scenario selects scenarios. --set=<set> selects one project set. Omitting --set runs all sets.
  • --case=<id> reruns one expanded case. Copy the opaque ID or rerun command from a failure report.
  • --json --pretty produces an indented JSON report.
  • --coverage reports observed rule and override selections. --uncovered lists features and global variables without associated specs.
  • --only-failures, --quiet, --verbose and --show-datafile control diagnostic output.

Bootstrap, argument and project discovery failures may produce no JSON report. Check the exit status and standard error as well as any report. Reruns retain the PHP executable and CLI script. Without a project path, rerun from the same project directory.

Printed rerun commands use POSIX shell quoting. JSON reports also include raw argument arrays.

Benchmark

The examples below use a project without environments. Add --environment=<name> if your project configures environments. In a sets project, omit --set to run every set or pass --set=<name>. Repeat --target=<key> to evaluate specific target datafiles. The Node.js CLI validates these options and builds the datafiles. PHP evaluates them. Replace checkout with a feature defined in your project.

vendor/bin/featurevisor benchmark \
  --project-directory-path=/path/to/featurevisor-project \
  --feature=checkout \
  --context='{"country":"nl"}' \
  --n=1000

Use --variation for a variation or --variable=<key> for a feature variable. Omit --feature to benchmark a global variable. Do not combine --variation with --variable. See Benchmarking.

Assess distribution

vendor/bin/featurevisor assess-distribution \
  --project-directory-path=/path/to/featurevisor-project \
  --feature=checkout \
  --populate-uuid=userId \
  --n=1000

The PHP CLI also accepts --variation or --variable=<key> to assess a variation or feature variable instead of the enabled state. These selectors are specific to this runner and are not supported by the Node.js assess-distribution command. Repeat --populate-uuid=<key> to generate UUID values for multiple context attributes. See Assess distribution.

GitHub repositories

The aggregate evaluation reference has moved to Getting evaluations.

The sticky state reference has moved to Sticky.

Development of this package

Setting up

Clone the repository, and install the dependencies using Composer:

$ composer install

Running tests

$ composer test

The native tests cover runtime conformance and project integration.

Run vendor/bin/phpunit tests/ReadmeInitializationTest.php to execute the actual initialization snippet against a local HTTP fixture. This check also runs in the ordinary SDK suite and CI.

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

Releasing

The next breaking release is 4.0.0. Composer derives the package version from the Git tag, so composer.json deliberately has no version field. The Packagist workflow accepts v4 semantic version tags. Run composer validate --strict, composer sast, make test-base and make test-openfeature before tagging. OpenFeature remains optional for base consumers. Provider consumers also install open-feature/sdk:^2.3.

  • Manually create a new release on GitHub
  • Tag it with a prefix of v, like vX.Y.Z
  • The Packagist workflow notifies Packagist after the tag is pushed

License

MIT © Fahad Heylaal