diff --git a/README.md b/README.md
index 9550bab..fe772fa 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for PHP](https://
| ❌ | Shutdown | Not supported — not available in the OpenFeature PHP SDK ([php-sdk#93](https://github.com/open-feature/php-sdk/issues/93)). |
| ❌ | Tracking | Not supported — the OpenFeature PHP SDK provider interface has no `track` method. LaunchDarkly custom events can be sent through an `LDClient` directly. |
| ❌ | Transaction Context Propagation | Not supported — not available in the OpenFeature PHP SDK, which has no transaction context. |
-| ❌ | Flag metadata | Not supported — not available in the OpenFeature PHP SDK, whose `EvaluationDetails` and `ResolutionDetails` carry no flag metadata, so LaunchDarkly evaluation reason details cannot be surfaced. |
+| ✅ | Flag metadata | LaunchDarkly evaluation reason details are returned as OpenFeature flag metadata. Requires OpenFeature PHP SDK 2.4.0 or later. See [Flag Metadata](#flag-metadata). |
Supported: ✅ | Partially supported: ⚠️ | Not supported: ❌
@@ -162,6 +162,24 @@ $attributes = [
$context = new EvaluationContext(null, new Attributes($attributes));
```
+### Flag Metadata
+
+Evaluations include flag metadata for the LaunchDarkly specific parts of the evaluation result which have no OpenFeature equivalent. Each entry is absent when it does not apply to the evaluation.
+
+| Key | Type | Description |
+|---------------------|---------|--------------------------------------------------------------------|
+| `variationIndex` | integer | The index of the returned variation. Absent for default values. |
+| `inExperiment` | boolean | Present, and `true`, when the evaluation was part of an experiment. |
+| `ruleIndex` | integer | The index of the rule that matched. |
+| `ruleId` | string | The identifier of the rule that matched. |
+| `prerequisiteKey` | string | The key of the prerequisite flag that failed. |
+| `bigSegmentsStatus` | string | The status of the Big Segments query made during the evaluation. |
+
+```php
+$details = $client->getBooleanDetails("my-flag", false, $context);
+$inExperiment = $details->getMetadata()["inExperiment"] ?? false;
+```
+
## Learn more
Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/php).
diff --git a/composer.json b/composer.json
index c035244..449ecc0 100644
--- a/composer.json
+++ b/composer.json
@@ -22,9 +22,9 @@
"require": {
"php": ">=8.2",
"guzzlehttp/guzzle": "^7",
- "launchdarkly/server-sdk": "^6.2",
+ "launchdarkly/server-sdk": "^6.4",
"myclabs/php-enum": ">1.8",
- "open-feature/sdk": "^2.0"
+ "open-feature/sdk": "^2.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.16",
diff --git a/src/ResolutionDetailsConverter.php b/src/ResolutionDetailsConverter.php
index 935185e..a11f8b7 100644
--- a/src/ResolutionDetailsConverter.php
+++ b/src/ResolutionDetailsConverter.php
@@ -36,9 +36,49 @@ public function toResolutionDetails(EvaluationDetail $result): ResolutionDetails
$builder->withVariant(strval($variationIndex));
}
+ $builder->withMetadata(self::toFlagMetadata($variationIndex, $reason));
+
return $builder->build();
}
+ /**
+ * The LaunchDarkly specific parts of an evaluation result which have no
+ * OpenFeature equivalent. Each entry is absent when it does not apply.
+ *
+ * @return array
+ */
+ private static function toFlagMetadata(?int $variationIndex, EvaluationReason $reason): array
+ {
+ $metadata = [];
+
+ if ($variationIndex !== null) {
+ $metadata['variationIndex'] = $variationIndex;
+ }
+
+ if ($reason->isInExperiment()) {
+ $metadata['inExperiment'] = true;
+ }
+
+ if ($reason->getRuleIndex() !== null) {
+ $metadata['ruleIndex'] = $reason->getRuleIndex();
+ }
+
+ if ($reason->getRuleId() !== null) {
+ $metadata['ruleId'] = $reason->getRuleId();
+ }
+
+ if ($reason->getPrerequisiteKey() !== null) {
+ $metadata['prerequisiteKey'] = $reason->getPrerequisiteKey();
+ }
+
+ $bigSegmentsStatus = $reason->bigSegmentsEvaluationStatus();
+ if ($bigSegmentsStatus !== null) {
+ $metadata['bigSegmentsStatus'] = $bigSegmentsStatus->value;
+ }
+
+ return $metadata;
+ }
+
private static function kindToString(string $kind): string
{
switch ($kind) {
diff --git a/tests/ResolutionDetailsConverterTest.php b/tests/ResolutionDetailsConverterTest.php
new file mode 100644
index 0000000..245fa01
--- /dev/null
+++ b/tests/ResolutionDetailsConverterTest.php
@@ -0,0 +1,110 @@
+converter = new ResolutionDetailsConverter();
+ }
+
+ public function testFallthroughHasOnlyVariationIndex(): void
+ {
+ $details = $this->converter->toResolutionDetails(
+ new EvaluationDetail(true, 1, EvaluationReason::fallthrough())
+ );
+
+ $this->assertSame(['variationIndex' => 1], $details->getMetadata());
+ }
+
+ public function testExperimentFallthroughIncludesInExperiment(): void
+ {
+ $details = $this->converter->toResolutionDetails(
+ new EvaluationDetail(true, 0, EvaluationReason::fallthrough(true))
+ );
+
+ $this->assertSame(['variationIndex' => 0, 'inExperiment' => true], $details->getMetadata());
+ }
+
+ public function testRuleMatchIncludesRuleIndexAndId(): void
+ {
+ $details = $this->converter->toResolutionDetails(
+ new EvaluationDetail('value', 2, EvaluationReason::ruleMatch(3, 'rule-id', true))
+ );
+
+ $this->assertSame(
+ ['variationIndex' => 2, 'inExperiment' => true, 'ruleIndex' => 3, 'ruleId' => 'rule-id'],
+ $details->getMetadata()
+ );
+ }
+
+ public function testPrerequisiteFailedIncludesPrerequisiteKey(): void
+ {
+ $details = $this->converter->toResolutionDetails(
+ new EvaluationDetail(false, 0, EvaluationReason::prerequisiteFailed('prereq-key'))
+ );
+
+ $this->assertSame(['variationIndex' => 0, 'prerequisiteKey' => 'prereq-key'], $details->getMetadata());
+ }
+
+ public function testBigSegmentsStatusIsIncluded(): void
+ {
+ $reason = EvaluationReason::fallthrough()
+ ->withBigSegmentsEvaluationStatus(BigSegmentsEvaluationStatus::STALE);
+ $details = $this->converter->toResolutionDetails(new EvaluationDetail(true, 0, $reason));
+
+ $this->assertSame(['variationIndex' => 0, 'bigSegmentsStatus' => 'STALE'], $details->getMetadata());
+ }
+
+ public function testDefaultValueHasNoMetadata(): void
+ {
+ $details = $this->converter->toResolutionDetails(
+ new EvaluationDetail(true, null, EvaluationReason::error(EvaluationReason::FLAG_NOT_FOUND_ERROR))
+ );
+
+ $this->assertSame([], $details->getMetadata());
+ }
+
+ public function testOffWithoutOffVariationHasNoMetadata(): void
+ {
+ $details = $this->converter->toResolutionDetails(
+ new EvaluationDetail(null, null, EvaluationReason::off())
+ );
+
+ $this->assertSame([], $details->getMetadata());
+ }
+
+ public function testProviderEvaluationIncludesRuleMetadata(): void
+ {
+ $td = new Integrations\TestData();
+ // @phpstan-ignore-next-line argument.type (the SDK documents the variadic values as an array)
+ $rule = $td->flag('flag-key')->ifMatch('key', 'user-key');
+ $td->update($rule->thenReturn(true));
+
+ $provider = new Provider('sdk-key', ['feature_requester' => $td, 'send_events' => false]);
+ $details = $provider->resolveBooleanValue('flag-key', false, new EvaluationContext('user-key'));
+
+ $metadata = $details->getMetadata();
+ $this->assertNotNull($metadata);
+ $this->assertSame(0, $metadata['variationIndex']);
+ $this->assertSame(0, $metadata['ruleIndex']);
+ $this->assertArrayHasKey('ruleId', $metadata);
+ $this->assertArrayNotHasKey('inExperiment', $metadata);
+ $this->assertArrayNotHasKey('prerequisiteKey', $metadata);
+ $this->assertArrayNotHasKey('bigSegmentsStatus', $metadata);
+ }
+}