Skip to content

Commit 6f220fa

Browse files
authored
feat: Add migration variation method to client (#132)
1 parent 2e5751c commit 6f220fa

File tree

3 files changed

+121
-10
lines changed

3 files changed

+121
-10
lines changed

src/LaunchDarkly/EvaluationReason.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ class EvaluationReason implements \JsonSerializable
8383
*/
8484
const EXCEPTION_ERROR = 'EXCEPTION';
8585

86+
/**
87+
* A possible value for getErrorKind(): indicates the value of the
88+
* evaluation did not match the PHP type expected.
89+
*/
90+
91+
const WRONG_TYPE_ERROR = 'WRONG_TYPE';
92+
8693
private string $_kind;
8794
private ?string $_errorKind;
8895
private ?int $_ruleIndex;

src/LaunchDarkly/LDClient.php

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use LaunchDarkly\Impl\UnrecoverableHTTPStatusException;
1616
use LaunchDarkly\Impl\Util;
1717
use LaunchDarkly\Integrations\Guzzle;
18+
use LaunchDarkly\Migrations\OpTracker;
19+
use LaunchDarkly\Migrations\Stage;
1820
use LaunchDarkly\Subsystems\FeatureRequester;
1921
use LaunchDarkly\Types\ApplicationInfo;
2022
use Monolog\Handler\ErrorLogHandler;
@@ -199,7 +201,7 @@ private function getFeatureRequester(string $sdkKey, array $options): FeatureReq
199201
*/
200202
public function variation(string $key, LDContext|LDUser $context, mixed $defaultValue = false): mixed
201203
{
202-
$detail = $this->variationDetailInternal($key, $context, $defaultValue, $this->_eventFactoryDefault);
204+
$detail = $this->variationDetailInternal($key, $context, $defaultValue, $this->_eventFactoryDefault)['detail'];
203205
return $detail->getValue();
204206
}
205207

@@ -219,7 +221,58 @@ public function variation(string $key, LDContext|LDUser $context, mixed $default
219221
*/
220222
public function variationDetail(string $key, LDContext|LDUser $context, mixed $defaultValue = false): EvaluationDetail
221223
{
222-
return $this->variationDetailInternal($key, $context, $defaultValue, $this->_eventFactoryWithReasons);
224+
return $this->variationDetailInternal($key, $context, $defaultValue, $this->_eventFactoryWithReasons)['detail'];
225+
}
226+
227+
/**
228+
* This method returns the migration stage of the migration feature flag
229+
* for the given evaluation context.
230+
*
231+
* This method returns the default stage if there is an error or the flag
232+
* does not exist. If the default stage is not a valid stage, then a
233+
* default stage of {@see Stage::OFF} will be used
234+
* instead.
235+
*
236+
* @psalm-return array{'stage': Stage, 'tracker': OpTracker}
237+
*/
238+
public function migrationVariation(string $key, LDContext|LDUser $context, Stage $defaultStage): array
239+
{
240+
$result = $this->variationDetailInternal($key, $context, $defaultStage->value, $this->_eventFactoryDefault);
241+
/** @var EvaluationDetail $detail */
242+
$detail = $result['detail'];
243+
/** @var ?FeatureFlag $flag */
244+
$flag = $result['flag'];
245+
246+
$valueAsString = Stage::tryFrom($detail->getValue());
247+
248+
if ($valueAsString !== null) {
249+
$tracker = new OpTracker(
250+
$this->_logger,
251+
$key,
252+
$flag,
253+
$context,
254+
$detail,
255+
$defaultStage
256+
);
257+
258+
return ['stage' => $valueAsString, 'tracker' => $tracker];
259+
}
260+
261+
$detail = new EvaluationDetail(
262+
$defaultStage->value,
263+
null,
264+
EvaluationReason::error(EvaluationReason::WRONG_TYPE_ERROR)
265+
);
266+
$tracker = new OpTracker(
267+
$this->_logger,
268+
$key,
269+
$flag,
270+
$context,
271+
$detail,
272+
$defaultStage
273+
);
274+
275+
return ['stage' => $defaultStage, 'tracker' => $tracker];
223276
}
224277

225278
/**
@@ -228,9 +281,9 @@ public function variationDetail(string $key, LDContext|LDUser $context, mixed $d
228281
* @param mixed $default
229282
* @param EventFactory $eventFactory
230283
*
231-
* @return EvaluationDetail
284+
* @psalm-return array{'detail': EvaluationDetail, 'flag': ?FeatureFlag}
232285
*/
233-
private function variationDetailInternal(string $key, LDContext|LDUser $contextOrUser, mixed $default, EventFactory $eventFactory): EvaluationDetail
286+
private function variationDetailInternal(string $key, LDContext|LDUser $contextOrUser, mixed $default, EventFactory $eventFactory): array
234287
{
235288
$context = $contextOrUser instanceof LDUser ? LDContext::fromUser($contextOrUser) : $contextOrUser;
236289
$default = $this->_get_default($key, $default);
@@ -258,25 +311,26 @@ private function variationDetailInternal(string $key, LDContext|LDUser $contextO
258311
]
259312
);
260313

261-
return $result;
314+
return ['detail' => $result, 'flag' => null];
262315
}
263316

264317
if ($this->_offline) {
265-
return $errorDetail(EvaluationReason::CLIENT_NOT_READY_ERROR);
318+
return ['detail' => $errorDetail(EvaluationReason::CLIENT_NOT_READY_ERROR), 'flag' => null];
266319
}
267320

321+
$flag = null;
268322
try {
269323
try {
270324
$flag = $this->_featureRequester->getFeature($key);
271325
} catch (UnrecoverableHTTPStatusException $e) {
272326
$this->handleUnrecoverableError();
273-
return $errorDetail(EvaluationReason::EXCEPTION_ERROR);
327+
return ['detail' => $errorDetail(EvaluationReason::EXCEPTION_ERROR), 'flag' => null];
274328
}
275329

276330
if (is_null($flag)) {
277331
$result = $errorDetail(EvaluationReason::FLAG_NOT_FOUND_ERROR);
278332
$sendEvent(new EvalResult($result, false), null);
279-
return $result;
333+
return ['detail' => $result, 'flag' => null];
280334
}
281335
$evalResult = $this->_evaluator->evaluate(
282336
$flag,
@@ -298,12 +352,12 @@ function (PrerequisiteEvaluationRecord $pe) use ($context, $eventFactory) {
298352
$evalResult = new EvalResult($detail, $evalResult->isForceReasonTracking());
299353
}
300354
$sendEvent($evalResult, $flag);
301-
return $detail;
355+
return ['detail' => $detail, 'flag' => $flag];
302356
} catch (\Exception $e) {
303357
Util::logExceptionAtErrorLevel($this->_logger, $e, "Unexpected error evaluating flag $key");
304358
$result = $errorDetail(EvaluationReason::EXCEPTION_ERROR);
305359
$sendEvent(new EvalResult($result, false), null);
306-
return $result;
360+
return ['detail' => $result, 'flag' => $flag];
307361
}
308362
}
309363

tests/LDClientTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use LaunchDarkly\LDContext;
1010
use LaunchDarkly\LDUser;
1111
use LaunchDarkly\LDUserBuilder;
12+
use LaunchDarkly\Migrations\OpTracker;
13+
use LaunchDarkly\Migrations\Stage;
1214
use Psr\Log\LoggerInterface;
1315

1416
class LDClientTest extends \PHPUnit\Framework\TestCase
@@ -654,4 +656,52 @@ public function testLoggerInterfaceWarn()
654656

655657
$client->variation('MyFeature', $invalidContext);
656658
}
659+
660+
public function testUsesDefaultIfFlagIsNotFound(): void
661+
{
662+
$client = $this->makeClient();
663+
$result = $client->migrationVariation('unknown-flag-key', LDContext::create('userkey'), Stage::LIVE);
664+
665+
$this->assertEquals(Stage::LIVE, $result['stage']);
666+
$this->assertInstanceOf(OpTracker::class, $result['tracker']);
667+
}
668+
669+
public function testUsesDefaultIfFlagReturnsInvalidStage(): void
670+
{
671+
$flag = $this->makeOffFlagWithValue('feature', 'invalid stage value');
672+
$this->mockRequester->addFlag($flag);
673+
$client = $this->makeClient();
674+
675+
$result = $client->migrationVariation('feature', LDContext::create('userkey'), Stage::LIVE);
676+
677+
$this->assertEquals(Stage::LIVE, $result['stage']);
678+
$this->assertInstanceOf(OpTracker::class, $result['tracker']);
679+
}
680+
681+
public function stageProvider(): array
682+
{
683+
return [
684+
[Stage::OFF],
685+
[Stage::DUALWRITE],
686+
[Stage::SHADOW],
687+
[Stage::LIVE],
688+
[Stage::RAMPDOWN],
689+
[Stage::COMPLETE],
690+
];
691+
}
692+
693+
/**
694+
* @dataProvider stageProvider
695+
*/
696+
public function testCanDetermineCorrectStage(Stage $stage): void
697+
{
698+
$flag = $this->makeOffFlagWithValue('feature', $stage->value);
699+
$this->mockRequester->addFlag($flag);
700+
$client = $this->makeClient();
701+
702+
$result = $client->migrationVariation('feature', LDContext::create('userkey'), Stage::OFF);
703+
704+
$this->assertEquals($stage, $result['stage']);
705+
$this->assertInstanceOf(OpTracker::class, $result['tracker']);
706+
}
657707
}

0 commit comments

Comments
 (0)