From aa34e8b4a9c6a0b5dac74f65f7b5d6f12d26ef36 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 16 Jul 2018 16:05:35 -0700 Subject: [PATCH 01/10] fix version string --- CHANGELOG.md | 4 ++++ VERSION | 2 +- src/LaunchDarkly/LDClient.php | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f9560107..bf3863c9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [3.2.1] - 2018-07-16 +### Fixed: +- The `LDClient::VERSION` constant has been fixed to report the current version. In the previous release, it was still set to 3.1.0. + ## [3.2.0] - 2018-06-26 ### Changed: - The client now treats most HTTP 4xx errors as unrecoverable: that is, after receiving such an error, it will take the client offline (for the lifetime of the client instance, which in most PHP applications is just the current request-response cycle). This is because such errors indicate either a configuration problem (invalid SDK key) or a bug, which is not likely to resolve without a restart or an upgrade. This does not apply if the error is 400, 408, 429, or any 5xx error. diff --git a/VERSION b/VERSION index 944880fa1..e4604e3af 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +3.2.1 diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 4d2ac10b1..7ab75fe69 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -12,7 +12,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '3.1.0'; + const VERSION = '3.2.1'; /** @var string */ protected $_sdkKey; From 0d4fc33e9a381e4efe8eccd0ba3227061fd89cec Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 20 Aug 2018 16:12:18 -0700 Subject: [PATCH 02/10] add new version of allFlags() that captures more metadata --- src/LaunchDarkly/EvalResult.php | 2 +- src/LaunchDarkly/FeatureFlag.php | 34 ++++++- src/LaunchDarkly/FeatureFlagsState.php | 90 +++++++++++++++++++ src/LaunchDarkly/LDClient.php | 47 +++++++--- tests/LDClientTest.php | 117 +++++++++++++++++++++++-- tests/MockFeatureRequester.php | 6 +- 6 files changed, 270 insertions(+), 26 deletions(-) create mode 100644 src/LaunchDarkly/FeatureFlagsState.php diff --git a/src/LaunchDarkly/EvalResult.php b/src/LaunchDarkly/EvalResult.php index f2fff0a4b..f692abac1 100644 --- a/src/LaunchDarkly/EvalResult.php +++ b/src/LaunchDarkly/EvalResult.php @@ -22,7 +22,7 @@ public function __construct($variation, $value, array $prerequisiteEvents) } /** - * @return int + * @return int | null */ public function getVariation() { diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 2c94c2498..c6b433052 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -27,6 +27,13 @@ class FeatureFlag protected $_variations = array(); /** @var bool */ protected $_deleted = false; + /** @var bool */ + protected $_trackEvents = false; + /** @var int | null */ + protected $_debugEventsUntilDate = null; + // Note, trackEvents and debugEventsUntilDate are not used in EventProcessor, because + // the PHP client doesn't do summary events. However, we need to capture them in case + // they want to pass the flag data to the front end with allFlagsState(). protected function __construct($key, $version, @@ -38,7 +45,9 @@ protected function __construct($key, $fallthrough, $offVariation, array $variations, - $deleted) + $deleted, + $trackEvents, + $debugEventsUntilDate) { $this->_key = $key; $this->_version = $version; @@ -51,6 +60,8 @@ protected function __construct($key, $this->_offVariation = $offVariation; $this->_variations = $variations; $this->_deleted = $deleted; + $this->_trackEvents = $trackEvents; + $this->_debugEventsUntilDate = $debugEventsUntilDate; } public static function getDecoder() @@ -67,7 +78,10 @@ public static function getDecoder() call_user_func(VariationOrRollout::getDecoder(), $v['fallthrough']), $v['offVariation'], $v['variations'] ?: [], - $v['deleted']); + $v['deleted'], + $v['trackEvents'], + $v['debugEventsUntilDate'] + ); }; } @@ -222,4 +236,20 @@ public function isDeleted() { return $this->_deleted; } + + /** + * @return boolean + */ + public function isTrackEvents() + { + return $this->_trackEvents; + } + + /** + * @return int | null + */ + public function getDebugEventsUntilDate() + { + return $this->_debugEventsUntilDate; + } } diff --git a/src/LaunchDarkly/FeatureFlagsState.php b/src/LaunchDarkly/FeatureFlagsState.php new file mode 100644 index 000000000..acf2b221c --- /dev/null +++ b/src/LaunchDarkly/FeatureFlagsState.php @@ -0,0 +1,90 @@ +_valid = $valid; + $this->_flagValues = array(); + $this->_flagMetadata = array(); + } + + /** + * Used internally to build the state map. + */ + public function addFlag($flag, $evalResult) + { + $this->_flagValues[$flag->getKey()] = $evalResult->getValue(); + $meta = array(); + if (!is_null($evalResult->getVariation())) { + $meta['variation'] = $evalResult->getVariation(); + } + $meta['version'] = $flag->getVersion(); + $meta['trackEvents'] = $flag->isTrackEvents(); + if ($flag->getDebugEventsUntilDate()) { + $meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate(); + } + $this->_flagMetadata[$flag->getKey()] = $meta; + } + + /** + * Returns true if this object contains a valid snapshot of feature flag state, or false if the + * state could not be computed (for instance, because the client was offline or there was no user). + * @return bool true if the state is valid + */ + public function isValid() + { + return $this->_valid; + } + + /** + * Returns the value of an individual feature flag at the time the state was recorded. + * @param $key string + * @return mixed the flag's value; null if the flag returned the default value, or if there was no such flag + */ + public function getFlagValue($key) + { + return $this->_flagValues[$key]; + } + + /** + * Returns an associative array of flag keys to flag values. If a flag would have evaluated to the default + * value, its value will be null. + *

+ * Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client. + * Instead, use toJson(). + * @return array an associative array of flag keys to JSON values + */ + public function toValuesMap() + { + return $this->_flagValues; + } + + /** + * Returns a JSON representation of the entire state map (as an associative array), in the format used + * by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end in + * order to "bootstrap" the JavaScript client. + * + * @return array an associative array suitable for passing as a JSON object + */ + public function toJson() + { + $ret = array_replace([], $this->_flagValues); + $ret['$flagsState'] = $this->_flagMetadata; + return $ret; + } +} diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 7ab75fe69..e86e1b874 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -261,37 +261,56 @@ public function identify($user) *

* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. * + * @deprecated Use allFlagsState() instead. Current versions of the client-side SDK will not + * generate analytics events correctly if you pass the result of allFlags(). * @param $user LDUser the end user requesting the feature flags * @return array()|null Mapping of feature flag keys to their evaluated results for $user */ public function allFlags($user) { - if (is_null($user) || is_null($user->getKey())) { - $this->_logger->warn("allFlags called with null user or null/empty user key! Returning null"); + $state = $this->allFlagsState($user); + if (!$state->isValid()) { return null; } + return $state->toValuesMap(); + } + + /** + * Returns an object that encapsulates the state of all feature flags for a given user, including the flag + * values and also metadata that can be used on the front end. This method does not send analytics events + * back to LaunchDarkly. + *

+ * The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. + * To convert the state object into a JSON data structure, call its toJson() method. + * + * @param $user LDUser the end user requesting the feature flags + * @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid()) + */ + public function allFlagsState($user) + { + if (is_null($user) || is_null($user->getKey())) { + $this->_logger->warn("allFlagsState called with null user or null/empty user key! Returning empty state"); + return new FeatureFlagsState(false); + } if ($this->isOffline()) { - return null; + return new FeatureFlagsState(false); } try { $flags = $this->_featureRequester->getAllFeatures(); } catch (UnrecoverableHTTPStatusException $e) { $this->handleUnrecoverableError(); - return null; + return new FeatureFlagsState(false); } if ($flags === null) { - return null; + return new FeatureFlagsState(false); } - /** - * @param $flag FeatureFlag - * @return mixed|null - */ - $eval = function ($flag) use ($user) { - return $flag->evaluate($user, $this->_featureRequester)->getValue(); - }; - - return array_map($eval, $flags); + $state = new FeatureFlagsState(true); + foreach ($flags as $key => $flag) { + $result = $flag->evaluate($user, $this->_featureRequester); + $state->addFlag($flag, $result); + } + return $state; } /** Generates an HMAC sha256 hash for use in Secure mode: https://github.com/launchdarkly/js-client#secure-mode diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index b5b36af6c..52d5493b2 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -2,6 +2,7 @@ namespace LaunchDarkly\Tests; use InvalidArgumentException; +use LaunchDarkly\FeatureFlag; use LaunchDarkly\FeatureRequester; use LaunchDarkly\LDClient; use LaunchDarkly\LDUser; @@ -15,9 +16,38 @@ public function testDefaultCtor() $this->assertInstanceOf(LDClient::class, new LDClient("BOGUS_SDK_KEY")); } - public function testToggleDefault() + public function testVariationReturnsFlagValue() { - MockFeatureRequester::$val = null; + $flagJson = array( + 'key' => 'feature', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $value = $client->variation('feature', $user, 'default'); + $this->assertEquals('off', $value); + } + + public function testVariationReturnsDefaultForUnknownFlag() + { + MockFeatureRequester::$flags = array(); $client = new LDClient("someKey", array( 'feature_requester_class' => MockFeatureRequester::class, 'events' => false @@ -28,9 +58,9 @@ public function testToggleDefault() $this->assertEquals('argdef', $client->variation('foo', $user, 'argdef')); } - public function testToggleFromArray() + public function testVariationReturnsDefaultFromConfigurationForUnknownFlag() { - MockFeatureRequester::$val = null; + MockFeatureRequester::$flags = array(); $client = new LDClient("someKey", array( 'feature_requester_class' => MockFeatureRequester::class, 'events' => false, @@ -42,9 +72,9 @@ public function testToggleFromArray() $this->assertEquals('fromarray', $client->variation('foo', $user, 'argdef')); } - public function testToggleEvent() + public function testVariationSendsEvent() { - MockFeatureRequester::$val = null; + MockFeatureRequester::$flags = array(); $client = new LDClient("someKey", array( 'feature_requester_class' => MockFeatureRequester::class, 'events' => true @@ -58,6 +88,81 @@ public function testToggleEvent() $this->assertEquals(1, sizeof($queue)); } + public function testAllFlagsReturnsFlagValues() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $values = $client->allFlags($user); + + $this->assertEquals(array('feature' => 'off'), $values); + } + + public function testAllFlagsStateReturnsState() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '', + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ); + $flag = FeatureFlag::decode($flagJson); + + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $state = $client->allFlagsState($user); + + $this->assertTrue($state->isValid()); + $this->assertEquals(array('feature' => 'off'), $state->toValuesMap()); + $expectedState = array( + 'feature' => 'off', + '$flagsState' => array( + 'feature' => array( + 'variation' => 1, + 'version' => 100, + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ) + ) + ); + $this->assertEquals($expectedState, $state->toJson()); + } + public function testOnlyValidFeatureRequester() { $this->setExpectedException(InvalidArgumentException::class); diff --git a/tests/MockFeatureRequester.php b/tests/MockFeatureRequester.php index b2e6cf847..5ecae1fe8 100644 --- a/tests/MockFeatureRequester.php +++ b/tests/MockFeatureRequester.php @@ -5,7 +5,7 @@ class MockFeatureRequester implements FeatureRequester { - public static $val = null; + public static $flags = array(); public function __construct($baseurl, $key, $options) { @@ -13,7 +13,7 @@ public function __construct($baseurl, $key, $options) public function getFeature($key) { - return self::$val; + return self::$flags[$key]; } public function getSegment($key) @@ -23,6 +23,6 @@ public function getSegment($key) public function getAllFeatures() { - return null; + return self::$flags; } } From 0f769695d9580a31f06b6343855b4731f0f00454 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 20 Aug 2018 16:16:11 -0700 Subject: [PATCH 03/10] linter --- src/LaunchDarkly/FeatureFlagsState.php | 3 +-- src/LaunchDarkly/LDClient.php | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlagsState.php b/src/LaunchDarkly/FeatureFlagsState.php index acf2b221c..6e5c8ac01 100644 --- a/src/LaunchDarkly/FeatureFlagsState.php +++ b/src/LaunchDarkly/FeatureFlagsState.php @@ -35,7 +35,7 @@ public function addFlag($flag, $evalResult) } $meta['version'] = $flag->getVersion(); $meta['trackEvents'] = $flag->isTrackEvents(); - if ($flag->getDebugEventsUntilDate()) { + if ($flag->getDebugEventsUntilDate()) { $meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate(); } $this->_flagMetadata[$flag->getKey()] = $meta; @@ -78,7 +78,6 @@ public function toValuesMap() * Returns a JSON representation of the entire state map (as an associative array), in the format used * by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end in * order to "bootstrap" the JavaScript client. - * * @return array an associative array suitable for passing as a JSON object */ public function toJson() diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index e86e1b874..f528d25a3 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -260,7 +260,6 @@ public function identify($user) * This method will not send analytics events back to LaunchDarkly. *

* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. - * * @deprecated Use allFlagsState() instead. Current versions of the client-side SDK will not * generate analytics events correctly if you pass the result of allFlags(). * @param $user LDUser the end user requesting the feature flags @@ -282,7 +281,6 @@ public function allFlags($user) *

* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. * To convert the state object into a JSON data structure, call its toJson() method. - * * @param $user LDUser the end user requesting the feature flags * @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid()) */ From 9023ea673deaff0065c790cf5025826105c7b3ea Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 20 Aug 2018 17:20:44 -0700 Subject: [PATCH 04/10] missing array key guard --- src/LaunchDarkly/FeatureFlagsState.php | 2 +- tests/MockFeatureRequester.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlagsState.php b/src/LaunchDarkly/FeatureFlagsState.php index 6e5c8ac01..a897f5e50 100644 --- a/src/LaunchDarkly/FeatureFlagsState.php +++ b/src/LaunchDarkly/FeatureFlagsState.php @@ -58,7 +58,7 @@ public function isValid() */ public function getFlagValue($key) { - return $this->_flagValues[$key]; + return isset($this->_flagValues[$key]) ? $this->_flagValues[$key] : null; } /** diff --git a/tests/MockFeatureRequester.php b/tests/MockFeatureRequester.php index 5ecae1fe8..995ec41bc 100644 --- a/tests/MockFeatureRequester.php +++ b/tests/MockFeatureRequester.php @@ -13,7 +13,7 @@ public function __construct($baseurl, $key, $options) public function getFeature($key) { - return self::$flags[$key]; + return isset(self::$flags[$key]) ? self::$flags[$key] : null; } public function getSegment($key) From e215364dc04b6d77bcfd15a6b9ba78657f021ce5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 20 Aug 2018 17:27:34 -0700 Subject: [PATCH 05/10] missing array key guards --- src/LaunchDarkly/FeatureFlag.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index c6b433052..f617cb672 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -79,8 +79,8 @@ public static function getDecoder() $v['offVariation'], $v['variations'] ?: [], $v['deleted'], - $v['trackEvents'], - $v['debugEventsUntilDate'] + isset($v['trackEvents']) && $v['trackEvents'], + isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null ); }; } From 30e9e3cf8a73ef5006642cb52a800baed6f30aef Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 20 Aug 2018 20:41:32 -0700 Subject: [PATCH 06/10] use the standard method for specifying custom JSON serialization --- src/LaunchDarkly/FeatureFlagsState.php | 14 +++- tests/FeatureFlagsStateTest.php | 111 +++++++++++++++++++++++++ tests/LDClientTest.php | 5 +- 3 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 tests/FeatureFlagsStateTest.php diff --git a/src/LaunchDarkly/FeatureFlagsState.php b/src/LaunchDarkly/FeatureFlagsState.php index a897f5e50..87c84d2b4 100644 --- a/src/LaunchDarkly/FeatureFlagsState.php +++ b/src/LaunchDarkly/FeatureFlagsState.php @@ -3,9 +3,11 @@ /** * A snapshot of the state of all feature flags with regard to a specific user, generated by - * calling LDClient.allFlagsState(). + * calling LDClient.allFlagsState(). Serializing this object to JSON using json_encode(), or + * the jsonSerialize() method, will produce the appropriate data structure for bootstrapping + * the LaunchDarkly JavaScript client. */ -class FeatureFlagsState +class FeatureFlagsState implements \JsonSerializable { /** @var bool */ protected $_valid = false; @@ -66,7 +68,7 @@ public function getFlagValue($key) * value, its value will be null. *

* Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client. - * Instead, use toJson(). + * Instead, use jsonSerialize(). * @return array an associative array of flag keys to JSON values */ public function toValuesMap() @@ -78,12 +80,16 @@ public function toValuesMap() * Returns a JSON representation of the entire state map (as an associative array), in the format used * by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end in * order to "bootstrap" the JavaScript client. + *

+ * Note that calling json_encode() on a FeatureFlagsState object will automatically use the + * jsonSerialize() method. * @return array an associative array suitable for passing as a JSON object */ - public function toJson() + public function jsonSerialize() { $ret = array_replace([], $this->_flagValues); $ret['$flagsState'] = $this->_flagMetadata; + $ret['$valid'] = $this->_valid; return $ret; } } diff --git a/tests/FeatureFlagsStateTest.php b/tests/FeatureFlagsStateTest.php new file mode 100644 index 000000000..f61981909 --- /dev/null +++ b/tests/FeatureFlagsStateTest.php @@ -0,0 +1,111 @@ + 'key1', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 0, + 'fallthrough' => array('variation' => 0), + 'variations' => array('value1'), + 'salt' => '', + 'trackEvents' => false + ); + private static $flag2Json = array( + 'key' => 'key2', + 'version' => 200, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 0, + 'fallthrough' => array('variation' => 0), + 'variations' => array('value2'), + 'salt' => '', + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ); + + public function testCanGetFlagValue() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvalResult(0, 'value1', array())); + + $this->assertEquals('value1', $state->getFlagValue('key1')); + } + + public function testUnknownFlagReturnsNullValue() + { + $state = new FeatureFlagsState(true); + + $this->assertNull($state->getFlagValue('key1')); + } + + public function testCanConvertToValuesMap() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(0, 'value2', array())); + + $expected = array('key1' => 'value1', 'key2' => 'value2'); + $this->assertEquals($expected, $state->toValuesMap()); + } + + public function testCanConvertToJson() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + + $expected = array( + 'key1' => 'value1', + 'key2' => 'value2', + '$flagsState' => array( + 'key1' => array( + 'variation' => 0, + 'version' => 100, + 'trackEvents' => false + ), + 'key2' => array( + 'variation' => 1, + 'version' => 200, + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ) + ), + '$valid' => true + ); + $this->assertEquals($expected, $state->jsonSerialize()); + } + + public function testJsonEncodeUsesCustomSerializer() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + + $expected = $state->jsonSerialize(); + $json = json_encode($state); + $decoded = json_decode($json, true); + $this->assertEquals($expected, $decoded); + } +} diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 52d5493b2..66f24dddf 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -158,9 +158,10 @@ public function testAllFlagsStateReturnsState() 'trackEvents' => true, 'debugEventsUntilDate' => 1000 ) - ) + ), + '$valid' => true ); - $this->assertEquals($expectedState, $state->toJson()); + $this->assertEquals($expectedState, $state->jsonSerialize()); } public function testOnlyValidFeatureRequester() From 775f0a13255cca5fb9ea8f07c92f95e3851a1739 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 20 Aug 2018 21:02:53 -0700 Subject: [PATCH 07/10] indents --- tests/FeatureFlagsStateTest.php | 102 ++++++++++++++++---------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/tests/FeatureFlagsStateTest.php b/tests/FeatureFlagsStateTest.php index f61981909..9b97d0a4b 100644 --- a/tests/FeatureFlagsStateTest.php +++ b/tests/FeatureFlagsStateTest.php @@ -8,7 +8,7 @@ class FeatureFlagsStateTest extends \PHPUnit_Framework_TestCase { - private static $flag1Json = array( + private static $flag1Json = array( 'key' => 'key1', 'version' => 100, 'deleted' => false, @@ -38,74 +38,74 @@ class FeatureFlagsStateTest extends \PHPUnit_Framework_TestCase 'debugEventsUntilDate' => 1000 ); - public function testCanGetFlagValue() - { - $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag, new EvalResult(0, 'value1', array())); + public function testCanGetFlagValue() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvalResult(0, 'value1', array())); - $this->assertEquals('value1', $state->getFlagValue('key1')); - } + $this->assertEquals('value1', $state->getFlagValue('key1')); + } - public function testUnknownFlagReturnsNullValue() - { - $state = new FeatureFlagsState(true); - - $this->assertNull($state->getFlagValue('key1')); - } + public function testUnknownFlagReturnsNullValue() + { + $state = new FeatureFlagsState(true); + + $this->assertNull($state->getFlagValue('key1')); + } - public function testCanConvertToValuesMap() - { - $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(0, 'value2', array())); + public function testCanConvertToValuesMap() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(0, 'value2', array())); - $expected = array('key1' => 'value1', 'key2' => 'value2'); - $this->assertEquals($expected, $state->toValuesMap()); - } + $expected = array('key1' => 'value1', 'key2' => 'value2'); + $this->assertEquals($expected, $state->toValuesMap()); + } - public function testCanConvertToJson() - { - $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + public function testCanConvertToJson() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); $expected = array( 'key1' => 'value1', 'key2' => 'value2', '$flagsState' => array( 'key1' => array( - 'variation' => 0, - 'version' => 100, - 'trackEvents' => false + 'variation' => 0, + 'version' => 100, + 'trackEvents' => false ), 'key2' => array( - 'variation' => 1, - 'version' => 200, - 'trackEvents' => true, - 'debugEventsUntilDate' => 1000 + 'variation' => 1, + 'version' => 200, + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 ) ), '$valid' => true ); $this->assertEquals($expected, $state->jsonSerialize()); - } + } - public function testJsonEncodeUsesCustomSerializer() - { - $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + public function testJsonEncodeUsesCustomSerializer() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); - $expected = $state->jsonSerialize(); - $json = json_encode($state); - $decoded = json_decode($json, true); - $this->assertEquals($expected, $decoded); - } + $expected = $state->jsonSerialize(); + $json = json_encode($state); + $decoded = json_decode($json, true); + $this->assertEquals($expected, $decoded); + } } From 2f80b4c2c099480b6a125365e8cdb0a53a0889fc Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 21 Aug 2018 15:48:36 -0700 Subject: [PATCH 08/10] add ability to filter for client-side flags only --- src/LaunchDarkly/FeatureFlag.php | 18 ++++++++++++++++-- src/LaunchDarkly/LDClient.php | 9 ++++++++- tests/LDClientTest.php | 26 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index f617cb672..893d855f0 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -31,6 +31,9 @@ class FeatureFlag protected $_trackEvents = false; /** @var int | null */ protected $_debugEventsUntilDate = null; + /** @var bool */ + protected $_clientSide = false; + // Note, trackEvents and debugEventsUntilDate are not used in EventProcessor, because // the PHP client doesn't do summary events. However, we need to capture them in case // they want to pass the flag data to the front end with allFlagsState(). @@ -47,7 +50,8 @@ protected function __construct($key, array $variations, $deleted, $trackEvents, - $debugEventsUntilDate) + $debugEventsUntilDate, + $clientSide) { $this->_key = $key; $this->_version = $version; @@ -62,6 +66,7 @@ protected function __construct($key, $this->_deleted = $deleted; $this->_trackEvents = $trackEvents; $this->_debugEventsUntilDate = $debugEventsUntilDate; + $this->_clientSide = $clientSide; } public static function getDecoder() @@ -80,7 +85,8 @@ public static function getDecoder() $v['variations'] ?: [], $v['deleted'], isset($v['trackEvents']) && $v['trackEvents'], - isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null + isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null, + isset($v['clientSide']) && $v['clientSide'] ); }; } @@ -252,4 +258,12 @@ public function getDebugEventsUntilDate() { return $this->_debugEventsUntilDate; } + + /** + * @return boolean + */ + public function isClientSide() + { + return $this->_clientSide; + } } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index f528d25a3..25becc552 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -282,9 +282,12 @@ public function allFlags($user) * The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. * To convert the state object into a JSON data structure, call its toJson() method. * @param $user LDUser the end user requesting the feature flags + * @param $options array optional properties affecting how the state is computed; set + * 'clientSideOnly' => true to specify that only flags marked for client-side use + * should be included (by default, all flags are included) * @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid()) */ - public function allFlagsState($user) + public function allFlagsState($user, $options = array()) { if (is_null($user) || is_null($user->getKey())) { $this->_logger->warn("allFlagsState called with null user or null/empty user key! Returning empty state"); @@ -304,7 +307,11 @@ public function allFlagsState($user) } $state = new FeatureFlagsState(true); + $clientOnly = isset($options['clientSideOnly']) && $options['clientSideOnly']; foreach ($flags as $key => $flag) { + if ($clientOnly && !$flag->isClientSide()) { + continue; + } $result = $flag->evaluate($user, $this->_featureRequester); $state->addFlag($flag, $result); } diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 66f24dddf..d5f74f1c1 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -164,6 +164,32 @@ public function testAllFlagsStateReturnsState() $this->assertEquals($expectedState, $state->jsonSerialize()); } + public function testAllFlagsStateCanFilterForClientSideFlags() + { + $flag1Json = array('key' => 'server-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('a'), 'clientSide' => false); + $flag1 = FeatureFlag::decode($flag1Json); + $flag2Json = array('key' => 'server-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('b'), 'clientSide' => false); + $flag2 = FeatureFlag::decode($flag2Json); + $flag3Json = array('key' => 'client-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('value1'), 'clientSide' => true); + $flag3 = FeatureFlag::decode($flag3Json); + $flag4Json = array('key' => 'client-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('value2'), 'clientSide' => true); + $flag4 = FeatureFlag::decode($flag4Json); + MockFeatureRequester::$flags = array( + $flag1->getKey() => $flag1, $flag2->getKey() => $flag2, $flag3->getKey() => $flag3, $flag4->getKey() => $flag4 + ); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $state = $client->allFlagsState($user, array('clientSideOnly' => true)); + + $this->assertTrue($state->isValid()); + $this->assertEquals(array('client-side-1' => 'value1', 'client-side-2' => 'value2'), $state->toValuesMap()); + } + public function testOnlyValidFeatureRequester() { $this->setExpectedException(InvalidArgumentException::class); From fd08375e6330a0b32785f21f1c62101acd9d37dc Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 21 Aug 2018 15:58:11 -0700 Subject: [PATCH 09/10] fix test to fill in all required flag fields --- tests/LDClientTest.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index d5f74f1c1..e523bd3e2 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -166,14 +166,19 @@ public function testAllFlagsStateReturnsState() public function testAllFlagsStateCanFilterForClientSideFlags() { - $flag1Json = array('key' => 'server-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('a'), 'clientSide' => false); - $flag1 = FeatureFlag::decode($flag1Json); - $flag2Json = array('key' => 'server-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('b'), 'clientSide' => false); - $flag2 = FeatureFlag::decode($flag2Json); - $flag3Json = array('key' => 'client-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('value1'), 'clientSide' => true); - $flag3 = FeatureFlag::decode($flag3Json); - $flag4Json = array('key' => 'client-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('value2'), 'clientSide' => true); - $flag4 = FeatureFlag::decode($flag4Json); + $flagJson = array('key' => 'server-side-1', 'version' => 1, 'on' => false, 'salt' => '', 'deleted' => false, + 'targets' => array(), 'rules' => array(), 'prerequisites' => array(), 'fallthrough' => array(), + 'offVariation' => 0, 'variations' => array('a'), 'clientSide' => false); + $flag1 = FeatureFlag::decode($flagJson); + $flagJson['key'] = 'server-side-2'; + $flag2 = FeatureFlag::decode($flagJson); + $flagJson['key'] = 'client-side-1'; + $flagJson['clientSide'] = true; + $flagJson['variations'] = array('value1'); + $flag3 = FeatureFlag::decode($flagJson); + $flagJson['key'] = 'client-side-2'; + $flagJson['variations'] = array('value2'); + $flag4 = FeatureFlag::decode($flagJson); MockFeatureRequester::$flags = array( $flag1->getKey() => $flag1, $flag2->getKey() => $flag2, $flag3->getKey() => $flag3, $flag4->getKey() => $flag4 ); From 2a7270777df0ce45ae3e71887d49bc0829d28788 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 24 Aug 2018 18:20:25 -0700 Subject: [PATCH 10/10] prepare 3.3.0 release --- CHANGELOG.md | 8 ++++++++ VERSION | 2 +- src/LaunchDarkly/LDClient.php | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3863c9a..4a09d76c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [3.3.0] - 2018-08-27 +### Added: +- The new `LDClient` method `allFlagsState()` should be used instead of `allFlags()` if you are passing flag data to the front end for use with the JavaScript SDK. It preserves some flag metadata that the front end requires in order to send analytics events correctly. Versions 2.5.0 and above of the JavaScript SDK are able to use this metadata, but the output of `allFlagsState()` will still work with older versions. +- The `allFlagsState()` method also allows you to select only client-side-enabled flags to pass to the front end, by using the option `clientSideOnly => true`. + +### Deprecated: +- `LDClient.allFlags()` + ## [3.2.1] - 2018-07-16 ### Fixed: - The `LDClient::VERSION` constant has been fixed to report the current version. In the previous release, it was still set to 3.1.0. diff --git a/VERSION b/VERSION index e4604e3af..15a279981 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.1 +3.3.0 diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 25becc552..75b12a142 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -12,7 +12,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '3.2.1'; + const VERSION = '3.3.0'; /** @var string */ protected $_sdkKey;