Skip to content

Commit c1ac079

Browse files
authored
Merge pull request #27 from launchdarkly/eb/ch22308/all-flags-state
add new version of allFlags() that captures more metadata
2 parents 6a08f47 + 775f0a1 commit c1ac079

File tree

7 files changed

+386
-27
lines changed

7 files changed

+386
-27
lines changed

src/LaunchDarkly/EvalResult.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct($variation, $value, array $prerequisiteEvents)
2222
}
2323

2424
/**
25-
* @return int
25+
* @return int | null
2626
*/
2727
public function getVariation()
2828
{

src/LaunchDarkly/FeatureFlag.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ class FeatureFlag
2727
protected $_variations = array();
2828
/** @var bool */
2929
protected $_deleted = false;
30+
/** @var bool */
31+
protected $_trackEvents = false;
32+
/** @var int | null */
33+
protected $_debugEventsUntilDate = null;
34+
// Note, trackEvents and debugEventsUntilDate are not used in EventProcessor, because
35+
// the PHP client doesn't do summary events. However, we need to capture them in case
36+
// they want to pass the flag data to the front end with allFlagsState().
3037

3138
protected function __construct($key,
3239
$version,
@@ -38,7 +45,9 @@ protected function __construct($key,
3845
$fallthrough,
3946
$offVariation,
4047
array $variations,
41-
$deleted)
48+
$deleted,
49+
$trackEvents,
50+
$debugEventsUntilDate)
4251
{
4352
$this->_key = $key;
4453
$this->_version = $version;
@@ -51,6 +60,8 @@ protected function __construct($key,
5160
$this->_offVariation = $offVariation;
5261
$this->_variations = $variations;
5362
$this->_deleted = $deleted;
63+
$this->_trackEvents = $trackEvents;
64+
$this->_debugEventsUntilDate = $debugEventsUntilDate;
5465
}
5566

5667
public static function getDecoder()
@@ -67,7 +78,10 @@ public static function getDecoder()
6778
call_user_func(VariationOrRollout::getDecoder(), $v['fallthrough']),
6879
$v['offVariation'],
6980
$v['variations'] ?: [],
70-
$v['deleted']);
81+
$v['deleted'],
82+
isset($v['trackEvents']) && $v['trackEvents'],
83+
isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null
84+
);
7185
};
7286
}
7387

@@ -222,4 +236,20 @@ public function isDeleted()
222236
{
223237
return $this->_deleted;
224238
}
239+
240+
/**
241+
* @return boolean
242+
*/
243+
public function isTrackEvents()
244+
{
245+
return $this->_trackEvents;
246+
}
247+
248+
/**
249+
* @return int | null
250+
*/
251+
public function getDebugEventsUntilDate()
252+
{
253+
return $this->_debugEventsUntilDate;
254+
}
225255
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
/**
5+
* A snapshot of the state of all feature flags with regard to a specific user, generated by
6+
* calling LDClient.allFlagsState(). Serializing this object to JSON using json_encode(), or
7+
* the jsonSerialize() method, will produce the appropriate data structure for bootstrapping
8+
* the LaunchDarkly JavaScript client.
9+
*/
10+
class FeatureFlagsState implements \JsonSerializable
11+
{
12+
/** @var bool */
13+
protected $_valid = false;
14+
15+
/** @var array */
16+
protected $_flagValues;
17+
18+
/** @var array */
19+
protected $_flagMetadata;
20+
21+
public function __construct($valid, $flagValues = array(), $flagMetadata = array())
22+
{
23+
$this->_valid = $valid;
24+
$this->_flagValues = array();
25+
$this->_flagMetadata = array();
26+
}
27+
28+
/**
29+
* Used internally to build the state map.
30+
*/
31+
public function addFlag($flag, $evalResult)
32+
{
33+
$this->_flagValues[$flag->getKey()] = $evalResult->getValue();
34+
$meta = array();
35+
if (!is_null($evalResult->getVariation())) {
36+
$meta['variation'] = $evalResult->getVariation();
37+
}
38+
$meta['version'] = $flag->getVersion();
39+
$meta['trackEvents'] = $flag->isTrackEvents();
40+
if ($flag->getDebugEventsUntilDate()) {
41+
$meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate();
42+
}
43+
$this->_flagMetadata[$flag->getKey()] = $meta;
44+
}
45+
46+
/**
47+
* Returns true if this object contains a valid snapshot of feature flag state, or false if the
48+
* state could not be computed (for instance, because the client was offline or there was no user).
49+
* @return bool true if the state is valid
50+
*/
51+
public function isValid()
52+
{
53+
return $this->_valid;
54+
}
55+
56+
/**
57+
* Returns the value of an individual feature flag at the time the state was recorded.
58+
* @param $key string
59+
* @return mixed the flag's value; null if the flag returned the default value, or if there was no such flag
60+
*/
61+
public function getFlagValue($key)
62+
{
63+
return isset($this->_flagValues[$key]) ? $this->_flagValues[$key] : null;
64+
}
65+
66+
/**
67+
* Returns an associative array of flag keys to flag values. If a flag would have evaluated to the default
68+
* value, its value will be null.
69+
* <p>
70+
* Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
71+
* Instead, use jsonSerialize().
72+
* @return array an associative array of flag keys to JSON values
73+
*/
74+
public function toValuesMap()
75+
{
76+
return $this->_flagValues;
77+
}
78+
79+
/**
80+
* Returns a JSON representation of the entire state map (as an associative array), in the format used
81+
* by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end in
82+
* order to "bootstrap" the JavaScript client.
83+
* <p>
84+
* Note that calling json_encode() on a FeatureFlagsState object will automatically use the
85+
* jsonSerialize() method.
86+
* @return array an associative array suitable for passing as a JSON object
87+
*/
88+
public function jsonSerialize()
89+
{
90+
$ret = array_replace([], $this->_flagValues);
91+
$ret['$flagsState'] = $this->_flagMetadata;
92+
$ret['$valid'] = $this->_valid;
93+
return $ret;
94+
}
95+
}

src/LaunchDarkly/LDClient.php

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -260,38 +260,55 @@ public function identify($user)
260260
* This method will not send analytics events back to LaunchDarkly.
261261
* <p>
262262
* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service.
263-
*
263+
* @deprecated Use allFlagsState() instead. Current versions of the client-side SDK will not
264+
* generate analytics events correctly if you pass the result of allFlags().
264265
* @param $user LDUser the end user requesting the feature flags
265266
* @return array()|null Mapping of feature flag keys to their evaluated results for $user
266267
*/
267268
public function allFlags($user)
268269
{
269-
if (is_null($user) || is_null($user->getKey())) {
270-
$this->_logger->warn("allFlags called with null user or null/empty user key! Returning null");
270+
$state = $this->allFlagsState($user);
271+
if (!$state->isValid()) {
271272
return null;
272273
}
274+
return $state->toValuesMap();
275+
}
276+
277+
/**
278+
* Returns an object that encapsulates the state of all feature flags for a given user, including the flag
279+
* values and also metadata that can be used on the front end. This method does not send analytics events
280+
* back to LaunchDarkly.
281+
* <p>
282+
* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service.
283+
* To convert the state object into a JSON data structure, call its toJson() method.
284+
* @param $user LDUser the end user requesting the feature flags
285+
* @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid())
286+
*/
287+
public function allFlagsState($user)
288+
{
289+
if (is_null($user) || is_null($user->getKey())) {
290+
$this->_logger->warn("allFlagsState called with null user or null/empty user key! Returning empty state");
291+
return new FeatureFlagsState(false);
292+
}
273293
if ($this->isOffline()) {
274-
return null;
294+
return new FeatureFlagsState(false);
275295
}
276296
try {
277297
$flags = $this->_featureRequester->getAllFeatures();
278298
} catch (UnrecoverableHTTPStatusException $e) {
279299
$this->handleUnrecoverableError();
280-
return null;
300+
return new FeatureFlagsState(false);
281301
}
282302
if ($flags === null) {
283-
return null;
303+
return new FeatureFlagsState(false);
284304
}
285305

286-
/**
287-
* @param $flag FeatureFlag
288-
* @return mixed|null
289-
*/
290-
$eval = function ($flag) use ($user) {
291-
return $flag->evaluate($user, $this->_featureRequester)->getValue();
292-
};
293-
294-
return array_map($eval, $flags);
306+
$state = new FeatureFlagsState(true);
307+
foreach ($flags as $key => $flag) {
308+
$result = $flag->evaluate($user, $this->_featureRequester);
309+
$state->addFlag($flag, $result);
310+
}
311+
return $state;
295312
}
296313

297314
/** Generates an HMAC sha256 hash for use in Secure mode: https://github.com/launchdarkly/js-client#secure-mode

tests/FeatureFlagsStateTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
namespace LaunchDarkly\Tests;
3+
4+
use InvalidArgumentException;
5+
use LaunchDarkly\EvalResult;
6+
use LaunchDarkly\FeatureFlag;
7+
use LaunchDarkly\FeatureFlagsState;
8+
9+
class FeatureFlagsStateTest extends \PHPUnit_Framework_TestCase
10+
{
11+
private static $flag1Json = array(
12+
'key' => 'key1',
13+
'version' => 100,
14+
'deleted' => false,
15+
'on' => false,
16+
'targets' => array(),
17+
'prerequisites' => array(),
18+
'rules' => array(),
19+
'offVariation' => 0,
20+
'fallthrough' => array('variation' => 0),
21+
'variations' => array('value1'),
22+
'salt' => '',
23+
'trackEvents' => false
24+
);
25+
private static $flag2Json = array(
26+
'key' => 'key2',
27+
'version' => 200,
28+
'deleted' => false,
29+
'on' => false,
30+
'targets' => array(),
31+
'prerequisites' => array(),
32+
'rules' => array(),
33+
'offVariation' => 0,
34+
'fallthrough' => array('variation' => 0),
35+
'variations' => array('value2'),
36+
'salt' => '',
37+
'trackEvents' => true,
38+
'debugEventsUntilDate' => 1000
39+
);
40+
41+
public function testCanGetFlagValue()
42+
{
43+
$flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json);
44+
$state = new FeatureFlagsState(true);
45+
$state->addFlag($flag, new EvalResult(0, 'value1', array()));
46+
47+
$this->assertEquals('value1', $state->getFlagValue('key1'));
48+
}
49+
50+
public function testUnknownFlagReturnsNullValue()
51+
{
52+
$state = new FeatureFlagsState(true);
53+
54+
$this->assertNull($state->getFlagValue('key1'));
55+
}
56+
57+
public function testCanConvertToValuesMap()
58+
{
59+
$flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json);
60+
$flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json);
61+
$state = new FeatureFlagsState(true);
62+
$state->addFlag($flag1, new EvalResult(0, 'value1', array()));
63+
$state->addFlag($flag2, new EvalResult(0, 'value2', array()));
64+
65+
$expected = array('key1' => 'value1', 'key2' => 'value2');
66+
$this->assertEquals($expected, $state->toValuesMap());
67+
}
68+
69+
public function testCanConvertToJson()
70+
{
71+
$flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json);
72+
$flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json);
73+
$state = new FeatureFlagsState(true);
74+
$state->addFlag($flag1, new EvalResult(0, 'value1', array()));
75+
$state->addFlag($flag2, new EvalResult(1, 'value2', array()));
76+
77+
$expected = array(
78+
'key1' => 'value1',
79+
'key2' => 'value2',
80+
'$flagsState' => array(
81+
'key1' => array(
82+
'variation' => 0,
83+
'version' => 100,
84+
'trackEvents' => false
85+
),
86+
'key2' => array(
87+
'variation' => 1,
88+
'version' => 200,
89+
'trackEvents' => true,
90+
'debugEventsUntilDate' => 1000
91+
)
92+
),
93+
'$valid' => true
94+
);
95+
$this->assertEquals($expected, $state->jsonSerialize());
96+
}
97+
98+
public function testJsonEncodeUsesCustomSerializer()
99+
{
100+
$flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json);
101+
$flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json);
102+
$state = new FeatureFlagsState(true);
103+
$state->addFlag($flag1, new EvalResult(0, 'value1', array()));
104+
$state->addFlag($flag2, new EvalResult(1, 'value2', array()));
105+
106+
$expected = $state->jsonSerialize();
107+
$json = json_encode($state);
108+
$decoded = json_decode($json, true);
109+
$this->assertEquals($expected, $decoded);
110+
}
111+
}

0 commit comments

Comments
 (0)