From d29f2d2d65231343f5ddeb46479c2e691f113137 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 2 Feb 2018 16:57:53 -0800 Subject: [PATCH 01/29] add segment matching logic & ability to get segments from LD --- src/LaunchDarkly/ApcLDDFeatureRequester.php | 12 +- src/LaunchDarkly/Clause.php | 23 ++- src/LaunchDarkly/FeatureFlag.php | 2 +- src/LaunchDarkly/FeatureRequester.php | 12 +- src/LaunchDarkly/GuzzleFeatureRequester.php | 30 +++- src/LaunchDarkly/LDClient.php | 4 +- src/LaunchDarkly/LDDFeatureRequester.php | 56 +++++-- src/LaunchDarkly/Segment.php | 165 ++++++++++++++++++++ src/LaunchDarkly/VariationOrRollout.php | 4 +- tests/LDClientTest.php | 4 +- tests/LDDFeatureRequesterTest.php | 4 +- 11 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 src/LaunchDarkly/Segment.php diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index 811b8ce13..6d850cf5b 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -32,9 +32,9 @@ protected function fetch($key, &$success = null) return \apc_fetch($key, $success); } - protected function get_from_cache($key) + protected function get_from_cache($namespace, $key) { - $key = self::make_cache_key($key); + $key = self::make_cache_key($namespace, $key); $enabled = $this->fetch($key); if ($enabled === false) { return null; @@ -54,13 +54,13 @@ protected function add($key, $var, $ttl = 0) return \apc_add($key, $var, $ttl); } - protected function store_in_cache($key, $val) + protected function store_in_cache($namespace, $key, $val) { - $this->add($this->make_cache_key($key), $val, $this->_expiration); + $this->add($this->make_cache_key($namespace, $key), $val, $this->_expiration); } - private function make_cache_key($name) + private function make_cache_key($namespace, $name) { - return $this->_features_key.'.'.$name; + return $namespace.'.'.$name; } } diff --git a/src/LaunchDarkly/Clause.php b/src/LaunchDarkly/Clause.php index c0330b009..26c8e74c1 100644 --- a/src/LaunchDarkly/Clause.php +++ b/src/LaunchDarkly/Clause.php @@ -32,7 +32,28 @@ public static function getDecoder() * @param $user LDUser * @return bool */ - public function matchesUser($user) + public function matchesUser($user, $featureRequester) + { + if ($this->_op === 'segmentMatch') { + foreach ($this->_values as $value) { + $segment = $featureRequester->getSegment($value); + if ($segment) { + if ($segment->matchesUser($user)) { + return $this->_maybeNegate(true); + } + } + } + return $this->_maybeNegate(false); + } else { + return $this->matchesUserNoSegments($user); + } + } + + /** + * @param $user LDUser + * @return bool + */ + public function matchesUserNoSegments($user) { $userValue = $user->getValueForEvaluation($this->_attribute); if ($userValue === null) { diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 9823e4a6e..dc19bad51 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -115,7 +115,7 @@ private function _evaluate($user, $featureRequester, &$events) foreach ($this->_prerequisites as $prereq) { try { $prereqEvalResult = null; - $prereqFeatureFlag = $featureRequester->get($prereq->getKey()); + $prereqFeatureFlag = $featureRequester->getFeature($prereq->getKey()); if ($prereqFeatureFlag == null) { return null; } elseif ($prereqFeatureFlag->isOn()) { diff --git a/src/LaunchDarkly/FeatureRequester.php b/src/LaunchDarkly/FeatureRequester.php index e8b436f7d..437f3d6fd 100644 --- a/src/LaunchDarkly/FeatureRequester.php +++ b/src/LaunchDarkly/FeatureRequester.php @@ -10,12 +10,20 @@ interface FeatureRequester * @param $key string feature key * @return FeatureFlag|null The decoded FeatureFlag, or null if missing */ - public function get($key); + public function getFeature($key); + + /** + * Gets segment data from a likely cached store + * + * @param $key string segment key + * @return Segment|null The decoded Segment, or null if missing + */ + public function getSegment($key); /** * Gets all features. * * @return array()|null The decoded FeatureFlags, or null if missing */ - public function getAll(); + public function getAllFeatures(); } diff --git a/src/LaunchDarkly/GuzzleFeatureRequester.php b/src/LaunchDarkly/GuzzleFeatureRequester.php index e435675a2..53dcd2c62 100644 --- a/src/LaunchDarkly/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/GuzzleFeatureRequester.php @@ -11,6 +11,7 @@ class GuzzleFeatureRequester implements FeatureRequester { const SDK_FLAGS = "/sdk/flags"; + const SDK_SEGMENTS = "/sdk/segments"; /** @var Client */ private $_client; /** @var string */ @@ -46,14 +47,13 @@ public function __construct($baseUri, $sdkKey, $options) $this->_client = new Client(['handler' => $stack, 'debug' => false]); } - /** * Gets feature data from a likely cached store * * @param $key string feature key * @return FeatureFlag|null The decoded FeatureFlag, or null if missing */ - public function get($key) + public function getFeature($key) { try { $uri = $this->_baseUri . self::SDK_FLAGS . "/" . $key; @@ -71,12 +71,36 @@ public function get($key) } } + /** + * Gets segment data from a likely cached store + * + * @param $key string segment key + * @return Segment|null The decoded Segment, or null if missing + */ + public function getSegment($key) + { + try { + $uri = $this->_baseUri . self::SDK_SEGMENTS . "/" . $key; + $response = $this->_client->get($uri, $this->_defaults); + $body = $response->getBody(); + return Segment::decode(json_decode($body, true)); + } catch (BadResponseException $e) { + $code = $e->getResponse()->getStatusCode(); + if ($code == 404) { + $this->_logger->warning("GuzzleFeatureRequester::get returned 404. Segment does not exist for key: " . $key); + } else { + $this->handleUnexpectedStatus($code, "GuzzleFeatureRequester::get"); + } + return null; + } + } + /** * Gets all features from a likely cached store * * @return array()|null The decoded FeatureFlags, or null if missing */ - public function getAll() + public function getAllFeatures() { try { $uri = $this->_baseUri . self::SDK_FLAGS; diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 0902d271b..41b102644 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -157,7 +157,7 @@ public function variation($key, $user, $default = false) $this->_logger->warning("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly."); } try { - $flag = $this->_featureRequester->get($key); + $flag = $this->_featureRequester->getFeature($key); } catch (InvalidSDKKeyException $e) { $this->handleInvalidSDKKey(); return $default; @@ -278,7 +278,7 @@ public function allFlags($user) return null; } try { - $flags = $this->_featureRequester->getAll(); + $flags = $this->_featureRequester->getAllFeatures(); } catch (InvalidSDKKeyException $e) { $this->handleInvalidSDKKey(); return null; diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index 88731c7e5..eed1e7e09 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -10,6 +10,7 @@ class LDDFeatureRequester implements FeatureRequester protected $_sdkKey; protected $_options; protected $_features_key; + protected $_segments_key; /** @var LoggerInterface */ private $_logger; /** @var ClientInterface */ @@ -33,6 +34,7 @@ public function __construct($baseUri, $sdkKey, $options) $prefix = $options['redis_prefix']; } $this->_features_key = "$prefix:features"; + $this->_segments_key = "$prefix:segments"; $this->_logger = $options['logger']; if (isset($this->_options['predis_client']) && $this->_options['predis_client'] instanceof ClientInterface) { @@ -56,21 +58,20 @@ protected function get_connection() "port" => $this->_options['redis_port'])); } - /** * Gets feature data from a likely cached store * * @param $key string feature key * @return FeatureFlag|null The decoded JSON feature data, or null if missing */ - public function get($key) + public function getFeature($key) { - $raw = $this->get_from_cache($key); + $raw = $this->get_from_cache($this->_features_key, $key); if ($raw === null) { $redis = $this->get_connection(); $raw = $redis->hget($this->_features_key, $key); if ($raw) { - $this->store_in_cache($key, $raw); + $this->store_in_cache($this->_features_key, $key, $raw); } } if ($raw) { @@ -86,22 +87,53 @@ public function get($key) } } + /** + * Gets segment data from a likely cached store + * + * @param $key string segment key + * @return Segment|null The decoded JSON segment data, or null if missing + */ + public function getSegment($key) + { + $raw = $this->get_from_cache(this->_segments_key, $key); + if ($raw === null) { + $redis = $this->get_connection(); + $raw = $redis->hget($this->_features_key, $key); + if ($raw) { + $this->store_in_cache(this->_segments_key, $key, $raw); + } + } + if ($raw) { + $segment = Segment::decode(json_decode($raw, true)); + if ($segment->isDeleted()) { + $this->_logger->warning("LDDFeatureRequester: Attempted to get deleted segment with key: " . $key); + return null; + } + return $segment; + } else { + $this->_logger->warning("LDDFeatureRequester: Attempted to get missing segment with key: " . $key); + return null; + } + } + /** * Gets the value from local cache. No-op by default. - * @param $key string The feature key - * @return null|array The feature data or null if missing + * @param $namespace string that denotes features or segments + * @param $key string The feature or segment key + * @return null|array The feature or segment data or null if missing */ - protected function get_from_cache($key) + protected function get_from_cache($namespace, $key) { return null; } /** - * Stores the feature data into the local cache. No-op by default. - * @param $key string The feature key - * @param $val array The feature data + * Stores the feature or segment data into the local cache. No-op by default. + * @param $namespace string that denotes features or segments + * @param $key string The feature or segment key + * @param $val array The feature or segment data */ - protected function store_in_cache($key, $val) + protected function store_in_cache($namespace, $key, $val) { } @@ -110,7 +142,7 @@ protected function store_in_cache($key, $val) * * @return array()|null The decoded FeatureFlags, or null if missing */ - public function getAll() + public function getAllFeatures() { $redis = $this->get_connection(); $raw = $redis->hgetall($this->_features_key); diff --git a/src/LaunchDarkly/Segment.php b/src/LaunchDarkly/Segment.php new file mode 100644 index 000000000..4b6241b81 --- /dev/null +++ b/src/LaunchDarkly/Segment.php @@ -0,0 +1,165 @@ +_key = $key; + $this->_version = $version; + $this->_included = $included; + $this->_excluded = $excluded; + $this->_salt = $salt; + $this->_rules = $rules; + $this->_deleted = $deleted; + } + + public static function getDecoder() + { + return function ($v) { + return new Segment( + $v['key'], + $v['version'], + $v['included'] ?: [], + $v['excluded'] ?: [], + $v['salt'], + array_map(SegmentRule::getDecoder(), $v['rules'] ?: []), + $v['deleted']); + }; + } + + public static function decode($v) + { + return call_user_func(Segment::getDecoder(), $v); + } + + /** + * @param $user LDUser + * @return boolean + */ + public function matchesUser($user) + { + $key = $user->getKey(); + if (!$key) { + return false; + } + if (in_array($key, $this->_included, true)) { + return true; + } + if (in_array($key, $this->_excluded, true)) { + return false; + } + foreach ($this->_rules as $rule) { + if ($rule->matchesUser($user, $this->key, $this->salt)) { + return true; + } + } + return false; + } + + /** + * @return int + */ + public function getVersion() + { + return $this->_version; + } + + /** + * @return string + */ + public function getKey() + { + return $this->_key; + } + + /** + * @return boolean + */ + public function isDeleted() + { + return $this->_deleted; + } +} + +class SegmentRule +{ + /** @var Clause[] */ + private $_clauses = array(); + /** @var int */ + private $_weight = null; + /** @var string */ + private $_bucketBy = null; + + protected function __construct(array $clauses, $weight, $bucketBy) + { + $this->_clauses = $clauses; + $this->_weight = $weight; + $this->_bucketBy = $bucketBy; + } + + public static function getDecoder() + { + return function ($v) { + return new SegmentRule( + array_map(Clause::getDecoder(), $v['clauses'] ?: []), + isset($v['weight']) ? $v['weight'] : null, + isset($v['bucketBy']) ? $v['bucketBy'] : null); + }; + } + + public function matchesUser($user, $segmentKey, $segmentSalt) + { + for ($this->_clauses as $clause) { + if (!$clause->matchesUserNoSegments($user)) { + return false; + } + } + // If the weight is absent, this rule matches + if ($this->_weight === null) { + return true; + } + // All of the clauses are met. See if the user buckets in + $bucketBy = ($this->_bucketBy === null) ? "key" : bucketBy; + $bucket = VariationOrRollout::bucketUser($user, $segmentKey, $bucketBy, $segmentSalt); + $weight = $this->_weight / 100000.0; + return $bucket < $weight; + } + + /** + * @return Clause[] + */ + public function getClauses() + { + return $this->_clauses; + } + + /** + * @return string + */ + public function getBucketBy() + { + return $this->_bucketBy; + } +} diff --git a/src/LaunchDarkly/VariationOrRollout.php b/src/LaunchDarkly/VariationOrRollout.php index 2c53ce231..df9407ea8 100644 --- a/src/LaunchDarkly/VariationOrRollout.php +++ b/src/LaunchDarkly/VariationOrRollout.php @@ -54,7 +54,7 @@ public function variationIndexForUser($user, $_key, $_salt) return $this->_variation; } elseif ($this->_rollout !== null) { $bucketBy = $this->_rollout->getBucketBy() === null ? "key" : $this->_rollout->getBucketBy(); - $bucket = $this->bucketUser($user, $_key, $bucketBy, $_salt); + $bucket = self::bucketUser($user, $_key, $bucketBy, $_salt); $sum = 0.0; foreach ($this->_rollout->getVariations() as $wv) { $sum += $wv->getWeight() / 100000.0; @@ -73,7 +73,7 @@ public function variationIndexForUser($user, $_key, $_salt) * @param $_salt string * @return float */ - private function bucketUser($user, $_key, $attr, $_salt) + public static function bucketUser($user, $_key, $attr, $_salt) { $userValue = $user->getValueForEvaluation($attr); $idHash = null; diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index cdaae3958..e887f87ef 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -107,12 +107,12 @@ function __construct($baseurl, $key, $options) { } - public function get($key) + public function getFeature($key) { return self::$val; } - public function getAll() + public function getAllFeatures() { return null; } diff --git a/tests/LDDFeatureRequesterTest.php b/tests/LDDFeatureRequesterTest.php index 2fb2d7d07..e4663b233 100644 --- a/tests/LDDFeatureRequesterTest.php +++ b/tests/LDDFeatureRequesterTest.php @@ -27,7 +27,7 @@ protected function setUp() ->getMockForAbstractClass(); } - public function testGet() + public function testGetFeature() { $sut = new LDDFeatureRequester('example.com', 'MySdkKey', [ 'logger' => $this->logger, @@ -64,7 +64,7 @@ public function testGet() 'deleted' => false, ])); - $featureFlag = $sut->get('foo'); + $featureFlag = $sut->getFeature('foo'); self::assertInstanceOf(FeatureFlag::class, $featureFlag); self::assertTrue($featureFlag->isOn()); From 60dec5369a83c0959f08cf802595813d3b80fbdf Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 2 Feb 2018 17:25:50 -0800 Subject: [PATCH 02/29] misc fixes --- src/LaunchDarkly/LDDFeatureRequester.php | 4 ++-- tests/LDClientTest.php | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index eed1e7e09..4ae4ecb44 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -95,12 +95,12 @@ public function getFeature($key) */ public function getSegment($key) { - $raw = $this->get_from_cache(this->_segments_key, $key); + $raw = $this->get_from_cache($this->_segments_key, $key); if ($raw === null) { $redis = $this->get_connection(); $raw = $redis->hget($this->_features_key, $key); if ($raw) { - $this->store_in_cache(this->_segments_key, $key, $raw); + $this->store_in_cache($this->_segments_key, $key, $raw); } } if ($raw) { diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index e887f87ef..2c938bca3 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -112,6 +112,11 @@ public function getFeature($key) return self::$val; } + public function getSegment($key) + { + return null; + } + public function getAllFeatures() { return null; From 252f2707376124a81196d8f9520fe4d3b31fe510 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 5 Feb 2018 13:03:41 -0800 Subject: [PATCH 03/29] misc fixes, tests --- src/LaunchDarkly/FeatureFlag.php | 6 +- src/LaunchDarkly/Rule.php | 4 +- src/LaunchDarkly/Segment.php | 4 +- tests/SegmentTest.php | 179 +++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 tests/SegmentTest.php diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index dc19bad51..0ac89d317 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -134,7 +134,7 @@ private function _evaluate($user, $featureRequester, &$events) } } if ($prereqOk) { - return $this->getVariation($this->evaluateIndex($user)); + return $this->getVariation($this->evaluateIndex($user, $featureRequester)); } return null; } @@ -143,7 +143,7 @@ private function _evaluate($user, $featureRequester, &$events) * @param $user LDUser * @return int|null */ - private function evaluateIndex($user) + private function evaluateIndex($user, $featureRequester) { // Check to see if targets match if ($this->_targets != null) { @@ -158,7 +158,7 @@ private function evaluateIndex($user) // Now walk through the rules and see if any match if ($this->_rules != null) { foreach ($this->_rules as $rule) { - if ($rule->matchesUser($user)) { + if ($rule->matchesUser($user, $featureRequester)) { return $rule->variationIndexForUser($user, $this->_key, $this->_salt); } } diff --git a/src/LaunchDarkly/Rule.php b/src/LaunchDarkly/Rule.php index 0da0a8b2c..daf9c6e11 100644 --- a/src/LaunchDarkly/Rule.php +++ b/src/LaunchDarkly/Rule.php @@ -27,10 +27,10 @@ public static function getDecoder() * @param $user LDUser * @return bool */ - public function matchesUser($user) + public function matchesUser($user, $featureRequester) { foreach ($this->_clauses as $clause) { - if (!$clause->matchesUser($user)) { + if (!$clause->matchesUser($user, $featureRequester)) { return false; } } diff --git a/src/LaunchDarkly/Segment.php b/src/LaunchDarkly/Segment.php index 4b6241b81..620f2a22a 100644 --- a/src/LaunchDarkly/Segment.php +++ b/src/LaunchDarkly/Segment.php @@ -71,7 +71,7 @@ public function matchesUser($user) return false; } foreach ($this->_rules as $rule) { - if ($rule->matchesUser($user, $this->key, $this->salt)) { + if ($rule->matchesUser($user, $this->_key, $this->_salt)) { return true; } } @@ -131,7 +131,7 @@ public static function getDecoder() public function matchesUser($user, $segmentKey, $segmentSalt) { - for ($this->_clauses as $clause) { + foreach ($this->_clauses as $clause) { if (!$clause->matchesUserNoSegments($user)) { return false; } diff --git a/tests/SegmentTest.php b/tests/SegmentTest.php new file mode 100644 index 000000000..5ffb1ffb9 --- /dev/null +++ b/tests/SegmentTest.php @@ -0,0 +1,179 @@ + 'test', + 'included' => array('foo'), + 'excluded' => array(), + 'rules' => array(), + 'salt' => 'salt', + 'version' => 1, + 'deleted' => false + ); + $segment = Segment::decode($json); + $ub = new LDUserBuilder('foo'); + $this->assertTrue($segment->matchesUser($ub->build())); + } + + public function testExplicitExcludeUser() { + $json = array( + 'key' => 'test', + 'included' => array(), + 'excluded' => array('foo'), + 'rules' => array(), + 'salt' => 'salt', + 'version' => 1, + 'deleted' => false + ); + $segment = Segment::decode($json); + $ub = new LDUserBuilder('foo'); + $this->assertFalse($segment->matchesUser($ub->build())); + } + + public function testExplicitIncludePasPrecedence() { + $json = array( + 'key' => 'test', + 'included' => array('foo'), + 'excluded' => array('foo'), + 'rules' => array(), + 'salt' => 'salt', + 'version' => 1, + 'deleted' => false + ); + $segment = Segment::decode($json); + $ub = new LDUserBuilder('foo'); + $this->assertTrue($segment->matchesUser($ub->build())); + } + + public function testMatchingRuleWithFullRollout() { + $json = array( + 'key' => 'test', + 'included' => array(), + 'excluded' => array(), + 'salt' => 'salt', + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => 'email', + 'op' => 'in', + 'values' => array('test@example.com'), + 'negate' => false + ) + ), + 'weight' => 100000 + ) + ), + 'version' => 1, + 'deleted' => false + ); + $segment = Segment::decode($json); + $ub = new LDUserBuilder('foo'); + $ub->email('test@example.com'); + $this->assertTrue($segment->matchesUser($ub->build())); + } + + public function testMatchingRuleWithZeroRollout() { + $json = array( + 'key' => 'test', + 'included' => array(), + 'excluded' => array(), + 'salt' => 'salt', + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => 'email', + 'op' => 'in', + 'values' => array('test@example.com'), + 'negate' => false + ) + ), + 'weight' => 0 + ) + ), + 'version' => 1, + 'deleted' => false + ); + $segment = Segment::decode($json); + $ub = new LDUserBuilder('foo'); + $ub->email('test@example.com'); + $this->assertFalse($segment->matchesUser($ub->build())); + } + + public function testMatchingRuleWithMultipleClauses() { + $json = array( + 'key' => 'test', + 'included' => array(), + 'excluded' => array(), + 'salt' => 'salt', + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => 'email', + 'op' => 'in', + 'values' => array('test@example.com'), + 'negate' => false + ), + array( + 'attribute' => 'name', + 'op' => 'in', + 'values' => array('bob'), + 'negate' => false + ) + ), + 'weight' => 100000 + ) + ), + 'version' => 1, + 'deleted' => false + ); + $segment = Segment::decode($json); + $ub = new LDUserBuilder('foo'); + $ub->email('test@example.com'); + $ub->name('bob'); + $this->assertTrue($segment->matchesUser($ub->build())); + } + + public function testNonMatchingRuleWithMultipleClauses() { + $json = array( + 'key' => 'test', + 'included' => array(), + 'excluded' => array(), + 'salt' => 'salt', + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => 'email', + 'op' => 'in', + 'values' => array('test@example.com'), + 'negate' => false + ), + array( + 'attribute' => 'name', + 'op' => 'in', + 'values' => array('bill'), + 'negate' => false + ) + ), + 'weight' => 100000 + ) + ), + 'version' => 1, + 'deleted' => false + ); + $segment = Segment::decode($json); + $ub = new LDUserBuilder('foo'); + $ub->email('test@example.com'); + $ub->name('bob'); + $this->assertFalse($segment->matchesUser($ub->build())); + } +} From 120c7e40717980d6f8f58fb63d67691bd9a5e944 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 5 Feb 2018 17:39:16 -0800 Subject: [PATCH 04/29] more tests --- tests/FeatureFlagTest.php | 102 +++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 93c98e35c..8b8dde742 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -2,10 +2,13 @@ namespace LaunchDarkly\Tests; use LaunchDarkly\FeatureFlag; +use LaunchDarkly\FeatureRequester; +use LaunchDarkly\LDUserBuilder; +use LaunchDarkly\Segment; + class FeatureFlagTest extends \PHPUnit_Framework_TestCase { - private static $json1 = "{ \"key\": \"integration.feature.0\", \"version\": 2210, @@ -189,5 +192,102 @@ public function testDecodeMulti(array $feature) self::assertInstanceOf(FeatureFlag::class, $featureFlag); } + + public function testSegmentMatchClauseRetrievesSegmentFromStore() + { + $segmentJson = array( + 'key' => 'segkey', + 'version' => 1, + 'deleted' => false, + 'included' => array('foo'), + 'excluded' => array(), + 'rules' => array(), + 'salt' => '' + ); + $segment = Segment::decode($segmentJson); + + $requester = new MockFeatureRequesterForSegment(); + $requester->key = 'segkey'; + $requester->val = $segment; + + $feature = $this->makeBooleanFeatureWithSegmentMatch('segkey'); + + $ub = new LDUserBuilder('foo'); + $user = $ub->build(); + + $result = $feature->evaluate($user, $requester); + + self::assertTrue($result->getValue()); + } + + public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound() + { + $requester = new MockFeatureRequesterForSegment(); + + $feature = $this->makeBooleanFeatureWithSegmentMatch('segkey'); + + $ub = new LDUserBuilder('foo'); + $user = $ub->build(); + + $result = $feature->evaluate($user, $requester); + + self::assertFalse($result->getValue()); + } + + private function makeBooleanFeatureWithSegmentMatch($segmentKey) + { + $featureJson = array( + 'key' => 'test', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'variations' => array(false, true), + 'fallthrough' => array('variation' => 0), + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => '', + 'op' => 'segmentMatch', + 'values' => array($segmentKey), + 'negate' => false + ) + ), + 'variation' => 1 + ) + ), + 'offVariation' => 0, + 'prerequisites' => array(), + 'targets' => array(), + 'salt' => '' + ); + return FeatureFlag::decode($featureJson); + } +} + + +class MockFeatureRequesterForSegment implements FeatureRequester +{ + public $key = null; + public $val = null; + + function __construct($baseurl = null, $key = null, $options = null) + { + } + + public function getFeature($key) + { + return null; + } + + public function getSegment($key) + { + return ($key == $this->key) ? $this->val : null; + } + + public function getAllFeatures() + { + return null; + } } From d0c46bb99ef8f8ad5521e531b13d79994f667f1f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 21 Feb 2018 13:01:09 -0800 Subject: [PATCH 05/29] version 3.0.0 --- 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 8785f9d51..9766e60a7 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.0.0] - 2018-02-21 +### Added +- Support for a new LaunchDarkly feature: reusable user segments. + ## [2.5.0] - 2018-02-13 ### Added - Adds support for a future LaunchDarkly feature, coming soon: semantic version user attributes. diff --git a/VERSION b/VERSION index 437459cd9..4a36342fc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5.0 +3.0.0 diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 7111732e6..fc21d080d 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -19,7 +19,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '2.5.0'; + const VERSION = '3.0.0'; /** @var string */ protected $_sdkKey; From e26219bf0e62d09525f9090099ac206ae06fad92 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:05:16 -0700 Subject: [PATCH 06/29] migrate to CircleCI 2 --- .circleci/config.yml | 55 ++++++++++++++++++++++++++++++++++++++++++++ circle.yml | 31 ------------------------- 2 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 circle.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..d38dd7377 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,55 @@ +version: 2 + +workflows: + version: 2 + test: + jobs: + - test-5.6 + - test-7.0 + - test-7.1 + - test-7.2 + - integration-test-5.6 + +php-docker-template: &test-template + steps: + - checkout + - run: + name: setup apcu + command: | + yes '' | pecl install -f apcu-4.0.10; + echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini; + echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini + - run: + name: run tests with current dependency versions + command: vendor/bin/phpunit tests --coverage-text + - run: + name: run tests with highest available dependency versions + command: composer update && vendor/bin/phpunit tests + - run: + name: run tests with lowest available dependency versions + command: composer update --prefer-lowest && vendor/bin/phpunit tests + +jobs: + test-5.6: + <<: *php-docker-template + docker: + - image: circleci/php:5.6.34-cli-jessie + test-7.0: + <<: *php-docker-template + docker: + - image: circleci/php:7.0.28-cli-jessie + test-7.1: + <<: *php-docker-template + docker: + - image: circleci/php:7.1.15-cli-jessie + test-7.2: + <<: *php-docker-template + docker: + - image: circleci/php:7.2.3-cli-stretch + + integration-test-5.6: + docker: + - image: circleci/php:5.6.34-cli-jessie + steps: + - checkout + - run: vendor/bin/phpunit integration-tests/LDDFeatureRequesterTest.php diff --git a/circle.yml b/circle.yml deleted file mode 100644 index c3bc0437b..000000000 --- a/circle.yml +++ /dev/null @@ -1,31 +0,0 @@ -machine: - php: - version: 5.6.14 - services: - - redis - - docker -dependencies: - pre: - - yes '' | pecl install -f apcu-4.0.10 - - echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini - - echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini - - docker pull php - - docker pull nyanpass/php5.5 - -test: - override: - - vendor/bin/phpunit tests --coverage-text - - vendor/bin/phpunit integration-tests/LDDFeatureRequesterTest.php - - - composer update && vendor/bin/phpunit tests - - composer update --prefer-lowest && vendor/bin/phpunit tests - - - docker run -it -v `pwd`:/php-client php:7.0-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update && vendor/bin/phpunit" - - docker run -it -v `pwd`:/php-client php:7.0-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update --prefer-lowest && vendor/bin/phpunit" - - - docker run -it -v `pwd`:/php-client php:7.1-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update && vendor/bin/phpunit" - - docker run -it -v `pwd`:/php-client php:7.1-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update --prefer-lowest && vendor/bin/phpunit" - - - docker run -it -v `pwd`:/php-client nyanpass/php5.5:5.5-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update && vendor/bin/phpunit" - - docker run -it -v `pwd`:/php-client nyanpass/php5.5:5.5-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update --prefer-lowest && vendor/bin/phpunit" - From a9e96470d8cdc3efeadcf6ce928f10b429e87067 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:06:26 -0700 Subject: [PATCH 07/29] typo --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d38dd7377..1872ca8db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ workflows: - test-7.2 - integration-test-5.6 -php-docker-template: &test-template +php-docker-template: &php-docker-template steps: - checkout - run: From 16dd1f50d39a088abc8a6f5e725ae5782670a1ee Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:07:01 -0700 Subject: [PATCH 08/29] sudo --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1872ca8db..03b1ce3bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,9 +16,9 @@ php-docker-template: &php-docker-template - run: name: setup apcu command: | - yes '' | pecl install -f apcu-4.0.10; - echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini; - echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini + sudo yes '' | pecl install -f apcu-4.0.10; + sudo echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini; + sudo echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini - run: name: run tests with current dependency versions command: vendor/bin/phpunit tests --coverage-text From 7d09d5335865862035b8f99d356655b786af3fc0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:14:17 -0700 Subject: [PATCH 09/29] misc fixes --- .circleci/config.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 03b1ce3bb..d2921a367 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,15 +13,9 @@ workflows: php-docker-template: &php-docker-template steps: - checkout - - run: - name: setup apcu - command: | - sudo yes '' | pecl install -f apcu-4.0.10; - sudo echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini; - sudo echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini - run: name: run tests with current dependency versions - command: vendor/bin/phpunit tests --coverage-text + command: composer install && vendor/bin/phpunit tests --coverage-text - run: name: run tests with highest available dependency versions command: composer update && vendor/bin/phpunit tests @@ -49,7 +43,13 @@ jobs: integration-test-5.6: docker: - - image: circleci/php:5.6.34-cli-jessie + - image: circleci/php:5.6.34-apache-jessie steps: - checkout + - run: + name: setup apcu + command: | + yes '' | sudo pecl install -f apcu-4.0.10; + sudo echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini; + sudo echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini - run: vendor/bin/phpunit integration-tests/LDDFeatureRequesterTest.php From 917b4f1155b470536bdcff2f6da94783c31b6567 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:31:07 -0700 Subject: [PATCH 10/29] fixing integration test --- .circleci/config.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d2921a367..9e54c35c3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,13 +15,13 @@ php-docker-template: &php-docker-template - checkout - run: name: run tests with current dependency versions - command: composer install && vendor/bin/phpunit tests --coverage-text + command: composer install --no-progress && vendor/bin/phpunit tests --coverage-text - run: name: run tests with highest available dependency versions - command: composer update && vendor/bin/phpunit tests + command: composer update --no-progress && vendor/bin/phpunit tests - run: name: run tests with lowest available dependency versions - command: composer update --prefer-lowest && vendor/bin/phpunit tests + command: composer update --prefer-lowest --no-progress && vendor/bin/phpunit tests jobs: test-5.6: @@ -44,12 +44,14 @@ jobs: integration-test-5.6: docker: - image: circleci/php:5.6.34-apache-jessie + - image: redis steps: - checkout - run: name: setup apcu command: | yes '' | sudo pecl install -f apcu-4.0.10; - sudo echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini; - sudo echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini + echo "extension=apcu.so" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini; + echo "apc.enable_cli = 1" | sudo tee -a /urs/local/etc/php/conf.d/apcu.ini + - run: composer update --no-progress - run: vendor/bin/phpunit integration-tests/LDDFeatureRequesterTest.php From 1e3160d97c461ca658d16c95f3060d0cf27090af Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:37:20 -0700 Subject: [PATCH 11/29] misc fixes --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9e54c35c3..bcd37d1ca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,8 +50,9 @@ jobs: - run: name: setup apcu command: | + pecl config-set php_ini /usr/local/etc/php/php.ini yes '' | sudo pecl install -f apcu-4.0.10; echo "extension=apcu.so" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini; - echo "apc.enable_cli = 1" | sudo tee -a /urs/local/etc/php/conf.d/apcu.ini + echo "apc.enable_cli = 1" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini - run: composer update --no-progress - run: vendor/bin/phpunit integration-tests/LDDFeatureRequesterTest.php From d9957d81507cae89bc3e76cb0f8941839c73d2ef Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:41:28 -0700 Subject: [PATCH 12/29] misc fixes --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bcd37d1ca..ed4bcd001 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ workflows: - test-5.6 - test-7.0 - test-7.1 - - test-7.2 + # - test-7.2 # TODO: make our code compatible with PHP 7.2 - integration-test-5.6 php-docker-template: &php-docker-template @@ -51,7 +51,7 @@ jobs: name: setup apcu command: | pecl config-set php_ini /usr/local/etc/php/php.ini - yes '' | sudo pecl install -f apcu-4.0.10; + yes '' | sudo pecl install -f apcu-4.0.10 || true; echo "extension=apcu.so" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini; echo "apc.enable_cli = 1" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini - run: composer update --no-progress From cddd7c0f0d19adb3a77987e1978310034a1db1ad Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:47:02 -0700 Subject: [PATCH 13/29] don't need Apache --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ed4bcd001..8929bac36 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,7 +43,7 @@ jobs: integration-test-5.6: docker: - - image: circleci/php:5.6.34-apache-jessie + - image: circleci/php:5.6.34-cli-jessie - image: redis steps: - checkout From b82233563e21c8b5d7b7de63b283526f26e89c5e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 17:58:12 -0700 Subject: [PATCH 14/29] misc fixes --- .circleci/config.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8929bac36..b84df894f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,9 +13,16 @@ workflows: php-docker-template: &php-docker-template steps: - checkout + - run: + name: install current dependencies + command: composer install --no-progress - run: name: run tests with current dependency versions - command: composer install --no-progress && vendor/bin/phpunit tests --coverage-text + command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests + - store_test_results: + path: ~/phpunit + - store_artifacts: + path: ~/phpunit - run: name: run tests with highest available dependency versions command: composer update --no-progress && vendor/bin/phpunit tests From 0c2b3519cbbb052ee18bb8fcbcbb8704051ac453 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 18:03:37 -0700 Subject: [PATCH 15/29] add a job to run PHP 5.5 --- .circleci/config.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b84df894f..422f35284 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ workflows: - integration-test-5.6 php-docker-template: &php-docker-template - steps: + steps: &php-build-steps - checkout - run: name: install current dependencies @@ -48,6 +48,12 @@ jobs: docker: - image: circleci/php:7.2.3-cli-stretch + test-5.5: # CircleCI doesn't provide a Docker image for 5.5 + machine: + - image: circleci/classic:latest # Ubuntu 14.04 + steps: + <<: *php-build-steps + integration-test-5.6: docker: - image: circleci/php:5.6.34-cli-jessie From e79714e9e58d50d65b1d96876b0c3c8680ffe9d7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 18:06:05 -0700 Subject: [PATCH 16/29] fix syntax --- .circleci/config.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 422f35284..c4fed6cb4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ workflows: - integration-test-5.6 php-docker-template: &php-docker-template - steps: &php-build-steps + steps: - checkout - run: name: install current dependencies @@ -52,7 +52,23 @@ jobs: machine: - image: circleci/classic:latest # Ubuntu 14.04 steps: - <<: *php-build-steps + - checkout + - run: + name: install current dependencies + command: composer install --no-progress + - run: + name: run tests with current dependency versions + command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests + - store_test_results: + path: ~/phpunit + - store_artifacts: + path: ~/phpunit + - run: + name: run tests with highest available dependency versions + command: composer update --no-progress && vendor/bin/phpunit tests + - run: + name: run tests with lowest available dependency versions + command: composer update --prefer-lowest --no-progress && vendor/bin/phpunit tests integration-test-5.6: docker: From 0df41529831a2070cd3e06ae2a66b63d78864a6c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 18:06:55 -0700 Subject: [PATCH 17/29] typo --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c4fed6cb4..16fdb5e58 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,7 @@ workflows: version: 2 test: jobs: + - test-5.5 - test-5.6 - test-7.0 - test-7.1 @@ -50,7 +51,7 @@ jobs: test-5.5: # CircleCI doesn't provide a Docker image for 5.5 machine: - - image: circleci/classic:latest # Ubuntu 14.04 + image: circleci/classic:latest # Ubuntu 14.04 steps: - checkout - run: From 6b840207c4adc9c4494098cc0ec6771520d21284 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 18:15:37 -0700 Subject: [PATCH 18/29] misc fixes --- .circleci/config.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 16fdb5e58..58aa03966 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ workflows: - test-7.0 - test-7.1 # - test-7.2 # TODO: make our code compatible with PHP 7.2 - - integration-test-5.6 + - integration-test php-docker-template: &php-docker-template steps: @@ -53,25 +53,26 @@ jobs: machine: image: circleci/classic:latest # Ubuntu 14.04 steps: + - run: + name: install PHP and Composer + command: | + sudo apt-get update && + sudo apt-get install circleci-php-5.5.36 && + php -r "copy('https://getcomposer.org/installer', '/tmp/composer-setup.php');" && + sudo php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer - checkout - run: - name: install current dependencies - command: composer install --no-progress + name: update dependencies # the dependencies in composer.lock don't work with 5.5 + command: composer update --no-progress - run: - name: run tests with current dependency versions + name: run tests command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests - store_test_results: path: ~/phpunit - store_artifacts: path: ~/phpunit - - run: - name: run tests with highest available dependency versions - command: composer update --no-progress && vendor/bin/phpunit tests - - run: - name: run tests with lowest available dependency versions - command: composer update --prefer-lowest --no-progress && vendor/bin/phpunit tests - integration-test-5.6: + integration-test: docker: - image: circleci/php:5.6.34-cli-jessie - image: redis From 99ff3dc3628b90a87db6184b57bd60429ef386b1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 18:21:52 -0700 Subject: [PATCH 19/29] try to make 7.2 build work --- .circleci/config.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 58aa03966..4018c1cf2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ workflows: - test-5.6 - test-7.0 - test-7.1 - # - test-7.2 # TODO: make our code compatible with PHP 7.2 + - test-7.2 - integration-test php-docker-template: &php-docker-template @@ -29,7 +29,12 @@ php-docker-template: &php-docker-template command: composer update --no-progress && vendor/bin/phpunit tests - run: name: run tests with lowest available dependency versions - command: composer update --prefer-lowest --no-progress && vendor/bin/phpunit tests + # we skip this for 7.2 because the lowest compatible version of PHPUnit has a bug: + # https://github.com/sebastianbergmann/comparator/pull/30 + command: | + if [[ $CIRCLE_JOB != test-7.2 ]]; then + composer update --prefer-lowest --no-progress && vendor/bin/phpunit tests; + fi jobs: test-5.6: From 52267f24149c28f29f6d241667ec9a0d7d7fd8d0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 29 Mar 2018 20:23:47 -0700 Subject: [PATCH 20/29] add JUnit output for integration tests --- .circleci/config.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4018c1cf2..159bc0e79 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -91,4 +91,8 @@ jobs: echo "extension=apcu.so" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini; echo "apc.enable_cli = 1" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini - run: composer update --no-progress - - run: vendor/bin/phpunit integration-tests/LDDFeatureRequesterTest.php + - run: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml integration-tests/LDDFeatureRequesterTest.php + - store_test_results: + path: ~/phpunit + - store_artifacts: + path: ~/phpunit From 847b0a733be0b42c7b57d4f0cb75122b1136bed3 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 16 Apr 2018 14:37:48 -0700 Subject: [PATCH 21/29] remove CodeClimate from readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 368d8ed30..8871b609e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly SDK for PHP =========================== -[![Code Climate](https://codeclimate.com/github/launchdarkly/php-client/badges/gpa.svg)](https://codeclimate.com/github/launchdarkly/php-client) - [![Circle CI](https://circleci.com/gh/launchdarkly/php-client.svg?style=svg)](https://circleci.com/gh/launchdarkly/php-client) Requirements From 3224f382d148d5686cbb02f22a8382c7a76808ca Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Apr 2018 13:49:02 -0700 Subject: [PATCH 22/29] add variation index to feature request events --- src/LaunchDarkly/CurlEventPublisher.php | 1 + src/LaunchDarkly/EventPublisher.php | 2 ++ src/LaunchDarkly/FeatureFlag.php | 33 ++++++++++++++++------- src/LaunchDarkly/GuzzleEventPublisher.php | 3 ++- src/LaunchDarkly/LDClient.php | 15 ++++++----- src/LaunchDarkly/Util.php | 3 ++- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/LaunchDarkly/CurlEventPublisher.php b/src/LaunchDarkly/CurlEventPublisher.php index d731377eb..1151b0494 100644 --- a/src/LaunchDarkly/CurlEventPublisher.php +++ b/src/LaunchDarkly/CurlEventPublisher.php @@ -60,6 +60,7 @@ private function createArgs($payload) $args.= " -H 'Content-Type: application/json'"; $args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey); $args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'"; + $args.= " -H 'X-LaunchDarkly-Event-Schema: " . EventPublisher::CURRENT_SCHEMA_VERSION . "'"; $args.= " -H 'Accept: application/json'"; $args.= " -d " . escapeshellarg($payload); $args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); diff --git a/src/LaunchDarkly/EventPublisher.php b/src/LaunchDarkly/EventPublisher.php index 38a344974..38dd94f44 100644 --- a/src/LaunchDarkly/EventPublisher.php +++ b/src/LaunchDarkly/EventPublisher.php @@ -6,6 +6,8 @@ */ interface EventPublisher { + const CURRENT_SCHEMA_VERSION = 2; + /** * @param string $sdkKey The SDK key for your account * @param mixed[] $options Client configuration settings diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 0ac89d317..1bc81855d 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -95,18 +95,18 @@ public function evaluate($user, $featureRequester) if ($this->isOn()) { $result = $this->_evaluate($user, $featureRequester, $prereqEvents); if ($result !== null) { - return new EvalResult($result, $prereqEvents); + return $result; } } - $offVariation = $this->getOffVariationValue(); - return new EvalResult($offVariation, $prereqEvents); + $offVariationValue = $this->getOffVariationValue(); + return new EvalResult($this->_offVariation, $offVariationValue, $prereqEvents); } /** * @param $user LDUser * @param $featureRequester FeatureRequester * @param $events - * @return mixed|null + * @return EvalResult|null */ private function _evaluate($user, $featureRequester, &$events) { @@ -120,21 +120,26 @@ private function _evaluate($user, $featureRequester, &$events) return null; } elseif ($prereqFeatureFlag->isOn()) { $prereqEvalResult = $prereqFeatureFlag->_evaluate($user, $featureRequester, $events); - $variation = $prereqFeatureFlag->getVariation($prereq->getVariation()); - if ($prereqEvalResult === null || $variation === null || $prereqEvalResult !== $variation) { + $variation = $prereq->getVariation(); + if ($prereqEvalResult === null || $variation === null || $prereqEvalResult->getVariation() !== $variation) { $prereqOk = false; } } else { $prereqOk = false; } - array_push($events, Util::newFeatureRequestEvent($prereqFeatureFlag->getKey(), $user, $prereqEvalResult, null, $prereqFeatureFlag->getVersion(), $this->_key)); + array_push($events, Util::newFeatureRequestEvent($prereqFeatureFlag->getKey(), $user, + $prereqEvalResult === null ? null : $prereqEvalResult->getVariation(), + $prereqEvalResult === null ? null : $prereqEvalResult->getValue(), + null, $prereqFeatureFlag->getVersion(), $this->_key)); } catch (EvaluationException $e) { $prereqOk = false; } } } if ($prereqOk) { - return $this->getVariation($this->evaluateIndex($user, $featureRequester)); + $variation = $this->evaluateIndex($user, $featureRequester); + $value = $this->getVariation($variation); + return new EvalResult($variation, $value, $events); } return null; } @@ -221,6 +226,7 @@ public function isDeleted() class EvalResult { + private $_variation = null; private $_value = null; /** @var array */ private $_prerequisiteEvents = []; @@ -230,12 +236,21 @@ class EvalResult * @param null $value * @param array $prerequisiteEvents */ - public function __construct($value, array $prerequisiteEvents) + public function __construct($variation, $value, array $prerequisiteEvents) { + $this->_variation = $variation; $this->_value = $value; $this->_prerequisiteEvents = $prerequisiteEvents; } + /** + * @return int + */ + public function getVariation() + { + return $this->_variation; + } + /** * @return null */ diff --git a/src/LaunchDarkly/GuzzleEventPublisher.php b/src/LaunchDarkly/GuzzleEventPublisher.php index ca9942542..ce18f0f14 100644 --- a/src/LaunchDarkly/GuzzleEventPublisher.php +++ b/src/LaunchDarkly/GuzzleEventPublisher.php @@ -39,7 +39,8 @@ public function __construct($sdkKey, array $options = array()) 'Content-Type' => 'application/json', 'Authorization' => $this->_sdkKey, 'User-Agent' => 'PHPClient/' . LDClient::VERSION, - 'Accept' => 'application/json' + 'Accept' => 'application/json', + 'X-LaunchDarkly-Event-Schema' => strval(EventPublisher::CURRENT_SCHEMA_VERSION) ], 'timeout' => $options['timeout'], 'connect_timeout' => $options['connect_timeout'] diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index fc21d080d..2179a0151 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -149,7 +149,7 @@ public function variation($key, $user, $default = false) try { if (is_null($user) || is_null($user->getKey())) { - $this->_sendFlagRequestEvent($key, $user, $default, $default); + $this->_sendFlagRequestEvent($key, $user, null, $default, $default); $this->_logger->warning("Variation called with null user or null user key! Returning default value"); return $default; } @@ -164,7 +164,7 @@ public function variation($key, $user, $default = false) } if (is_null($flag)) { - $this->_sendFlagRequestEvent($key, $user, $default, $default); + $this->_sendFlagRequestEvent($key, $user, null, $default, $default); return $default; } $evalResult = $flag->evaluate($user, $this->_featureRequester); @@ -173,15 +173,15 @@ public function variation($key, $user, $default = false) $this->_eventProcessor->enqueue($e); } } - if ($evalResult->getValue() !== null) { - $this->_sendFlagRequestEvent($key, $user, $evalResult->getValue(), $default, $flag->getVersion()); + if ($evalResult !== null && $evalResult->getValue() !== null) { + $this->_sendFlagRequestEvent($key, $user, $evalResult->getVariation(), $evalResult->getValue(), $default, $flag->getVersion()); return $evalResult->getValue(); } } catch (\Exception $e) { $this->_logger->error("Caught $e"); } try { - $this->_sendFlagRequestEvent($key, $user, $default, $default); + $this->_sendFlagRequestEvent($key, $user, null, $default, $default); } catch (\Exception $e) { $this->_logger->error("Caught $e"); } @@ -326,17 +326,18 @@ public function flush() /** * @param $key string * @param $user LDUser + * @param $variation int | null * @param $value mixed * @param $default * @param $version int | null * @param string | null $prereqOf */ - protected function _sendFlagRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) + protected function _sendFlagRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null) { if ($this->isOffline() || !$this->_send_events) { return; } - $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $value, $default, $version, $prereqOf)); + $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $variation, $value, $default, $version, $prereqOf)); } protected function _get_default($key, $default) diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php index 231f4b47c..6ab2ae28c 100644 --- a/src/LaunchDarkly/Util.php +++ b/src/LaunchDarkly/Util.php @@ -37,10 +37,11 @@ public static function currentTimeUnixMillis() * @param null $prereqOf string | null * @return array */ - public static function newFeatureRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) + public static function newFeatureRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null) { $event = array(); $event['user'] = $user; + $event['variation'] = $variation; $event['value'] = $value; $event['kind'] = "feature"; $event['creationDate'] = Util::currentTimeUnixMillis(); From 639a0e794ac716ecda8960e9d25af287fc5458bf Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Apr 2018 13:58:21 -0700 Subject: [PATCH 23/29] add more unit tests for flag evaluation --- tests/FeatureFlagTest.php | 351 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 339 insertions(+), 12 deletions(-) diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 8b8dde742..51f865b08 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -193,6 +193,308 @@ public function testDecodeMulti(array $feature) self::assertInstanceOf(FeatureFlag::class, $featureFlag); } + public function testFlagReturnsOffVariationIfFlagIsOff() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + '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); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(1, $result->getVariation()); + self::assertEquals('off', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsNullIfFlagIsOffAndOffVariationIsUnspecified() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => null, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertNull($result->getVariation()); + self::assertNull($result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsOffVariationIfPrerequisiteIsNotFound() + { + $flagJson = array( + 'key' => 'feature0', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array( + array('key' => 'feature1', 'variation' => 1) + ), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + $requester = new MockFeatureRequesterForFeature(); + + $result = $flag->evaluate($user, $requester); + self::assertEquals(1, $result->getVariation()); + self::assertEquals('off', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() + { + $flag0Json = array( + 'key' => 'feature0', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array( + array('key' => 'feature1', 'variation' => 1) + ), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag1Json = array( + 'key' => 'feature1', + 'version' => 2, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('nogo', 'go'), + 'salt' => '' + ); + $flag0 = FeatureFlag::decode($flag0Json); + $flag1 = FeatureFlag::decode($flag1Json); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + $requester = new MockFeatureRequesterForFeature(); + $requester->key = $flag1->getKey(); + $requester->val = $flag1; + + $result = $flag0->evaluate($user, $requester); + self::assertEquals(1, $result->getVariation()); + self::assertEquals('off', $result->getValue()); + + $events = $result->getPrerequisiteEvents(); + self::assertEquals(1, count($events)); + $event = $events[0]; + self::assertEquals('feature', $event['kind']); + self::assertEquals($flag1->getKey(), $event['key']); + self::assertEquals('nogo', $event['value']); + self::assertEquals($flag1->getVersion(), $event['version']); + self::assertEquals($flag0->getKey(), $event['prereqOf']); + } + + public function testFlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAreNoRules() + { + $flag0Json = array( + 'key' => 'feature0', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array( + array('key' => 'feature1', 'variation' => 1) + ), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag1Json = array( + 'key' => 'feature1', + 'version' => 2, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 1), + 'variations' => array('nogo', 'go'), + 'salt' => '' + ); + $flag0 = FeatureFlag::decode($flag0Json); + $flag1 = FeatureFlag::decode($flag1Json); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + $requester = new MockFeatureRequesterForFeature(); + $requester->key = $flag1->getKey(); + $requester->val = $flag1; + + $result = $flag0->evaluate($user, $requester); + self::assertEquals(0, $result->getVariation()); + self::assertEquals('fall', $result->getValue()); + + $events = $result->getPrerequisiteEvents(); + self::assertEquals(1, count($events)); + $event = $events[0]; + self::assertEquals('feature', $event['kind']); + self::assertEquals($flag1->getKey(), $event['key']); + self::assertEquals('go', $event['value']); + self::assertEquals($flag1->getVersion(), $event['version']); + self::assertEquals($flag0->getKey(), $event['prereqOf']); + } + + public function testFlagMatchesUserFromTargets() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array( + array('values' => array('whoever', 'userkey'), 'variation' => 2) + ), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(2, $result->getVariation()); + self::assertEquals('on', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagMatchesUserFromRules() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => 'key', + 'op' => 'in', + 'values' => array('userkey'), + 'negate' => false + ) + ), + 'variation' => 2 + ) + ), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(2, $result->getVariation()); + self::assertEquals('on', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function clauseCanMatchBuiltInAttribute() + { + $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(true, $result->getValue()); + } + + public function clauseCanMatchCustomAttribute() + { + $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $ub->customAttribute('legs', 4); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(true, $result->getValue()); + } + + public function clauseReturnsFalseForMissingAttribute() + { + $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(false, $result->getValue()); + } + + public function clauseCanBeNegated() + { + $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => true); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(false, $result->getValue()); + } + + public function clauseWithUnknownOperatorDoesNotMatch() + { + $clause = array('attribute' => 'name', 'op' => 'doesSomethingUnsupported', 'values' => array('Bob'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(false, $result->getValue()); + } + public function testSegmentMatchClauseRetrievesSegmentFromStore() { $segmentJson = array( @@ -234,7 +536,7 @@ public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound( self::assertFalse($result->getValue()); } - private function makeBooleanFeatureWithSegmentMatch($segmentKey) + private function booleanFlagWithClauses($clauses) { $featureJson = array( 'key' => 'test', @@ -244,17 +546,7 @@ private function makeBooleanFeatureWithSegmentMatch($segmentKey) 'variations' => array(false, true), 'fallthrough' => array('variation' => 0), 'rules' => array( - array( - 'clauses' => array( - array( - 'attribute' => '', - 'op' => 'segmentMatch', - 'values' => array($segmentKey), - 'negate' => false - ) - ), - 'variation' => 1 - ) + array('clauses' => $clauses, 'variation' => 1) ), 'offVariation' => 0, 'prerequisites' => array(), @@ -263,8 +555,43 @@ private function makeBooleanFeatureWithSegmentMatch($segmentKey) ); return FeatureFlag::decode($featureJson); } + + private function makeBooleanFeatureWithSegmentMatch($segmentKey) + { + $clause = array( + 'attribute' => '', + 'op' => 'segmentMatch', + 'values' => array($segmentKey), + 'negate' => false + ); + return $this->booleanFlagWithClauses(array($clause)); + } } +class MockFeatureRequesterForFeature implements FeatureRequester +{ + public $key = null; + public $val = null; + + function __construct($baseurl = null, $key = null, $options = null) + { + } + + public function getFeature($key) + { + return ($key == $this->key) ? $this->val : null; + } + + public function getSegment($key) + { + return null; + } + + public function getAllFeatures() + { + return null; + } +} class MockFeatureRequesterForSegment implements FeatureRequester { From 3d1d538e6874b96cb68669b9faa555bd80fb6cb8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Apr 2018 16:49:51 -0700 Subject: [PATCH 24/29] prepare 3.1.0 release --- 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 9766e60a7..3d5ea4984 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.1.0] - 2018-04-30 +### Added +- Analytics events for feature evaluations now have a `variation` property (the variation index) as well as `value`. This will allow for better performance in future versions of [`ld-relay`](https://github.com/launchdarkly/ld-relay) when it is used with the PHP client. + ## [3.0.0] - 2018-02-21 ### Added - Support for a new LaunchDarkly feature: reusable user segments. diff --git a/VERSION b/VERSION index 4a36342fc..fd2a01863 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0 +3.1.0 diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 2179a0151..6dedbac01 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -19,7 +19,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '3.0.0'; + const VERSION = '3.1.0'; /** @var string */ protected $_sdkKey; From 9b16b6abcd545e5461db471f6d2d427fbff1541b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 4 May 2018 10:42:12 -0700 Subject: [PATCH 25/29] get flag properties for event even if user is invalid --- src/LaunchDarkly/LDClient.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 2179a0151..68e917b3b 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -148,12 +148,7 @@ public function variation($key, $user, $default = false) } try { - if (is_null($user) || is_null($user->getKey())) { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default); - $this->_logger->warning("Variation called with null user or null user key! Returning default value"); - return $default; - } - if ($user->isKeyBlank()) { + if (!is_null(user) && $user->isKeyBlank()) { $this->_logger->warning("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly."); } try { @@ -167,6 +162,11 @@ public function variation($key, $user, $default = false) $this->_sendFlagRequestEvent($key, $user, null, $default, $default); return $default; } + if (is_null($user) || is_null($user->getKey())) { + $this->_sendFlagRequestEvent($key, $user, null, $default, $default, $flag->getVersion()); + $this->_logger->warning("Variation called with null user or null user key! Returning default value"); + return $default; + } $evalResult = $flag->evaluate($user, $this->_featureRequester); if (!$this->isOffline() && $this->_send_events) { foreach ($evalResult->getPrerequisiteEvents() as $e) { From f87bbc1aedbf3fdc63043ec97780e345e71b7f2c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 4 May 2018 10:45:43 -0700 Subject: [PATCH 26/29] typo --- src/LaunchDarkly/LDClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 68e917b3b..dd4231846 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -148,7 +148,7 @@ public function variation($key, $user, $default = false) } try { - if (!is_null(user) && $user->isKeyBlank()) { + if (!is_null($user) && $user->isKeyBlank()) { $this->_logger->warning("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly."); } try { From aed77596e4488d0568f89f98e541164901e22f01 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 4 May 2018 11:33:51 -0700 Subject: [PATCH 27/29] fix Redix key for segment retrieval --- src/LaunchDarkly/LDDFeatureRequester.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index 4ae4ecb44..1058f54ca 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -98,7 +98,7 @@ public function getSegment($key) $raw = $this->get_from_cache($this->_segments_key, $key); if ($raw === null) { $redis = $this->get_connection(); - $raw = $redis->hget($this->_features_key, $key); + $raw = $redis->hget($this->_segments_key, $key); if ($raw) { $this->store_in_cache($this->_segments_key, $key, $raw); } From a23be99a3c936b30ab57243bb57ce53eebff6aba Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 4 May 2018 12:40:52 -0700 Subject: [PATCH 28/29] set event version even if evaluation falls through to default --- src/LaunchDarkly/LDClient.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index dd4231846..76f3f4390 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -176,6 +176,9 @@ public function variation($key, $user, $default = false) if ($evalResult !== null && $evalResult->getValue() !== null) { $this->_sendFlagRequestEvent($key, $user, $evalResult->getVariation(), $evalResult->getValue(), $default, $flag->getVersion()); return $evalResult->getValue(); + } else { + $this->_sendFlagRequestEvent($key, $user, null, $default, $default, $flag->getVersion()); + return $default; } } catch (\Exception $e) { $this->_logger->error("Caught $e"); From 442db7f768676941382df52e3313bbe2f487aaba Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 4 May 2018 13:06:46 -0700 Subject: [PATCH 29/29] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5ea4984..a9c853cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. ## [3.1.0] - 2018-04-30 ### Added - Analytics events for feature evaluations now have a `variation` property (the variation index) as well as `value`. This will allow for better performance in future versions of [`ld-relay`](https://github.com/launchdarkly/ld-relay) when it is used with the PHP client. +### Fixed +- Fixed a bug that made segment-based rules always fall through when using `LDDFeatureRequester`. ## [3.0.0] - 2018-02-21 ### Added