diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..159bc0e79 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,98 @@ +version: 2 + +workflows: + version: 2 + test: + jobs: + - test-5.5 + - test-5.6 + - test-7.0 + - test-7.1 + - test-7.2 + - integration-test + +php-docker-template: &php-docker-template + steps: + - checkout + - run: + name: install current dependencies + command: composer install --no-progress + - run: + name: run tests with current dependency versions + command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests + - store_test_results: + path: ~/phpunit + - store_artifacts: + path: ~/phpunit + - run: + name: run tests with highest available dependency versions + command: composer update --no-progress && vendor/bin/phpunit tests + - run: + name: run tests with lowest available dependency versions + # we skip this for 7.2 because the lowest compatible version of PHPUnit has a bug: + # https://github.com/sebastianbergmann/comparator/pull/30 + command: | + if [[ $CIRCLE_JOB != test-7.2 ]]; then + composer update --prefer-lowest --no-progress && vendor/bin/phpunit tests; + fi + +jobs: + test-5.6: + <<: *php-docker-template + docker: + - image: circleci/php:5.6.34-cli-jessie + test-7.0: + <<: *php-docker-template + docker: + - image: circleci/php:7.0.28-cli-jessie + test-7.1: + <<: *php-docker-template + docker: + - image: circleci/php:7.1.15-cli-jessie + test-7.2: + <<: *php-docker-template + docker: + - image: circleci/php:7.2.3-cli-stretch + + test-5.5: # CircleCI doesn't provide a Docker image for 5.5 + machine: + image: circleci/classic:latest # Ubuntu 14.04 + steps: + - run: + name: install PHP and Composer + command: | + sudo apt-get update && + sudo apt-get install circleci-php-5.5.36 && + php -r "copy('https://getcomposer.org/installer', '/tmp/composer-setup.php');" && + sudo php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer + - checkout + - run: + name: update dependencies # the dependencies in composer.lock don't work with 5.5 + command: composer update --no-progress + - run: + name: run tests + command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests + - store_test_results: + path: ~/phpunit + - store_artifacts: + path: ~/phpunit + + integration-test: + docker: + - image: circleci/php:5.6.34-cli-jessie + - image: redis + steps: + - checkout + - run: + name: setup apcu + command: | + pecl config-set php_ini /usr/local/etc/php/php.ini + yes '' | sudo pecl install -f apcu-4.0.10 || true; + echo "extension=apcu.so" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini; + echo "apc.enable_cli = 1" | sudo tee -a /usr/local/etc/php/conf.d/apcu.ini + - run: composer update --no-progress + - run: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml integration-tests/LDDFeatureRequesterTest.php + - store_test_results: + path: ~/phpunit + - store_artifacts: + path: ~/phpunit diff --git a/CHANGELOG.md b/CHANGELOG.md index 9766e60a7..a9c853cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [3.1.0] - 2018-04-30 +### Added +- Analytics events for feature evaluations now have a `variation` property (the variation index) as well as `value`. This will allow for better performance in future versions of [`ld-relay`](https://github.com/launchdarkly/ld-relay) when it is used with the PHP client. +### Fixed +- Fixed a bug that made segment-based rules always fall through when using `LDDFeatureRequester`. + ## [3.0.0] - 2018-02-21 ### Added - Support for a new LaunchDarkly feature: reusable user segments. diff --git a/README.md b/README.md index 368d8ed30..8871b609e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly SDK for PHP =========================== -[![Code Climate](https://codeclimate.com/github/launchdarkly/php-client/badges/gpa.svg)](https://codeclimate.com/github/launchdarkly/php-client) - [![Circle CI](https://circleci.com/gh/launchdarkly/php-client.svg?style=svg)](https://circleci.com/gh/launchdarkly/php-client) Requirements diff --git a/VERSION b/VERSION index 4a36342fc..fd2a01863 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0 +3.1.0 diff --git a/circle.yml b/circle.yml deleted file mode 100644 index c3bc0437b..000000000 --- a/circle.yml +++ /dev/null @@ -1,31 +0,0 @@ -machine: - php: - version: 5.6.14 - services: - - redis - - docker -dependencies: - pre: - - yes '' | pecl install -f apcu-4.0.10 - - echo "extension=apcu.so" >> $(php-config --prefix)/etc/conf.d/apcu.ini - - echo "apc.enable_cli = 1" >> $(php-config --prefix)/etc/conf.d/apcu.ini - - docker pull php - - docker pull nyanpass/php5.5 - -test: - override: - - vendor/bin/phpunit tests --coverage-text - - vendor/bin/phpunit integration-tests/LDDFeatureRequesterTest.php - - - composer update && vendor/bin/phpunit tests - - composer update --prefer-lowest && vendor/bin/phpunit tests - - - docker run -it -v `pwd`:/php-client php:7.0-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update && vendor/bin/phpunit" - - docker run -it -v `pwd`:/php-client php:7.0-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update --prefer-lowest && vendor/bin/phpunit" - - - docker run -it -v `pwd`:/php-client php:7.1-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update && vendor/bin/phpunit" - - docker run -it -v `pwd`:/php-client php:7.1-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update --prefer-lowest && vendor/bin/phpunit" - - - docker run -it -v `pwd`:/php-client nyanpass/php5.5:5.5-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update && vendor/bin/phpunit" - - docker run -it -v `pwd`:/php-client nyanpass/php5.5:5.5-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update --prefer-lowest && vendor/bin/phpunit" - diff --git a/src/LaunchDarkly/CurlEventPublisher.php b/src/LaunchDarkly/CurlEventPublisher.php index d731377eb..1151b0494 100644 --- a/src/LaunchDarkly/CurlEventPublisher.php +++ b/src/LaunchDarkly/CurlEventPublisher.php @@ -60,6 +60,7 @@ private function createArgs($payload) $args.= " -H 'Content-Type: application/json'"; $args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey); $args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'"; + $args.= " -H 'X-LaunchDarkly-Event-Schema: " . EventPublisher::CURRENT_SCHEMA_VERSION . "'"; $args.= " -H 'Accept: application/json'"; $args.= " -d " . escapeshellarg($payload); $args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); diff --git a/src/LaunchDarkly/EventPublisher.php b/src/LaunchDarkly/EventPublisher.php index 38a344974..38dd94f44 100644 --- a/src/LaunchDarkly/EventPublisher.php +++ b/src/LaunchDarkly/EventPublisher.php @@ -6,6 +6,8 @@ */ interface EventPublisher { + const CURRENT_SCHEMA_VERSION = 2; + /** * @param string $sdkKey The SDK key for your account * @param mixed[] $options Client configuration settings diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 0ac89d317..1bc81855d 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -95,18 +95,18 @@ public function evaluate($user, $featureRequester) if ($this->isOn()) { $result = $this->_evaluate($user, $featureRequester, $prereqEvents); if ($result !== null) { - return new EvalResult($result, $prereqEvents); + return $result; } } - $offVariation = $this->getOffVariationValue(); - return new EvalResult($offVariation, $prereqEvents); + $offVariationValue = $this->getOffVariationValue(); + return new EvalResult($this->_offVariation, $offVariationValue, $prereqEvents); } /** * @param $user LDUser * @param $featureRequester FeatureRequester * @param $events - * @return mixed|null + * @return EvalResult|null */ private function _evaluate($user, $featureRequester, &$events) { @@ -120,21 +120,26 @@ private function _evaluate($user, $featureRequester, &$events) return null; } elseif ($prereqFeatureFlag->isOn()) { $prereqEvalResult = $prereqFeatureFlag->_evaluate($user, $featureRequester, $events); - $variation = $prereqFeatureFlag->getVariation($prereq->getVariation()); - if ($prereqEvalResult === null || $variation === null || $prereqEvalResult !== $variation) { + $variation = $prereq->getVariation(); + if ($prereqEvalResult === null || $variation === null || $prereqEvalResult->getVariation() !== $variation) { $prereqOk = false; } } else { $prereqOk = false; } - array_push($events, Util::newFeatureRequestEvent($prereqFeatureFlag->getKey(), $user, $prereqEvalResult, null, $prereqFeatureFlag->getVersion(), $this->_key)); + array_push($events, Util::newFeatureRequestEvent($prereqFeatureFlag->getKey(), $user, + $prereqEvalResult === null ? null : $prereqEvalResult->getVariation(), + $prereqEvalResult === null ? null : $prereqEvalResult->getValue(), + null, $prereqFeatureFlag->getVersion(), $this->_key)); } catch (EvaluationException $e) { $prereqOk = false; } } } if ($prereqOk) { - return $this->getVariation($this->evaluateIndex($user, $featureRequester)); + $variation = $this->evaluateIndex($user, $featureRequester); + $value = $this->getVariation($variation); + return new EvalResult($variation, $value, $events); } return null; } @@ -221,6 +226,7 @@ public function isDeleted() class EvalResult { + private $_variation = null; private $_value = null; /** @var array */ private $_prerequisiteEvents = []; @@ -230,12 +236,21 @@ class EvalResult * @param null $value * @param array $prerequisiteEvents */ - public function __construct($value, array $prerequisiteEvents) + public function __construct($variation, $value, array $prerequisiteEvents) { + $this->_variation = $variation; $this->_value = $value; $this->_prerequisiteEvents = $prerequisiteEvents; } + /** + * @return int + */ + public function getVariation() + { + return $this->_variation; + } + /** * @return null */ diff --git a/src/LaunchDarkly/GuzzleEventPublisher.php b/src/LaunchDarkly/GuzzleEventPublisher.php index ca9942542..ce18f0f14 100644 --- a/src/LaunchDarkly/GuzzleEventPublisher.php +++ b/src/LaunchDarkly/GuzzleEventPublisher.php @@ -39,7 +39,8 @@ public function __construct($sdkKey, array $options = array()) 'Content-Type' => 'application/json', 'Authorization' => $this->_sdkKey, 'User-Agent' => 'PHPClient/' . LDClient::VERSION, - 'Accept' => 'application/json' + 'Accept' => 'application/json', + 'X-LaunchDarkly-Event-Schema' => strval(EventPublisher::CURRENT_SCHEMA_VERSION) ], 'timeout' => $options['timeout'], 'connect_timeout' => $options['connect_timeout'] diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index fc21d080d..3604db65f 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -19,7 +19,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '3.0.0'; + const VERSION = '3.1.0'; /** @var string */ protected $_sdkKey; @@ -148,12 +148,7 @@ public function variation($key, $user, $default = false) } try { - if (is_null($user) || is_null($user->getKey())) { - $this->_sendFlagRequestEvent($key, $user, $default, $default); - $this->_logger->warning("Variation called with null user or null user key! Returning default value"); - return $default; - } - if ($user->isKeyBlank()) { + if (!is_null($user) && $user->isKeyBlank()) { $this->_logger->warning("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly."); } try { @@ -164,7 +159,12 @@ public function variation($key, $user, $default = false) } if (is_null($flag)) { - $this->_sendFlagRequestEvent($key, $user, $default, $default); + $this->_sendFlagRequestEvent($key, $user, null, $default, $default); + return $default; + } + if (is_null($user) || is_null($user->getKey())) { + $this->_sendFlagRequestEvent($key, $user, null, $default, $default, $flag->getVersion()); + $this->_logger->warning("Variation called with null user or null user key! Returning default value"); return $default; } $evalResult = $flag->evaluate($user, $this->_featureRequester); @@ -173,15 +173,18 @@ public function variation($key, $user, $default = false) $this->_eventProcessor->enqueue($e); } } - if ($evalResult->getValue() !== null) { - $this->_sendFlagRequestEvent($key, $user, $evalResult->getValue(), $default, $flag->getVersion()); + if ($evalResult !== null && $evalResult->getValue() !== null) { + $this->_sendFlagRequestEvent($key, $user, $evalResult->getVariation(), $evalResult->getValue(), $default, $flag->getVersion()); return $evalResult->getValue(); + } else { + $this->_sendFlagRequestEvent($key, $user, null, $default, $default, $flag->getVersion()); + return $default; } } catch (\Exception $e) { $this->_logger->error("Caught $e"); } try { - $this->_sendFlagRequestEvent($key, $user, $default, $default); + $this->_sendFlagRequestEvent($key, $user, null, $default, $default); } catch (\Exception $e) { $this->_logger->error("Caught $e"); } @@ -326,17 +329,18 @@ public function flush() /** * @param $key string * @param $user LDUser + * @param $variation int | null * @param $value mixed * @param $default * @param $version int | null * @param string | null $prereqOf */ - protected function _sendFlagRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) + protected function _sendFlagRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null) { if ($this->isOffline() || !$this->_send_events) { return; } - $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $value, $default, $version, $prereqOf)); + $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $variation, $value, $default, $version, $prereqOf)); } protected function _get_default($key, $default) diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index 4ae4ecb44..1058f54ca 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -98,7 +98,7 @@ public function getSegment($key) $raw = $this->get_from_cache($this->_segments_key, $key); if ($raw === null) { $redis = $this->get_connection(); - $raw = $redis->hget($this->_features_key, $key); + $raw = $redis->hget($this->_segments_key, $key); if ($raw) { $this->store_in_cache($this->_segments_key, $key, $raw); } diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php index 231f4b47c..6ab2ae28c 100644 --- a/src/LaunchDarkly/Util.php +++ b/src/LaunchDarkly/Util.php @@ -37,10 +37,11 @@ public static function currentTimeUnixMillis() * @param null $prereqOf string | null * @return array */ - public static function newFeatureRequestEvent($key, $user, $value, $default, $version = null, $prereqOf = null) + public static function newFeatureRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null) { $event = array(); $event['user'] = $user; + $event['variation'] = $variation; $event['value'] = $value; $event['kind'] = "feature"; $event['creationDate'] = Util::currentTimeUnixMillis(); diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 8b8dde742..51f865b08 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -193,6 +193,308 @@ public function testDecodeMulti(array $feature) self::assertInstanceOf(FeatureFlag::class, $featureFlag); } + public function testFlagReturnsOffVariationIfFlagIsOff() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(1, $result->getVariation()); + self::assertEquals('off', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsNullIfFlagIsOffAndOffVariationIsUnspecified() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => null, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertNull($result->getVariation()); + self::assertNull($result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsOffVariationIfPrerequisiteIsNotFound() + { + $flagJson = array( + 'key' => 'feature0', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array( + array('key' => 'feature1', 'variation' => 1) + ), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + $requester = new MockFeatureRequesterForFeature(); + + $result = $flag->evaluate($user, $requester); + self::assertEquals(1, $result->getVariation()); + self::assertEquals('off', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() + { + $flag0Json = array( + 'key' => 'feature0', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array( + array('key' => 'feature1', 'variation' => 1) + ), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag1Json = array( + 'key' => 'feature1', + 'version' => 2, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('nogo', 'go'), + 'salt' => '' + ); + $flag0 = FeatureFlag::decode($flag0Json); + $flag1 = FeatureFlag::decode($flag1Json); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + $requester = new MockFeatureRequesterForFeature(); + $requester->key = $flag1->getKey(); + $requester->val = $flag1; + + $result = $flag0->evaluate($user, $requester); + self::assertEquals(1, $result->getVariation()); + self::assertEquals('off', $result->getValue()); + + $events = $result->getPrerequisiteEvents(); + self::assertEquals(1, count($events)); + $event = $events[0]; + self::assertEquals('feature', $event['kind']); + self::assertEquals($flag1->getKey(), $event['key']); + self::assertEquals('nogo', $event['value']); + self::assertEquals($flag1->getVersion(), $event['version']); + self::assertEquals($flag0->getKey(), $event['prereqOf']); + } + + public function testFlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAreNoRules() + { + $flag0Json = array( + 'key' => 'feature0', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array( + array('key' => 'feature1', 'variation' => 1) + ), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag1Json = array( + 'key' => 'feature1', + 'version' => 2, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 1), + 'variations' => array('nogo', 'go'), + 'salt' => '' + ); + $flag0 = FeatureFlag::decode($flag0Json); + $flag1 = FeatureFlag::decode($flag1Json); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + $requester = new MockFeatureRequesterForFeature(); + $requester->key = $flag1->getKey(); + $requester->val = $flag1; + + $result = $flag0->evaluate($user, $requester); + self::assertEquals(0, $result->getVariation()); + self::assertEquals('fall', $result->getValue()); + + $events = $result->getPrerequisiteEvents(); + self::assertEquals(1, count($events)); + $event = $events[0]; + self::assertEquals('feature', $event['kind']); + self::assertEquals($flag1->getKey(), $event['key']); + self::assertEquals('go', $event['value']); + self::assertEquals($flag1->getVersion(), $event['version']); + self::assertEquals($flag0->getKey(), $event['prereqOf']); + } + + public function testFlagMatchesUserFromTargets() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array( + array('values' => array('whoever', 'userkey'), 'variation' => 2) + ), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(2, $result->getVariation()); + self::assertEquals('on', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagMatchesUserFromRules() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => 'key', + 'op' => 'in', + 'values' => array('userkey'), + 'negate' => false + ) + ), + 'variation' => 2 + ) + ), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(2, $result->getVariation()); + self::assertEquals('on', $result->getValue()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function clauseCanMatchBuiltInAttribute() + { + $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(true, $result->getValue()); + } + + public function clauseCanMatchCustomAttribute() + { + $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $ub->customAttribute('legs', 4); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(true, $result->getValue()); + } + + public function clauseReturnsFalseForMissingAttribute() + { + $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(false, $result->getValue()); + } + + public function clauseCanBeNegated() + { + $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => true); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(false, $result->getValue()); + } + + public function clauseWithUnknownOperatorDoesNotMatch() + { + $clause = array('attribute' => 'name', 'op' => 'doesSomethingUnsupported', 'values' => array('Bob'), 'negate' => false); + $flag = $this->booleanFlagWithClauses(array($clause)); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + self::assertEquals(false, $result->getValue()); + } + public function testSegmentMatchClauseRetrievesSegmentFromStore() { $segmentJson = array( @@ -234,7 +536,7 @@ public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound( self::assertFalse($result->getValue()); } - private function makeBooleanFeatureWithSegmentMatch($segmentKey) + private function booleanFlagWithClauses($clauses) { $featureJson = array( 'key' => 'test', @@ -244,17 +546,7 @@ private function makeBooleanFeatureWithSegmentMatch($segmentKey) 'variations' => array(false, true), 'fallthrough' => array('variation' => 0), 'rules' => array( - array( - 'clauses' => array( - array( - 'attribute' => '', - 'op' => 'segmentMatch', - 'values' => array($segmentKey), - 'negate' => false - ) - ), - 'variation' => 1 - ) + array('clauses' => $clauses, 'variation' => 1) ), 'offVariation' => 0, 'prerequisites' => array(), @@ -263,8 +555,43 @@ private function makeBooleanFeatureWithSegmentMatch($segmentKey) ); return FeatureFlag::decode($featureJson); } + + private function makeBooleanFeatureWithSegmentMatch($segmentKey) + { + $clause = array( + 'attribute' => '', + 'op' => 'segmentMatch', + 'values' => array($segmentKey), + 'negate' => false + ); + return $this->booleanFlagWithClauses(array($clause)); + } } +class MockFeatureRequesterForFeature implements FeatureRequester +{ + public $key = null; + public $val = null; + + function __construct($baseurl = null, $key = null, $options = null) + { + } + + public function getFeature($key) + { + return ($key == $this->key) ? $this->val : null; + } + + public function getSegment($key) + { + return null; + } + + public function getAllFeatures() + { + return null; + } +} class MockFeatureRequesterForSegment implements FeatureRequester {