From 3d73351a2ebbe4ddf76a70a987fe783de7fbace1 Mon Sep 17 00:00:00 2001 From: Don Brown Date: Tue, 22 Sep 2015 17:59:43 -0600 Subject: [PATCH 1/5] Support LDD feature retrieval * Clean up formatting and add types * New FeatureRequester interface to abstract grizzly and ldd support * Support PHP 5.3 * New optional predis for LDD retrieval --- .gitignore | 4 +- composer.json | 6 +- composer.lock | 63 +++++++++++-- src/LaunchDarkly/EventProcessor.php | 5 +- src/LaunchDarkly/FeatureRep.php | 26 ++++-- src/LaunchDarkly/FeatureRequester.php | 13 +++ src/LaunchDarkly/GuzzleFeatureRequester.php | 51 +++++++++++ src/LaunchDarkly/LDClient.php | 74 ++++++++-------- src/LaunchDarkly/LDDFeatureRetriever.php | 54 ++++++++++++ src/LaunchDarkly/LDUser.php | 6 +- src/LaunchDarkly/LDUserBuilder.php | 2 +- src/LaunchDarkly/TargetRule.php | 8 +- src/LaunchDarkly/Variation.php | 10 ++- tests/FeatureRepTest.php | 37 ++++---- tests/LDClientTest.php | 2 +- tests/LDUserTest.php | 97 +++++++++++++-------- 16 files changed, 339 insertions(+), 119 deletions(-) create mode 100644 src/LaunchDarkly/FeatureRequester.php create mode 100644 src/LaunchDarkly/GuzzleFeatureRequester.php create mode 100644 src/LaunchDarkly/LDDFeatureRetriever.php diff --git a/.gitignore b/.gitignore index b54ecd3ba..a72a9de14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /vendor/ -/doc/ \ No newline at end of file +/doc/ +*.iml +composer.phar diff --git a/composer.json b/composer.json index 448f48eca..c369803ae 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,11 @@ }, "require-dev": { "phpunit/phpunit": "4.3.*", - "phpdocumentor/phpdocumentor": "2.*" + "phpdocumentor/phpdocumentor": "2.*", + "predis/predis": "1.0.*" + }, + "suggested": { + "predis/predis": "1.0.*" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 3a666c5e7..c20781140 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "8ac74c2aa340f82f7e631d950498d197", + "hash": "98a393714a8efc64704980d689db7329", + "content-hash": "6dc1608d0635b4153e196011d2c6b841", "packages": [ { "name": "doctrine/cache", @@ -1962,6 +1963,56 @@ ], "time": "2013-11-22 08:30:29" }, + { + "name": "predis/predis", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/nrk/predis.git", + "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nrk/predis/zipball/84060b9034d756b4d79641667d7f9efe1aeb8e04", + "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "ext-curl": "Allows access to Webdis when paired with phpiredis", + "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniele Alessandri", + "email": "suppakilla@gmail.com", + "homepage": "http://clorophilla.net" + } + ], + "description": "Flexible and feature-complete PHP client library for Redis", + "homepage": "http://github.com/nrk/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "time": "2015-07-30 18:34:15" + }, { "name": "psr/log", "version": "1.0.0", @@ -2909,16 +2960,16 @@ }, { "name": "twig/twig", - "version": "v1.22.1", + "version": "v1.22.2", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "b7fc2469fa009897871fb95b68237286fc54a5ad" + "reference": "79249fc8c9ff62e41e217e0c630e2e00bcadda6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/b7fc2469fa009897871fb95b68237286fc54a5ad", - "reference": "b7fc2469fa009897871fb95b68237286fc54a5ad", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/79249fc8c9ff62e41e217e0c630e2e00bcadda6a", + "reference": "79249fc8c9ff62e41e217e0c630e2e00bcadda6a", "shasum": "" }, "require": { @@ -2966,7 +3017,7 @@ "keywords": [ "templating" ], - "time": "2015-09-15 06:50:16" + "time": "2015-09-22 13:59:32" }, { "name": "zendframework/zend-cache", diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index b2c813673..e5f04c390 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -10,12 +10,11 @@ class EventProcessor { private $_queue; private $_capacity; private $_timeout; - private $_socket_failed; private $_host; private $_port; private $_ssl; - public function __construct($apiKey, $options = []) { + public function __construct($apiKey, $options = array()) { $this->_apiKey = $apiKey; if (!isset($options['base_uri'])) { $this->_host = 'app.launchdarkly.com'; @@ -60,7 +59,7 @@ public function enqueue($event) { protected function flush() { if (empty($this->_queue)) { - return; + return null; } $payload = json_encode($this->_queue); diff --git a/src/LaunchDarkly/FeatureRep.php b/src/LaunchDarkly/FeatureRep.php index b410619e5..36d297d1a 100644 --- a/src/LaunchDarkly/FeatureRep.php +++ b/src/LaunchDarkly/FeatureRep.php @@ -11,16 +11,22 @@ class FeatureRep { protected $_key = null; protected $_salt = null; protected $_on = false; - protected $_variations = []; - public function __construct($name, $key, $salt, $on = true, $variations = []) { + /** @var Variation[] */ + protected $_variations = array(); + + public function __construct($name, $key, $salt, $on = true, $variations = array()) { $this->_name = $name; - $this->_key = $key; + $this->_key = $key; $this->_salt = $salt; - $this->_on = $on; + $this->_on = $on; $this->_variations = $variations; } + /** + * @param $user LDUser + * @return mixed + */ public function evaluate($user) { if (!$this->_on || !$user) { return null; @@ -29,7 +35,8 @@ public function evaluate($user) { $param = $this->_get_param($user); if (is_null($param)) { return null; - } else { + } + else { foreach ($this->_variations as $variation) { if ($variation->matchUser($user)) { return $variation->getValue(); @@ -55,18 +62,23 @@ public function evaluate($user) { 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 { + } + else { return null; } if ($user->getSecondary()) { - $id_hash += "." . $user->getSecondary(); + $id_hash .= "." . $user->getSecondary(); } $hash = substr(sha1($this->_key . "." . $this->_salt . "." . $id_hash), 0, 15); diff --git a/src/LaunchDarkly/FeatureRequester.php b/src/LaunchDarkly/FeatureRequester.php new file mode 100644 index 000000000..e16ea3349 --- /dev/null +++ b/src/LaunchDarkly/FeatureRequester.php @@ -0,0 +1,13 @@ +_client = new Client(array( + 'base_url' => $baseUri, + 'defaults' => array( + 'headers' => array( + 'Authorization' => "api_key {$apiKey}", + 'Content-Type' => 'application/json', + 'User-Agent' => 'PHPClient/' . LDClient::VERSION + ), + 'debug' => false, + 'timeout' => $options['timeout'], + 'connect_timeout' => $options['connect_timeout'] + ) + )); + + if (!isset($options['cache_storage'])) { + $csOptions = array('validate' => false); + } + else { + $csOptions = array('storage' => $options['cache_storage'], 'validate' => false); + } + + CacheSubscriber::attach($this->_client, $csOptions); + } + + + /** + * Gets feature data from a likely cached store + * + * @param $key string feature key + * @return mixed The decoded JSON feature data, or null if missing + */ + public function get($key) { + try { + $response = $this->_client->get("/api/eval/features/$key"); + return $response->json(); + } catch (BadResponseException $e) { + $code = $e->getResponse()->getStatusCode(); + error_log("GuzzleFeatureRetriever::get received an unexpected HTTP status code $code"); + return null; + } + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 9eafd501e..2b5dd8878 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -1,8 +1,7 @@ _apiKey = $apiKey; if (!isset($options['base_uri'])) { $this->_baseUri = self::DEFAULT_BASE_URI; @@ -46,9 +48,14 @@ public function __construct($apiKey, $options = []) { $options['capacity'] = 1000; } - $this->_eventProcessor = new \LaunchDarkly\EventProcessor($apiKey, $options); + $this->_eventProcessor = new EventProcessor($apiKey, $options); - $this->_client = $this->_make_client($options); + if (isset($options['feature_retriever_class'])) { + $featureRetrieverClass = $options['feature_retriever_class']; + } else { + $featureRetrieverClass = '\\LaunchDarkly\\GuzzleFeatureRequester'; + } + $this->_featureRetriever = new $featureRetrieverClass($this->_baseUri, $apiKey, $options); } public function getFlag($key, $user, $default = false) { @@ -120,9 +127,9 @@ public function isOffline() { /** * Tracks that a user performed an event. * - * @param string $eventName The name of the event - * @param LDUser $user The user that performed the event - * + * @param $eventName string The name of the event + * @param $user LDUser The user that performed the event + * @param $data mixed */ public function track($eventName, $user, $data) { if ($this->isOffline()) { @@ -140,6 +147,9 @@ public function track($eventName, $user, $data) { $this->_eventProcessor->enqueue($event); } + /** + * @param $user LDUser + */ public function identify($user) { if ($this->isOffline()) { return; @@ -153,6 +163,11 @@ public function identify($user) { $this->_eventProcessor->enqueue($event); } + /** + * @param $key string + * @param $user LDUser + * @param $value mixed + */ protected function _sendFlagRequestEvent($key, $user, $value) { if ($this->isOffline()) { return; @@ -169,47 +184,26 @@ protected function _sendFlagRequestEvent($key, $user, $value) { protected function _toggle($key, $user, $default) { try { - $response = $this->_client->get("/api/eval/features/$key"); - return self::_decode($response->json(), $user); - } catch (BadResponseException $e) { - $code = $e->getResponse()->getStatusCode(); - error_log("LDClient::toggle received HTTP status code $code, using default"); + $data = $this->_featureRetriever->get($key); + if ($data == null) { + error_log("LDClient::_toggle received null from retriever, using default"); + return $default; + } + return self::_decode($data, $user); + } catch (Exception $e) { + $msg = $e->getMessage(); + error_log("LDClient::_toggle received error $msg, using default"); return $default; } } - protected function _make_client($options) { - $client = new \GuzzleHttp\Client([ - 'base_url' => $this->_baseUri, - 'defaults' => [ - 'headers' => [ - 'Authorization' => "api_key {$this->_apiKey}", - 'Content-Type' => 'application/json', - 'User-Agent' => 'PHPClient/' . self::VERSION - ], - 'debug' => false, - 'timeout' => $options['timeout'], - 'connect_timeout' => $options['connect_timeout'] - ] - ]); - - if (!isset($options['cache_storage'])) { - $csOptions = ['validate' => false]; - } else { - $csOptions = ['storage' => $options['cache_storage'], 'validate' => false]; - } - - CacheSubscriber::attach($client, $csOptions); - return $client; - } - protected static function _decode($json, $user) { $makeVariation = function ($v) { $makeTarget = function ($t) { return new TargetRule($t['attribute'], $t['op'], $t['values']); }; - $ts = empty($v['targets']) ? [] : $v['targets']; + $ts = empty($v['targets']) ? array() : $v['targets']; $targets = array_map($makeTarget, $ts); if (isset($v['userTarget'])) { return new Variation($v['value'], $v['weight'], $targets, $makeTarget($v['userTarget'])); @@ -219,7 +213,7 @@ protected static function _decode($json, $user) { } }; - $vs = empty($json['variations']) ? [] : $json['variations']; + $vs = empty($json['variations']) ? array() : $json['variations']; $variations = array_map($makeVariation, $vs); $feature = new FeatureRep($json['name'], $json['key'], $json['salt'], $json['on'], $variations); diff --git a/src/LaunchDarkly/LDDFeatureRetriever.php b/src/LaunchDarkly/LDDFeatureRetriever.php new file mode 100644 index 000000000..1085a0bcf --- /dev/null +++ b/src/LaunchDarkly/LDDFeatureRetriever.php @@ -0,0 +1,54 @@ +_baseUri = $baseUri; + $this->_apiKey = $apiKey; + $this->_options = $options; + if (!isset($options['redis_host'])) { + $options['redis_host'] = 'localhost'; + } + if (!isset($options['redis_port'])) { + $options['redis_port'] = 6379; + } + + $prefix = "launchdarkly"; + if (isset($options['redis_prefix'])) { + $prefix = $options['redis_prefix']; + } + $this->_features_key = "$prefix:features"; + } + + protected function get_connection() { + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + return new \Predis\Client(array( + "scheme" => "tcp", + "host" => $this->_options['redis_host'], + "port" => $this->_options['redis_port'])); + } + + + /** + * Gets feature data from a likely cached store + * + * @param $key string feature key + * @return mixed The decoded JSON feature data, or null if missing + */ + public function get($key) { + $redis = $this->get_connection(); + $raw = $redis->hget($this->_features_key, $key); + if ($raw) { + return json_decode($raw); + } + else { + return null; + } + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/LDUser.php b/src/LaunchDarkly/LDUser.php index aa5de90e3..fe8c14fa1 100644 --- a/src/LaunchDarkly/LDUser.php +++ b/src/LaunchDarkly/LDUser.php @@ -17,7 +17,7 @@ class LDUser { protected $_firstName = null; protected $_lastName = null; protected $_anonyomus = false; - protected $_custom = []; + 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. @@ -32,7 +32,7 @@ class LDUser { * @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 */ - public function __construct($key, $secondary = null, $ip = null, $country = null, $email = null, $name = null, $avatar = null, $firstName = null, $lastName= null, $anonymous = null, $custom = []) { + 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; @@ -91,7 +91,7 @@ public function getAnonymous() { } public function toJSON() { - $json = ["key" => $this->_key]; + $json = array("key" => $this->_key); if (isset($this->_secondary)) { $json['secondary'] = $this->_secondary; diff --git a/src/LaunchDarkly/LDUserBuilder.php b/src/LaunchDarkly/LDUserBuilder.php index 70fefead8..e0a4484d7 100644 --- a/src/LaunchDarkly/LDUserBuilder.php +++ b/src/LaunchDarkly/LDUserBuilder.php @@ -12,7 +12,7 @@ class LDUserBuilder { protected $_firstName = null; protected $_lastName = null; protected $_anonymous = null; - protected $_custom = []; + protected $_custom = array(); public function __construct($key) { $this->_key = $key; diff --git a/src/LaunchDarkly/TargetRule.php b/src/LaunchDarkly/TargetRule.php index 0f75b8c3d..9388df083 100644 --- a/src/LaunchDarkly/TargetRule.php +++ b/src/LaunchDarkly/TargetRule.php @@ -7,7 +7,7 @@ class TargetRule { protected $_attribute = null; protected $_operator = null; - protected $_values = []; + protected $_values = array(); public function __construct($attribute, $operator, $values) { $this->_attribute = $attribute; @@ -20,6 +20,10 @@ public function isKey() { } + /** + * @param $user LDUser + * @return bool + */ public function matchTarget($user) { $u_value = null; @@ -31,7 +35,7 @@ public function matchTarget($user) { $u_value = $user->getIP(); break; case "country": - $u_value = $user->getCountryCode(); + $u_value = $user->getCountry(); break; case "email": $u_value = $user->getEmail(); diff --git a/src/LaunchDarkly/Variation.php b/src/LaunchDarkly/Variation.php index 35a38c72b..40cdd97a8 100644 --- a/src/LaunchDarkly/Variation.php +++ b/src/LaunchDarkly/Variation.php @@ -8,8 +8,12 @@ class Variation { protected $_value = null; protected $_weight = 0; protected $_targetRule = null; + + /** @var TargetRule */ protected $_userTarget = null; - protected $_targets = []; + + /** @var TargetRule[] */ + protected $_targets = array(); public function __construct($value, $weight, $targets, $userTarget) { $this->_value = $value; @@ -18,6 +22,10 @@ public function __construct($value, $weight, $targets, $userTarget) { $this->_userTarget = $userTarget; } + /** + * @param $user LDUser + * @return bool + */ public function matchUser($user) { if ($this->_userTarget != null) { return $this->_userTarget->matchTarget($user); diff --git a/tests/FeatureRepTest.php b/tests/FeatureRepTest.php index 28e38d353..16b1f207b 100644 --- a/tests/FeatureRepTest.php +++ b/tests/FeatureRepTest.php @@ -9,27 +9,30 @@ class FeatureRepTest extends \PHPUnit_Framework_TestCase { + /** @var FeatureRep */ protected $_simpleFlag = null; + /** @var FeatureRep */ protected $_disabledFlag = null; + /** @var FeatureRep */ protected $_userTargetFlag = null; protected function setUp() { parent::setUp(); - $targetUserOn = new TargetRule("key", "in", ["targetOn@test.com"]); - $targetGroupOn = new TargetRule("groups", "in", ["google", "microsoft"]); - $targetUserOff = new TargetRule("key", "in", ["targetOff@test.com"]); - $targetGroupOff = new TargetRule("groups", "in", ["oracle"]); - $targetEmailOn = new TargetRule("email", "in", ["targetEmailOn@test.com"]); + $targetUserOn = new TargetRule("key", "in", array("targetOn@test.com")); + $targetGroupOn = new TargetRule("groups", "in", array("google", "microsoft")); + $targetUserOff = new TargetRule("key", "in", array("targetOff@test.com")); + $targetGroupOff = new TargetRule("groups", "in", array("oracle")); + $targetEmailOn = new TargetRule("email", "in", array("targetEmailOn@test.com")); - $trueVariation = new Variation(true, 80, [$targetUserOn, $targetGroupOn, $targetEmailOn], null); - $falseVariation = new Variation(false, 20, [$targetUserOff, $targetGroupOff], null); + $trueVariation = new Variation(true, 80, array($targetUserOn, $targetGroupOn, $targetEmailOn), null); + $falseVariation = new Variation(false, 20, array($targetUserOff, $targetGroupOff), null); - $this->_simpleFlag = new FeatureRep("Sample flag", "sample.flag", "feefifofum", true, [$trueVariation, $falseVariation]); - $this->_disabledFlag = new FeatureRep("Sample flag", "sample.flag", "feefifofum", false, [$trueVariation, $falseVariation]); + $this->_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, [], $targetUserOn); + $userTargetVariation = new Variation(false, 20, array(), $targetUserOn); - $this->_userTargetFlag = new FeatureRep("Sample flag", "sample.flag", "feefifofum", true, [$trueVariation, $userTargetVariation]); + $this->_userTargetFlag = new FeatureRep("Sample flag", "sample.flag", "feefifofum", true, array($trueVariation, $userTargetVariation)); } protected function tearDown() { @@ -50,13 +53,15 @@ public function testFlagForTargetedUserOn() { } public function testFlagForTargetGroupOn() { - $user = (new LDUserBuilder("targetOther@test.com"))->custom(["groups" => ["google", "microsoft"]])->build(); + $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() { - $user = (new LDUserBuilder("targetOther@test.com"))->custom(["groups" => ["oracle"]])->build(); + $builder = new LDUserBuilder("targetOther@test.com"); + $user = $builder->custom(array("groups" => array("oracle")))->build(); $b = $this->_simpleFlag->evaluate($user); $this->assertEquals(false, $b); } @@ -68,13 +73,15 @@ public function testDisabledFlagAlwaysOff() { } public function testUserRuleFlagForTargetUserOff() { - $user = (new LDUserBuilder("targetOff@test.com"))->build(); + $builder = new LDUserBuilder("targetOff@test.com"); + $user = $builder->build(); $b = $this->_userTargetFlag->evaluate($user); $this->assertEquals(false, $b); } public function testFlagForTargetEmailOff() { - $user = (new LDUserBuilder("targetOff@test.com"))->email("targetEmailOn@test.com")->build(); + $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 44d1c15e4..78a0fa5e8 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -6,7 +6,7 @@ class LDClientTest extends \PHPUnit_Framework_TestCase { public function testDefaultCtor() { - $client = new LDClient("BOGUS_API_KEY"); + new LDClient("BOGUS_API_KEY"); } } diff --git a/tests/LDUserTest.php b/tests/LDUserTest.php index 59f430319..ca684f241 100644 --- a/tests/LDUserTest.php +++ b/tests/LDUserTest.php @@ -2,74 +2,95 @@ namespace LaunchDarkly\Tests; use LaunchDarkly\LDUserBuilder; -use LaunchDarkly\LDUser; class LDUserTest extends \PHPUnit_Framework_TestCase { public function testLDUserKey() { - $user = (new LDUserBuilder("foo@bar.com"))->build(); - $this->assertEquals("foo@bar.com", $user->getKey()); - $this->assertEquals("foo@bar.com", $user->toJSON()['key']); + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->build(); + $this->assertEquals("foo@bar.com", $user->getKey()); + $json = $user->toJSON(); + $this->assertEquals("foo@bar.com", $json['key']); } public function testCoerceLDUserKey() { - $user = (new LDUserBuilder(3))->build(); - $this->assertEquals("string", gettype($user->getKey())); - $this->assertEquals("string", gettype($user->toJSON()['key'])); + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $this->assertEquals("string", gettype($user->getKey())); + $json = $user->toJSON(); + $this->assertEquals("string", gettype($json['key'])); } public function testLDUserSecondary() { - $user = (new LDUserBuilder("foo@bar.com"))->secondary("secondary")->build(); - $this->assertEquals("secondary", $user->getSecondary()); - $this->assertEquals("secondary", $user->toJSON()['secondary']); + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->secondary("secondary")->build(); + $this->assertEquals("secondary", $user->getSecondary()); + $json = $user->toJSON(); + $this->assertEquals("secondary", $json['secondary']); } public function testLDUserIP() { - $user = (new LDUserBuilder("foo@bar.com"))->ip("127.0.0.1")->build(); - $this->assertEquals("127.0.0.1", $user->getIP()); - $this->assertEquals("127.0.0.1", $user->toJSON()['ip']); + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->ip("127.0.0.1")->build(); + $this->assertEquals("127.0.0.1", $user->getIP()); + $json = $user->toJSON(); + $this->assertEquals("127.0.0.1", $json['ip']); } public function testLDUserCountry() { - $user = (new LDUserBuilder("foo@bar.com"))->country("US")->build(); - $this->assertEquals("US", $user->getCountry()); - $this->assertEquals("US", $user->toJSON()['country']); + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->country("US")->build(); + $this->assertEquals("US", $user->getCountry()); + $json = $user->toJSON(); + $this->assertEquals("US", $json['country']); } public function testLDUserEmail() { - $user = (new LDUserBuilder("foo@bar.com"))->email("foo+test@bar.com")->build(); - $this->assertEquals("foo+test@bar.com", $user->getEmail()); - $this->assertEquals("foo+test@bar.com", $user->toJSON()['email']); + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->email("foo+test@bar.com")->build(); + $this->assertEquals("foo+test@bar.com", $user->getEmail()); + $json = $user->toJSON(); + $this->assertEquals("foo+test@bar.com", $json['email']); } public function testLDUserName() { - $user = (new LDUserBuilder("foo@bar.com"))->name("Foo Bar")->build(); - $this->assertEquals("Foo Bar", $user->getName()); - $this->assertEquals("Foo Bar", $user->toJSON()['name']); - } + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->name("Foo Bar")->build(); + $this->assertEquals("Foo Bar", $user->getName()); + $json = $user->toJSON(); + $this->assertEquals("Foo Bar", $json['name']); + } public function testLDUserAvatar() { - $user = (new LDUserBuilder("foo@bar.com"))->avatar("http://www.gravatar.com/avatar/1")->build(); - $this->assertEquals("http://www.gravatar.com/avatar/1", $user->getAvatar()); - $this->assertEquals("http://www.gravatar.com/avatar/1", $user->toJSON()['avatar']); - } + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->avatar("http://www.gravatar.com/avatar/1")->build(); + $this->assertEquals("http://www.gravatar.com/avatar/1", $user->getAvatar()); + $json = $user->toJSON(); + $this->assertEquals("http://www.gravatar.com/avatar/1", $json['avatar']); + } public function testLDUserFirstName() { - $user = (new LDUserBuilder("foo@bar.com"))->firstName("Foo")->build(); - $this->assertEquals("Foo", $user->getFirstName()); - $this->assertEquals("Foo", $user->toJSON()['firstName']); - } + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->firstName("Foo")->build(); + $this->assertEquals("Foo", $user->getFirstName()); + $json = $user->toJSON(); + $this->assertEquals("Foo", $json['firstName']); + } public function testLDUserLastName() { - $user = (new LDUserBuilder("foo@bar.com"))->lastName("Bar")->build(); - $this->assertEquals("Bar", $user->getLastName()); - $this->assertEquals("Bar", $user->toJSON()['lastName']); - } + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->lastName("Bar")->build(); + $this->assertEquals("Bar", $user->getLastName()); + $json = $user->toJSON(); + $this->assertEquals("Bar", $json['lastName']); + } public function testLDUserAnonymous() { - $user = (new LDUserBuilder("foo@bar.com"))->anonymous(true)->build(); - $this->assertEquals(true, $user->getAnonymous()); - $this->assertEquals(true, $user->toJSON()['anonymous']); + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->anonymous(true)->build(); + $this->assertEquals(true, $user->getAnonymous()); + $json = $user->toJSON(); + $this->assertEquals(true, $json['anonymous']); } } From aa9c43009fd2263c2fa7246b7a4cd87425683a52 Mon Sep 17 00:00:00 2001 From: Don Brown Date: Tue, 22 Sep 2015 18:07:38 -0600 Subject: [PATCH 2/5] Have circle test 5.3 --- circle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index 19d8954e1..22fdc539d 100644 --- a/circle.yml +++ b/circle.yml @@ -1,7 +1,7 @@ machine: php: - version: 5.4.10 + version: 5.3.29 test: override: - - vendor/bin/phpunit tests \ No newline at end of file + - vendor/bin/phpunit tests From 9fde3257544d589f373ae65936e35b266589ee54 Mon Sep 17 00:00:00 2001 From: Don Brown Date: Tue, 22 Sep 2015 18:12:06 -0600 Subject: [PATCH 3/5] Use a circle-supported version --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 22fdc539d..747a45d25 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: php: - version: 5.3.29 + version: 5.3.25 test: override: From 9988908c8cc5b630cbcad75b9c3bde358afd5cce Mon Sep 17 00:00:00 2001 From: Don Brown Date: Tue, 22 Sep 2015 18:14:29 -0600 Subject: [PATCH 4/5] Back to 5.4 for CI due to guzzle deps --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 747a45d25..77aae221c 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: php: - version: 5.3.25 + version: 5.4.37 test: override: From 4ccaf62b54d5ea5b594934e5a88a3a5439e3878c Mon Sep 17 00:00:00 2001 From: Don Brown Date: Wed, 23 Sep 2015 12:35:58 -0600 Subject: [PATCH 5/5] Add APC-enabled LDD requester, integ tests --- .gitignore | 3 + integration-tests/LDDFeatureRequesterTest.php | 66 +++++++++++++++++++ integration-tests/README.txt | 7 ++ integration-tests/Vagrantfile | 51 ++++++++++++++ integration-tests/bootstrap.sh | 59 +++++++++++++++++ integration-tests/composer.json | 29 ++++++++ src/LaunchDarkly/ApcLDDFeatureRequester.php | 40 +++++++++++ src/LaunchDarkly/FeatureRequester.php | 2 +- src/LaunchDarkly/GuzzleFeatureRequester.php | 2 +- src/LaunchDarkly/LDClient.php | 12 ++-- ...eRetriever.php => LDDFeatureRequester.php} | 36 ++++++++-- 11 files changed, 292 insertions(+), 15 deletions(-) create mode 100644 integration-tests/LDDFeatureRequesterTest.php create mode 100644 integration-tests/README.txt create mode 100644 integration-tests/Vagrantfile create mode 100755 integration-tests/bootstrap.sh create mode 100644 integration-tests/composer.json create mode 100644 src/LaunchDarkly/ApcLDDFeatureRequester.php rename src/LaunchDarkly/{LDDFeatureRetriever.php => LDDFeatureRequester.php} (60%) diff --git a/.gitignore b/.gitignore index a72a9de14..20328172b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /doc/ *.iml composer.phar +.vagrant +integration-tests/vendor +integration-tests/composer.lock diff --git a/integration-tests/LDDFeatureRequesterTest.php b/integration-tests/LDDFeatureRequesterTest.php new file mode 100644 index 000000000..5afeb7a88 --- /dev/null +++ b/integration-tests/LDDFeatureRequesterTest.php @@ -0,0 +1,66 @@ + "tcp", + "host" => 'localhost', + "port" => 6379)); + $client = new LDClient("BOGUS_API_KEY", array('feature_requester_class' => '\\LaunchDarkly\\LDDFeatureRequester')); + $builder = new LDUserBuilder(3); + $user = $builder->build(); + + $redis->del("launchdarkly:features"); + $this->assertEquals("jim", $client->toggle('foo', $user, 'jim')); + $redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "bar")); + $this->assertEquals("bar", $client->toggle('foo', $user, 'jim')); + } + + public function testGetApc() { + $redis = new \Predis\Client(array( + "scheme" => "tcp", + "host" => 'localhost', + "port" => 6379)); + $client = new LDClient("BOGUS_API_KEY", array('feature_requester_class' => '\\LaunchDarkly\\ApcLDDFeatureRequester', + 'apc_expiration' => 1)); + $builder = new LDUserBuilder(3); + $user = $builder->build(); + + $redis->del("launchdarkly:features"); + $this->assertEquals("jim", $client->toggle('foo', $user, 'jim')); + $redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "bar")); + $this->assertEquals("bar", $client->toggle('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')); + + apc_delete("launchdarkly:features.foo"); + $this->assertEquals("baz", $client->toggle('foo', $user, 'jim')); + } + + private function gen_feature($key, $val) { + $data = << /dev/null + +# redis +apt-get install -y redis-server 2> /dev/null + +# ntp +apt-get install ntp -y 2> /dev/null +service ntp restart + +# install dependencies and services +apt-get install unzip -y 2> /dev/null +apt-get install -y vim curl 2> /dev/null +apt-get install git -y 2> /dev/null + +# PHP things +echo "Install PHP things" +apt-get install -y php-apc 2> /dev/null +apt-get install -y phpunit 2> /dev/null + +# phpbrew stuff for 5.4 +apt-get build-dep php5 2> /dev/null +apt-get install -y php5 php5-dev php-pear autoconf automake curl build-essential libxslt1-dev re2c libxml2 libxml2-dev php5-cli bison libbz2-dev libreadline-dev 2> /dev/null +apt-get install -y libfreetype6 libfreetype6-dev libpng12-0 libpng12-dev libjpeg-dev libjpeg8-dev libjpeg8 libgd-dev libgd3 libxpm4 libltdl7 libltdl-dev 2> /dev/null +apt-get install -y libssl-dev openssl 2> /dev/null +apt-get install -y gettext libgettextpo-dev libgettextpo0 2> /dev/null +apt-get install -y php5-cli 2> /dev/null +apt-get install -y libmcrypt-dev 2> /dev/null +apt-get install -y libreadline-dev 2> /dev/null + +# set vim tabs +cat < /home/vagrant/.vimrc +set tabstop=4 +EOF +chown vagrant.vagrant /home/vagrant/.vimrc + +su - vagrant +cd ~vagrant +pwd +curl -s -L -O https://github.com/phpbrew/phpbrew/raw/master/phpbrew +chmod +x phpbrew +sudo mv phpbrew /usr/bin/phpbrew +phpbrew init +phpbrew known --update +phpbrew update +phpbrew install 5.4.34 +default + +echo "source $HOME/.phpbrew/bashrc" >> /home/vagrant/.bashrc +source $HOME/.bashrc +phpbrew switch php-5.4.34 +phpbrew ext install apc +echo "apc.enable_cli = 1" >> ~/.phpbrew/php/php-5.4.34/etc/php.ini + + +cd /home/vagrant/project/integration-tests +curl -sS https://getcomposer.org/installer | php +php composer.phar install diff --git a/integration-tests/composer.json b/integration-tests/composer.json new file mode 100644 index 000000000..e0cd69628 --- /dev/null +++ b/integration-tests/composer.json @@ -0,0 +1,29 @@ +{ + "name": "launchdarkly/launchdarkly-php-integration-tests", + "description": "Integration tests", + "homepage": "https://github.com/launchdarkly/php-client", + "license": "Apache-2.0", + "authors": [ + { + "name": "LaunchDarkly ", + "homepage": "http://launchdarkly.com/" + } + ], + "require": { + "php": ">=5.3", + "predis/predis": "1.0.*" + }, + "require-dev": { + "phpunit/phpunit": "4.3.*" + }, + "autoload": { + "psr-4": { + "": "../src/" + } + }, + "autoload-dev": { + "psr-4": { + "": "." + } + } +} diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php new file mode 100644 index 000000000..9ad8e7f82 --- /dev/null +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -0,0 +1,40 @@ +_expiration = (int)$options['apc_expiration']; + } + } + + + protected function get_from_cache($key) { + $key = self::make_cache_key($key); + $enabled = apc_fetch($key); + if ($enabled === false) { + return null; + } + else { + return $enabled; + } + } + + protected function store_in_cache($key, $val) { + apc_add($this->make_cache_key($key), $val, $this->_expiration); + } + + private function make_cache_key($name) { + return $this->_features_key.'.'.$name; + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/FeatureRequester.php b/src/LaunchDarkly/FeatureRequester.php index e16ea3349..f330dba76 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 mixed|null The decoded JSON feature data, or null if missing + * @return array|null The decoded JSON feature data, 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 d850224e7..c6f41bce5 100644 --- a/src/LaunchDarkly/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/GuzzleFeatureRequester.php @@ -36,7 +36,7 @@ function __construct($baseUri, $apiKey, $options) { * Gets feature data from a likely cached store * * @param $key string feature key - * @return mixed The decoded JSON feature data, or null if missing + * @return array|null The decoded JSON feature data, or null if missing */ public function get($key) { try { diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 2b5dd8878..599026e39 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -17,7 +17,7 @@ class LDClient { protected $_offline; /** @var FeatureRequester */ - protected $_featureRetriever; + protected $_featureRequester; /** * Creates a new client instance that connects to LaunchDarkly. @@ -50,12 +50,12 @@ public function __construct($apiKey, $options = array()) { $this->_eventProcessor = new EventProcessor($apiKey, $options); - if (isset($options['feature_retriever_class'])) { - $featureRetrieverClass = $options['feature_retriever_class']; + if (isset($options['feature_requester_class'])) { + $featureRequesterClass = $options['feature_requester_class']; } else { - $featureRetrieverClass = '\\LaunchDarkly\\GuzzleFeatureRequester'; + $featureRequesterClass = '\\LaunchDarkly\\GuzzleFeatureRequester'; } - $this->_featureRetriever = new $featureRetrieverClass($this->_baseUri, $apiKey, $options); + $this->_featureRequester = new $featureRequesterClass($this->_baseUri, $apiKey, $options); } public function getFlag($key, $user, $default = false) { @@ -184,7 +184,7 @@ protected function _sendFlagRequestEvent($key, $user, $value) { protected function _toggle($key, $user, $default) { try { - $data = $this->_featureRetriever->get($key); + $data = $this->_featureRequester->get($key); if ($data == null) { error_log("LDClient::_toggle received null from retriever, using default"); return $default; diff --git a/src/LaunchDarkly/LDDFeatureRetriever.php b/src/LaunchDarkly/LDDFeatureRequester.php similarity index 60% rename from src/LaunchDarkly/LDDFeatureRetriever.php rename to src/LaunchDarkly/LDDFeatureRequester.php index 1085a0bcf..08cdf665b 100644 --- a/src/LaunchDarkly/LDDFeatureRetriever.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -11,7 +11,6 @@ class LDDFeatureRequester implements FeatureRequester { function __construct($baseUri, $apiKey, $options) { $this->_baseUri = $baseUri; $this->_apiKey = $apiKey; - $this->_options = $options; if (!isset($options['redis_host'])) { $options['redis_host'] = 'localhost'; } @@ -19,6 +18,8 @@ function __construct($baseUri, $apiKey, $options) { $options['redis_port'] = 6379; } + $this->_options = $options; + $prefix = "launchdarkly"; if (isset($options['redis_prefix'])) { $prefix = $options['redis_prefix']; @@ -39,16 +40,37 @@ protected function get_connection() { * Gets feature data from a likely cached store * * @param $key string feature key - * @return mixed The decoded JSON feature data, or null if missing + * @return array|null The decoded JSON feature data, or null if missing */ public function get($key) { - $redis = $this->get_connection(); - $raw = $redis->hget($this->_features_key, $key); - if ($raw) { - return json_decode($raw); + $raw = $this->get_from_cache($key); + if ($raw === null) { + $redis = $this->get_connection(); + $raw = $redis->hget($this->_features_key, $key); + if ($raw) { + $this->store_in_cache($key, $raw); + } } - else { + if ($raw) { + return json_decode($raw, True); + } else { return null; } } + + /** + * Gets the value from local cache. No-op by default. + * @param $key string The feature key + * @return null|array The feature data or null if missing + */ + protected function get_from_cache($key) { + return null; + } + + /** + * Stores the feature data into the local cache. No-op by default. + * @param $key string The feature key + * @param $val array The feature data + */ + protected function store_in_cache($key, $val) {} } \ No newline at end of file