Skip to content

Commit 0d4fc33

Browse files
committed
add new version of allFlags() that captures more metadata
1 parent 6a08f47 commit 0d4fc33

File tree

6 files changed

+270
-26
lines changed

6 files changed

+270
-26
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+
$v['trackEvents'],
83+
$v['debugEventsUntilDate']
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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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().
7+
*/
8+
class FeatureFlagsState
9+
{
10+
/** @var bool */
11+
protected $_valid = false;
12+
13+
/** @var array */
14+
protected $_flagValues;
15+
16+
/** @var array */
17+
protected $_flagMetadata;
18+
19+
public function __construct($valid, $flagValues = array(), $flagMetadata = array())
20+
{
21+
$this->_valid = $valid;
22+
$this->_flagValues = array();
23+
$this->_flagMetadata = array();
24+
}
25+
26+
/**
27+
* Used internally to build the state map.
28+
*/
29+
public function addFlag($flag, $evalResult)
30+
{
31+
$this->_flagValues[$flag->getKey()] = $evalResult->getValue();
32+
$meta = array();
33+
if (!is_null($evalResult->getVariation())) {
34+
$meta['variation'] = $evalResult->getVariation();
35+
}
36+
$meta['version'] = $flag->getVersion();
37+
$meta['trackEvents'] = $flag->isTrackEvents();
38+
if ($flag->getDebugEventsUntilDate()) {
39+
$meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate();
40+
}
41+
$this->_flagMetadata[$flag->getKey()] = $meta;
42+
}
43+
44+
/**
45+
* Returns true if this object contains a valid snapshot of feature flag state, or false if the
46+
* state could not be computed (for instance, because the client was offline or there was no user).
47+
* @return bool true if the state is valid
48+
*/
49+
public function isValid()
50+
{
51+
return $this->_valid;
52+
}
53+
54+
/**
55+
* Returns the value of an individual feature flag at the time the state was recorded.
56+
* @param $key string
57+
* @return mixed the flag's value; null if the flag returned the default value, or if there was no such flag
58+
*/
59+
public function getFlagValue($key)
60+
{
61+
return $this->_flagValues[$key];
62+
}
63+
64+
/**
65+
* Returns an associative array of flag keys to flag values. If a flag would have evaluated to the default
66+
* value, its value will be null.
67+
* <p>
68+
* Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
69+
* Instead, use toJson().
70+
* @return array an associative array of flag keys to JSON values
71+
*/
72+
public function toValuesMap()
73+
{
74+
return $this->_flagValues;
75+
}
76+
77+
/**
78+
* Returns a JSON representation of the entire state map (as an associative array), in the format used
79+
* by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end in
80+
* order to "bootstrap" the JavaScript client.
81+
*
82+
* @return array an associative array suitable for passing as a JSON object
83+
*/
84+
public function toJson()
85+
{
86+
$ret = array_replace([], $this->_flagValues);
87+
$ret['$flagsState'] = $this->_flagMetadata;
88+
return $ret;
89+
}
90+
}

src/LaunchDarkly/LDClient.php

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -261,37 +261,56 @@ public function identify($user)
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.
263263
*
264+
* @deprecated Use allFlagsState() instead. Current versions of the client-side SDK will not
265+
* generate analytics events correctly if you pass the result of allFlags().
264266
* @param $user LDUser the end user requesting the feature flags
265267
* @return array()|null Mapping of feature flag keys to their evaluated results for $user
266268
*/
267269
public function allFlags($user)
268270
{
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");
271+
$state = $this->allFlagsState($user);
272+
if (!$state->isValid()) {
271273
return null;
272274
}
275+
return $state->toValuesMap();
276+
}
277+
278+
/**
279+
* Returns an object that encapsulates the state of all feature flags for a given user, including the flag
280+
* values and also metadata that can be used on the front end. This method does not send analytics events
281+
* back to LaunchDarkly.
282+
* <p>
283+
* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service.
284+
* To convert the state object into a JSON data structure, call its toJson() method.
285+
*
286+
* @param $user LDUser the end user requesting the feature flags
287+
* @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid())
288+
*/
289+
public function allFlagsState($user)
290+
{
291+
if (is_null($user) || is_null($user->getKey())) {
292+
$this->_logger->warn("allFlagsState called with null user or null/empty user key! Returning empty state");
293+
return new FeatureFlagsState(false);
294+
}
273295
if ($this->isOffline()) {
274-
return null;
296+
return new FeatureFlagsState(false);
275297
}
276298
try {
277299
$flags = $this->_featureRequester->getAllFeatures();
278300
} catch (UnrecoverableHTTPStatusException $e) {
279301
$this->handleUnrecoverableError();
280-
return null;
302+
return new FeatureFlagsState(false);
281303
}
282304
if ($flags === null) {
283-
return null;
305+
return new FeatureFlagsState(false);
284306
}
285307

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);
308+
$state = new FeatureFlagsState(true);
309+
foreach ($flags as $key => $flag) {
310+
$result = $flag->evaluate($user, $this->_featureRequester);
311+
$state->addFlag($flag, $result);
312+
}
313+
return $state;
295314
}
296315

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

tests/LDClientTest.php

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace LaunchDarkly\Tests;
33

44
use InvalidArgumentException;
5+
use LaunchDarkly\FeatureFlag;
56
use LaunchDarkly\FeatureRequester;
67
use LaunchDarkly\LDClient;
78
use LaunchDarkly\LDUser;
@@ -15,9 +16,38 @@ public function testDefaultCtor()
1516
$this->assertInstanceOf(LDClient::class, new LDClient("BOGUS_SDK_KEY"));
1617
}
1718

18-
public function testToggleDefault()
19+
public function testVariationReturnsFlagValue()
1920
{
20-
MockFeatureRequester::$val = null;
21+
$flagJson = array(
22+
'key' => 'feature',
23+
'version' => 100,
24+
'deleted' => false,
25+
'on' => false,
26+
'targets' => array(),
27+
'prerequisites' => array(),
28+
'rules' => array(),
29+
'offVariation' => 1,
30+
'fallthrough' => array('variation' => 0),
31+
'variations' => array('fall', 'off', 'on'),
32+
'salt' => ''
33+
);
34+
$flag = FeatureFlag::decode($flagJson);
35+
36+
MockFeatureRequester::$flags = array('feature' => $flag);
37+
$client = new LDClient("someKey", array(
38+
'feature_requester_class' => MockFeatureRequester::class,
39+
'events' => false
40+
));
41+
42+
$builder = new LDUserBuilder(3);
43+
$user = $builder->build();
44+
$value = $client->variation('feature', $user, 'default');
45+
$this->assertEquals('off', $value);
46+
}
47+
48+
public function testVariationReturnsDefaultForUnknownFlag()
49+
{
50+
MockFeatureRequester::$flags = array();
2151
$client = new LDClient("someKey", array(
2252
'feature_requester_class' => MockFeatureRequester::class,
2353
'events' => false
@@ -28,9 +58,9 @@ public function testToggleDefault()
2858
$this->assertEquals('argdef', $client->variation('foo', $user, 'argdef'));
2959
}
3060

31-
public function testToggleFromArray()
61+
public function testVariationReturnsDefaultFromConfigurationForUnknownFlag()
3262
{
33-
MockFeatureRequester::$val = null;
63+
MockFeatureRequester::$flags = array();
3464
$client = new LDClient("someKey", array(
3565
'feature_requester_class' => MockFeatureRequester::class,
3666
'events' => false,
@@ -42,9 +72,9 @@ public function testToggleFromArray()
4272
$this->assertEquals('fromarray', $client->variation('foo', $user, 'argdef'));
4373
}
4474

45-
public function testToggleEvent()
75+
public function testVariationSendsEvent()
4676
{
47-
MockFeatureRequester::$val = null;
77+
MockFeatureRequester::$flags = array();
4878
$client = new LDClient("someKey", array(
4979
'feature_requester_class' => MockFeatureRequester::class,
5080
'events' => true
@@ -58,6 +88,81 @@ public function testToggleEvent()
5888
$this->assertEquals(1, sizeof($queue));
5989
}
6090

91+
public function testAllFlagsReturnsFlagValues()
92+
{
93+
$flagJson = array(
94+
'key' => 'feature',
95+
'version' => 100,
96+
'deleted' => false,
97+
'on' => false,
98+
'targets' => array(),
99+
'prerequisites' => array(),
100+
'rules' => array(),
101+
'offVariation' => 1,
102+
'fallthrough' => array('variation' => 0),
103+
'variations' => array('fall', 'off', 'on'),
104+
'salt' => ''
105+
);
106+
$flag = FeatureFlag::decode($flagJson);
107+
108+
MockFeatureRequester::$flags = array('feature' => $flag);
109+
$client = new LDClient("someKey", array(
110+
'feature_requester_class' => MockFeatureRequester::class,
111+
'events' => false
112+
));
113+
114+
$builder = new LDUserBuilder(3);
115+
$user = $builder->build();
116+
$values = $client->allFlags($user);
117+
118+
$this->assertEquals(array('feature' => 'off'), $values);
119+
}
120+
121+
public function testAllFlagsStateReturnsState()
122+
{
123+
$flagJson = array(
124+
'key' => 'feature',
125+
'version' => 100,
126+
'deleted' => false,
127+
'on' => false,
128+
'targets' => array(),
129+
'prerequisites' => array(),
130+
'rules' => array(),
131+
'offVariation' => 1,
132+
'fallthrough' => array('variation' => 0),
133+
'variations' => array('fall', 'off', 'on'),
134+
'salt' => '',
135+
'trackEvents' => true,
136+
'debugEventsUntilDate' => 1000
137+
);
138+
$flag = FeatureFlag::decode($flagJson);
139+
140+
MockFeatureRequester::$flags = array('feature' => $flag);
141+
$client = new LDClient("someKey", array(
142+
'feature_requester_class' => MockFeatureRequester::class,
143+
'events' => false
144+
));
145+
146+
$builder = new LDUserBuilder(3);
147+
$user = $builder->build();
148+
$state = $client->allFlagsState($user);
149+
150+
$this->assertTrue($state->isValid());
151+
$this->assertEquals(array('feature' => 'off'), $state->toValuesMap());
152+
$expectedState = array(
153+
'feature' => 'off',
154+
'$flagsState' => array(
155+
'feature' => array(
156+
'variation' => 1,
157+
'version' => 100,
158+
'trackEvents' => true,
159+
'debugEventsUntilDate' => 1000
160+
)
161+
)
162+
);
163+
$this->assertEquals($expectedState, $state->toJson());
164+
}
165+
61166
public function testOnlyValidFeatureRequester()
62167
{
63168
$this->setExpectedException(InvalidArgumentException::class);

0 commit comments

Comments
 (0)