From dc4ee02e221b4587f25a3377cda339a8896aabcf Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Thu, 28 Jul 2016 14:51:46 -0600 Subject: [PATCH 01/11] [wip] V2 data model + evaluation start. --- src/LaunchDarkly/EvaluationException.php | 22 + src/LaunchDarkly/FeatureFlag.php | 510 ++++++++++++++++++++ src/LaunchDarkly/FeatureRequester.php | 2 +- src/LaunchDarkly/GuzzleFeatureRequester.php | 7 +- src/LaunchDarkly/LDClient.php | 62 ++- src/LaunchDarkly/LDUser.php | 58 ++- src/LaunchDarkly/Operator.php | 71 +++ tests/FeatureFlagTest.php | 156 ++++++ 8 files changed, 846 insertions(+), 42 deletions(-) create mode 100644 src/LaunchDarkly/EvaluationException.php create mode 100644 src/LaunchDarkly/FeatureFlag.php create mode 100644 src/LaunchDarkly/Operator.php create mode 100644 tests/FeatureFlagTest.php diff --git a/src/LaunchDarkly/EvaluationException.php b/src/LaunchDarkly/EvaluationException.php new file mode 100644 index 000000000..5980b667a --- /dev/null +++ b/src/LaunchDarkly/EvaluationException.php @@ -0,0 +1,22 @@ +_key = $key; + $this->_version = $version; + $this->_on = $on; + $this->_prerequisites = $prerequisites; + $this->_salt = $salt; + $this->_targets = $targets; + $this->_rules = $rules; + $this->_fallthrough = $fallthrough; + $this->_offVariation = $offVariation; + $this->_variations = $variations; + $this->_deleted = $deleted; + } + + public static function decode($v) { + return new FeatureFlag( + $v['key'], + $v['version'], + $v['on'], + array_map(Prerequisite::getDecoder(), $v['prerequisites']), + $v['salt'], + array_map(Target::getDecoder(), $v['targets']), + array_map(Rule::getDecoder(), $v['rules']), + call_user_func(VariationOrRollout::getDecoder(), $v['fallthrough']), + $v['offVariation'], + $v['variations'], + $v['deleted']); + } + + public function isOn() { + return $this->_on; + } + + /** + * @param $user LDUser + * @param $featureRequester FeatureRequester + * @return mixed|null + */ + public function evaluate($user, $featureRequester) { + $prereqEvents = array(); + $value = $this->_evaluate($user, $featureRequester, $prereqEvents); + return $value; + } + + /** + * @param $user LDUser + * @param $featureRequester FeatureRequester + * @param $events + * @return mixed|null + */ + private function _evaluate($user, $featureRequester, $events) { + $prereqOk = true; + if ($this->_prerequisites != null) { + foreach ($this->_prerequisites as $prereq) { + try { + $prereqFeatureFlag = $featureRequester->get($prereq->getKey()); + if ($prereqFeatureFlag == null) { + return null; + } else if ($prereqFeatureFlag->isOn()) { + $prereqEvalResult = $prereqFeatureFlag->evaluate($user, $featureRequester); + $variation = $prereqFeatureFlag->getVariation($prereq->getVariation()); + if ($prereqEvalResult == null || $variation == null || $prereqEvalResult != $variation) { + $prereqOk = false; + } + } else { + $prereqOk = false; + } + } catch (EvaluationException $e) { + $prereqOk = false; + } + //TODO: Add event. + } + } + if ($prereqOk) { + return $this->getVariation($this->evaluateIndex($user)); + } + } + + /** + * @param $user LDUser + * @return int|null + */ + private function evaluateIndex($user) { + // Check to see if targets match + if ($this->_targets != null) { + foreach ($this->_targets as $target) { + foreach ($target->getValues() as $value) { + if ($value == $user->getKey()) { + return $target->getVariation(); + } + } + } + } + // Now walk through the rules and see if any match + if ($this->_rules != null) { + foreach ($this->_rules as $rule) { + if ($rule->matchesUser($user)) { + return $rule->variationIndexForUser($user, $this->_key, $this->_salt); + } + } + } + // Walk through the fallthrough and see if it matches + return $this->_fallthrough->variationIndexForUser($user, $this->_key, $this->_salt); + + } + + private function getVariation($index) { + // If the supplied index is null, then rules didn't match, and we want to return + // the off variation + if (!isset($index)) { + return null; + } + // If the index doesn't refer to a valid variation, that's an unexpected exception and we will + // return the default variation + if ($index >= count($this->_variations)) { + throw new EvaluationException("Invalid Index"); + } else { + return $this->_variations[$index]; + } + } + + public function getOffVariationValue() { + if ($this->_offVariation == null) { + return null; + } + if ($this->_offVariation >= count($this->_variations)) { + throw new EvaluationException("Invalid offVariation index"); + } + return $this->_variations[$this->_offVariation]; + } +} + +class VariationOrRollout { + private static $LONG_SCALE = 0xFFFFFFFFFFFFFFF; + + /** @var int */ + private $_variation = null; + /** @var Rollout */ + private $_rollout = null; + + protected function __construct($variation, $rollout) { + $this->_variation = $variation; + $this->_rollout = $rollout; + } + + public static function getDecoder() { + return function ($v) { + return new VariationOrRollout( + isset($v['variation']) ? $v['variation'] : null, + isset($v['rollout']) ? $v['rollout'] : null); + }; + } + + /** + * @return int + */ + public function getVariation() { + return $this->_variation; + } + + /** + * @return Rollout + */ + public function getRollout() { + return $this->_rollout; + } + + /** + * @param $user LDUser + * @param $_key string + * @param $_salt string + * @return int|null + */ + public function variationIndexForUser($user, $_key, $_salt) { + if ($this->_variation != null) { + return $this->_variation; + } else if ($this->_rollout != null) { + $bucketBy = $this->_rollout->getBucketBy() == null ? "key" : $this->_rollout->getBucketBy(); + $bucket = $this->bucketUser($user, $_key, $bucketBy, $_salt); + $sum = 0.0; + foreach ($this->_rollout->getVariations() as $wv) { + $sum += $wv->getWeight() / 100000.0; + if ($bucket < $sum) { + return $wv->getVariation(); + } + } + } + return null; + } + + /** + * @param $user LDUser + * @param $_key string + * @param $attr string + * @param $_salt string + * @return float + */ + private function bucketUser($user, $_key, $attr, $_salt) { + $userValue = $user->getValueForEvaluation($attr); + $idHash = null; + if ($userValue != null) { + if (is_string($userValue)) { + $idHash = $userValue; + if ($user->getSecondary() != null) { + $idHash = $idHash . "." . $user->getSecondary(); + } + $hash = substr(sha1($_key . "." . $_salt . "." . $idHash), 0, 15); + $longVal = base_convert($hash, 16, 10); + $result = $longVal / self::$LONG_SCALE; + + return $result; + } + } + return 0.0; + } +} + +class Clause { + private $_attribute = null; + private $_op = null; + private $_values = array(); + private $_negate = false; + + private function __construct($attribute, $op, array $values, $negate) { + $this->_attribute = $attribute; + $this->_op = $op; + $this->_values = $values; + $this->_negate = $negate; + } + + public static function getDecoder() { + return function ($v) { + return new Clause($v['attribute'], $v['op'], $v['values'], $v['negate']); + }; + } + + /** + * @param $user LDUser + * @return bool + */ + public function matchesUser($user) { + $userValue = $user->getValueForEvaluation($this->_attribute); + if ($userValue == null) { + return false; + } + if (is_array($userValue)) { + foreach ($userValue as $element) { + if ($this->matchAny($userValue)) { + return $this->_maybeNegate(true); + } + } + return $this->maybeNegate(false); + } else { + return $this->maybeNegate($this->matchAny($userValue)); + } + } + + /** + * @return null + */ + public function getAttribute() { + return $this->_attribute; + } + + /** + * @return null + */ + public function getOp() { + return $this->_op; + } + + /** + * @return array + */ + public function getValues() { + return $this->_values; + } + + /** + * @return boolean + */ + public function isNegate() { + return $this->_negate; + } + + /** + * @param $userValue + * @return bool + */ + private function matchAny($userValue) { + foreach ($this->_values as $v) { + if (Operator::apply($this->_op, $userValue, $v)) { + return true; + } + } + return false; + } + + private function _maybeNegate($b) { + if ($this->_negate) { + return !$b; + } else { + return $b; + } + } +} + +class Rule extends VariationOrRollout { + /** @var Clause[] */ + private $_clauses = array(); + + protected function __construct($variation, $rollout, array $clauses) { + parent::__construct($variation, $rollout); + $this->_clauses = $clauses; + } + + public static function getDecoder() { + return function ($v) { + return new Rule( + isset($v['variation']) ? $v['variation'] : null, + isset($v['rollout']) ? $v['rollout'] : null, + array_map(Clause::getDecoder(), $v['clauses'])); + }; + } + + /** + * @param $user LDUser + * @return bool + */ + public function matchesUser($user) { + foreach ($this->_clauses as $clause) { + if (!$clause->matchesUser($user)) { + return false; + } + } + return true; + } + + /** + * @return Clause[] + */ + public function getClauses() { + return $this->_clauses; + } +} + +class WeightedVariation { + /** @var int */ + private $_variation = null; + /** @var int */ + private $_weight = null; + + private function __construct($variation, $weight) { + $this->_variation = $variation; + $this->_weight = $weight; + } + + public static function getDecoder() { + return function ($v) { + return new WeightedVariation($v['variation'], $v['weight']); + }; + } + + /** + * @return int + */ + public function getVariation() { + return $this->_variation; + } + + /** + * @return int + */ + public function getWeight() { + return $this->_weight; + } +} + +class Target { + /** @var string[] */ + private $_values = array(); + /** @var int */ + private $_variation = null; + + protected function __construct(array $values, $variation) { + $this->_values = $values; + $this->_variation = $variation; + } + + public static function getDecoder() { + return function ($v) { + return new Target($v['values'], $v['variation']); + }; + } + + /** + * @return \string[] + */ + public function getValues() { + return $this->_values; + } + + /** + * @return int + */ + public function getVariation() { + return $this->_variation; + } +} + +class Prerequisite { + /** @var string */ + private $_key = null; + /** @var int */ + private $_variation = null; + + protected function __construct($key, $variation) { + $this->_key = $key; + $this->_variation = $variation; + } + + public static function getDecoder() { + return function ($v) { + return new Prerequisite($v['key'], $v['variation']); + }; + } + + /** + * @return string + */ + public function getKey() { + return $this->_key; + } + + /** + * @return int + */ + public function getVariation() { + return $this->_variation; + } +} + +class Rollout { + /** @var WeightedVariation[] */ + private $_variations = array(); + /** @var string */ + private $_bucketBy = null; + + protected function __construct(array $variations, $bucketBy) { + $this->_variations = $variations; + $this->_bucketBy = $bucketBy; + } + + public static function getDecoder() { + return function ($v) { + return new Rollout($v['variations'], $v['bucketBy']); + }; + } + + /** + * @return WeightedVariation[] + */ + public function getVariations() { + return $this->_variations; + } + + /** + * @return string + */ + public function getBucketBy() { + return $this->_bucketBy; + } +} diff --git a/src/LaunchDarkly/FeatureRequester.php b/src/LaunchDarkly/FeatureRequester.php index f330dba76..25a7c9186 100644 --- a/src/LaunchDarkly/FeatureRequester.php +++ b/src/LaunchDarkly/FeatureRequester.php @@ -7,7 +7,7 @@ interface FeatureRequester { * Gets feature data from a likely cached store * * @param $key string feature key - * @return array|null The decoded JSON feature data, or null if missing + * @return FeatureFlag|null The decoded FeatureFlag, or null if missing */ public function get($key); } \ No newline at end of file diff --git a/src/LaunchDarkly/GuzzleFeatureRequester.php b/src/LaunchDarkly/GuzzleFeatureRequester.php index a53385e67..d1c29458b 100644 --- a/src/LaunchDarkly/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/GuzzleFeatureRequester.php @@ -16,7 +16,6 @@ class GuzzleFeatureRequester implements FeatureRequester function __construct($baseUri, $apiKey, $options) { $this->_baseUri = $baseUri; - error_log("uri: $baseUri"); $stack = HandlerStack::create(); $stack->push(new CacheMiddleware(new PublicCacheStrategy(isset($options['cache']) ? $options['cache'] : null), 'cache')); @@ -37,15 +36,15 @@ function __construct($baseUri, $apiKey, $options) * Gets feature data from a likely cached store * * @param $key string feature key - * @return array|null The decoded JSON feature data, or null if missing + * @return FeatureFlag|null The decoded FeatureFlag, or null if missing */ public function get($key) { try { - $uri = $this->_baseUri . "/api/eval/features/$key"; + $uri = $this->_baseUri . "/sdk/latest-flags/" . $key; $response = $this->_client->get($uri, $this->_defaults); $body = $response->getBody(); - return json_decode($body, true); + return FeatureFlag::decode(json_decode($body, true)); } catch (BadResponseException $e) { $code = $e->getResponse()->getStatusCode(); error_log("GuzzleFeatureRetriever::get received an unexpected HTTP status code $code"); diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 6b38f25f6..c440e26a0 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -24,8 +24,8 @@ class LDClient { /** * Creates a new client instance that connects to LaunchDarkly. * - * @param string $apiKey The API key for your account - * @param array $options Client configuration settings + * @param string $apiKey The API key for your account + * @param array $options Client configuration settings * - base_uri: Base URI of the LaunchDarkly API. Defaults to `DEFAULT_BASE_URI` * - timeout: Float describing the maximum length of a request in seconds. Defaults to 3 * - connect_timeout: Float describing the number of seconds to wait while trying to connect to a server. Defaults to 3 @@ -35,8 +35,7 @@ public function __construct($apiKey, $options = array()) { $this->_apiKey = $apiKey; if (!isset($options['base_uri'])) { $this->_baseUri = self::DEFAULT_BASE_URI; - } - else { + } else { $this->_baseUri = rtrim($options['base_uri'], '/'); } if (isset($options['events'])) { @@ -75,15 +74,15 @@ public function getFlag($key, $user, $default = false) { return $this->toggle($key, $user, $default); } - /** - * Calculates the value of a feature flag for a given user. - * - * @param string $key The unique key for the feature flag - * @param LDUser $user The end user requesting the flag - * @param boolean $default The default value of the flag - * - * @return boolean Whether or not the flag should be enabled, or `default` if the flag is disabled in the LaunchDarkly control panel - */ + /** + * Calculates the value of a feature flag for a given user. + * + * @param string $key The unique key for the feature flag + * @param LDUser $user The end user requesting the flag + * @param boolean $default The default value of the flag + * + * @return boolean Whether or not the flag should be enabled, or `default` if the flag is disabled in the LaunchDarkly control panel + */ public function toggle($key, $user, $default = false) { if ($this->_offline) { return $default; @@ -91,22 +90,36 @@ public function toggle($key, $user, $default = false) { try { $default = $this->_get_default($key, $default); - $flag = $this->_toggle($key, $user); + if (is_null($user) || strlen($user['key']) == 0) { + $this->_sendFlagRequestEvent($key, $user, $default, $default); + return $default; + } + $flag = $this->_featureRequester->get($key); +// $flag = $this->_get_flag($key, $user); if (is_null($flag)) { $this->_sendFlagRequestEvent($key, $user, $default, $default); return $default; + } else if ($flag->isOn()) { + $result = $flag->evaluate($user, $this->_featureRequester); + if (!$this->_offline) { + //TODO: send prereq events + } + if ($result != null) { + $this->_sendFlagRequestEvent($key, $user, $result, $default); + return $result; + } } - else { - $this->_sendFlagRequestEvent($key, $user, $flag, $default); - return $flag; + $offVariation = $flag->getOffVariationValue(); + if ($offVariation != null) { + $this->_sendFlagRequestEvent($key, $user, $offVariation, $default); + return $offVariation; } } catch (\Exception $e) { error_log("LaunchDarkly caught $e"); try { - $this->_sendFlagRequestEvent($key, $user, $default, $default); - } - catch (\Exception $e) { + $this->_sendFlagRequestEvent($key, $user, $default, $default); + } catch (\Exception $e) { error_log("LaunchDarkly caught $e"); } return $default; @@ -174,7 +187,7 @@ public function identify($user) { $event['kind'] = "identify"; $event['creationDate'] = round(microtime(1) * 1000); $event['key'] = $user->getKey(); - $this->_eventProcessor->enqueue($event); + $this->_eventProcessor->enqueue($event); } /** @@ -194,10 +207,10 @@ protected function _sendFlagRequestEvent($key, $user, $value, $default) { $event['creationDate'] = round(microtime(1) * 1000); $event['key'] = $key; $event['default'] = $default; - $this->_eventProcessor->enqueue($event); + $this->_eventProcessor->enqueue($event); } - protected function _toggle($key, $user) { + protected function _get_flag($key, $user) { try { $data = $this->_featureRequester->get($key); if ($data == null) { @@ -229,8 +242,7 @@ protected static function _decode($json, $user) { $targets = array_map($makeTarget, $ts); if (isset($v['userTarget'])) { return new Variation($v['value'], $v['weight'], $targets, $makeTarget($v['userTarget'])); - } - else { + } else { return new Variation($v['value'], $v['weight'], $targets, null); } }; diff --git a/src/LaunchDarkly/LDUser.php b/src/LaunchDarkly/LDUser.php index fb6ea96cb..02c806e0c 100644 --- a/src/LaunchDarkly/LDUser.php +++ b/src/LaunchDarkly/LDUser.php @@ -20,19 +20,19 @@ class LDUser { protected $_custom = array(); /** - * @param string $key Unique key for the user. For authenticated users, this may be a username or e-mail address. For anonymous users, this could be an IP address or session ID. - * @param string|null $secondary An optional secondary identifier - * @param string|null $ip The user's IP address (optional) - * @param string|null $country The user's country, as an ISO 3166-1 alpha-2 code (e.g. 'US') (optional) - * @param string|null $email The user's e-mail address (optional) - * @param string|null $name The user's full name (optional) - * @param string|null $avatar A URL pointing to the user's avatar image (optional) - * @param string|null $firstName The user's first name (optional) - * @param string|null $lastName The user's last name (optional) + * @param string $key Unique key for the user. For authenticated users, this may be a username or e-mail address. For anonymous users, this could be an IP address or session ID. + * @param string|null $secondary An optional secondary identifier + * @param string|null $ip The user's IP address (optional) + * @param string|null $country The user's country, as an ISO 3166-1 alpha-2 code (e.g. 'US') (optional) + * @param string|null $email The user's e-mail address (optional) + * @param string|null $name The user's full name (optional) + * @param string|null $avatar A URL pointing to the user's avatar image (optional) + * @param string|null $firstName The user's first name (optional) + * @param string|null $lastName The user's last name (optional) * @param boolean|null $anonymous Whether this is an anonymous user - * @param array|null $custom Other custom attributes that can be used to create custom rules + * @param array|null $custom Other custom attributes that can be used to create custom rules */ - public function __construct($key, $secondary = null, $ip = null, $country = null, $email = null, $name = null, $avatar = null, $firstName = null, $lastName= null, $anonymous = null, $custom = array()) { + public function __construct($key, $secondary = null, $ip = null, $country = null, $email = null, $name = null, $avatar = null, $firstName = null, $lastName = null, $anonymous = null, $custom = array()) { $this->_key = strval($key); $this->_secondary = $secondary; $this->_ip = $ip; @@ -46,6 +46,40 @@ public function __construct($key, $secondary = null, $ip = null, $country = null $this->_custom = $custom; } + public function getValueForEvaluation($attr) { + switch ($attr) { + case "key": + return $this->getKey(); + case "secondary": //not available for evaluation. + return null; + case "ip": + return $this->getIP(); + case "country": + return $this->getCountry(); + case "email": + return $this->getEmail(); + case "name": + return $this->getName(); + case "avatar": + return $this->getAvatar(); + case "firstName": + return $this->getFirstName(); + case "lastName": + return $this->getLastName(); + case "anonymous": + return $this->getAnonymous(); + default: + $custom = $this->getCustom(); + if (is_null($custom)) { + return null; + } + if (!array_key_exists($attr, $custom)) { + return null; + } + return $custom[$attr]; + } + } + public function getCountry() { return $this->_country; } @@ -122,7 +156,7 @@ public function toJSON() { } if (isset($this->_anonymous)) { $json['anonymous'] = $this->_anonymous; - } + } return $json; } } diff --git a/src/LaunchDarkly/Operator.php b/src/LaunchDarkly/Operator.php new file mode 100644 index 000000000..eb0ca25d2 --- /dev/null +++ b/src/LaunchDarkly/Operator.php @@ -0,0 +1,71 @@ + $c; + } + break; + case "greaterThanOrEqual": + if (is_numeric($u) && is_numeric($c)) { + return $u >= $c; + } + break; + case "before": + break; + case "after": + break; + } + } finally { + return false; + } + } +} \ No newline at end of file diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php new file mode 100644 index 000000000..3d084f047 --- /dev/null +++ b/tests/FeatureFlagTest.php @@ -0,0 +1,156 @@ + Date: Thu, 28 Jul 2016 16:44:21 -0600 Subject: [PATCH 02/11] [wip] V2 data model + evaluation start. --- src/LaunchDarkly/FeatureFlag.php | 2 +- src/LaunchDarkly/LDClient.php | 2 +- .../{Operator.php => Operators.php} | 57 ++++++++++++++++++- tests/OperatorsTest.php | 22 +++++++ 4 files changed, 80 insertions(+), 3 deletions(-) rename src/LaunchDarkly/{Operator.php => Operators.php} (57%) create mode 100644 tests/OperatorsTest.php diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 545ed451a..95e4cc9ca 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -326,7 +326,7 @@ public function isNegate() { */ private function matchAny($userValue) { foreach ($this->_values as $v) { - if (Operator::apply($this->_op, $userValue, $v)) { + if (Operators::apply($this->_op, $userValue, $v)) { return true; } } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index c440e26a0..6d21233b1 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -90,7 +90,7 @@ public function toggle($key, $user, $default = false) { try { $default = $this->_get_default($key, $default); - if (is_null($user) || strlen($user['key']) == 0) { + if (is_null($user) || strlen($user->getKey()) == 0) { $this->_sendFlagRequestEvent($key, $user, $default, $default); return $default; } diff --git a/src/LaunchDarkly/Operator.php b/src/LaunchDarkly/Operators.php similarity index 57% rename from src/LaunchDarkly/Operator.php rename to src/LaunchDarkly/Operators.php index eb0ca25d2..a747c0911 100644 --- a/src/LaunchDarkly/Operator.php +++ b/src/LaunchDarkly/Operators.php @@ -1,7 +1,12 @@ $cTime; + } + } break; } } finally { return false; } } + + /** + * @param $in + * @return null|int|float + */ + public static function parseTime($in) { + if (is_numeric($in)) { + return $in; + } + + if ($in instanceof DateTime) { + return self::dateTimeToUnixMillis($in); + } + + if (is_string($in)) { + try { + $dateTime = new DateTime($in); + return self::dateTimeToUnixMillis($dateTime); + } catch (Exception $e) { + error_log("LaunchDarkly: Could not parse timestamp: " . $in); + return null; + } + } + return null; + } + + /** + * @param $dateTime DateTime + * @return int + */ + private static function dateTimeToUnixMillis($dateTime) { + $timeStampSeconds = (int)$dateTime->getTimeStamp(); + $timestampMicros = $dateTime->format('u'); + return $timeStampSeconds * 1000 + (int)($timestampMicros / 1000); + } + } \ No newline at end of file diff --git a/tests/OperatorsTest.php b/tests/OperatorsTest.php new file mode 100644 index 000000000..383c3220e --- /dev/null +++ b/tests/OperatorsTest.php @@ -0,0 +1,22 @@ +assertEquals(0, Operators::parseTime(0)); + $this->assertEquals(100, Operators::parseTime(100)); + $this->assertEquals(100, Operators::parseTime(100)); + $this->assertEquals(1000, Operators::parseTime("1970-01-01T00:00:01Z")); + $this->assertEquals(1001, Operators::parseTime("1970-01-01T00:00:01.001Z")); + + + $this->assertEquals(null, Operators::parseTime("NOT A REAL TIMESTAMP")); + $this->assertEquals(null, Operators::parseTime([])); + + } +} \ No newline at end of file From 02f4118e15c4c59098178ef49e674f42433442ac Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 13:16:39 -0700 Subject: [PATCH 03/11] [wip] V2 eval works. --- src/LaunchDarkly/FeatureFlag.php | 45 ++++++++++++++++++++------------ src/LaunchDarkly/LDClient.php | 16 +++++++----- src/LaunchDarkly/Operators.php | 24 ++++++++++++----- tests/OperatorsTest.php | 37 ++++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 95e4cc9ca..993dc03bf 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -96,7 +96,7 @@ private function _evaluate($user, $featureRequester, $events) { } else if ($prereqFeatureFlag->isOn()) { $prereqEvalResult = $prereqFeatureFlag->evaluate($user, $featureRequester); $variation = $prereqFeatureFlag->getVariation($prereq->getVariation()); - if ($prereqEvalResult == null || $variation == null || $prereqEvalResult != $variation) { + if ($prereqEvalResult === null || $variation === null || $prereqEvalResult !== $variation) { $prereqOk = false; } } else { @@ -111,6 +111,7 @@ private function _evaluate($user, $featureRequester, $events) { if ($prereqOk) { return $this->getVariation($this->evaluateIndex($user)); } + return null; } /** @@ -122,7 +123,7 @@ private function evaluateIndex($user) { if ($this->_targets != null) { foreach ($this->_targets as $target) { foreach ($target->getValues() as $value) { - if ($value == $user->getKey()) { + if ($value === $user->getKey()) { return $target->getVariation(); } } @@ -138,7 +139,6 @@ private function evaluateIndex($user) { } // Walk through the fallthrough and see if it matches return $this->_fallthrough->variationIndexForUser($user, $this->_key, $this->_salt); - } private function getVariation($index) { @@ -157,7 +157,7 @@ private function getVariation($index) { } public function getOffVariationValue() { - if ($this->_offVariation == null) { + if ($this->_offVariation === null) { return null; } if ($this->_offVariation >= count($this->_variations)) { @@ -184,7 +184,7 @@ public static function getDecoder() { return function ($v) { return new VariationOrRollout( isset($v['variation']) ? $v['variation'] : null, - isset($v['rollout']) ? $v['rollout'] : null); + isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null); }; } @@ -209,10 +209,11 @@ public function getRollout() { * @return int|null */ public function variationIndexForUser($user, $_key, $_salt) { - if ($this->_variation != null) { + if ($this->_variation !== null) { + error_log("Returning: $this->_variation"); return $this->_variation; - } else if ($this->_rollout != null) { - $bucketBy = $this->_rollout->getBucketBy() == null ? "key" : $this->_rollout->getBucketBy(); + } else if ($this->_rollout !== null) { + $bucketBy = $this->_rollout->getBucketBy() === null ? "key" : $this->_rollout->getBucketBy(); $bucket = $this->bucketUser($user, $_key, $bucketBy, $_salt); $sum = 0.0; foreach ($this->_rollout->getVariations() as $wv) { @@ -222,6 +223,7 @@ public function variationIndexForUser($user, $_key, $_salt) { } } } + error_log("both fallthrough and variation are null!!"); return null; } @@ -238,7 +240,7 @@ private function bucketUser($user, $_key, $attr, $_salt) { if ($userValue != null) { if (is_string($userValue)) { $idHash = $userValue; - if ($user->getSecondary() != null) { + if ($user->getSecondary() !== null) { $idHash = $idHash . "." . $user->getSecondary(); } $hash = substr(sha1($_key . "." . $_salt . "." . $idHash), 0, 15); @@ -277,18 +279,22 @@ public static function getDecoder() { */ public function matchesUser($user) { $userValue = $user->getValueForEvaluation($this->_attribute); - if ($userValue == null) { +// error_log("user value: $userValue"); + if ($userValue === null) { + error_log("null user value"); return false; } if (is_array($userValue)) { + error_log("uservalue is array"); foreach ($userValue as $element) { - if ($this->matchAny($userValue)) { + if ($this->matchAny($element)) { return $this->_maybeNegate(true); } } - return $this->maybeNegate(false); + return $this->_maybeNegate(false); } else { - return $this->maybeNegate($this->matchAny($userValue)); + error_log("else..."); + return $this->_maybeNegate($this->matchAny($userValue)); } } @@ -326,7 +332,10 @@ public function isNegate() { */ private function matchAny($userValue) { foreach ($this->_values as $v) { - if (Operators::apply($this->_op, $userValue, $v)) { + $result = Operators::apply($this->_op, $userValue, $v); + error_log("clause.matchany operator result for v: $v $result"); + if ($result) { + error_log("true for $userValue"); return true; } } @@ -355,7 +364,7 @@ public static function getDecoder() { return function ($v) { return new Rule( isset($v['variation']) ? $v['variation'] : null, - isset($v['rollout']) ? $v['rollout'] : null, + isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null, array_map(Clause::getDecoder(), $v['clauses'])); }; } @@ -367,9 +376,11 @@ public static function getDecoder() { public function matchesUser($user) { foreach ($this->_clauses as $clause) { if (!$clause->matchesUser($user)) { + error_log("false from rule.matchesuser with attr: " . $clause->getAttribute()); return false; } } + error_log("true from rule.matchesuser"); return true; } @@ -490,7 +501,9 @@ protected function __construct(array $variations, $bucketBy) { public static function getDecoder() { return function ($v) { - return new Rollout($v['variations'], $v['bucketBy']); + return new Rollout( + array_map(WeightedVariation::getDecoder(), $v['variations']), + isset($v['bucketBy']) ? $v['bucketBy'] : null); }; } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 6d21233b1..8fd7f3f5b 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -81,7 +81,7 @@ public function getFlag($key, $user, $default = false) { * @param LDUser $user The end user requesting the flag * @param boolean $default The default value of the flag * - * @return boolean Whether or not the flag should be enabled, or `default` if the flag is disabled in the LaunchDarkly control panel + * @return mixed Whether or not the flag should be enabled, or `default` */ public function toggle($key, $user, $default = false) { if ($this->_offline) { @@ -101,11 +101,13 @@ public function toggle($key, $user, $default = false) { $this->_sendFlagRequestEvent($key, $user, $default, $default); return $default; } else if ($flag->isOn()) { + error_log("got a flag and it's on"); $result = $flag->evaluate($user, $this->_featureRequester); if (!$this->_offline) { //TODO: send prereq events } if ($result != null) { +// error_log("result: $result"); $this->_sendFlagRequestEvent($key, $user, $result, $default); return $result; } @@ -117,13 +119,13 @@ public function toggle($key, $user, $default = false) { } } catch (\Exception $e) { error_log("LaunchDarkly caught $e"); - try { - $this->_sendFlagRequestEvent($key, $user, $default, $default); - } catch (\Exception $e) { - error_log("LaunchDarkly caught $e"); - } - return $default; } + try { + $this->_sendFlagRequestEvent($key, $user, $default, $default); + } catch (\Exception $e) { + error_log("LaunchDarkly caught $e"); + } + return $default; } /** diff --git a/src/LaunchDarkly/Operators.php b/src/LaunchDarkly/Operators.php index a747c0911..21ed43510 100644 --- a/src/LaunchDarkly/Operators.php +++ b/src/LaunchDarkly/Operators.php @@ -14,29 +14,38 @@ class Operators { * @return bool */ public static function apply($op, $u, $c) { + error_log("apply with op: $op u: $u c: $c"); try { - if ($u == null || $c == null) { + if ($u === null || $c === null) { + error_log("one or both are null"); return false; } switch ($op) { case "in": - if ($u == $c) { + error_log("in with u: $u and c: $c"); + if ($u === $c) { + error_log("returning true from in op"); return true; } + if (is_numeric($u) && is_numeric($c)) { + return $u == $c; + } break; case "endsWith": if (is_string($u) && is_string($c)) { - return substr_compare($u, $c, strlen($c)) === 0; + return $c === "" || (($temp = strlen($u) - strlen($c)) >= 0 && strpos($u, $c, $temp) !== false); } break; case "startsWith": if (is_string($u) && is_string($c)) { - return substr_compare($u, $c, -strlen($c)) === 0; + return strpos($u, $c) === 0; } break; case "matches": if (is_string($u) && is_string($c)) { - return preg_match($c, $u) == 1; + error_log("u: $u c: $c"); + //PHP can do subpatterns, but everything needs to be wrapped in an outer (): + return preg_match("($c)", $u) === 1; } break; case "contains": @@ -83,9 +92,10 @@ public static function apply($op, $u, $c) { } break; } - } finally { - return false; + } catch (Exception $e) { + //TODO: log warning } + return false; } /** diff --git a/tests/OperatorsTest.php b/tests/OperatorsTest.php index 383c3220e..15e04e8da 100644 --- a/tests/OperatorsTest.php +++ b/tests/OperatorsTest.php @@ -7,7 +7,41 @@ class OperatorsTest extends \PHPUnit_Framework_TestCase { - public function testDefaultCtor() { + public function testIn() { + $this->assertTrue(Operators::apply("in", "A string to match", "A string to match")); + $this->assertFalse(Operators::apply("in", "A string to match", true)); + $this->assertTrue(Operators::apply("in", 34, 34)); + $this->assertTrue(Operators::apply("in", 34, 34.0)); + $this->assertFalse(Operators::apply("in", 34, true)); + $this->assertTrue(Operators::apply("in", false, false)); + $this->assertTrue(Operators::apply("in", true, true)); + $this->assertFalse(Operators::apply("in", true, false)); + $this->assertFalse(Operators::apply("in", false, true)); + + } + + public function testStartsWith() { + $this->assertTrue(Operators::apply("startsWith", "start", "start")); + $this->assertTrue(Operators::apply("startsWith", "start plus more", "start")); + $this->assertFalse(Operators::apply("startsWith", "does not contain", "start")); + $this->assertFalse(Operators::apply("startsWith", "does not start with", "start")); + } + + public function testEndsWith() { + $this->assertTrue(Operators::apply("endsWith", "end", "end")); + $this->assertTrue(Operators::apply("endsWith", "something somethingend", "end")); + $this->assertFalse(Operators::apply("endsWith", "does not contain", "end")); + $this->assertFalse(Operators::apply("endsWith", "does not end with", "end")); + } + + public function testMatches() { + $this->assertTrue(Operators::apply("matches", "anything", ".*")); + $this->assertTrue(Operators::apply("matches", "darn", "(\\W|^)(baloney|darn|drat|fooey|gosh\\sdarnit|heck)(\\W|$)")); + } + + + + public function testParseTime() { $this->assertEquals(0, Operators::parseTime(0)); $this->assertEquals(100, Operators::parseTime(100)); $this->assertEquals(100, Operators::parseTime(100)); @@ -17,6 +51,5 @@ public function testDefaultCtor() { $this->assertEquals(null, Operators::parseTime("NOT A REAL TIMESTAMP")); $this->assertEquals(null, Operators::parseTime([])); - } } \ No newline at end of file From 5b9defde96977fd3a6ae8679cf51f7117cfa80b0 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 13:32:20 -0700 Subject: [PATCH 04/11] [wip] Move some things out of big file. --- src/LaunchDarkly/Clause.php | 99 +++++++++++ src/LaunchDarkly/FeatureFlag.php | 225 ------------------------ src/LaunchDarkly/Rule.php | 50 ++++++ src/LaunchDarkly/VariationOrRollout.php | 91 ++++++++++ 4 files changed, 240 insertions(+), 225 deletions(-) create mode 100644 src/LaunchDarkly/Clause.php create mode 100644 src/LaunchDarkly/Rule.php create mode 100644 src/LaunchDarkly/VariationOrRollout.php diff --git a/src/LaunchDarkly/Clause.php b/src/LaunchDarkly/Clause.php new file mode 100644 index 000000000..dd7d61c0d --- /dev/null +++ b/src/LaunchDarkly/Clause.php @@ -0,0 +1,99 @@ +_attribute = $attribute; + $this->_op = $op; + $this->_values = $values; + $this->_negate = $negate; + } + + public static function getDecoder() { + return function ($v) { + return new Clause($v['attribute'], $v['op'], $v['values'], $v['negate']); + }; + } + + /** + * @param $user LDUser + * @return bool + */ + public function matchesUser($user) { + $userValue = $user->getValueForEvaluation($this->_attribute); + if ($userValue === null) { + error_log("null user value"); + return false; + } + if (is_array($userValue)) { + error_log("uservalue is array"); + foreach ($userValue as $element) { + if ($this->matchAny($element)) { + return $this->_maybeNegate(true); + } + } + return $this->_maybeNegate(false); + } else { + error_log("else..."); + return $this->_maybeNegate($this->matchAny($userValue)); + } + } + + /** + * @return null + */ + public function getAttribute() { + return $this->_attribute; + } + + /** + * @return null + */ + public function getOp() { + return $this->_op; + } + + /** + * @return array + */ + public function getValues() { + return $this->_values; + } + + /** + * @return boolean + */ + public function isNegate() { + return $this->_negate; + } + + /** + * @param $userValue + * @return bool + */ + private function matchAny($userValue) { + foreach ($this->_values as $v) { + $result = Operators::apply($this->_op, $userValue, $v); + error_log("clause.matchany operator result for v: $v $result"); + if ($result) { + error_log("true for $userValue"); + return true; + } + } + return false; + } + + private function _maybeNegate($b) { + if ($this->_negate) { + return !$b; + } else { + return $b; + } + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 993dc03bf..bf884aeec 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -167,231 +167,6 @@ public function getOffVariationValue() { } } -class VariationOrRollout { - private static $LONG_SCALE = 0xFFFFFFFFFFFFFFF; - - /** @var int */ - private $_variation = null; - /** @var Rollout */ - private $_rollout = null; - - protected function __construct($variation, $rollout) { - $this->_variation = $variation; - $this->_rollout = $rollout; - } - - public static function getDecoder() { - return function ($v) { - return new VariationOrRollout( - isset($v['variation']) ? $v['variation'] : null, - isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null); - }; - } - - /** - * @return int - */ - public function getVariation() { - return $this->_variation; - } - - /** - * @return Rollout - */ - public function getRollout() { - return $this->_rollout; - } - - /** - * @param $user LDUser - * @param $_key string - * @param $_salt string - * @return int|null - */ - public function variationIndexForUser($user, $_key, $_salt) { - if ($this->_variation !== null) { - error_log("Returning: $this->_variation"); - return $this->_variation; - } else if ($this->_rollout !== null) { - $bucketBy = $this->_rollout->getBucketBy() === null ? "key" : $this->_rollout->getBucketBy(); - $bucket = $this->bucketUser($user, $_key, $bucketBy, $_salt); - $sum = 0.0; - foreach ($this->_rollout->getVariations() as $wv) { - $sum += $wv->getWeight() / 100000.0; - if ($bucket < $sum) { - return $wv->getVariation(); - } - } - } - error_log("both fallthrough and variation are null!!"); - return null; - } - - /** - * @param $user LDUser - * @param $_key string - * @param $attr string - * @param $_salt string - * @return float - */ - private function bucketUser($user, $_key, $attr, $_salt) { - $userValue = $user->getValueForEvaluation($attr); - $idHash = null; - if ($userValue != null) { - if (is_string($userValue)) { - $idHash = $userValue; - if ($user->getSecondary() !== null) { - $idHash = $idHash . "." . $user->getSecondary(); - } - $hash = substr(sha1($_key . "." . $_salt . "." . $idHash), 0, 15); - $longVal = base_convert($hash, 16, 10); - $result = $longVal / self::$LONG_SCALE; - - return $result; - } - } - return 0.0; - } -} - -class Clause { - private $_attribute = null; - private $_op = null; - private $_values = array(); - private $_negate = false; - - private function __construct($attribute, $op, array $values, $negate) { - $this->_attribute = $attribute; - $this->_op = $op; - $this->_values = $values; - $this->_negate = $negate; - } - - public static function getDecoder() { - return function ($v) { - return new Clause($v['attribute'], $v['op'], $v['values'], $v['negate']); - }; - } - - /** - * @param $user LDUser - * @return bool - */ - public function matchesUser($user) { - $userValue = $user->getValueForEvaluation($this->_attribute); -// error_log("user value: $userValue"); - if ($userValue === null) { - error_log("null user value"); - return false; - } - if (is_array($userValue)) { - error_log("uservalue is array"); - foreach ($userValue as $element) { - if ($this->matchAny($element)) { - return $this->_maybeNegate(true); - } - } - return $this->_maybeNegate(false); - } else { - error_log("else..."); - return $this->_maybeNegate($this->matchAny($userValue)); - } - } - - /** - * @return null - */ - public function getAttribute() { - return $this->_attribute; - } - - /** - * @return null - */ - public function getOp() { - return $this->_op; - } - - /** - * @return array - */ - public function getValues() { - return $this->_values; - } - - /** - * @return boolean - */ - public function isNegate() { - return $this->_negate; - } - - /** - * @param $userValue - * @return bool - */ - private function matchAny($userValue) { - foreach ($this->_values as $v) { - $result = Operators::apply($this->_op, $userValue, $v); - error_log("clause.matchany operator result for v: $v $result"); - if ($result) { - error_log("true for $userValue"); - return true; - } - } - return false; - } - - private function _maybeNegate($b) { - if ($this->_negate) { - return !$b; - } else { - return $b; - } - } -} - -class Rule extends VariationOrRollout { - /** @var Clause[] */ - private $_clauses = array(); - - protected function __construct($variation, $rollout, array $clauses) { - parent::__construct($variation, $rollout); - $this->_clauses = $clauses; - } - - public static function getDecoder() { - return function ($v) { - return new Rule( - isset($v['variation']) ? $v['variation'] : null, - isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null, - array_map(Clause::getDecoder(), $v['clauses'])); - }; - } - - /** - * @param $user LDUser - * @return bool - */ - public function matchesUser($user) { - foreach ($this->_clauses as $clause) { - if (!$clause->matchesUser($user)) { - error_log("false from rule.matchesuser with attr: " . $clause->getAttribute()); - return false; - } - } - error_log("true from rule.matchesuser"); - return true; - } - - /** - * @return Clause[] - */ - public function getClauses() { - return $this->_clauses; - } -} - class WeightedVariation { /** @var int */ private $_variation = null; diff --git a/src/LaunchDarkly/Rule.php b/src/LaunchDarkly/Rule.php new file mode 100644 index 000000000..326229168 --- /dev/null +++ b/src/LaunchDarkly/Rule.php @@ -0,0 +1,50 @@ +_clauses = $clauses; + } + + public static function getDecoder() { + return function ($v) { + return new Rule( + isset($v['variation']) ? $v['variation'] : null, + isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null, + array_map(Clause::getDecoder(), $v['clauses'])); + }; + } + + /** + * @param $user LDUser + * @return bool + */ + public function matchesUser($user) { + foreach ($this->_clauses as $clause) { + if (!$clause->matchesUser($user)) { + error_log("false from rule.matchesuser with attr: " . $clause->getAttribute()); + return false; + } + } + error_log("true from rule.matchesuser"); + return true; + } + + /** + * @return Clause[] + */ + public function getClauses() { + return $this->_clauses; + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/VariationOrRollout.php b/src/LaunchDarkly/VariationOrRollout.php new file mode 100644 index 000000000..7a6511e6d --- /dev/null +++ b/src/LaunchDarkly/VariationOrRollout.php @@ -0,0 +1,91 @@ +_variation = $variation; + $this->_rollout = $rollout; + } + + public static function getDecoder() { + return function ($v) { + return new VariationOrRollout( + isset($v['variation']) ? $v['variation'] : null, + isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null); + }; + } + + /** + * @return int + */ + public function getVariation() { + return $this->_variation; + } + + /** + * @return Rollout + */ + public function getRollout() { + return $this->_rollout; + } + + /** + * @param $user LDUser + * @param $_key string + * @param $_salt string + * @return int|null + */ + public function variationIndexForUser($user, $_key, $_salt) { + if ($this->_variation !== null) { + error_log("Returning: $this->_variation"); + return $this->_variation; + } else if ($this->_rollout !== null) { + $bucketBy = $this->_rollout->getBucketBy() === null ? "key" : $this->_rollout->getBucketBy(); + $bucket = $this->bucketUser($user, $_key, $bucketBy, $_salt); + $sum = 0.0; + foreach ($this->_rollout->getVariations() as $wv) { + $sum += $wv->getWeight() / 100000.0; + if ($bucket < $sum) { + return $wv->getVariation(); + } + } + } + error_log("both fallthrough and variation are null!!"); + return null; + } + + /** + * @param $user LDUser + * @param $_key string + * @param $attr string + * @param $_salt string + * @return float + */ + private function bucketUser($user, $_key, $attr, $_salt) { + $userValue = $user->getValueForEvaluation($attr); + $idHash = null; + if ($userValue != null) { + if (is_string($userValue)) { + $idHash = $userValue; + if ($user->getSecondary() !== null) { + $idHash = $idHash . "." . $user->getSecondary(); + } + $hash = substr(sha1($_key . "." . $_salt . "." . $idHash), 0, 15); + $longVal = base_convert($hash, 16, 10); + $result = $longVal / self::$LONG_SCALE; + + return $result; + } + } + return 0.0; + } +} \ No newline at end of file From 2ce8d58961b6f5aa2aa5c49aed6198b072f44a3c Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 13:51:58 -0700 Subject: [PATCH 05/11] [wip] v2 events work ok- but not prereqs. --- src/LaunchDarkly/FeatureFlag.php | 7 +++++++ src/LaunchDarkly/LDClient.php | 15 +++++++++++---- src/LaunchDarkly/LDUser.php | 2 +- src/LaunchDarkly/Operators.php | 15 ++------------- src/LaunchDarkly/Util.php | 20 ++++++++++++++++++++ 5 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 src/LaunchDarkly/Util.php diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index bf884aeec..00073e505 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -165,6 +165,13 @@ public function getOffVariationValue() { } return $this->_variations[$this->_offVariation]; } + + /** + * @return int + */ + public function getVersion() { + return $this->_version; + } } class WeightedVariation { diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 8fd7f3f5b..09beb0e11 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -1,6 +1,8 @@ _sendFlagRequestEvent($key, $user, $result, $default); + $this->_sendFlagRequestEvent($key, $user, $result, $default, $flag->getVersion()); return $result; } } $offVariation = $flag->getOffVariationValue(); if ($offVariation != null) { - $this->_sendFlagRequestEvent($key, $user, $offVariation, $default); + $this->_sendFlagRequestEvent($key, $user, $offVariation, $default, $flag->getVersion()); return $offVariation; } } catch (\Exception $e) { @@ -196,8 +198,11 @@ public function identify($user) { * @param $key string * @param $user LDUser * @param $value mixed + * @param $default + * @param $version int | null + * @param string | null $prereqOf */ - protected function _sendFlagRequestEvent($key, $user, $value, $default) { + protected function _sendFlagRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) { if ($this->isOffline() || !$this->_events) { return; } @@ -206,9 +211,11 @@ protected function _sendFlagRequestEvent($key, $user, $value, $default) { $event['user'] = $user->toJSON(); $event['value'] = $value; $event['kind'] = "feature"; - $event['creationDate'] = round(microtime(1) * 1000); + $event['creationDate'] = Util::dateTimeToUnixMillis(new DateTime('now', new DateTimeZone("UTC"))); $event['key'] = $key; $event['default'] = $default; + $event['version'] = $version; + $event['prereqOf'] = $prereqOf; $this->_eventProcessor->enqueue($event); } diff --git a/src/LaunchDarkly/LDUser.php b/src/LaunchDarkly/LDUser.php index 02c806e0c..933fb6121 100644 --- a/src/LaunchDarkly/LDUser.php +++ b/src/LaunchDarkly/LDUser.php @@ -33,7 +33,7 @@ class LDUser { * @param array|null $custom Other custom attributes that can be used to create custom rules */ public function __construct($key, $secondary = null, $ip = null, $country = null, $email = null, $name = null, $avatar = null, $firstName = null, $lastName = null, $anonymous = null, $custom = array()) { - $this->_key = strval($key); + $this->_key = $key; $this->_secondary = $secondary; $this->_ip = $ip; $this->_country = $country; diff --git a/src/LaunchDarkly/Operators.php b/src/LaunchDarkly/Operators.php index 21ed43510..1f2ca9266 100644 --- a/src/LaunchDarkly/Operators.php +++ b/src/LaunchDarkly/Operators.php @@ -108,13 +108,13 @@ public static function parseTime($in) { } if ($in instanceof DateTime) { - return self::dateTimeToUnixMillis($in); + return Util::dateTimeToUnixMillis($in); } if (is_string($in)) { try { $dateTime = new DateTime($in); - return self::dateTimeToUnixMillis($dateTime); + return Util::dateTimeToUnixMillis($dateTime); } catch (Exception $e) { error_log("LaunchDarkly: Could not parse timestamp: " . $in); return null; @@ -122,15 +122,4 @@ public static function parseTime($in) { } return null; } - - /** - * @param $dateTime DateTime - * @return int - */ - private static function dateTimeToUnixMillis($dateTime) { - $timeStampSeconds = (int)$dateTime->getTimeStamp(); - $timestampMicros = $dateTime->format('u'); - return $timeStampSeconds * 1000 + (int)($timestampMicros / 1000); - } - } \ No newline at end of file diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php new file mode 100644 index 000000000..d4a08ed7f --- /dev/null +++ b/src/LaunchDarkly/Util.php @@ -0,0 +1,20 @@ +getTimeStamp(); + $timestampMicros = $dateTime->format('u'); + return $timeStampSeconds * 1000 + (int)($timestampMicros / 1000); + } + +} \ No newline at end of file From 100d34dff62e8d90eadc90439b65c84d47fec007 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 14:28:13 -0700 Subject: [PATCH 06/11] [wip] v2 events work ok including prereqs. --- src/LaunchDarkly/FeatureFlag.php | 48 ++++++++++++++++++++++++++++---- src/LaunchDarkly/LDClient.php | 26 ++++++----------- src/LaunchDarkly/Util.php | 22 +++++++++++++++ 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 00073e505..78be4e6cd 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -71,12 +71,12 @@ public function isOn() { /** * @param $user LDUser * @param $featureRequester FeatureRequester - * @return mixed|null + * @return EvalResult|null */ public function evaluate($user, $featureRequester) { $prereqEvents = array(); $value = $this->_evaluate($user, $featureRequester, $prereqEvents); - return $value; + return new EvalResult($value, $prereqEvents); } /** @@ -85,16 +85,17 @@ public function evaluate($user, $featureRequester) { * @param $events * @return mixed|null */ - private function _evaluate($user, $featureRequester, $events) { + private function _evaluate($user, $featureRequester, &$events) { $prereqOk = true; if ($this->_prerequisites != null) { foreach ($this->_prerequisites as $prereq) { try { + $prereqEvalResult = null; $prereqFeatureFlag = $featureRequester->get($prereq->getKey()); if ($prereqFeatureFlag == null) { return null; } else if ($prereqFeatureFlag->isOn()) { - $prereqEvalResult = $prereqFeatureFlag->evaluate($user, $featureRequester); + $prereqEvalResult = $prereqFeatureFlag->_evaluate($user, $featureRequester, $events); $variation = $prereqFeatureFlag->getVariation($prereq->getVariation()); if ($prereqEvalResult === null || $variation === null || $prereqEvalResult !== $variation) { $prereqOk = false; @@ -102,10 +103,10 @@ private function _evaluate($user, $featureRequester, $events) { } else { $prereqOk = false; } + array_push($events, Util::newFeatureRequestEvent($prereqFeatureFlag->getKey(), $user, $prereqEvalResult, null, $prereqFeatureFlag->getVersion(), $this->_key)); } catch (EvaluationException $e) { $prereqOk = false; } - //TODO: Add event. } } if ($prereqOk) { @@ -172,6 +173,43 @@ public function getOffVariationValue() { public function getVersion() { return $this->_version; } + + /** + * @return string + */ + public function getKey() { + return $this->_key; + } +} + +class EvalResult { + private $_value = null; + /** @var array */ + private $_prerequisiteEvents = []; + + /** + * EvalResult constructor. + * @param null $value + * @param array $prerequisiteEvents + */ + public function __construct($value, array $prerequisiteEvents) { + $this->_value = $value; + $this->_prerequisiteEvents = $prerequisiteEvents; + } + + /** + * @return null + */ + public function getValue() { + return $this->_value; + } + + /** + * @return array + */ + public function getPrerequisiteEvents() { + return $this->_prerequisiteEvents; + } } class WeightedVariation { diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 09beb0e11..04aa48c67 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -105,13 +105,15 @@ public function toggle($key, $user, $default = false) { } else if ($flag->isOn()) { error_log("got a flag and it's on"); $result = $flag->evaluate($user, $this->_featureRequester); - if (!$this->_offline) { - //TODO: send prereq events + if (!$this->isOffline() && $this->_events) { + foreach ($result->getPrerequisiteEvents() as $e) { + error_log("enqueueing prereq event..."); + $this->_eventProcessor->enqueue($e); + } } - if ($result != null) { -// error_log("result: $result"); - $this->_sendFlagRequestEvent($key, $user, $result, $default, $flag->getVersion()); - return $result; + if ($result->getValue() != null) { + $this->_sendFlagRequestEvent($key, $user, $result->getValue(), $default, $flag->getVersion()); + return $result->getValue(); } } $offVariation = $flag->getOffVariationValue(); @@ -206,17 +208,7 @@ protected function _sendFlagRequestEvent($key, $user, $value, $default, $version if ($this->isOffline() || !$this->_events) { return; } - - $event = array(); - $event['user'] = $user->toJSON(); - $event['value'] = $value; - $event['kind'] = "feature"; - $event['creationDate'] = Util::dateTimeToUnixMillis(new DateTime('now', new DateTimeZone("UTC"))); - $event['key'] = $key; - $event['default'] = $default; - $event['version'] = $version; - $event['prereqOf'] = $prereqOf; - $this->_eventProcessor->enqueue($event); + $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $value, $default, $version, $prereqOf)); } protected function _get_flag($key, $user) { diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php index d4a08ed7f..a1a38cd75 100644 --- a/src/LaunchDarkly/Util.php +++ b/src/LaunchDarkly/Util.php @@ -4,6 +4,7 @@ use DateTime; +use DateTimeZone; class Util { @@ -17,4 +18,25 @@ public static function dateTimeToUnixMillis($dateTime) { return $timeStampSeconds * 1000 + (int)($timestampMicros / 1000); } + /** + * @return int + */ + public static function currentTimeUnixMillis() { + return Util::dateTimeToUnixMillis(new DateTime('now', new DateTimeZone("UTC"))); + } + + + public static function newFeatureRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) { + $event = array(); + $event['user'] = $user->toJSON(); + $event['value'] = $value; + $event['kind'] = "feature"; + $event['creationDate'] = Util::currentTimeUnixMillis(); + $event['key'] = $key; + $event['default'] = $default; + $event['version'] = $version; + $event['prereqOf'] = $prereqOf; + return $event; + } + } \ No newline at end of file From 6beeb750cfe3b7c7ef039a3be4f89b079e9f0103 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 15:39:25 -0700 Subject: [PATCH 07/11] Use standard logging interface. Log more/better things --- composer.json | 4 +- composer.lock | 187 ++++++++++---------- src/LaunchDarkly/Clause.php | 7 +- src/LaunchDarkly/FeatureFlag.php | 7 + src/LaunchDarkly/GuzzleFeatureRequester.php | 9 +- src/LaunchDarkly/LDClient.php | 95 +++++----- src/LaunchDarkly/LDDFeatureRequester.php | 17 +- src/LaunchDarkly/Operators.php | 6 - src/LaunchDarkly/Rule.php | 2 - src/LaunchDarkly/VariationOrRollout.php | 3 +- 10 files changed, 173 insertions(+), 164 deletions(-) diff --git a/composer.json b/composer.json index 0329dee7e..afa5ae0f9 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ } ], "require": { - "php": ">=5.5" + "php": ">=5.5", + "psr/log": "1.0.0" }, "require-dev": { "guzzlehttp/guzzle": "6.2.1", @@ -27,6 +28,7 @@ "suggested": { "guzzlehttp/guzzle": "6.2.1", "kevinrob/guzzle-cache-middleware": "1.4.1", + "monolog/monolog": "1.21.0", "predis/predis": "1.0.*" }, "autoload": { diff --git a/composer.lock b/composer.lock index 87ffb3aaf..c07149b50 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,48 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "cf5c4602f9f9aff086005edc75728d0f", - "content-hash": "f09e942238b22b8ef39b297cd1808ee6", - "packages": [], + "hash": "fc1de559a4df16ad00c93dd5c20fcc28", + "content-hash": "a8cf37cf9851938001d2b8babe963b5e", + "packages": [ + { + "name": "psr/log", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2012-12-21 11:40:51" + } + ], "packages-dev": [ { "name": "cilex/cilex", @@ -1004,16 +1043,16 @@ }, { "name": "monolog/monolog", - "version": "1.20.0", + "version": "1.21.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037" + "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/55841909e2bcde01b5318c35f2b74f8ecc86e037", - "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f42fbdfd53e306bda545845e4dbfd3e72edb4952", + "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952", "shasum": "" }, "require": { @@ -1078,7 +1117,7 @@ "logging", "psr-3" ], - "time": "2016-07-02 14:02:10" + "time": "2016-07-29 03:23:52" }, { "name": "nikic/php-parser", @@ -2079,44 +2118,6 @@ ], "time": "2015-05-04 20:22:00" }, - { - "name": "psr/log", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", - "shasum": "" - }, - "type": "library", - "autoload": { - "psr-0": { - "Psr\\Log\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2012-12-21 11:40:51" - }, { "name": "sebastian/comparator", "version": "1.2.0", @@ -2537,16 +2538,16 @@ }, { "name": "symfony/config", - "version": "v2.8.8", + "version": "v2.8.9", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "0926e69411eba491803dbafb9f1f233e2ced58d0" + "reference": "4275ef5b59f18959df0eee3991e9ca0cc208ffd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/0926e69411eba491803dbafb9f1f233e2ced58d0", - "reference": "0926e69411eba491803dbafb9f1f233e2ced58d0", + "url": "https://api.github.com/repos/symfony/config/zipball/4275ef5b59f18959df0eee3991e9ca0cc208ffd4", + "reference": "4275ef5b59f18959df0eee3991e9ca0cc208ffd4", "shasum": "" }, "require": { @@ -2586,20 +2587,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:31:50" + "time": "2016-07-26 08:02:44" }, { "name": "symfony/console", - "version": "v2.8.8", + "version": "v2.8.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c392a6ec72f2122748032c2ad6870420561ffcfa" + "reference": "36e62335caca8a6e909c5c5bac4a8128149911c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c392a6ec72f2122748032c2ad6870420561ffcfa", - "reference": "c392a6ec72f2122748032c2ad6870420561ffcfa", + "url": "https://api.github.com/repos/symfony/console/zipball/36e62335caca8a6e909c5c5bac4a8128149911c9", + "reference": "36e62335caca8a6e909c5c5bac4a8128149911c9", "shasum": "" }, "require": { @@ -2646,20 +2647,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-06-29 07:02:14" + "time": "2016-07-30 07:20:35" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.8", + "version": "v2.8.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b180b70439dca70049b6b9b7e21d75e6e5d7aca9" + "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b180b70439dca70049b6b9b7e21d75e6e5d7aca9", - "reference": "b180b70439dca70049b6b9b7e21d75e6e5d7aca9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/889983a79a043dfda68f38c38b6dba092dd49cd8", + "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8", "shasum": "" }, "require": { @@ -2706,20 +2707,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:29:29" + "time": "2016-07-28 16:56:28" }, { "name": "symfony/filesystem", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "a108b1d603ccb52addb5da9b14a3ba259f8b3db0" + "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/a108b1d603ccb52addb5da9b14a3ba259f8b3db0", - "reference": "a108b1d603ccb52addb5da9b14a3ba259f8b3db0", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b2da5009d9bacbd91d83486aa1f44c793a8c380d", + "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d", "shasum": "" }, "require": { @@ -2755,20 +2756,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:40:00" + "time": "2016-07-20 05:43:46" }, { "name": "symfony/finder", - "version": "v2.8.8", + "version": "v2.8.9", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "bf0506ef4e7778fd3f0f1f141ab5e8c1ef35dd7d" + "reference": "60804d88691e4a73bbbb3035eb1d9f075c5c2c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/bf0506ef4e7778fd3f0f1f141ab5e8c1ef35dd7d", - "reference": "bf0506ef4e7778fd3f0f1f141ab5e8c1ef35dd7d", + "url": "https://api.github.com/repos/symfony/finder/zipball/60804d88691e4a73bbbb3035eb1d9f075c5c2c10", + "reference": "60804d88691e4a73bbbb3035eb1d9f075c5c2c10", "shasum": "" }, "require": { @@ -2804,7 +2805,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:29:29" + "time": "2016-07-26 08:02:44" }, { "name": "symfony/polyfill-mbstring", @@ -2867,16 +2868,16 @@ }, { "name": "symfony/process", - "version": "v2.8.8", + "version": "v2.8.9", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "89f33c16796415ccfd8bb3cf8d520cbb79899bfe" + "reference": "d20332e43e8774ff8870b394f3dd6020cc7f8e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/89f33c16796415ccfd8bb3cf8d520cbb79899bfe", - "reference": "89f33c16796415ccfd8bb3cf8d520cbb79899bfe", + "url": "https://api.github.com/repos/symfony/process/zipball/d20332e43e8774ff8870b394f3dd6020cc7f8e0c", + "reference": "d20332e43e8774ff8870b394f3dd6020cc7f8e0c", "shasum": "" }, "require": { @@ -2912,11 +2913,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:29:29" + "time": "2016-07-28 11:13:19" }, { "name": "symfony/stopwatch", - "version": "v2.8.8", + "version": "v2.8.9", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -2965,16 +2966,16 @@ }, { "name": "symfony/translation", - "version": "v3.0.8", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "6bf844e1ee3c820c012386c10427a5c67bbefec8" + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/6bf844e1ee3c820c012386c10427a5c67bbefec8", - "reference": "6bf844e1ee3c820c012386c10427a5c67bbefec8", + "url": "https://api.github.com/repos/symfony/translation/zipball/eee6c664853fd0576f21ae25725cfffeafe83f26", + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26", "shasum": "" }, "require": { @@ -3025,20 +3026,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:40:00" + "time": "2016-07-30 07:22:48" }, { "name": "symfony/validator", - "version": "v2.8.8", + "version": "v2.8.9", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "366674c28bf7a107a8bce280c668bac40aeb090e" + "reference": "244ff2a7f0283d1c854abbf17181dbb02c0af95f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/366674c28bf7a107a8bce280c668bac40aeb090e", - "reference": "366674c28bf7a107a8bce280c668bac40aeb090e", + "url": "https://api.github.com/repos/symfony/validator/zipball/244ff2a7f0283d1c854abbf17181dbb02c0af95f", + "reference": "244ff2a7f0283d1c854abbf17181dbb02c0af95f", "shasum": "" }, "require": { @@ -3052,7 +3053,7 @@ "egulias/email-validator": "~1.2,>=1.2.1", "symfony/config": "~2.2|~3.0.0", "symfony/expression-language": "~2.4|~3.0.0", - "symfony/http-foundation": "~2.1|~3.0.0", + "symfony/http-foundation": "~2.3|~3.0.0", "symfony/intl": "~2.7.4|~2.8|~3.0.0", "symfony/property-access": "~2.3|~3.0.0", "symfony/yaml": "~2.0,>=2.0.5|~3.0.0" @@ -3098,20 +3099,20 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:29:29" + "time": "2016-07-26 08:02:44" }, { "name": "symfony/yaml", - "version": "v3.1.2", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de" + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/2884c26ce4c1d61aebf423a8b912950fe7c764de", - "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1819adf2066880c7967df7180f4f662b6f0567ac", + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac", "shasum": "" }, "require": { @@ -3147,7 +3148,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:41:56" + "time": "2016-07-17 14:02:08" }, { "name": "twig/twig", diff --git a/src/LaunchDarkly/Clause.php b/src/LaunchDarkly/Clause.php index dd7d61c0d..9f4534830 100644 --- a/src/LaunchDarkly/Clause.php +++ b/src/LaunchDarkly/Clause.php @@ -28,11 +28,9 @@ public static function getDecoder() { public function matchesUser($user) { $userValue = $user->getValueForEvaluation($this->_attribute); if ($userValue === null) { - error_log("null user value"); return false; } if (is_array($userValue)) { - error_log("uservalue is array"); foreach ($userValue as $element) { if ($this->matchAny($element)) { return $this->_maybeNegate(true); @@ -40,7 +38,6 @@ public function matchesUser($user) { } return $this->_maybeNegate(false); } else { - error_log("else..."); return $this->_maybeNegate($this->matchAny($userValue)); } } @@ -80,9 +77,7 @@ public function isNegate() { private function matchAny($userValue) { foreach ($this->_values as $v) { $result = Operators::apply($this->_op, $userValue, $v); - error_log("clause.matchany operator result for v: $v $result"); - if ($result) { - error_log("true for $userValue"); + if ($result === true) { return true; } } diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 78be4e6cd..26684e713 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -180,6 +180,13 @@ public function getVersion() { public function getKey() { return $this->_key; } + + /** + * @return boolean + */ + public function isDeleted() { + return $this->_deleted; + } } class EvalResult { diff --git a/src/LaunchDarkly/GuzzleFeatureRequester.php b/src/LaunchDarkly/GuzzleFeatureRequester.php index d1c29458b..958547a8d 100644 --- a/src/LaunchDarkly/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/GuzzleFeatureRequester.php @@ -6,12 +6,18 @@ use GuzzleHttp\HandlerStack; use Kevinrob\GuzzleCache\CacheMiddleware; use Kevinrob\GuzzleCache\Strategy\PublicCacheStrategy; +use Psr\Log\LoggerInterface; class GuzzleFeatureRequester implements FeatureRequester { + /** @var Client */ private $_client; + /** @var string */ private $_baseUri; + /** @var array */ private $_defaults; + /** @var LoggerInterface */ + private $_logger; function __construct($baseUri, $apiKey, $options) { @@ -28,6 +34,7 @@ function __construct($baseUri, $apiKey, $options) 'timeout' => $options['timeout'], 'connect_timeout' => $options['connect_timeout'] ); + $this->_logger = $options['logger']; $this->_client = new Client(['handler' => $stack, 'debug' => false]); } @@ -47,7 +54,7 @@ public function get($key) return FeatureFlag::decode(json_decode($body, true)); } catch (BadResponseException $e) { $code = $e->getResponse()->getStatusCode(); - error_log("GuzzleFeatureRetriever::get received an unexpected HTTP status code $code"); + $this->_logger->error("GuzzleFeatureRetriever::get received an unexpected HTTP status code $code"); return null; } } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 04aa48c67..486835306 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -1,24 +1,32 @@ _baseUri = rtrim($options['base_uri'], '/'); } - if (isset($options['events'])) { - $this->_events = $options['events']; + if (isset($options['send_events'])) { + $this->_send_events = $options['send_events']; } + if (isset($options['offline']) && $options['offline'] === true) { + $this->_offline = true; + $this->_send_events = false; + } + if (isset($options['defaults'])) { $this->_defaults = $options['defaults']; } @@ -58,6 +71,12 @@ public function __construct($apiKey, $options = array()) { $options['capacity'] = 1000; } + if (!isset($options['logger'])) { + $logger = new Logger("LaunchDarkly", [new ErrorLogHandler()]); + $options['logger'] = $logger; + } + $this->_logger = $options['logger']; + $this->_eventProcessor = new EventProcessor($apiKey, $options); if (isset($options['feature_requester_class'])) { @@ -86,28 +105,27 @@ public function getFlag($key, $user, $default = false) { * @return mixed Whether or not the flag should be enabled, or `default` */ public function toggle($key, $user, $default = false) { + $default = $this->_get_default($key, $default); + if ($this->_offline) { return $default; } try { - $default = $this->_get_default($key, $default); - if (is_null($user) || strlen($user->getKey()) == 0) { + if (is_null($user) || strlen($user->getKey()) === 0) { $this->_sendFlagRequestEvent($key, $user, $default, $default); + $this->_logger->warn("Toggle called with null user or null/empty user key! Returning default value"); return $default; } $flag = $this->_featureRequester->get($key); -// $flag = $this->_get_flag($key, $user); if (is_null($flag)) { $this->_sendFlagRequestEvent($key, $user, $default, $default); return $default; } else if ($flag->isOn()) { - error_log("got a flag and it's on"); $result = $flag->evaluate($user, $this->_featureRequester); - if (!$this->isOffline() && $this->_events) { + if (!$this->isOffline() && $this->_send_events) { foreach ($result->getPrerequisiteEvents() as $e) { - error_log("enqueueing prereq event..."); $this->_eventProcessor->enqueue($e); } } @@ -122,35 +140,18 @@ public function toggle($key, $user, $default = false) { return $offVariation; } } catch (\Exception $e) { - error_log("LaunchDarkly caught $e"); + $this->_logger->error("Caught $e"); } try { $this->_sendFlagRequestEvent($key, $user, $default, $default); } catch (\Exception $e) { - error_log("LaunchDarkly caught $e"); + $this->_logger->error("Caught $e"); } return $default; } /** - * Puts the LaunchDarkly client in offline mode. - * In offline mode, all calls to `toggle` will return the default value, and `track` will be a no-op. - * - */ - public function setOffline() { - $this->_offline = true; - } - - /** - * Puts the LaunchDarkly client in online mode. - * - */ - public function setOnline() { - $this->_offline = false; - } - - /** - * Returns whether the LaunchDarlkly client is in offline mode. + * Returns whether the LaunchDarkly client is in offline mode. * */ public function isOffline() { @@ -168,11 +169,14 @@ public function track($eventName, $user, $data) { if ($this->isOffline()) { return; } + if (is_null($user) || strlen($user->getKey()) === 0) { + $this->_logger->warn("Track called with null user or null/empty user key!"); + } $event = array(); $event['user'] = $user->toJSON(); $event['kind'] = "custom"; - $event['creationDate'] = round(microtime(1) * 1000); + $event['creationDate'] = Util::currentTimeUnixMillis(); $event['key'] = $eventName; if (isset($data)) { $event['data'] = $data; @@ -187,11 +191,14 @@ public function identify($user) { if ($this->isOffline()) { return; } + if (is_null($user) || strlen($user->getKey()) === 0) { + $this->_logger->warn("Track called with null user or null/empty user key!"); + } $event = array(); $event['user'] = $user->toJSON(); $event['kind'] = "identify"; - $event['creationDate'] = round(microtime(1) * 1000); + $event['creationDate'] = Util::currentTimeUnixMillis(); $event['key'] = $user->getKey(); $this->_eventProcessor->enqueue($event); } @@ -205,26 +212,12 @@ public function identify($user) { * @param string | null $prereqOf */ protected function _sendFlagRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) { - if ($this->isOffline() || !$this->_events) { + if ($this->isOffline() || !$this->_send_events) { return; } $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $value, $default, $version, $prereqOf)); } - protected function _get_flag($key, $user) { - try { - $data = $this->_featureRequester->get($key); - if ($data == null) { - return null; - } - return self::_decode($data, $user); - } catch (Exception $e) { - $msg = $e->getMessage(); - error_log("LDClient::_toggle received error $msg, using default"); - return null; - } - } - protected function _get_default($key, $default) { if (array_key_exists($key, $this->_defaults)) { return $this->_defaults[$key]; diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index 08cdf665b..06fecb7d5 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -2,11 +2,15 @@ namespace LaunchDarkly; +use Psr\Log\LoggerInterface; + class LDDFeatureRequester implements FeatureRequester { protected $_baseUri; protected $_apiKey; protected $_options; protected $_features_key; + /** @var LoggerInterface */ + private $_logger; function __construct($baseUri, $apiKey, $options) { $this->_baseUri = $baseUri; @@ -25,6 +29,8 @@ function __construct($baseUri, $apiKey, $options) { $prefix = $options['redis_prefix']; } $this->_features_key = "$prefix:features"; + $this->_logger = $options['logger']; + } protected function get_connection() { @@ -40,7 +46,7 @@ protected function get_connection() { * Gets feature data from a likely cached store * * @param $key string feature key - * @return array|null The decoded JSON feature data, or null if missing + * @return FeatureFlag|null The decoded JSON feature data, or null if missing */ public function get($key) { $raw = $this->get_from_cache($key); @@ -52,8 +58,15 @@ public function get($key) { } } if ($raw) { - return json_decode($raw, True); + $flag = FeatureFlag::decode(json_decode($raw, True)); + if ($flag->isDeleted()) { + $this->_logger->warning("LDDFeatureRequester: Attempted to get deleted feature with key: " . $key); + return null; + } + return $flag; + } else { + $this->_logger->warning("LDDFeatureRequester: Attempted to get missing feature with key: " . $key); return null; } } diff --git a/src/LaunchDarkly/Operators.php b/src/LaunchDarkly/Operators.php index 1f2ca9266..9c415ebbb 100644 --- a/src/LaunchDarkly/Operators.php +++ b/src/LaunchDarkly/Operators.php @@ -14,17 +14,13 @@ class Operators { * @return bool */ public static function apply($op, $u, $c) { - error_log("apply with op: $op u: $u c: $c"); try { if ($u === null || $c === null) { - error_log("one or both are null"); return false; } switch ($op) { case "in": - error_log("in with u: $u and c: $c"); if ($u === $c) { - error_log("returning true from in op"); return true; } if (is_numeric($u) && is_numeric($c)) { @@ -43,7 +39,6 @@ public static function apply($op, $u, $c) { break; case "matches": if (is_string($u) && is_string($c)) { - error_log("u: $u c: $c"); //PHP can do subpatterns, but everything needs to be wrapped in an outer (): return preg_match("($c)", $u) === 1; } @@ -116,7 +111,6 @@ public static function parseTime($in) { $dateTime = new DateTime($in); return Util::dateTimeToUnixMillis($dateTime); } catch (Exception $e) { - error_log("LaunchDarkly: Could not parse timestamp: " . $in); return null; } } diff --git a/src/LaunchDarkly/Rule.php b/src/LaunchDarkly/Rule.php index 326229168..be9f1778b 100644 --- a/src/LaunchDarkly/Rule.php +++ b/src/LaunchDarkly/Rule.php @@ -33,11 +33,9 @@ public static function getDecoder() { public function matchesUser($user) { foreach ($this->_clauses as $clause) { if (!$clause->matchesUser($user)) { - error_log("false from rule.matchesuser with attr: " . $clause->getAttribute()); return false; } } - error_log("true from rule.matchesuser"); return true; } diff --git a/src/LaunchDarkly/VariationOrRollout.php b/src/LaunchDarkly/VariationOrRollout.php index 7a6511e6d..9e507df6f 100644 --- a/src/LaunchDarkly/VariationOrRollout.php +++ b/src/LaunchDarkly/VariationOrRollout.php @@ -46,7 +46,6 @@ public function getRollout() { */ public function variationIndexForUser($user, $_key, $_salt) { if ($this->_variation !== null) { - error_log("Returning: $this->_variation"); return $this->_variation; } else if ($this->_rollout !== null) { $bucketBy = $this->_rollout->getBucketBy() === null ? "key" : $this->_rollout->getBucketBy(); @@ -59,7 +58,7 @@ public function variationIndexForUser($user, $_key, $_salt) { } } } - error_log("both fallthrough and variation are null!!"); + //TODO: throw exception? return null; } From 35766f377ea5d6c8cefb6f316814a1cf02f208d3 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 16:04:11 -0700 Subject: [PATCH 08/11] Remove v1 code. Other cleanup --- src/LaunchDarkly/EvaluationException.php | 7 -- src/LaunchDarkly/FeatureRep.php | 90 ------------------------ src/LaunchDarkly/LDClient.php | 23 ------ src/LaunchDarkly/LDUser.php | 2 +- src/LaunchDarkly/Operators.php | 3 +- src/LaunchDarkly/Rule.php | 6 -- src/LaunchDarkly/TargetRule.php | 81 --------------------- src/LaunchDarkly/Util.php | 10 ++- src/LaunchDarkly/Variation.php | 55 --------------- tests/FeatureRepTest.php | 89 ----------------------- tests/LDClientTest.php | 15 ---- 11 files changed, 11 insertions(+), 370 deletions(-) delete mode 100644 src/LaunchDarkly/FeatureRep.php delete mode 100644 src/LaunchDarkly/TargetRule.php delete mode 100644 src/LaunchDarkly/Variation.php delete mode 100644 tests/FeatureRepTest.php diff --git a/src/LaunchDarkly/EvaluationException.php b/src/LaunchDarkly/EvaluationException.php index 5980b667a..294688752 100644 --- a/src/LaunchDarkly/EvaluationException.php +++ b/src/LaunchDarkly/EvaluationException.php @@ -1,11 +1,4 @@ _name = $name; - $this->_key = $key; - $this->_salt = $salt; - $this->_on = $on; - $this->_variations = $variations; - } - - /** - * @param $user LDUser - * @return mixed - */ - public function evaluate($user) { - if (!$this->_on || !$user) { - return null; - } - - $param = $this->_get_param($user); - if (is_null($param)) { - return null; - } - else { - foreach ($this->_variations as $variation) { - if ($variation->matchUser($user)) { - return $variation->getValue(); - } - } - - foreach ($this->_variations as $variation) { - if ($variation->matchTarget($user)) { - return $variation->getValue(); - } - } - - $sum = 0.0; - foreach ($this->_variations as $variation) { - $sum += $variation->getWeight() / 100.0; - - if ($param < $sum) { - return $variation->getValue(); - } - } - } - - return null; - } - - /** - * @param $user LDUser - * @return float|null - */ - private function _get_param($user) { - $id_hash = null; - $hash = null; - - if ($user->getKey()) { - $id_hash = $user->getKey(); - } - else { - return null; - } - - if ($user->getSecondary()) { - $id_hash .= "." . $user->getSecondary(); - } - - $hash = substr(sha1($this->_key . "." . $this->_salt . "." . $id_hash), 0, 15); - $longVal = base_convert($hash, 16, 10); - $result = $longVal / self::$LONG_SCALE; - - return $result; - } -} diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 486835306..8107f06cf 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -1,7 +1,6 @@ evaluate($user); - } } diff --git a/src/LaunchDarkly/LDUser.php b/src/LaunchDarkly/LDUser.php index 933fb6121..02c806e0c 100644 --- a/src/LaunchDarkly/LDUser.php +++ b/src/LaunchDarkly/LDUser.php @@ -33,7 +33,7 @@ class LDUser { * @param array|null $custom Other custom attributes that can be used to create custom rules */ public function __construct($key, $secondary = null, $ip = null, $country = null, $email = null, $name = null, $avatar = null, $firstName = null, $lastName = null, $anonymous = null, $custom = array()) { - $this->_key = $key; + $this->_key = strval($key); $this->_secondary = $secondary; $this->_ip = $ip; $this->_country = $country; diff --git a/src/LaunchDarkly/Operators.php b/src/LaunchDarkly/Operators.php index 9c415ebbb..a491fc932 100644 --- a/src/LaunchDarkly/Operators.php +++ b/src/LaunchDarkly/Operators.php @@ -87,8 +87,7 @@ public static function apply($op, $u, $c) { } break; } - } catch (Exception $e) { - //TODO: log warning + } catch (Exception $ignored) { } return false; } diff --git a/src/LaunchDarkly/Rule.php b/src/LaunchDarkly/Rule.php index be9f1778b..c2ac20d43 100644 --- a/src/LaunchDarkly/Rule.php +++ b/src/LaunchDarkly/Rule.php @@ -1,10 +1,4 @@ _attribute = $attribute; - $this->_operator = $operator; - $this->_values = $values; - } - - public function isKey() { - return $this->_attribute == "key"; - } - - - /** - * @param $user LDUser - * @return bool - */ - public function matchTarget($user) { - $u_value = null; - - switch ($this->_attribute) { - case "key": - $u_value = $user->getKey(); - break; - case "ip": - $u_value = $user->getIP(); - break; - case "country": - $u_value = $user->getCountry(); - break; - case "email": - $u_value = $user->getEmail(); - break; - case "name": - $u_value = $user->getName(); - break; - case "avatar": - $u_value = $user->getAvatar(); - break; - case "firstName": - $u_value = $user->getFirstName(); - break; - case "lastName": - $u_value = $user->getLastName(); - break; - case "anonymous": - $u_value = $user->getAnonymous(); - break; - default: - $custom = $user->getCustom(); - if (is_null($custom)) { - return false; - } - if (!array_key_exists($this->_attribute, $custom)) { - return false; - } - $u_value = $custom[$this->_attribute]; - - if (is_array($u_value)) { - foreach ($u_value as $elt) { - if (in_array($elt, $this->_values)) { - return true; - } - } - return false; - } - break; - } - - return isset($u_value) && in_array($u_value, $this->_values); - } -} \ No newline at end of file diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php index a1a38cd75..e1d84897a 100644 --- a/src/LaunchDarkly/Util.php +++ b/src/LaunchDarkly/Util.php @@ -2,7 +2,6 @@ namespace LaunchDarkly; - use DateTime; use DateTimeZone; @@ -26,6 +25,15 @@ public static function currentTimeUnixMillis() { } + /** + * @param $key string + * @param $user LDUser + * @param $value + * @param $default + * @param null $version int | null + * @param null $prereqOf string | null + * @return array + */ public static function newFeatureRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) { $event = array(); $event['user'] = $user->toJSON(); diff --git a/src/LaunchDarkly/Variation.php b/src/LaunchDarkly/Variation.php deleted file mode 100644 index 40cdd97a8..000000000 --- a/src/LaunchDarkly/Variation.php +++ /dev/null @@ -1,55 +0,0 @@ -_value = $value; - $this->_weight = $weight; - $this->_targets = $targets; - $this->_userTarget = $userTarget; - } - - /** - * @param $user LDUser - * @return bool - */ - public function matchUser($user) { - if ($this->_userTarget != null) { - return $this->_userTarget->matchTarget($user); - } - return false; - } - - public function matchTarget($user) { - foreach($this->_targets as $target) { - if ($this->_userTarget != null && $target->isKey()) { - continue; - } - if ($target->matchTarget($user)) { - return true; - } - } - return false; - } - - public function getValue() { - return $this->_value; - } - - public function getWeight() { - return $this->_weight; - } -} \ No newline at end of file diff --git a/tests/FeatureRepTest.php b/tests/FeatureRepTest.php deleted file mode 100644 index 16b1f207b..000000000 --- a/tests/FeatureRepTest.php +++ /dev/null @@ -1,89 +0,0 @@ -_simpleFlag = new FeatureRep("Sample flag", "sample.flag", "feefifofum", true, array($trueVariation, $falseVariation)); - $this->_disabledFlag = new FeatureRep("Sample flag", "sample.flag", "feefifofum", false, array($trueVariation, $falseVariation)); - - $userTargetVariation = new Variation(false, 20, array(), $targetUserOn); - - $this->_userTargetFlag = new FeatureRep("Sample flag", "sample.flag", "feefifofum", true, array($trueVariation, $userTargetVariation)); - } - - protected function tearDown() { - parent::tearDown(); - $this->_simpleFlag = null; - } - - public function testFlagForTargetedUserOff() { - $user = new LDUser("targetOff@test.com"); - $b = $this->_simpleFlag->evaluate($user); - $this->assertEquals(false, $b); - } - - public function testFlagForTargetedUserOn() { - $user = new LDUser("targetOn@test.com"); - $b = $this->_simpleFlag->evaluate($user); - $this->assertEquals(true, $b); - } - - public function testFlagForTargetGroupOn() { - $builder = new LDUserBuilder("targetOther@test.com"); - $user = $builder->custom(array("groups" => array("google", "microsoft")))->build(); - $b = $this->_simpleFlag->evaluate($user); - $this->assertEquals(true, $b); - } - - public function testFlagForTargetGroupOff() { - $builder = new LDUserBuilder("targetOther@test.com"); - $user = $builder->custom(array("groups" => array("oracle")))->build(); - $b = $this->_simpleFlag->evaluate($user); - $this->assertEquals(false, $b); - } - - public function testDisabledFlagAlwaysOff() { - $user = new LDUser("targetOn@test.com"); - $b = $this->_disabledFlag->evaluate($user); - $this->assertEquals(null, $b); - } - - public function testUserRuleFlagForTargetUserOff() { - $builder = new LDUserBuilder("targetOff@test.com"); - $user = $builder->build(); - $b = $this->_userTargetFlag->evaluate($user); - $this->assertEquals(false, $b); - } - - public function testFlagForTargetEmailOff() { - $builder = new LDUserBuilder("targetOff@test.com"); - $user = $builder->email("targetEmailOn@test.com")->build(); - $b = $this->_simpleFlag->evaluate($user); - $this->assertEquals(true,$b); - } -} - diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 42f86ee06..50da3c704 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -53,21 +53,6 @@ public function testToggleEvent() { $this->assertEquals(1, sizeof($queue)); } - public function testToggleEventsOff() { - MockFeatureRequester::$val = null; - $client = new LDClient("someKey", array( - 'feature_requester_class' => '\\LaunchDarkly\Tests\\MockFeatureRequester', - 'events' => false - )); - - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $client->toggle('foo', $user, 'argdef'); - $proc = getPrivateField($client, '_eventProcessor'); - $queue = getPrivateField($proc, '_queue'); - $this->assertEquals(0, sizeof($queue)); - } - public function testOnlyValidFeatureRequester() { $this->setExpectedException(InvalidArgumentException::class); new LDClient("BOGUS_API_KEY", ['feature_requester_class' => 'stdClass']); From c191a07abc0fd36aea703807ffce3d8b3b46b4e1 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 16:15:10 -0700 Subject: [PATCH 09/11] update version. --- VERSION | 2 +- tests/FeatureFlagTest.php | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/VERSION b/VERSION index afaf360d3..359a5b952 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 \ No newline at end of file +2.0.0 \ No newline at end of file diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 3d084f047..a81cd11e4 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -2,7 +2,6 @@ namespace LaunchDarkly\Tests; use LaunchDarkly\FeatureFlag; -use LaunchDarkly\LDUser; class FeatureFlagTest extends \PHPUnit_Framework_TestCase { @@ -137,20 +136,9 @@ class FeatureFlagTest extends \PHPUnit_Framework_TestCase { \"deleted\": false }"; - protected function setUp() { - parent::setUp(); - } - - protected function tearDown() { - parent::tearDown(); - } - public function testDecode() { FeatureFlag::decode(\GuzzleHttp\json_decode(FeatureFlagTest::$json1, true)); FeatureFlag::decode(\GuzzleHttp\json_decode(FeatureFlagTest::$json2, true)); - - } - } From efe65f942da81314a6a13160edce7738bea0f75f Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 1 Aug 2016 16:15:46 -0700 Subject: [PATCH 10/11] remove whitespace --- tests/OperatorsTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/OperatorsTest.php b/tests/OperatorsTest.php index 15e04e8da..bb25ed423 100644 --- a/tests/OperatorsTest.php +++ b/tests/OperatorsTest.php @@ -17,7 +17,6 @@ public function testIn() { $this->assertTrue(Operators::apply("in", true, true)); $this->assertFalse(Operators::apply("in", true, false)); $this->assertFalse(Operators::apply("in", false, true)); - } public function testStartsWith() { @@ -39,8 +38,6 @@ public function testMatches() { $this->assertTrue(Operators::apply("matches", "darn", "(\\W|^)(baloney|darn|drat|fooey|gosh\\sdarnit|heck)(\\W|$)")); } - - public function testParseTime() { $this->assertEquals(0, Operators::parseTime(0)); $this->assertEquals(100, Operators::parseTime(100)); From 674dc1b6dc68592a9ceff24d99aa31b42d324af2 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Tue, 2 Aug 2016 11:43:40 -0700 Subject: [PATCH 11/11] Address PR comments. Deprecate toggle function. Remove getFlag function --- README.md | 2 +- integration-tests/LDDFeatureRequesterTest.php | 12 +++++----- src/LaunchDarkly/Clause.php | 9 +++++-- src/LaunchDarkly/FeatureFlag.php | 2 ++ src/LaunchDarkly/LDClient.php | 24 ++++++++++++++----- src/LaunchDarkly/VariationOrRollout.php | 9 ++++--- tests/LDClientTest.php | 6 ++--- 7 files changed, 41 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 325158603..33703af20 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Your first feature flag 2. In your application code, use the feature's key to check whether the flag is on for each user: $user = new LaunchDarkly\LDUser("user@test.com"); - if ($client->toggle("your.flag.key", $user)) { + if ($client->variation("your.flag.key", $user)) { # application code to show the feature } else { # the code to run if the feature is off diff --git a/integration-tests/LDDFeatureRequesterTest.php b/integration-tests/LDDFeatureRequesterTest.php index 5afeb7a88..602b87717 100644 --- a/integration-tests/LDDFeatureRequesterTest.php +++ b/integration-tests/LDDFeatureRequesterTest.php @@ -18,9 +18,9 @@ public function testGet() { $user = $builder->build(); $redis->del("launchdarkly:features"); - $this->assertEquals("jim", $client->toggle('foo', $user, 'jim')); + $this->assertEquals("jim", $client->variation('foo', $user, 'jim')); $redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "bar")); - $this->assertEquals("bar", $client->toggle('foo', $user, 'jim')); + $this->assertEquals("bar", $client->variation('foo', $user, 'jim')); } public function testGetApc() { @@ -34,16 +34,16 @@ public function testGetApc() { $user = $builder->build(); $redis->del("launchdarkly:features"); - $this->assertEquals("jim", $client->toggle('foo', $user, 'jim')); + $this->assertEquals("jim", $client->variation('foo', $user, 'jim')); $redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "bar")); - $this->assertEquals("bar", $client->toggle('foo', $user, 'jim')); + $this->assertEquals("bar", $client->variation('foo', $user, 'jim')); # cached value so not updated $redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "baz")); - $this->assertEquals("bar", $client->toggle('foo', $user, 'jim')); + $this->assertEquals("bar", $client->variation('foo', $user, 'jim')); apc_delete("launchdarkly:features.foo"); - $this->assertEquals("baz", $client->toggle('foo', $user, 'jim')); + $this->assertEquals("baz", $client->variation('foo', $user, 'jim')); } private function gen_feature($key, $val) { diff --git a/src/LaunchDarkly/Clause.php b/src/LaunchDarkly/Clause.php index 9f4534830..adf01615e 100644 --- a/src/LaunchDarkly/Clause.php +++ b/src/LaunchDarkly/Clause.php @@ -3,9 +3,13 @@ namespace LaunchDarkly; class Clause { + /** @var string */ private $_attribute = null; + /** @var string */ private $_op = null; + /** @var array */ private $_values = array(); + /** @var bool */ private $_negate = false; private function __construct($attribute, $op, array $values, $negate) { @@ -42,15 +46,16 @@ public function matchesUser($user) { } } + /** - * @return null + * @return string */ public function getAttribute() { return $this->_attribute; } /** - * @return null + * @return string */ public function getOp() { return $this->_op; diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 26684e713..ba70f6106 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -20,7 +20,9 @@ class FeatureFlag { protected $_rules = array(); /** @var VariationOrRollout */ protected $_fallthrough = null; + /** @var int | null */ protected $_offVariation = null; + /** @var array */ protected $_variations = array(); /** @var bool */ protected $_deleted = false; diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 8107f06cf..5e589499a 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -36,9 +36,13 @@ class LDClient { * @param string $apiKey The API key for your account * @param array $options Client configuration settings * - base_uri: Base URI of the LaunchDarkly API. Defaults to `DEFAULT_BASE_URI` + * - events_uri: Base URI of the LaunchDarkly API. Defaults to `DEFAULT_BASE_URI` * - timeout: Float describing the maximum length of a request in seconds. Defaults to 3 * - connect_timeout: Float describing the number of seconds to wait while trying to connect to a server. Defaults to 3 * - cache: An optional Kevinrob\GuzzleCache\Strategy\CacheStorageInterface. Defaults to an in-memory cache. + * - send_events: An optional bool that can disable the sending of events to LaunchDarkly. Defaults to false. + * - logger: An optional Psr\Log\LoggerInterface. Defaults to a Monolog\Logger sending all messages to the php error_log. + * - offline: An optional boolean which will disable all network calls and always return the default value. Defaults to false. */ public function __construct($apiKey, $options = array()) { $this->_apiKey = $apiKey; @@ -90,10 +94,6 @@ public function __construct($apiKey, $options = array()) { $this->_featureRequester = new $featureRequesterClass($this->_baseUri, $apiKey, $options); } - public function getFlag($key, $user, $default = false) { - return $this->toggle($key, $user, $default); - } - /** * Calculates the value of a feature flag for a given user. * @@ -101,9 +101,9 @@ public function getFlag($key, $user, $default = false) { * @param LDUser $user The end user requesting the flag * @param boolean $default The default value of the flag * - * @return mixed Whether or not the flag should be enabled, or `default` + * @return mixed The result of the Feature Flag evaluation, or $default if any errors occurred. */ - public function toggle($key, $user, $default = false) { + public function variation($key, $user, $default = false) { $default = $this->_get_default($key, $default); if ($this->_offline) { @@ -149,6 +149,18 @@ public function toggle($key, $user, $default = false) { return $default; } + + /** @deprecated Use variation() instead. + * @param $key + * @param $user + * @param bool $default + * @return mixed + */ + public function toggle($key, $user, $default = false) { + $this->_logger->warning("Deprecated function: toggle() called. Use variation() instead."); + return $this->variation($key, $user, $default); + } + /** * Returns whether the LaunchDarkly client is in offline mode. * diff --git a/src/LaunchDarkly/VariationOrRollout.php b/src/LaunchDarkly/VariationOrRollout.php index 9e507df6f..e1ff63b60 100644 --- a/src/LaunchDarkly/VariationOrRollout.php +++ b/src/LaunchDarkly/VariationOrRollout.php @@ -6,9 +6,9 @@ class VariationOrRollout { private static $LONG_SCALE = 0xFFFFFFFFFFFFFFF; - /** @var int */ + /** @var int | null */ private $_variation = null; - /** @var Rollout */ + /** @var Rollout | null */ private $_rollout = null; protected function __construct($variation, $rollout) { @@ -25,14 +25,14 @@ public static function getDecoder() { } /** - * @return int + * @return int | null */ public function getVariation() { return $this->_variation; } /** - * @return Rollout + * @return Rollout | null */ public function getRollout() { return $this->_rollout; @@ -58,7 +58,6 @@ public function variationIndexForUser($user, $_key, $_salt) { } } } - //TODO: throw exception? return null; } diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 50da3c704..c524bcd20 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -22,7 +22,7 @@ public function testToggleDefault() { $builder = new LDUserBuilder(3); $user = $builder->build(); - $this->assertEquals('argdef', $client->toggle('foo', $user, 'argdef')); + $this->assertEquals('argdef', $client->variation('foo', $user, 'argdef')); } public function testToggleFromArray() { @@ -35,7 +35,7 @@ public function testToggleFromArray() { $builder = new LDUserBuilder(3); $user = $builder->build(); - $this->assertEquals('fromarray', $client->toggle('foo', $user, 'argdef')); + $this->assertEquals('fromarray', $client->variation('foo', $user, 'argdef')); } public function testToggleEvent() { @@ -47,7 +47,7 @@ public function testToggleEvent() { $builder = new LDUserBuilder(3); $user = $builder->build(); - $client->toggle('foo', $user, 'argdef'); + $client->variation('foo', $user, 'argdef'); $proc = getPrivateField($client, '_eventProcessor'); $queue = getPrivateField($proc, '_queue'); $this->assertEquals(1, sizeof($queue));