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

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

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

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

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

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

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

+ * Note that calling json_encode() on a FeatureFlagsState object will automatically use the + * jsonSerialize() method. * @return array an associative array suitable for passing as a JSON object */ - public function toJson() + public function jsonSerialize() { $ret = array_replace([], $this->_flagValues); $ret['$flagsState'] = $this->_flagMetadata; + $ret['$valid'] = $this->_valid; return $ret; } } diff --git a/tests/FeatureFlagsStateTest.php b/tests/FeatureFlagsStateTest.php new file mode 100644 index 000000000..f61981909 --- /dev/null +++ b/tests/FeatureFlagsStateTest.php @@ -0,0 +1,111 @@ + 'key1', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 0, + 'fallthrough' => array('variation' => 0), + 'variations' => array('value1'), + 'salt' => '', + 'trackEvents' => false + ); + private static $flag2Json = array( + 'key' => 'key2', + 'version' => 200, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 0, + 'fallthrough' => array('variation' => 0), + 'variations' => array('value2'), + 'salt' => '', + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ); + + public function testCanGetFlagValue() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvalResult(0, 'value1', array())); + + $this->assertEquals('value1', $state->getFlagValue('key1')); + } + + public function testUnknownFlagReturnsNullValue() + { + $state = new FeatureFlagsState(true); + + $this->assertNull($state->getFlagValue('key1')); + } + + public function testCanConvertToValuesMap() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(0, 'value2', array())); + + $expected = array('key1' => 'value1', 'key2' => 'value2'); + $this->assertEquals($expected, $state->toValuesMap()); + } + + public function testCanConvertToJson() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + + $expected = array( + 'key1' => 'value1', + 'key2' => 'value2', + '$flagsState' => array( + 'key1' => array( + 'variation' => 0, + 'version' => 100, + 'trackEvents' => false + ), + 'key2' => array( + 'variation' => 1, + 'version' => 200, + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ) + ), + '$valid' => true + ); + $this->assertEquals($expected, $state->jsonSerialize()); + } + + public function testJsonEncodeUsesCustomSerializer() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + + $expected = $state->jsonSerialize(); + $json = json_encode($state); + $decoded = json_decode($json, true); + $this->assertEquals($expected, $decoded); + } +} diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 52d5493b2..66f24dddf 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -158,9 +158,10 @@ public function testAllFlagsStateReturnsState() 'trackEvents' => true, 'debugEventsUntilDate' => 1000 ) - ) + ), + '$valid' => true ); - $this->assertEquals($expectedState, $state->toJson()); + $this->assertEquals($expectedState, $state->jsonSerialize()); } public function testOnlyValidFeatureRequester() From 775f0a13255cca5fb9ea8f07c92f95e3851a1739 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 20 Aug 2018 21:02:53 -0700 Subject: [PATCH 07/72] indents --- tests/FeatureFlagsStateTest.php | 102 ++++++++++++++++---------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/tests/FeatureFlagsStateTest.php b/tests/FeatureFlagsStateTest.php index f61981909..9b97d0a4b 100644 --- a/tests/FeatureFlagsStateTest.php +++ b/tests/FeatureFlagsStateTest.php @@ -8,7 +8,7 @@ class FeatureFlagsStateTest extends \PHPUnit_Framework_TestCase { - private static $flag1Json = array( + private static $flag1Json = array( 'key' => 'key1', 'version' => 100, 'deleted' => false, @@ -38,74 +38,74 @@ class FeatureFlagsStateTest extends \PHPUnit_Framework_TestCase 'debugEventsUntilDate' => 1000 ); - public function testCanGetFlagValue() - { - $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag, new EvalResult(0, 'value1', array())); + public function testCanGetFlagValue() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvalResult(0, 'value1', array())); - $this->assertEquals('value1', $state->getFlagValue('key1')); - } + $this->assertEquals('value1', $state->getFlagValue('key1')); + } - public function testUnknownFlagReturnsNullValue() - { - $state = new FeatureFlagsState(true); - - $this->assertNull($state->getFlagValue('key1')); - } + public function testUnknownFlagReturnsNullValue() + { + $state = new FeatureFlagsState(true); + + $this->assertNull($state->getFlagValue('key1')); + } - public function testCanConvertToValuesMap() - { - $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(0, 'value2', array())); + public function testCanConvertToValuesMap() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(0, 'value2', array())); - $expected = array('key1' => 'value1', 'key2' => 'value2'); - $this->assertEquals($expected, $state->toValuesMap()); - } + $expected = array('key1' => 'value1', 'key2' => 'value2'); + $this->assertEquals($expected, $state->toValuesMap()); + } - public function testCanConvertToJson() - { - $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + public function testCanConvertToJson() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); $expected = array( 'key1' => 'value1', 'key2' => 'value2', '$flagsState' => array( 'key1' => array( - 'variation' => 0, - 'version' => 100, - 'trackEvents' => false + 'variation' => 0, + 'version' => 100, + 'trackEvents' => false ), 'key2' => array( - 'variation' => 1, - 'version' => 200, - 'trackEvents' => true, - 'debugEventsUntilDate' => 1000 + 'variation' => 1, + 'version' => 200, + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 ) ), '$valid' => true ); $this->assertEquals($expected, $state->jsonSerialize()); - } + } - public function testJsonEncodeUsesCustomSerializer() - { - $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); - $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); - $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + public function testJsonEncodeUsesCustomSerializer() + { + $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag1, new EvalResult(0, 'value1', array())); + $state->addFlag($flag2, new EvalResult(1, 'value2', array())); - $expected = $state->jsonSerialize(); - $json = json_encode($state); - $decoded = json_decode($json, true); - $this->assertEquals($expected, $decoded); - } + $expected = $state->jsonSerialize(); + $json = json_encode($state); + $decoded = json_decode($json, true); + $this->assertEquals($expected, $decoded); + } } From 2f80b4c2c099480b6a125365e8cdb0a53a0889fc Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 21 Aug 2018 15:48:36 -0700 Subject: [PATCH 08/72] add ability to filter for client-side flags only --- src/LaunchDarkly/FeatureFlag.php | 18 ++++++++++++++++-- src/LaunchDarkly/LDClient.php | 9 ++++++++- tests/LDClientTest.php | 26 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index f617cb672..893d855f0 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -31,6 +31,9 @@ class FeatureFlag protected $_trackEvents = false; /** @var int | null */ protected $_debugEventsUntilDate = null; + /** @var bool */ + protected $_clientSide = false; + // Note, trackEvents and debugEventsUntilDate are not used in EventProcessor, because // the PHP client doesn't do summary events. However, we need to capture them in case // they want to pass the flag data to the front end with allFlagsState(). @@ -47,7 +50,8 @@ protected function __construct($key, array $variations, $deleted, $trackEvents, - $debugEventsUntilDate) + $debugEventsUntilDate, + $clientSide) { $this->_key = $key; $this->_version = $version; @@ -62,6 +66,7 @@ protected function __construct($key, $this->_deleted = $deleted; $this->_trackEvents = $trackEvents; $this->_debugEventsUntilDate = $debugEventsUntilDate; + $this->_clientSide = $clientSide; } public static function getDecoder() @@ -80,7 +85,8 @@ public static function getDecoder() $v['variations'] ?: [], $v['deleted'], isset($v['trackEvents']) && $v['trackEvents'], - isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null + isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null, + isset($v['clientSide']) && $v['clientSide'] ); }; } @@ -252,4 +258,12 @@ public function getDebugEventsUntilDate() { return $this->_debugEventsUntilDate; } + + /** + * @return boolean + */ + public function isClientSide() + { + return $this->_clientSide; + } } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index f528d25a3..25becc552 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -282,9 +282,12 @@ public function allFlags($user) * The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. * To convert the state object into a JSON data structure, call its toJson() method. * @param $user LDUser the end user requesting the feature flags + * @param $options array optional properties affecting how the state is computed; set + * 'clientSideOnly' => true to specify that only flags marked for client-side use + * should be included (by default, all flags are included) * @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid()) */ - public function allFlagsState($user) + public function allFlagsState($user, $options = array()) { if (is_null($user) || is_null($user->getKey())) { $this->_logger->warn("allFlagsState called with null user or null/empty user key! Returning empty state"); @@ -304,7 +307,11 @@ public function allFlagsState($user) } $state = new FeatureFlagsState(true); + $clientOnly = isset($options['clientSideOnly']) && $options['clientSideOnly']; foreach ($flags as $key => $flag) { + if ($clientOnly && !$flag->isClientSide()) { + continue; + } $result = $flag->evaluate($user, $this->_featureRequester); $state->addFlag($flag, $result); } diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 66f24dddf..d5f74f1c1 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -164,6 +164,32 @@ public function testAllFlagsStateReturnsState() $this->assertEquals($expectedState, $state->jsonSerialize()); } + public function testAllFlagsStateCanFilterForClientSideFlags() + { + $flag1Json = array('key' => 'server-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('a'), 'clientSide' => false); + $flag1 = FeatureFlag::decode($flag1Json); + $flag2Json = array('key' => 'server-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('b'), 'clientSide' => false); + $flag2 = FeatureFlag::decode($flag2Json); + $flag3Json = array('key' => 'client-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('value1'), 'clientSide' => true); + $flag3 = FeatureFlag::decode($flag3Json); + $flag4Json = array('key' => 'client-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('value2'), 'clientSide' => true); + $flag4 = FeatureFlag::decode($flag4Json); + MockFeatureRequester::$flags = array( + $flag1->getKey() => $flag1, $flag2->getKey() => $flag2, $flag3->getKey() => $flag3, $flag4->getKey() => $flag4 + ); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $state = $client->allFlagsState($user, array('clientSideOnly' => true)); + + $this->assertTrue($state->isValid()); + $this->assertEquals(array('client-side-1' => 'value1', 'client-side-2' => 'value2'), $state->toValuesMap()); + } + public function testOnlyValidFeatureRequester() { $this->setExpectedException(InvalidArgumentException::class); From fd08375e6330a0b32785f21f1c62101acd9d37dc Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 21 Aug 2018 15:58:11 -0700 Subject: [PATCH 09/72] fix test to fill in all required flag fields --- tests/LDClientTest.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index d5f74f1c1..e523bd3e2 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -166,14 +166,19 @@ public function testAllFlagsStateReturnsState() public function testAllFlagsStateCanFilterForClientSideFlags() { - $flag1Json = array('key' => 'server-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('a'), 'clientSide' => false); - $flag1 = FeatureFlag::decode($flag1Json); - $flag2Json = array('key' => 'server-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('b'), 'clientSide' => false); - $flag2 = FeatureFlag::decode($flag2Json); - $flag3Json = array('key' => 'client-side-1', 'on' => false, 'offVariation' => 0, 'variations' => array('value1'), 'clientSide' => true); - $flag3 = FeatureFlag::decode($flag3Json); - $flag4Json = array('key' => 'client-side-2', 'on' => false, 'offVariation' => 0, 'variations' => array('value2'), 'clientSide' => true); - $flag4 = FeatureFlag::decode($flag4Json); + $flagJson = array('key' => 'server-side-1', 'version' => 1, 'on' => false, 'salt' => '', 'deleted' => false, + 'targets' => array(), 'rules' => array(), 'prerequisites' => array(), 'fallthrough' => array(), + 'offVariation' => 0, 'variations' => array('a'), 'clientSide' => false); + $flag1 = FeatureFlag::decode($flagJson); + $flagJson['key'] = 'server-side-2'; + $flag2 = FeatureFlag::decode($flagJson); + $flagJson['key'] = 'client-side-1'; + $flagJson['clientSide'] = true; + $flagJson['variations'] = array('value1'); + $flag3 = FeatureFlag::decode($flagJson); + $flagJson['key'] = 'client-side-2'; + $flagJson['variations'] = array('value2'); + $flag4 = FeatureFlag::decode($flagJson); MockFeatureRequester::$flags = array( $flag1->getKey() => $flag1, $flag2->getKey() => $flag2, $flag3->getKey() => $flag3, $flag4->getKey() => $flag4 ); From 2e9829c6656a2746bccf4606456f6920bdce01a5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 24 Aug 2018 16:49:28 -0700 Subject: [PATCH 10/72] implement evaluation with explanations --- src/LaunchDarkly/EvalResult.php | 25 +-- src/LaunchDarkly/EvaluationDetail.php | 68 ++++++++ src/LaunchDarkly/EvaluationReason.php | 222 +++++++++++++++++++++++++ src/LaunchDarkly/FeatureFlag.php | 165 ++++++++++-------- src/LaunchDarkly/FeatureFlagsState.php | 39 ++++- src/LaunchDarkly/LDClient.php | 105 +++++++----- src/LaunchDarkly/Rule.php | 14 +- src/LaunchDarkly/Util.php | 5 +- tests/EvaluationReasonTest.php | 57 +++++++ tests/FeatureFlagTest.php | 203 +++++++++++++++++----- tests/FeatureFlagsStateTest.php | 42 ++++- tests/LDClientTest.php | 193 +++++++++++++++++++-- 12 files changed, 937 insertions(+), 201 deletions(-) create mode 100644 src/LaunchDarkly/EvaluationDetail.php create mode 100644 src/LaunchDarkly/EvaluationReason.php create mode 100644 tests/EvaluationReasonTest.php diff --git a/src/LaunchDarkly/EvalResult.php b/src/LaunchDarkly/EvalResult.php index f692abac1..6b4b4ab21 100644 --- a/src/LaunchDarkly/EvalResult.php +++ b/src/LaunchDarkly/EvalResult.php @@ -4,37 +4,28 @@ class EvalResult { - private $_variation = null; - private $_value = null; + /** @var EvaluationDetail */ + private $_detail = null; /** @var array */ private $_prerequisiteEvents = []; /** * EvalResult constructor. - * @param null $value + * @param EvaluationDetail $detail * @param array $prerequisiteEvents */ - public function __construct($variation, $value, array $prerequisiteEvents) + public function __construct($detail, array $prerequisiteEvents) { - $this->_variation = $variation; - $this->_value = $value; + $this->_detail = $detail; $this->_prerequisiteEvents = $prerequisiteEvents; } /** - * @return int | null + * @return EvaluationDetail */ - public function getVariation() + public function getDetail() { - return $this->_variation; - } - - /** - * @return null - */ - public function getValue() - { - return $this->_value; + return $this->_detail; } /** diff --git a/src/LaunchDarkly/EvaluationDetail.php b/src/LaunchDarkly/EvaluationDetail.php new file mode 100644 index 000000000..7983ba1a1 --- /dev/null +++ b/src/LaunchDarkly/EvaluationDetail.php @@ -0,0 +1,68 @@ +_value = $value; + $this->_variationIndex = $variationIndex; + $this->_reason = $reason; + } + + /** + * Returns the value of the flag variation for the user. + * + * @return mixed + */ + public function getValue() + { + return $this->_value; + } + + /** + * Returns the index of the flag variation for the user, e.g. 0 for the first variation - + * or null if it was the default value. + * + * @return int | null + */ + public function getVariationIndex() + { + return $this->_variationIndex; + } + + /** + * Returns information about how the flag value was calculated. + * + * @return EvaluationReason + */ + public function getReason() + { + return $this->_reason; + } + + /** + * Returns true if the flag evaluated to the default value, rather than one of its variations. + * + * @return bool + */ + public function isDefaultValue() + { + return ($this->_variationIndex === null); + } +} diff --git a/src/LaunchDarkly/EvaluationReason.php b/src/LaunchDarkly/EvaluationReason.php new file mode 100644 index 000000000..92e2b3403 --- /dev/null +++ b/src/LaunchDarkly/EvaluationReason.php @@ -0,0 +1,222 @@ +_kind = $kind; + $this->_errorKind = $errorKind; + $this->_ruleIndex = $ruleIndex; + $this->_ruleId = $ruleId; + $this->_prerequisiteKey = $prerequisiteKey; + } + + /** + * Returns a constant indicating the general category of the reason, such as OFF. + * @return string + */ + public function getKind() + { + return $this->_kind; + } + + /** + * Returns a constant indicating the nature of the error, if getKind() is OFF. Otherwise + * returns null. + * @return string|null + */ + public function getErrorKind() + { + return $this->_errorKind; + } + + /** + * Returns the positional index of the rule that was matched (0 for the first), if getKind() + * is RULE_MATCH. Otherwise returns null. + * @return int|null + */ + public function getRuleIndex() + { + return $this->_ruleIndex; + } + + /** + * Returns the unique identifier of the rule that was matched, if getKind() is RULE_MATCH. + * Otherwise returns null. + * @return string|null + */ + public function getRuleId() + { + return $this->_ruleId; + } + + /** + * Returns the key of the prerequisite feature flag that failed, if getKind() is + * PREREQUISITE_FAILED. Otherwise returns null. + * @return string|null + */ + public function getPrerequisiteKey() + { + return $this->_prerequisiteKey; + } + + /** + * Returns a simple string representation of this object. + */ + public function __toString() + { + switch ($this->_kind) { + case self::RULE_MATCH: + return $this->_kind . '(' . $this->_ruleIndex . ',' . $this->_ruleId . ')'; + case self::PREREQUISITE_FAILED: + return $this->_kind . '(' . $this->_prerequisiteKey . ')'; + case self::ERROR: + return $this->_kind . '(' . $this->_errorKind . ')'; + default: + return $this->_kind; + } + } + + /** + * Returns a JSON representation of this object. This method is used automatically + * if you call json_encode(). + */ + public function jsonSerialize() + { + $ret = array('kind' => $this->_kind); + if ($this->_errorKind !== null) { + $ret['errorKind'] = $this->_errorKind; + } + if ($this->_ruleIndex !== null) { + $ret['ruleIndex'] = $this->_ruleIndex; + } + if ($this->_ruleId !== null) { + $ret['ruleId'] = $this->_ruleId; + } + if ($this->_prerequisiteKey !== null) { + $ret['prerequisiteKey'] = $this->_prerequisiteKey; + } + return $ret; + } +} diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 893d855f0..1ea5af29a 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -102,121 +102,140 @@ public function isOn() } /** - * @param $user LDUser - * @param $featureRequester FeatureRequester - * @return EvalResult|null + * @param LDUser $user + * @param FeatureRequester $featureRequester + * @param bool $includeReasonsInEvents + * @return EvalResult */ - public function evaluate($user, $featureRequester) + public function evaluate($user, $featureRequester, $includeReasonsInEvents = false) { - $prereqEvents = array(); - if (is_null($user) || is_null($user->getKey())) { - return new EvalResult(null, $prereqEvents); - } if ($this->isOn()) { - $result = $this->_evaluate($user, $featureRequester, $prereqEvents); - if ($result !== null) { - return $result; + $prereqEvents = array(); + $detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $includeReasonsInEvents); + return new EvalResult($detail, $prereqEvents); + } + return new EvalResult($this->getOffValue(EvaluationReason::off()), array()); + } + + /** + * @param LDUser $user + * @param FeatureRequester $featureRequester + * @param array $events + * @param bool $includeReasonsInEvents + * @return EvaluationDetail + */ + private function evaluateInternal($user, $featureRequester, &$events, $includeReasonsInEvents) + { + $prereqFailureReason = $this->checkPrerequisites($user, $featureRequester, $events, $includeReasonsInEvents); + if ($prereqFailureReason !== null) { + return $this->getOffValue($prereqFailureReason); + } + + // 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 $this->getVariation($target->getVariation(), EvaluationReason::targetMatch()); + } + } + } + } + // Now walk through the rules and see if any match + if ($this->_rules != null) { + foreach ($this->_rules as $i => $rule) { + if ($rule->matchesUser($user, $featureRequester)) { + return $this->getValueForVariationOrRollout($rule, $user, + EvaluationReason::ruleMatch($i, $rule->getId())); + } } } - $offVariationValue = $this->getOffVariationValue(); - return new EvalResult($this->_offVariation, $offVariationValue, $prereqEvents); + return $this->getValueForVariationOrRollout($this->_fallthrough, $user, EvaluationReason::fallthrough()); } /** - * @param $user LDUser - * @param $featureRequester FeatureRequester - * @param $events - * @return EvalResult|null + * @param LDUser $user + * @param FeatureRequester $featureRequester + * @param array $events + * @param bool $includeReasonsInEvents + * @return EvaluationReason|null */ - private function _evaluate($user, $featureRequester, &$events) + private function checkPrerequisites($user, $featureRequester, &$events, $includeReasonsInEvents) { - $prereqOk = true; if ($this->_prerequisites != null) { foreach ($this->_prerequisites as $prereq) { + $prereqOk = true; try { $prereqEvalResult = null; $prereqFeatureFlag = $featureRequester->getFeature($prereq->getKey()); if ($prereqFeatureFlag == null) { - return null; + $prereqOk = false; } elseif ($prereqFeatureFlag->isOn()) { - $prereqEvalResult = $prereqFeatureFlag->_evaluate($user, $featureRequester, $events); + $prereqEvalResult = $prereqFeatureFlag->evaluateInternal($user, $featureRequester, $events, $includeReasonsInEvents); $variation = $prereq->getVariation(); - if ($prereqEvalResult === null || $variation === null || $prereqEvalResult->getVariation() !== $variation) { + if ($prereqEvalResult->getVariationIndex() !== $variation) { $prereqOk = false; } } else { $prereqOk = false; } - array_push($events, Util::newFeatureRequestEvent($prereqFeatureFlag->getKey(), $user, - $prereqEvalResult === null ? null : $prereqEvalResult->getVariation(), - $prereqEvalResult === null ? null : $prereqEvalResult->getValue(), - null, $prereqFeatureFlag->getVersion(), $this->_key)); + if ($prereqFeatureFlag) { + array_push($events, Util::newFeatureRequestEvent($prereq->getKey(), $user, + $prereqEvalResult ? $prereqEvalResult->getVariationIndex() : null, + $prereqEvalResult ? $prereqEvalResult->getValue() : null, + null, $prereqFeatureFlag->getVersion(), $this->_key, + ($includeReasonsInEvents && $prereqEvalResult) ? $prereqEvalResult->getReason() : null + )); + } } catch (EvaluationException $e) { $prereqOk = false; } + if (!$prereqOk) { + return EvaluationReason::prerequisiteFailed($prereq->getKey()); + } } } - if ($prereqOk) { - $variation = $this->evaluateIndex($user, $featureRequester); - $value = $this->getVariation($variation); - return new EvalResult($variation, $value, $events); - } return null; } /** - * @param $user LDUser - * @return int|null + * @param int $index + * @param EvaluationReason $reason + * @return EvaluationDetail */ - private function evaluateIndex($user, $featureRequester) + private function getVariation($index, $reason) { - // 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, $featureRequester)) { - return $rule->variationIndexForUser($user, $this->_key, $this->_salt); - } - } + if ($index < 0 || $index >= count($this->_variations)) { + return new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); } - // Walk through the fallthrough and see if it matches - return $this->_fallthrough->variationIndexForUser($user, $this->_key, $this->_salt); + return new EvaluationDetail($this->_variations[$index], $index, $reason); } - private function getVariation($index) + /** + * @param EvaluationReason reason + * @return EvaluationDetail + */ + private function getOffValue($reason) { - // 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]; + if ($this->_offVariation === null) { + return new EvaluationDetail(null, null, $reason); } + return $this->getVariation($this->_offVariation, $reason); } - - public function getOffVariationValue() + + /** + * @param VariationOrRollout $r + * @param LDUser $user + * @param EvaluationReason $reason + * @return EvaluationDetail + */ + private function getValueForVariationOrRollout($r, $user, $reason) { - if ($this->_offVariation === null) { - return null; - } - if ($this->_offVariation >= count($this->_variations)) { - throw new EvaluationException("Invalid offVariation index"); + $index = $r->variationIndexForUser($user, $this->_key, $this->_salt); + if ($index === null) { + return new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); } - return $this->_variations[$this->_offVariation]; + return $this->getVariation($index, $reason); } /** diff --git a/src/LaunchDarkly/FeatureFlagsState.php b/src/LaunchDarkly/FeatureFlagsState.php index 87c84d2b4..3c11338ef 100644 --- a/src/LaunchDarkly/FeatureFlagsState.php +++ b/src/LaunchDarkly/FeatureFlagsState.php @@ -28,15 +28,18 @@ public function __construct($valid, $flagValues = array(), $flagMetadata = array /** * Used internally to build the state map. */ - public function addFlag($flag, $evalResult) + public function addFlag($flag, $detail, $withReason = false) { - $this->_flagValues[$flag->getKey()] = $evalResult->getValue(); + $this->_flagValues[$flag->getKey()] = $detail->getValue(); $meta = array(); - if (!is_null($evalResult->getVariation())) { - $meta['variation'] = $evalResult->getVariation(); + if (!is_null($detail->getVariationIndex())) { + $meta['variation'] = $detail->getVariationIndex(); } $meta['version'] = $flag->getVersion(); $meta['trackEvents'] = $flag->isTrackEvents(); + if ($withReason) { + $meta['reason'] = $detail->getReason(); + } if ($flag->getDebugEventsUntilDate()) { $meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate(); } @@ -55,7 +58,7 @@ public function isValid() /** * Returns the value of an individual feature flag at the time the state was recorded. - * @param $key string + * @param $key string the feature flag key * @return mixed the flag's value; null if the flag returned the default value, or if there was no such flag */ public function getFlagValue($key) @@ -63,6 +66,22 @@ public function getFlagValue($key) return isset($this->_flagValues[$key]) ? $this->_flagValues[$key] : null; } + /** + * Returns the evaluation reason for an individual feature flag (as returned by LDClient.variationDetail()) + * at the time the state was recorded. + * @param $key string the feature flag key + * @return EvaluationReason|null the evaluation reason; null if reasons were not recorded, or if there + * was no such flag + */ + public function getFlagReason($key) + { + if (isset($this->_flagMetadata[$key])) { + $meta = $this->_flagMetadata[$key]; + return isset($meta['reason']) ? $meta['reason'] : null; + } + return null; + } + /** * Returns an associative array of flag keys to flag values. If a flag would have evaluated to the default * value, its value will be null. @@ -88,7 +107,15 @@ public function toValuesMap() public function jsonSerialize() { $ret = array_replace([], $this->_flagValues); - $ret['$flagsState'] = $this->_flagMetadata; + $metaMap = array(); + foreach ($this->_flagMetadata as $key => $meta) { + $meta = array_replace([], $meta); + if (isset($meta['reason'])) { + $meta['reason'] = $meta['reason']->jsonSerialize(); + } + $metaMap[$key] = $meta; + } + $ret['$flagsState'] = $metaMap; $ret['$valid'] = $this->_valid; return $ret; } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 25becc552..7ff437e21 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -134,10 +134,51 @@ private function getFeatureRequester($sdkKey, array $options) */ public function variation($key, $user, $default = false) { + $detail = $this->variationDetailInternal($key, $user, $default, false); + return $detail->getValue(); + } + + /** + * Calculates the value of a feature flag, and returns an object that describes the way the + * value was determined. The "reason" property in the result will also be included in + * analytics events, if you are capturing detailed event data for this flag. + * + * @param string $key The unique key for the feature flag + * @param LDUser $user The end user requesting the flag + * @param mixed $default The default value of the flag + * + * @return EvaluationDetail An EvaluationDetail object that includes the feature flag value + * and evaluation reason. + */ + public function variationDetail($key, $user, $default = false) + { + return $this->variationDetailInternal($key, $user, $default, true); + } + + /** + * @param string $key + * @param LDUser $user + * @param mixed $default + * @param bool $includeReasonsInEvents + */ + private function variationDetailInternal($key, $user, $default, $includeReasonsInEvents) { $default = $this->_get_default($key, $default); + $errorResult = function($errorKind) use ($key, $default) { + return new EvaluationDetail($default, null, EvaluationReason::error($errorKind)); + }; + $sendEvent = function($detail, $flag) use ($key, $user, $default, $includeReasonsInEvents) { + if ($this->isOffline() || !$this->_send_events) { + return; + } + $event = Util::newFeatureRequestEvent($key, $user, $detail->getVariationIndex(), $detail->getValue(), + $default, $flag ? $flag->getVersion() : null, null, + $includeReasonsInEvents ? $detail->getReason() : null); + $this->_eventProcessor->enqueue($event); + }; + if ($this->_offline) { - return $default; + return $errorResult(EvaluationReason::CLIENT_NOT_READY_ERROR); } try { @@ -148,43 +189,44 @@ public function variation($key, $user, $default = false) $flag = $this->_featureRequester->getFeature($key); } catch (UnrecoverableHTTPStatusException $e) { $this->handleUnrecoverableError(); - return $default; + return $errorResult(EvaluationReason::EXCEPTION_ERROR); } if (is_null($flag)) { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default); - return $default; + $result = $errorResult(EvaluationReason::FLAG_NOT_FOUND_ERROR); + $sendEvent($result, null); + return $result; } if (is_null($user) || is_null($user->getKey())) { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default, $flag->getVersion()); + $result = $errorResult(EvaluationReason::USER_NOT_SPECIFIED_ERROR); + $sendEvent($result, $flag); $this->_logger->warning("Variation called with null user or null user key! Returning default value"); - return $default; + return $result; } - $evalResult = $flag->evaluate($user, $this->_featureRequester); + $evalResult = $flag->evaluate($user, $this->_featureRequester, $includeReasonsInEvents); if (!$this->isOffline() && $this->_send_events) { foreach ($evalResult->getPrerequisiteEvents() as $e) { $this->_eventProcessor->enqueue($e); } } - 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; + $detail = $evalResult->getDetail(); + if ($detail->isDefaultValue()) { + $detail = new EvaluationDetail($default, null, $detail->getReason()); } + $sendEvent($detail, $flag); + return $detail; } catch (\Exception $e) { $this->_logger->error("Caught $e"); + $result = $errorResult(EvaluationReason::EXCEPTION_ERROR); + try { + $sendEvent($result, null); + } catch (\Exception $e) { + $this->_logger->error("Caught $e"); + } + return $result; } - try { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default); - } catch (\Exception $e) { - $this->_logger->error("Caught $e"); - } - return $default; } - /** @deprecated Use variation() instead. * @param $key * @param $user @@ -284,7 +326,8 @@ public function allFlags($user) * @param $user LDUser the end user requesting the feature flags * @param $options array optional properties affecting how the state is computed; set * 'clientSideOnly' => true to specify that only flags marked for client-side use - * should be included (by default, all flags are included) + * should be included (by default, all flags are included); set 'withReasons' => true + * to include evaluation reasons (see variationDetail) * @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid()) */ public function allFlagsState($user, $options = array()) @@ -308,12 +351,13 @@ public function allFlagsState($user, $options = array()) $state = new FeatureFlagsState(true); $clientOnly = isset($options['clientSideOnly']) && $options['clientSideOnly']; + $withReasons = isset($options['withReasons']) && $options['withReasons']; foreach ($flags as $key => $flag) { if ($clientOnly && !$flag->isClientSide()) { continue; } $result = $flag->evaluate($user, $this->_featureRequester); - $state->addFlag($flag, $result); + $state->addFlag($flag, $result->getDetail(), $withReasons); } return $state; } @@ -343,23 +387,6 @@ 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, $variation, $value, $default, $version = null, $prereqOf = null) - { - if ($this->isOffline() || !$this->_send_events) { - return; - } - $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $variation, $value, $default, $version, $prereqOf)); - } - protected function _get_default($key, $default) { if (array_key_exists($key, $this->_defaults)) { diff --git a/src/LaunchDarkly/Rule.php b/src/LaunchDarkly/Rule.php index daf9c6e11..39d1a9622 100644 --- a/src/LaunchDarkly/Rule.php +++ b/src/LaunchDarkly/Rule.php @@ -4,12 +4,15 @@ class Rule extends VariationOrRollout { + /** @var string */ + private $_id = null; /** @var Clause[] */ private $_clauses = array(); - protected function __construct($variation, $rollout, array $clauses) + protected function __construct($variation, $rollout, $id, array $clauses) { parent::__construct($variation, $rollout); + $this->_id = $id; $this->_clauses = $clauses; } @@ -19,6 +22,7 @@ public static function getDecoder() return new Rule( isset($v['variation']) ? $v['variation'] : null, isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null, + isset($v['id']) ? $v['id'] : null, array_map(Clause::getDecoder(), $v['clauses'])); }; } @@ -37,6 +41,14 @@ public function matchesUser($user, $featureRequester) return true; } + /** + * @return string + */ + public function getId() + { + return $this->_id; + } + /** * @return Clause[] */ diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php index 1fbf79010..e9afd00af 100644 --- a/src/LaunchDarkly/Util.php +++ b/src/LaunchDarkly/Util.php @@ -37,7 +37,7 @@ public static function currentTimeUnixMillis() * @param null $prereqOf string | null * @return array */ - public static function newFeatureRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null) + public static function newFeatureRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null, $reason = null) { $event = array(); $event['user'] = $user; @@ -49,6 +49,9 @@ public static function newFeatureRequestEvent($key, $user, $variation, $value, $ $event['default'] = $default; $event['version'] = $version; $event['prereqOf'] = $prereqOf; + if ($reason !== null) { + $event['reason'] = $reason->jsonSerialize(); + } return $event; } diff --git a/tests/EvaluationReasonTest.php b/tests/EvaluationReasonTest.php new file mode 100644 index 000000000..759dc3ece --- /dev/null +++ b/tests/EvaluationReasonTest.php @@ -0,0 +1,57 @@ +assertEquals(array('kind' => 'OFF'), json_decode($json, true)); + $this->assertEquals('OFF', (string)$reason); + } + + public function testFallthroughReasonSerialization() + { + $reason = EvaluationReason::fallthrough(); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'FALLTHROUGH'), json_decode($json, true)); + $this->assertEquals('FALLTHROUGH', (string)$reason); + } + + public function testTargetMatchReasonSerialization() + { + $reason = EvaluationReason::targetMatch(); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'TARGET_MATCH'), json_decode($json, true)); + $this->assertEquals('TARGET_MATCH', (string)$reason); + } + + public function testRuleMatchReasonSerialization() + { + $reason = EvaluationReason::ruleMatch(0, 'id'); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'), + json_decode($json, true)); + $this->assertEquals('RULE_MATCH(0,id)', (string)$reason); + } + + public function testPrerequisiteFailedReasonSerialization() + { + $reason = EvaluationReason::prerequisiteFailed('key'); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'PREREQUISITE_FAILED', 'prerequisiteKey' => 'key'), + json_decode($json, true)); + $this->assertEquals('PREREQUISITE_FAILED(key)', (string)$reason); + } + + public function testErrorReasonSerialization() + { + $reason = EvaluationReason::error(EvaluationReason::EXCEPTION_ERROR); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'ERROR', 'errorKind' => 'EXCEPTION'), json_decode($json, true)); + $this->assertEquals('ERROR(EXCEPTION)', (string)$reason); + } +} diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index e73531b76..2ea5f9e03 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -1,7 +1,10 @@ '' ); $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()); + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail('off', 1, EvaluationReason::off()); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } @@ -232,12 +233,56 @@ public function testFlagReturnsNullIfFlagIsOffAndOffVariationIsUnspecified() '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()); + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail(null, null, EvaluationReason::off()); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfOffVariationIsTooHigh() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 999, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfOffVariationIsNegative() + { + $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); + + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } @@ -264,8 +309,8 @@ public function testFlagReturnsOffVariationIfPrerequisiteIsNotFound() $requester = new MockFeatureRequesterForFeature(); $result = $flag->evaluate($user, $requester); - self::assertEquals(1, $result->getVariation()); - self::assertEquals('off', $result->getValue()); + $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } @@ -308,8 +353,8 @@ public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() $requester->val = $flag1; $result = $flag0->evaluate($user, $requester); - self::assertEquals(1, $result->getVariation()); - self::assertEquals('off', $result->getValue()); + $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); + self::assertEquals($detail, $result->getDetail()); $events = $result->getPrerequisiteEvents(); self::assertEquals(1, count($events)); @@ -360,8 +405,8 @@ public function testFlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAn $requester->val = $flag1; $result = $flag0->evaluate($user, $requester); - self::assertEquals(0, $result->getVariation()); - self::assertEquals('fall', $result->getValue()); + $detail = new EvaluationDetail('fall', 0, EvaluationReason::fallthrough()); + self::assertEquals($detail, $result->getDetail()); $events = $result->getPrerequisiteEvents(); self::assertEquals(1, count($events)); @@ -395,12 +440,12 @@ public function testFlagMatchesUserFromTargets() $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(2, $result->getVariation()); - self::assertEquals('on', $result->getValue()); + $detail = new EvaluationDetail('on', 2, EvaluationReason::targetMatch()); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } - public function testFlagMatchesUserFromRules() + private function makeBooleanFlagWithRules(array $rules) { $flagJson = array( 'key' => 'feature', @@ -409,31 +454,111 @@ public function testFlagMatchesUserFromRules() '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, + 'rules' => $rules, + 'offVariation' => 0, 'fallthrough' => array('variation' => 0), - 'variations' => array('fall', 'off', 'on'), + 'variations' => array(false, true), 'salt' => '' ); - $flag = FeatureFlag::decode($flagJson); + return FeatureFlag::decode($flagJson); + } + + public function testFlagMatchesUserFromRules() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'variation' => 1 + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, 'ruleid')); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleVariationIsTooHigh() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'variation' => 999 + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleVariationIsNegative() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'variation' => -1 + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleHasNoVariationOrRollout() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ) + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleHasRolloutWithNoVariations() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'rollout' => array('variations' => array()) + ) + )); $ub = new LDUserBuilder('userkey'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(2, $result->getVariation()); - self::assertEquals('on', $result->getValue()); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } @@ -517,7 +642,7 @@ public function testSegmentMatchClauseRetrievesSegmentFromStore() $result = $feature->evaluate($user, $requester); - self::assertTrue($result->getValue()); + self::assertTrue($result->getDetail()->getValue()); } public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound() @@ -531,7 +656,7 @@ public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound( $result = $feature->evaluate($user, $requester); - self::assertFalse($result->getValue()); + self::assertFalse($result->getDetail()->getValue()); } private function booleanFlagWithClauses($clauses) diff --git a/tests/FeatureFlagsStateTest.php b/tests/FeatureFlagsStateTest.php index 9b97d0a4b..e584f1e13 100644 --- a/tests/FeatureFlagsStateTest.php +++ b/tests/FeatureFlagsStateTest.php @@ -2,7 +2,8 @@ namespace LaunchDarkly\Tests; use InvalidArgumentException; -use LaunchDarkly\EvalResult; +use LaunchDarkly\EvaluationDetail; +use LaunchDarkly\EvaluationReason; use LaunchDarkly\FeatureFlag; use LaunchDarkly\FeatureFlagsState; @@ -42,7 +43,7 @@ public function testCanGetFlagValue() { $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag, new EvalResult(0, 'value1', array())); + $state->addFlag($flag, new EvaluationDetail('value1', 0)); $this->assertEquals('value1', $state->getFlagValue('key1')); } @@ -54,13 +55,38 @@ public function testUnknownFlagReturnsNullValue() $this->assertNull($state->getFlagValue('key1')); } + public function testCanGetFlagReason() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvaluationDetail('value1', 0, EvaluationReason::off()), true); + + $this->assertEquals(EvaluationReason::off(), $state->getFlagReason('key1')); + } + + public function testUnknownFlagReturnsNullReason() + { + $state = new FeatureFlagsState(true); + + $this->assertNull($state->getFlagReason('key1')); + } + + public function testReasonIsNullIfReasonsWereNotRecorded() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvaluationDetail('value1', 0, EvaluationReason::off()), false); + + $this->assertNull($state->getFlagReason('key1')); + } + public function testCanConvertToValuesMap() { $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(0, 'value2', array())); + $state->addFlag($flag1, new EvaluationDetail('value1', 0)); + $state->addFlag($flag2, new EvaluationDetail('value2', 0)); $expected = array('key1' => 'value1', 'key2' => 'value2'); $this->assertEquals($expected, $state->toValuesMap()); @@ -71,8 +97,8 @@ public function testCanConvertToJson() $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + $state->addFlag($flag1, new EvaluationDetail('value1', 0)); + $state->addFlag($flag2, new EvaluationDetail('value2', 1)); $expected = array( 'key1' => 'value1', @@ -100,8 +126,8 @@ public function testJsonEncodeUsesCustomSerializer() $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + $state->addFlag($flag1, new EvaluationDetail('value1', 0)); + $state->addFlag($flag2, new EvaluationDetail('value2', 1)); $expected = $state->jsonSerialize(); $json = json_encode($state); diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index e523bd3e2..73aa68d74 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -2,6 +2,7 @@ namespace LaunchDarkly\Tests; use InvalidArgumentException; +use LaunchDarkly\EvaluationReason; use LaunchDarkly\FeatureFlag; use LaunchDarkly\FeatureRequester; use LaunchDarkly\LDClient; @@ -16,10 +17,10 @@ public function testDefaultCtor() $this->assertInstanceOf(LDClient::class, new LDClient("BOGUS_SDK_KEY")); } - public function testVariationReturnsFlagValue() + private function makeOffFlagWithValue($key, $value) { $flagJson = array( - 'key' => 'feature', + 'key' => $key, 'version' => 100, 'deleted' => false, 'on' => false, @@ -28,21 +29,39 @@ public function testVariationReturnsFlagValue() 'rules' => array(), 'offVariation' => 1, 'fallthrough' => array('variation' => 0), - 'variations' => array('fall', 'off', 'on'), + 'variations' => array('FALLTHROUGH', $value), 'salt' => '' ); - $flag = FeatureFlag::decode($flagJson); + return FeatureFlag::decode($flagJson); + } + public function testVariationReturnsFlagValue() + { + $flag = $this->makeOffFlagWithValue('feature', 'value'); MockFeatureRequester::$flags = array('feature' => $flag); $client = new LDClient("someKey", array( 'feature_requester_class' => MockFeatureRequester::class, 'events' => false )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $value = $client->variation('feature', $user, 'default'); - $this->assertEquals('off', $value); + $value = $client->variation('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('value', $value); + } + + public function testVariationDetailReturnsFlagValue() + { + $flag = $this->makeOffFlagWithValue('feature', 'value'); + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $detail = $client->variationDetail('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('value', $detail->getValue()); + $this->assertFalse($detail->isDefaultValue()); + $this->assertEquals(1, $detail->getVariationIndex()); + $this->assertEquals(EvaluationReason::off(), $detail->getReason()); } public function testVariationReturnsDefaultForUnknownFlag() @@ -53,9 +72,22 @@ public function testVariationReturnsDefaultForUnknownFlag() 'events' => false )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $this->assertEquals('argdef', $client->variation('foo', $user, 'argdef')); + $this->assertEquals('argdef', $client->variation('foo', new LDUser('userkey'), 'argdef')); + } + + public function testVariationDetailReturnsDefaultForUnknownFlag() + { + MockFeatureRequester::$flags = array(); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $detail = $client->variationDetail('foo', new LDUser('userkey'), 'default'); + $this->assertEquals('default', $detail->getValue()); + $this->assertTrue($detail->isDefaultValue()); + $this->assertNull($detail->getVariationIndex()); + $this->assertEquals(EvaluationReason::error(EvaluationReason::FLAG_NOT_FOUND_ERROR), $detail->getReason()); } public function testVariationReturnsDefaultFromConfigurationForUnknownFlag() @@ -67,12 +99,60 @@ public function testVariationReturnsDefaultFromConfigurationForUnknownFlag() 'defaults' => array('foo' => 'fromarray') )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $this->assertEquals('fromarray', $client->variation('foo', $user, 'argdef')); + $this->assertEquals('fromarray', $client->variation('foo', new LDUser('userkey'), 'argdef')); } public function testVariationSendsEvent() + { + $flag = $this->makeOffFlagWithValue('flagkey', 'flagvalue'); + MockFeatureRequester::$flags = array('flagkey' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => true + )); + + $user = new LDUser('userkey'); + $client->variation('flagkey', new LDUser('userkey'), 'default'); + $proc = $this->getPrivateField($client, '_eventProcessor'); + $queue = $this->getPrivateField($proc, '_queue'); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('flagkey', $event['key']); + $this->assertEquals($flag->getVersion(), $event['version']); + $this->assertEquals('flagvalue', $event['value']); + $this->assertEquals(1, $event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertFalse(isset($event['reason'])); + } + + public function testVariationDetailSendsEvent() + { + $flag = $this->makeOffFlagWithValue('FUCKINGWEIRDflagkey', 'flagvalue'); + MockFeatureRequester::$flags = array('FUCKINGWEIRDflagkey' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => true + )); + + $user = new LDUser('userkey'); + $client->variationDetail('FUCKINGWEIRDflagkey', $user, 'default'); + $proc = $this->getPrivateField($client, '_eventProcessor'); + $queue = $this->getPrivateField($proc, '_queue'); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('FUCKINGWEIRDflagkey', $event['key']); + $this->assertEquals($flag->getVersion(), $event['version']); + $this->assertEquals('flagvalue', $event['value']); + $this->assertEquals(1, $event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertEquals(array('kind' => 'OFF'), $event['reason']); + } + + public function testVariationSendsEventForUnknownFlag() { MockFeatureRequester::$flags = array(); $client = new LDClient("someKey", array( @@ -80,12 +160,44 @@ public function testVariationSendsEvent() 'events' => true )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $client->variation('foo', $user, 'argdef'); + $user = new LDUser('userkey'); + $client->variation('flagkey', new LDUser('userkey'), 'default'); $proc = $this->getPrivateField($client, '_eventProcessor'); $queue = $this->getPrivateField($proc, '_queue'); $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('flagkey', $event['key']); + $this->assertNull($event['version']); + $this->assertEquals('default', $event['value']); + $this->assertNull($event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertFalse(isset($event['reason'])); + } + + public function testVariationDetailSendsEventForUnknownFlag() + { + MockFeatureRequester::$flags = array(); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => true + )); + + $user = new LDUser('userkey'); + $client->variationDetail('flagkey', new LDUser('userkey'), 'default'); + $proc = $this->getPrivateField($client, '_eventProcessor'); + $queue = $this->getPrivateField($proc, '_queue'); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('flagkey', $event['key']); + $this->assertNull($event['version']); + $this->assertEquals('default', $event['value']); + $this->assertNull($event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertEquals(array('kind' => 'ERROR', 'errorKind' => 'FLAG_NOT_FOUND'), $event['reason']); } public function testAllFlagsReturnsFlagValues() @@ -164,6 +276,53 @@ public function testAllFlagsStateReturnsState() $this->assertEquals($expectedState, $state->jsonSerialize()); } + public function testAllFlagsStateReturnsStateWithReasons() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '', + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ); + $flag = FeatureFlag::decode($flagJson); + + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $state = $client->allFlagsState($user, array('withReasons' => true)); + + $this->assertTrue($state->isValid()); + $this->assertEquals(array('feature' => 'off'), $state->toValuesMap()); + $expectedState = array( + 'feature' => 'off', + '$flagsState' => array( + 'feature' => array( + 'variation' => 1, + 'version' => 100, + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000, + 'reason' => array('kind' => 'OFF') + ) + ), + '$valid' => true + ); + $this->assertEquals($expectedState, $state->jsonSerialize()); + } + public function testAllFlagsStateCanFilterForClientSideFlags() { $flagJson = array('key' => 'server-side-1', 'version' => 1, 'on' => false, 'salt' => '', 'deleted' => false, From 5d8e2b065066b9c6166f9a9f7324238170a25d78 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 24 Aug 2018 16:53:32 -0700 Subject: [PATCH 11/72] add another evaluation test --- tests/LDClientTest.php | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 73aa68d74..dffc3afe2 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -35,6 +35,24 @@ private function makeOffFlagWithValue($key, $value) return FeatureFlag::decode($flagJson); } + private function makeFlagThatEvaluatesToNull($key) + { + $flagJson = array( + 'key' => $key, + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => null, + 'fallthrough' => array('variation' => 0), + 'variations' => array('FALLTHROUGH', $value), + 'salt' => '' + ); + return FeatureFlag::decode($flagJson); + } + public function testVariationReturnsFlagValue() { $flag = $this->makeOffFlagWithValue('feature', 'value'); @@ -64,6 +82,35 @@ public function testVariationDetailReturnsFlagValue() $this->assertEquals(EvaluationReason::off(), $detail->getReason()); } + public function testVariationReturnsDefaultIfFlagEvaluatesToNull() + { + $flag = $this->makeFlagThatEvaluatesToNull('feature'); + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $value = $client->variation('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('default', $value); + } + + public function testVariationDetailReturnsDefaultIfFlagEvaluatesToNull() + { + $flag = $this->makeFlagThatEvaluatesToNull('feature'); + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $detail = $client->variationDetail('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('default', $detail->getValue()); + $this->assertTrue($detail->isDefaultValue()); + $this->assertNull($detail->getVariationIndex()); + $this->assertEquals(EvaluationReason::off(), $detail->getReason()); + } + public function testVariationReturnsDefaultForUnknownFlag() { MockFeatureRequester::$flags = array(); From 19770970ca0e79c810b21ba313f6f221f3ab9f46 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 24 Aug 2018 16:58:27 -0700 Subject: [PATCH 12/72] linter --- src/LaunchDarkly/EvaluationDetail.php | 8 ++++---- src/LaunchDarkly/LDClient.php | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/LaunchDarkly/EvaluationDetail.php b/src/LaunchDarkly/EvaluationDetail.php index 7983ba1a1..b2ffb970b 100644 --- a/src/LaunchDarkly/EvaluationDetail.php +++ b/src/LaunchDarkly/EvaluationDetail.php @@ -27,7 +27,7 @@ public function __construct($value, $variationIndex, $reason = null) /** * Returns the value of the flag variation for the user. - * + * * @return mixed */ public function getValue() @@ -38,7 +38,7 @@ public function getValue() /** * Returns the index of the flag variation for the user, e.g. 0 for the first variation - * or null if it was the default value. - * + * * @return int | null */ public function getVariationIndex() @@ -48,7 +48,7 @@ public function getVariationIndex() /** * Returns information about how the flag value was calculated. - * + * * @return EvaluationReason */ public function getReason() @@ -58,7 +58,7 @@ public function getReason() /** * Returns true if the flag evaluated to the default value, rather than one of its variations. - * + * * @return bool */ public function isDefaultValue() diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 7ff437e21..0c19e17bf 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -142,7 +142,7 @@ public function variation($key, $user, $default = false) * Calculates the value of a feature flag, and returns an object that describes the way the * value was determined. The "reason" property in the result will also be included in * analytics events, if you are capturing detailed event data for this flag. - * + * * @param string $key The unique key for the feature flag * @param LDUser $user The end user requesting the flag * @param mixed $default The default value of the flag @@ -161,13 +161,14 @@ public function variationDetail($key, $user, $default = false) * @param mixed $default * @param bool $includeReasonsInEvents */ - private function variationDetailInternal($key, $user, $default, $includeReasonsInEvents) { + private function variationDetailInternal($key, $user, $default, $includeReasonsInEvents) + { $default = $this->_get_default($key, $default); - $errorResult = function($errorKind) use ($key, $default) { + $errorResult = function ($errorKind) use ($key, $default) { return new EvaluationDetail($default, null, EvaluationReason::error($errorKind)); }; - $sendEvent = function($detail, $flag) use ($key, $user, $default, $includeReasonsInEvents) { + $sendEvent = function ($detail, $flag) use ($key, $user, $default, $includeReasonsInEvents) { if ($this->isOffline() || !$this->_send_events) { return; } From 54759db95c42eafb92e99b63d46e4ebfbaaea79c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 24 Aug 2018 17:03:13 -0700 Subject: [PATCH 13/72] fix test method --- tests/LDClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index dffc3afe2..6893a3bd4 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -47,7 +47,7 @@ private function makeFlagThatEvaluatesToNull($key) 'rules' => array(), 'offVariation' => null, 'fallthrough' => array('variation' => 0), - 'variations' => array('FALLTHROUGH', $value), + 'variations' => array('none'), 'salt' => '' ); return FeatureFlag::decode($flagJson); From 39d5105a262553202349a33e7b77ad598065e929 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 29 Aug 2018 11:46:19 -0700 Subject: [PATCH 14/72] fix for ch22995 - include prereq value in event even if prereq is off --- src/LaunchDarkly/FeatureFlag.php | 24 ++++++--------- tests/FeatureFlagTest.php | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 1ea5af29a..46123f8a4 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -109,12 +109,9 @@ public function isOn() */ public function evaluate($user, $featureRequester, $includeReasonsInEvents = false) { - if ($this->isOn()) { - $prereqEvents = array(); - $detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $includeReasonsInEvents); - return new EvalResult($detail, $prereqEvents); - } - return new EvalResult($this->getOffValue(EvaluationReason::off()), array()); + $prereqEvents = array(); + $detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $includeReasonsInEvents); + return new EvalResult($detail, $prereqEvents); } /** @@ -126,6 +123,10 @@ public function evaluate($user, $featureRequester, $includeReasonsInEvents = fal */ private function evaluateInternal($user, $featureRequester, &$events, $includeReasonsInEvents) { + if (!$this->isOn()) { + return $this->getOffValue(EvaluationReason::off()); + } + $prereqFailureReason = $this->checkPrerequisites($user, $featureRequester, $events, $includeReasonsInEvents); if ($prereqFailureReason !== null) { return $this->getOffValue($prereqFailureReason); @@ -170,19 +171,14 @@ private function checkPrerequisites($user, $featureRequester, &$events, $include $prereqFeatureFlag = $featureRequester->getFeature($prereq->getKey()); if ($prereqFeatureFlag == null) { $prereqOk = false; - } elseif ($prereqFeatureFlag->isOn()) { + } else { $prereqEvalResult = $prereqFeatureFlag->evaluateInternal($user, $featureRequester, $events, $includeReasonsInEvents); $variation = $prereq->getVariation(); - if ($prereqEvalResult->getVariationIndex() !== $variation) { + if (!$prereqFeatureFlag->isOn() || $prereqEvalResult->getVariationIndex() !== $variation) { $prereqOk = false; } - } else { - $prereqOk = false; - } - if ($prereqFeatureFlag) { array_push($events, Util::newFeatureRequestEvent($prereq->getKey(), $user, - $prereqEvalResult ? $prereqEvalResult->getVariationIndex() : null, - $prereqEvalResult ? $prereqEvalResult->getValue() : null, + $prereqEvalResult->getVariationIndex(), $prereqEvalResult->getValue(), null, $prereqFeatureFlag->getVersion(), $this->_key, ($includeReasonsInEvents && $prereqEvalResult) ? $prereqEvalResult->getReason() : null )); diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 2ea5f9e03..f0f09f02e 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -314,6 +314,59 @@ public function testFlagReturnsOffVariationIfPrerequisiteIsNotFound() self::assertEquals(array(), $result->getPrerequisiteEvents()); } + public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsOff() + { + $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' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + // note that even though it returns the desired variation, it is still off and therefore not a match + '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); + $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); + self::assertEquals($detail, $result->getDetail()); + + $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 testFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() { $flag0Json = array( From 180679f05ae8c112d6e0b479beaa9539131d5d6d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Sep 2018 16:41:12 -0700 Subject: [PATCH 15/72] cache flag data in allFlags --- src/LaunchDarkly/LDClient.php | 5 +- .../PreloadedFeatureRequester.php | 59 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/LaunchDarkly/PreloadedFeatureRequester.php diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 2df377edd..2c5743df4 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -350,6 +350,9 @@ public function allFlagsState($user, $options = array()) return new FeatureFlagsState(false); } + $preloadedRequester = new PreloadedFeatureRequester($this->_featureRequester, $flags); + // This saves us from doing repeated queries for prerequisite flags during evaluation + $state = new FeatureFlagsState(true); $clientOnly = isset($options['clientSideOnly']) && $options['clientSideOnly']; $withReasons = isset($options['withReasons']) && $options['withReasons']; @@ -357,7 +360,7 @@ public function allFlagsState($user, $options = array()) if ($clientOnly && !$flag->isClientSide()) { continue; } - $result = $flag->evaluate($user, $this->_featureRequester); + $result = $flag->evaluate($user, $preloadedRequester); $state->addFlag($flag, $result->getDetail(), $withReasons); } return $state; diff --git a/src/LaunchDarkly/PreloadedFeatureRequester.php b/src/LaunchDarkly/PreloadedFeatureRequester.php new file mode 100644 index 000000000..9e739ef14 --- /dev/null +++ b/src/LaunchDarkly/PreloadedFeatureRequester.php @@ -0,0 +1,59 @@ +_baseRequester = $baseRequester; + $this->_knownFeatures = $knownFeatures; + } + + /** + * Gets feature data from cached values + * + * @param $key string feature key + * @return FeatureFlag|null The decoded FeatureFlag, or null if missing + */ + public function getFeature($key) + { + if (isset($this->_knownFeatures[$key])) { + return $this->_knownFeatures[$key]; + } + return null; + } + + /** + * Gets segment data from the regular feature requester + * + * @param $key string segment key + * @return Segment|null The decoded Segment, or null if missing + */ + public function getSegment($key) + { + return $this->_baseRequester->getSegment($key); + } + + /** + * Gets all features from cached values + * + * @return array()|null The decoded FeatureFlags, or null if missing + */ + public function getAllFeatures() + { + return $this->_knownFeatures; + } +} From 15720d0694a6bf19b9f5264c5070590b4b81f26c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 24 Sep 2018 16:51:05 -0700 Subject: [PATCH 16/72] rm unused imports --- src/LaunchDarkly/PreloadedFeatureRequester.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/LaunchDarkly/PreloadedFeatureRequester.php b/src/LaunchDarkly/PreloadedFeatureRequester.php index 9e739ef14..ba307f6c0 100644 --- a/src/LaunchDarkly/PreloadedFeatureRequester.php +++ b/src/LaunchDarkly/PreloadedFeatureRequester.php @@ -1,13 +1,6 @@ Date: Thu, 4 Oct 2018 19:36:52 -0700 Subject: [PATCH 17/72] add option to reduce front-end metadata for untracked flags --- src/LaunchDarkly/FeatureFlagsState.php | 14 +++-- src/LaunchDarkly/LDClient.php | 3 +- tests/FeatureFlagsStateTest.php | 3 +- tests/LDClientTest.php | 87 ++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/LaunchDarkly/FeatureFlagsState.php b/src/LaunchDarkly/FeatureFlagsState.php index 3c11338ef..5c191e4df 100644 --- a/src/LaunchDarkly/FeatureFlagsState.php +++ b/src/LaunchDarkly/FeatureFlagsState.php @@ -28,17 +28,21 @@ public function __construct($valid, $flagValues = array(), $flagMetadata = array /** * Used internally to build the state map. */ - public function addFlag($flag, $detail, $withReason = false) + public function addFlag($flag, $detail, $withReason = false, $detailsOnlyIfTracked = false) { $this->_flagValues[$flag->getKey()] = $detail->getValue(); $meta = array(); + if (!$detailsOnlyIfTracked || $flag->isTrackEvents() || $flag->getDebugEventsUntilDate()) { + $meta['version'] = $flag->getVersion(); + if ($withReason) { + $meta['reason'] = $detail->getReason(); + } + } if (!is_null($detail->getVariationIndex())) { $meta['variation'] = $detail->getVariationIndex(); } - $meta['version'] = $flag->getVersion(); - $meta['trackEvents'] = $flag->isTrackEvents(); - if ($withReason) { - $meta['reason'] = $detail->getReason(); + if ($flag->isTrackEvents()) { + $meta['trackEvents'] = true; } if ($flag->getDebugEventsUntilDate()) { $meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate(); diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 2e23c55b0..134496c67 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -356,12 +356,13 @@ public function allFlagsState($user, $options = array()) $state = new FeatureFlagsState(true); $clientOnly = isset($options['clientSideOnly']) && $options['clientSideOnly']; $withReasons = isset($options['withReasons']) && $options['withReasons']; + $detailsOnlyIfTracked = isset($options['detailsOnlyForTrackedFlags']) && $options['detailsOnlyForTrackedFlags']; foreach ($flags as $key => $flag) { if ($clientOnly && !$flag->isClientSide()) { continue; } $result = $flag->evaluate($user, $preloadedRequester); - $state->addFlag($flag, $result->getDetail(), $withReasons); + $state->addFlag($flag, $result->getDetail(), $withReasons, $detailsOnlyIfTracked); } return $state; } diff --git a/tests/FeatureFlagsStateTest.php b/tests/FeatureFlagsStateTest.php index e584f1e13..e25785e71 100644 --- a/tests/FeatureFlagsStateTest.php +++ b/tests/FeatureFlagsStateTest.php @@ -106,8 +106,7 @@ public function testCanConvertToJson() '$flagsState' => array( 'key1' => array( 'variation' => 0, - 'version' => 100, - 'trackEvents' => false + 'version' => 100 ), 'key2' => array( 'variation' => 1, diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 6893a3bd4..26b1900ea 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -401,6 +401,93 @@ public function testAllFlagsStateCanFilterForClientSideFlags() $this->assertEquals(array('client-side-1' => 'value1', 'client-side-2' => 'value2'), $state->toValuesMap()); } + public function testAllFlagsStateCanOmitDetailsForUntrackedFlags() + { + $flag1Json = array( + 'key' => 'flag1', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 0, + 'fallthrough' => null, + 'variations' => array('value1'), + 'salt' => '', + 'trackEvents' => false + ); + $flag2Json = array( + 'key' => 'flag2', + 'version' => 200, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 0, + 'fallthrough' => null, + 'variations' => array('value2'), + 'salt' => '', + 'trackEvents' => true + ); + $flag3Json = array( + 'key' => 'flag3', + 'version' => 300, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 0, + 'fallthrough' => null, + 'variations' => array('value3'), + 'salt' => '', + 'trackEvents' => false, + 'debugEventsUntilDate' => 1000 + ); + $flag1 = FeatureFlag::decode($flag1Json); + $flag2 = FeatureFlag::decode($flag2Json); + $flag3 = FeatureFlag::decode($flag3Json); + + MockFeatureRequester::$flags = array('flag1' => $flag1, 'flag2' => $flag2, 'flag3' => $flag3); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $state = $client->allFlagsState($user, array('withReasons' => true, 'detailsOnlyForTrackedFlags' => true)); + + $this->assertTrue($state->isValid()); + $this->assertEquals(array('flag1' => 'value1', 'flag2' => 'value2', 'flag3' => 'value3'), $state->toValuesMap()); + $expectedState = array( + 'flag1' => 'value1', + 'flag2' => 'value2', + 'flag3' => 'value3', + '$flagsState' => array( + 'flag1' => array( + 'variation' => 0, + ), + 'flag2' => array( + 'variation' => 0, + 'version' => 200, + 'reason' => array('kind' => 'OFF'), + 'trackEvents' => true + ), + 'flag3' => array( + 'variation' => 0, + 'version' => 300, + 'reason' => array('kind' => 'OFF'), + 'debugEventsUntilDate' => 1000 + ) + ), + '$valid' => true + ); + $this->assertEquals($expectedState, $state->jsonSerialize()); + } + public function testOnlyValidFeatureRequester() { $this->setExpectedException(InvalidArgumentException::class); From 37e7eaffe31d8eaffeaceeca47f0d35a99bffeaa Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 9 Jan 2019 23:33:13 -0800 Subject: [PATCH 18/72] add ability to load flags from a file --- README.md | 7 +- src/LaunchDarkly/FileDataFeatureRequester.php | 137 ++++++++++++++++++ tests/FileDataFeatureRequesterTest.php | 38 +++++ tests/filedata/all-properties.json | 21 +++ tests/filedata/flag-only.json | 12 ++ tests/filedata/segment-only.json | 8 + 6 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/LaunchDarkly/FileDataFeatureRequester.php create mode 100644 tests/FileDataFeatureRequesterTest.php create mode 100644 tests/filedata/all-properties.json create mode 100644 tests/filedata/flag-only.json create mode 100644 tests/filedata/segment-only.json diff --git a/README.md b/README.md index 8871b609e..d301d29f1 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,11 @@ a Redis cache operating in your production environment. The ld-relay offers many 'redis_port' => 6379 ]); +Using flag data from a file +--------------------------- + +For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`FileDataFeatureRequester`](https://github.com/launchdarkly/node-client/blob/master/FileDataFeatureRequester.php) for more details. + Testing ------- @@ -119,9 +124,9 @@ About LaunchDarkly * [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK") * [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK") * [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK") - * [Python Twisted](http://docs.launchdarkly.com/docs/python-twisted-sdk-reference "LaunchDarkly Python Twisted SDK") * [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK") * [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK") + * [Electron](http://docs.launchdarkly.com/docs/electron-sdk-reference "LaunchDarkly Electron SDK") * [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK") * [Ruby](http://docs.launchdarkly.com/docs/ruby-sdk-reference "LaunchDarkly Ruby SDK") * [iOS](http://docs.launchdarkly.com/docs/ios-sdk-reference "LaunchDarkly iOS SDK") diff --git a/src/LaunchDarkly/FileDataFeatureRequester.php b/src/LaunchDarkly/FileDataFeatureRequester.php new file mode 100644 index 000000000..1455a2915 --- /dev/null +++ b/src/LaunchDarkly/FileDataFeatureRequester.php @@ -0,0 +1,137 @@ + + * To use this component, create an instance of this class, passing the path(s) of your data + * file(s). Then place the resulting object in your LaunchDarkly client configuration with the + * key "feature_requester". + *

+ *     $file_data = new FileDataFeatureRequester("./testData/flags.json");
+ *     $config = array("feature_requester" => $file_data, "send_events" => false);
+ *     $client = new LDClient("sdk_key", $config);
+ * 
+ *

+ * This will cause the client not to connect to LaunchDarkly to get feature flags. (Note + * that in this example, send_events is also set to false so that it will not + * connect to LaunchDarkly to send analytics events either.) + *

+ */ +class FileDataFeatureRequester implements FeatureRequester +{ + /** @var array */ + private $_filePaths; + /** @var array */ + private $_flags; + /** @var array */ + private $_segments; + /** @var LoggerInterface */ + private $_logger; + + public function __construct($filePaths, $options = array()) + { + $this->_filePaths = is_array($filePaths) ? $filePaths : array($filePaths); + $this->_options = $options; + $this->_flags = array(); + $this->_segments = array(); + $this->_logger = isset($options['logger']) ? $options['logger'] : null; + $this->readAllData(); + } + + /** + * Gets an individual feature flag + * + * @param $key string feature key + * @return FeatureFlag|null The decoded FeatureFlag, or null if missing + */ + public function getFeature($key) + { + return isset($this->_flags[$key]) ? $this->_flags[$key] : null; + } + + /** + * Gets an individual user segment + * + * @param $key string segment key + * @return Segment|null The decoded Segment, or null if missing + */ + public function getSegment($key) + { + return isset($this->_segments[$key]) ? $this->_segments[$key] : null; + } + + /** + * Gets all feature flags + * + * @return array()|null The decoded FeatureFlags, or null if missing + */ + public function getAllFeatures() + { + return $this->_flags; + } + + private function readAllData() + { + $flags = array(); + $segments = array(); + foreach ($this->_filePaths as $filePath) { + $this->loadFile($filePath, $flags, $segments); + } + $this->_flags = $flags; + $this->_segments = $segments; + } + + private function loadFile($filePath, &$flags, &$segments) + { + $content = file_get_contents($filePath); + $data = json_decode($content, true); + if ($data == null) { + throw new \InvalidArgumentException("File is not valid JSON: " + $filePath); + } + if (isset($data['flags'])) { + foreach ($data['flags'] as $key => $value) { + $flag = FeatureFlag::decode($value); + $this->tryToAdd($flags, $key, $flag, "feature flag"); + } + } + if (isset($data['flagValues'])) { + foreach ($data['flagValues'] as $key => $value) { + $flag = FeatureFlag::decode(array( + "key" => $key, + "version" => 1, + "on" => false, + "prerequisites" => array(), + "salt" => "", + "targets" => array(), + "rules" => array(), + "fallthrough" => array(), + "offVariation" => 0, + "variations" => array($value), + "deleted" => false, + "trackEvents" => false, + "clientSide" => false + )); + $this->tryToAdd($flags, $key, $flag, "feature flag"); + } + } + if (isset($data['segments'])) { + foreach ($data['segments'] as $key => $value) { + $segment = Segment::decode($value); + $this->tryToAdd($segments, $key, $segment, "user segment"); + } + } + } + + private function tryToAdd(&$array, $key, $item, $kind) { + if (isset($array[$key])) { + throw new \InvalidArgumentException("File data contains more than one " . $kind . " with key: " . $key); + } else { + $array[$key] = $item; + } + } +} diff --git a/tests/FileDataFeatureRequesterTest.php b/tests/FileDataFeatureRequesterTest.php new file mode 100644 index 000000000..87e4b2a0d --- /dev/null +++ b/tests/FileDataFeatureRequesterTest.php @@ -0,0 +1,38 @@ +getFeature("flag1"); + $this->assertEquals("flag1", $flag1->getKey()); + $flag2 = $fr->getFeature("flag2"); + $this->assertEquals("flag2", $flag2->getKey()); + $seg1 = $fr->getSegment("seg1"); + $this->assertEquals("seg1", $seg1->getKey()); + } + + public function testLoadsMultipleFiles() + { + $fr = new FileDataFeatureRequester(array("./tests/filedata/flag-only.json", + "./tests/filedata/segment-only.json")); + $flag1 = $fr->getFeature("flag1"); + $this->assertEquals("flag1", $flag1->getKey()); + $seg1 = $fr->getSegment("seg1"); + $this->assertEquals("seg1", $seg1->getKey()); + } + + public function testShortcutFlagCanBeEvaluated() + { + $fr = new FileDataFeatureRequester("./tests/filedata/all-properties.json"); + $flag2 = $fr->getFeature("flag2"); + $this->assertEquals("flag2", $flag2->getKey()); + $result = $flag2->evaluate(new LDUser("user"), null); + $this->assertEquals("value2", $result->getDetail()->getValue()); + } +} diff --git a/tests/filedata/all-properties.json b/tests/filedata/all-properties.json new file mode 100644 index 000000000..d5eea610d --- /dev/null +++ b/tests/filedata/all-properties.json @@ -0,0 +1,21 @@ +{ + "flags": { + "flag1": { + "key": "flag1", + "on": true, + "fallthrough": { + "variation": 2 + }, + "variations": [ "fall", "off", "on" ] + } + }, + "flagValues": { + "flag2": "value2" + }, + "segments": { + "seg1": { + "key": "seg1", + "include": ["user1"] + } + } +} diff --git a/tests/filedata/flag-only.json b/tests/filedata/flag-only.json new file mode 100644 index 000000000..7352bb946 --- /dev/null +++ b/tests/filedata/flag-only.json @@ -0,0 +1,12 @@ +{ + "flags": { + "flag1": { + "key": "flag1", + "on": true, + "fallthrough": { + "variation": 2 + }, + "variations": [ "fall", "off", "on" ] + } + } +} diff --git a/tests/filedata/segment-only.json b/tests/filedata/segment-only.json new file mode 100644 index 000000000..6f9e31dd2 --- /dev/null +++ b/tests/filedata/segment-only.json @@ -0,0 +1,8 @@ +{ + "segments": { + "seg1": { + "key": "seg1", + "include": ["user1"] + } + } +} From 4b2f9ef1fef66611340bedef8e8f0207b77df566 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 9 Jan 2019 23:35:45 -0800 Subject: [PATCH 19/72] linter --- src/LaunchDarkly/FileDataFeatureRequester.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/LaunchDarkly/FileDataFeatureRequester.php b/src/LaunchDarkly/FileDataFeatureRequester.php index 1455a2915..7e646979c 100644 --- a/src/LaunchDarkly/FileDataFeatureRequester.php +++ b/src/LaunchDarkly/FileDataFeatureRequester.php @@ -127,7 +127,8 @@ private function loadFile($filePath, &$flags, &$segments) } } - private function tryToAdd(&$array, $key, $item, $kind) { + private function tryToAdd(&$array, $key, $item, $kind) + { if (isset($array[$key])) { throw new \InvalidArgumentException("File data contains more than one " . $kind . " with key: " . $key); } else { From 226fd95883156522a24052faed1bf9291e753499 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 9 Jan 2019 23:39:51 -0800 Subject: [PATCH 20/72] data completeness --- tests/filedata/all-properties.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/filedata/all-properties.json b/tests/filedata/all-properties.json index d5eea610d..0773d1486 100644 --- a/tests/filedata/all-properties.json +++ b/tests/filedata/all-properties.json @@ -2,11 +2,20 @@ "flags": { "flag1": { "key": "flag1", + "version": 1, + "deleted": false, "on": true, + "prerequisites": [], + "targets": [], + "rules": [], "fallthrough": { "variation": 2 }, - "variations": [ "fall", "off", "on" ] + "variations": [ "fall", "off", "on" ], + "offVariation": 0, + "salt": "", + "trackEvents": false, + "clientSide": false } }, "flagValues": { @@ -15,6 +24,8 @@ "segments": { "seg1": { "key": "seg1", + "version": 1, + "deleted": false, "include": ["user1"] } } From e997d9f2eec0cd58ff29dc387539540040e43cc4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 9 Jan 2019 23:44:07 -0800 Subject: [PATCH 21/72] more data fixes --- tests/filedata/all-properties.json | 5 ++++- tests/filedata/flag-only.json | 11 ++++++++++- tests/filedata/segment-only.json | 5 ++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/filedata/all-properties.json b/tests/filedata/all-properties.json index 0773d1486..5f4adaaf7 100644 --- a/tests/filedata/all-properties.json +++ b/tests/filedata/all-properties.json @@ -26,7 +26,10 @@ "key": "seg1", "version": 1, "deleted": false, - "include": ["user1"] + "included": ["user1"], + "excluded": [], + "rules": [], + "salt": "" } } } diff --git a/tests/filedata/flag-only.json b/tests/filedata/flag-only.json index 7352bb946..f95e29518 100644 --- a/tests/filedata/flag-only.json +++ b/tests/filedata/flag-only.json @@ -2,11 +2,20 @@ "flags": { "flag1": { "key": "flag1", + "version": 1, + "deleted": false, "on": true, + "prerequisites": [], + "targets": [], + "rules": [], "fallthrough": { "variation": 2 }, - "variations": [ "fall", "off", "on" ] + "variations": [ "fall", "off", "on" ], + "offVariation": 0, + "salt": "", + "trackEvents": false, + "clientSide": false } } } diff --git a/tests/filedata/segment-only.json b/tests/filedata/segment-only.json index 6f9e31dd2..58d88a8af 100644 --- a/tests/filedata/segment-only.json +++ b/tests/filedata/segment-only.json @@ -2,7 +2,10 @@ "segments": { "seg1": { "key": "seg1", - "include": ["user1"] + "included": ["user1"], + "excluded": [], + "rules": [], + "salt": "" } } } From 804a17041d29de8e4590125662d363e65bf6526c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 9 Jan 2019 23:46:48 -0800 Subject: [PATCH 22/72] more data fixes --- tests/filedata/segment-only.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/filedata/segment-only.json b/tests/filedata/segment-only.json index 58d88a8af..f5f5e4c76 100644 --- a/tests/filedata/segment-only.json +++ b/tests/filedata/segment-only.json @@ -1,7 +1,8 @@ { "segments": { "seg1": { - "key": "seg1", + "key": "seg1", + "version": 1, "included": ["user1"], "excluded": [], "rules": [], From 5f74cb2a76d7a934ab8c11edff296f52030fd466 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 9 Jan 2019 23:50:15 -0800 Subject: [PATCH 23/72] more data fixes --- tests/filedata/segment-only.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/filedata/segment-only.json b/tests/filedata/segment-only.json index f5f5e4c76..cdd603482 100644 --- a/tests/filedata/segment-only.json +++ b/tests/filedata/segment-only.json @@ -3,6 +3,7 @@ "seg1": { "key": "seg1", "version": 1, + "deleted": false, "included": ["user1"], "excluded": [], "rules": [], From 4587c04d092968898f0e1be7989af30bfb5249ab Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 10 Jan 2019 12:43:50 -0800 Subject: [PATCH 24/72] readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d301d29f1..cb6f5cc7e 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ a Redis cache operating in your production environment. The ld-relay offers many Using flag data from a file --------------------------- -For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`FileDataFeatureRequester`](https://github.com/launchdarkly/node-client/blob/master/FileDataFeatureRequester.php) for more details. +For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`FileDataFeatureRequester`](https://github.com/launchdarkly/node-client/blob/master/FileDataFeatureRequester.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). Testing ------- From 4ae2bd9a88c3a70789b370d29784fe9844260fe4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 10 Jan 2019 12:49:19 -0800 Subject: [PATCH 25/72] rm unused parameters --- src/LaunchDarkly/FileDataFeatureRequester.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/LaunchDarkly/FileDataFeatureRequester.php b/src/LaunchDarkly/FileDataFeatureRequester.php index 7e646979c..ce25f3bca 100644 --- a/src/LaunchDarkly/FileDataFeatureRequester.php +++ b/src/LaunchDarkly/FileDataFeatureRequester.php @@ -1,8 +1,6 @@ _filePaths = is_array($filePaths) ? $filePaths : array($filePaths); - $this->_options = $options; $this->_flags = array(); $this->_segments = array(); - $this->_logger = isset($options['logger']) ? $options['logger'] : null; $this->readAllData(); } From 529ec435ac792d1a58c9cf429f4997180b22c9e1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 10 Jan 2019 12:55:16 -0800 Subject: [PATCH 26/72] fix incorrect doc comment --- src/LaunchDarkly/LDClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 134496c67..baca95a28 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -43,7 +43,7 @@ class LDClient * - 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\Storage\CacheStorageInterface. Defaults to an in-memory cache. - * - send_events: An optional bool that can disable the sending of events to LaunchDarkly. Defaults to false. + * - send_events: An optional bool that can disable the sending of events to LaunchDarkly. Defaults to true. * - 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. * - feature_requester: An optional LaunchDarkly\FeatureRequester instance. From 96dc0b432c96019a84992638fa813f1b922c39b5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 22 Jan 2019 20:42:24 -0800 Subject: [PATCH 27/72] add DynamoDB integration --- .circleci/config.yml | 15 ++ README.md | 44 +++-- composer.json | 1 + composer.lock | 142 +++++++++++++++- src/LaunchDarkly/DynamoDbFeatureRequester.php | 160 ++++++++++++++++++ tests/DynamoDbFeatureRequesterTest.php | 120 +++++++++++++ tests/FeatureRequesterTestBase.php | 154 +++++++++++++++++ 7 files changed, 621 insertions(+), 15 deletions(-) create mode 100644 src/LaunchDarkly/DynamoDbFeatureRequester.php create mode 100644 tests/DynamoDbFeatureRequesterTest.php create mode 100644 tests/FeatureRequesterTestBase.php diff --git a/.circleci/config.yml b/.circleci/config.yml index af78ad3e9..e478c111c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -44,18 +44,22 @@ jobs: <<: *php-docker-template docker: - image: circleci/php:5.6.34-cli-jessie + - image: amazon/dynamodb-local test-7.0: <<: *php-docker-template docker: - image: circleci/php:7.0.28-cli-jessie + - image: amazon/dynamodb-local test-7.1: <<: *php-docker-template docker: - image: circleci/php:7.1.15-cli-jessie + - image: amazon/dynamodb-local test-7.2: <<: *php-docker-template docker: - image: circleci/php:7.2.3-cli-stretch + - image: amazon/dynamodb-local test-5.5: # CircleCI doesn't provide a Docker image for 5.5 machine: @@ -68,6 +72,13 @@ jobs: 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 + - run: + name: install Docker + command: | + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" && + sudo apt-get -q update && + sudo apt-cache policy docker-ce && + sudo apt-get -qy install docker-ce - checkout - run: name: validate composer.json @@ -78,6 +89,10 @@ jobs: - run: name: run php-cs-fixer command: vendor/bin/php-cs-fixer fix --diff --dry-run --verbose + - run: + name: start DynamoDB + command: docker run -p 8000:8000 amazon/dynamodb-local + background: true - run: name: run tests command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests diff --git a/README.md b/README.md index 8871b609e..72005e49a 100644 --- a/README.md +++ b/README.md @@ -64,33 +64,51 @@ With Guzzle, you could persist your cache somewhere other than the default in-me Using LD-Relay ============== -The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update -a Redis cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a Redis store. +The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update a database cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a database store. The database can be Redis or DynamoDB. 1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis -2. Require Predis as a dependency: +2. Add the necessary dependency for the chosen database. + + For Redis: php composer.phar require "predis/predis:1.0.*" -3. Create the LDClient with the Redis feature requester as an option: + For DynamoDB: + + php composer.phar require "aws/aws-sdk-php:3.*" + +3. Create the LDClient with the appropriate parameters for the chosen database. These examples show all of the available options. + + For Redis: $client = new LaunchDarkly\LDClient("your_sdk_key", [ 'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', - 'redis_host' => 'your.redis.host', - 'redis_port' => 6379 + 'redis_host' => 'your.redis.host', // defaults to "localhost" if not specified + 'redis_port' => 6379, // defaults to 6379 if not specified + 'redis_timeout' => 5, // connection timeout in seconds; defaults to 5 + 'redis_prefix' => 'env1' // corresponds to the prefix setting in ld-relay, + 'predis_client' => $myClient // use this if you have already configured a Predis client instance ]); -4. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using `GuzzleEventPublisher` with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. + For DynamoDB: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'event_publisher_class' => 'LaunchDarkly\GuzzleEventPublisher', - 'events_uri' => 'http://your-ldrelay-host:8030', - 'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', - 'redis_host' => 'your.redis.host', - 'redis_port' => 6379 + 'feature_requester_class' => 'LaunchDarkly\DynamoDbFeatureRequester', + 'dynamodb_table' => 'your.table.name', // required + 'dynamodb_prefix' => 'env1', // corresponds to the prefix setting in ld-relay + 'dynamodb_options' => array() // you may pass any options supported by the AWS SDK ]); +4. If you are using DynamoDB, you must create your table manually. It must have a partition key called "namespace", and a sort key called "key" (both strings). + +5. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using `GuzzleEventPublisher` with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. + + To forward events, add the following configuration properties to the configuration shown above: + + 'event_publisher_class' => 'LaunchDarkly\GuzzleEventPublisher', + 'events_uri' => 'http://your-ldrelay-host:8030' + Testing ------- @@ -119,9 +137,9 @@ About LaunchDarkly * [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK") * [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK") * [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK") - * [Python Twisted](http://docs.launchdarkly.com/docs/python-twisted-sdk-reference "LaunchDarkly Python Twisted SDK") * [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK") * [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK") + * [Electron](http://docs.launchdarkly.com/docs/electron-sdk-reference "LaunchDarkly Electron SDK") * [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK") * [Ruby](http://docs.launchdarkly.com/docs/ruby-sdk-reference "LaunchDarkly Ruby SDK") * [iOS](http://docs.launchdarkly.com/docs/ios-sdk-reference "LaunchDarkly iOS SDK") diff --git a/composer.json b/composer.json index ea8ef4cbf..48a51ebe2 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "psr/log": "^1.0" }, "require-dev": { + "aws/aws-sdk-php": "^3.86", "friendsofphp/php-cs-fixer": "~2.2.19", "guzzlehttp/guzzle": "^6.2.1", "kevinrob/guzzle-cache-middleware": "^1.4.1", diff --git a/composer.lock b/composer.lock index d7eb9bb67..eb1c4302a 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "b7b9d1818e9117bf34ae546c08c5593e", + "content-hash": "67839eb605966e8495f5c47f7fdec4d9", "packages": [ { "name": "monolog/monolog", @@ -133,6 +133,89 @@ } ], "packages-dev": [ + { + "name": "aws/aws-sdk-php", + "version": "3.86.0", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "971494dfc1ef63aaffc7e909936d19140bef9ad6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/971494dfc1ef63aaffc7e909936d19140bef9ad6", + "reference": "971494dfc1ef63aaffc7e909936d19140bef9ad6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1", + "guzzlehttp/promises": "~1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "~2.2", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "phpunit/phpunit": "^4.8.35|^5.4.3", + "psr/cache": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2019-01-16T18:53:53+00:00" + }, { "name": "cilex/cilex", "version": "1.1.0", @@ -1351,6 +1434,61 @@ "abandoned": true, "time": "2012-08-16T17:13:03+00:00" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2016-12-03T22:08:25+00:00" + }, { "name": "nikic/php-parser", "version": "v1.4.1", diff --git a/src/LaunchDarkly/DynamoDbFeatureRequester.php b/src/LaunchDarkly/DynamoDbFeatureRequester.php new file mode 100644 index 000000000..2811f3ec9 --- /dev/null +++ b/src/LaunchDarkly/DynamoDbFeatureRequester.php @@ -0,0 +1,160 @@ +_tableName = $options['dynamodb_table']; + + $dynamoDbOptions = isset($options['dynamodb_options']) ? $options['dynamodb_options'] : array(); + $dynamoDbOptions['version'] = '2012-08-10'; // in the AWS SDK for PHP, this is how you specify the API version + $this->_client = new DynamoDbClient($dynamoDbOptions); + + $prefix = isset($options['dynamodb_prefix']) ? $options['dynamodb_prefix'] : ''; + $this->_prefix = ($prefix != null && count($prefix) > 0) ? $prefix . '/' : ''; + + $this->_logger = $options['logger']; + } + + /** + * Gets an individual feature flag. + * + * @param $key string feature flag key + * @return FeatureFlag|null The decoded JSON feature data, or null if missing + */ + public function getFeature($key) + { + $json = $this->getJsonItem('features', $key); + if (!$json) { + $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing feature with key: " . $key); + return null; + } + $flag = FeatureFlag::decode($json); + if ($flag) { + if ($flag->isDeleted()) { + $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get deleted feature with key: " . $key); + return null; + } + return $flag; + } else { + $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing feature with key: " . $key); + return null; + } + } + + /** + * Gets an individual user segment. + * + * @param $key string segment key + * @return Segment|null The decoded JSON segment data, or null if missing + */ + public function getSegment($key) + { + $json = $this->getJsonItem('segments', $key); + if (!$json) { + $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing segment with key: " . $key); + return null; + } + $segment = Segment::decode($json); + if ($segment) { + if ($segment->isDeleted()) { + $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get deleted segment with key: " . $key); + return null; + } + return $segment; + } else { + $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing segment with key: " . $key); + return null; + } + } + + /** + * Gets all features + * + * @return array()|null The decoded FeatureFlags, or null if missing + */ + public function getAllFeatures() + { + $jsonItems = $this->queryJsonItems('features'); + $itemsOut = array(); + foreach ($jsonItems as $json) { + $flag = FeatureFlag::decode($json); + if ($flag && !$flag->isDeleted()) { + $itemsOut[$flag->getKey()] = $flag; + } + } + return $itemsOut; + } + + protected function getJsonItem($namespace, $key) + { + $request = array( + 'TableName' => $this->_tableName, + 'ConsistentRead' => true, + 'Key' => array( + 'namespace' => array('S' => $this->_prefix . $namespace), + 'key' => array('S' => $key) + ) + ); + $result = $this->_client->getItem($request); + if (!$result) { + return null; + } + $item = $result->get('Item'); + if (!$item || !isset($item['item'])) { + return null; + } + $attr = $item['item']; + return isset($attr['S']) ? json_decode($attr['S'], true) : null; + } + + protected function queryJsonItems($namespace) + { + $items = array(); + $request = array( + 'TableName' => $this->_tableName, + 'ConsistentRead' => true, + 'KeyConditions' => array( + 'namespace' => array( + 'ComparisonOperator' => 'EQ', + 'AttributeValueList' => array(array('S' => $this->_prefix . $namespace)) + ) + ) + ); + // We may need to repeat this query several times due to pagination + $moreItems = true; + while ($moreItems) { + $result = $this->_client->query($request); + foreach ($result->get('Items') as $item) { + if (isset($item['item'])) { + $attr = $item['item']; + if (isset($attr['S'])) { + $items[] = json_decode($attr['S'], true); + } + } + } + if (isset($result['LastEvaluatedKey']) && $result['LastEvaluatedKey']) { + $request['ExclusiveStartKey'] = $result['LastEvaluatedKey']; + } else { + $moreItems = false; + } + } + return $items; + } +} diff --git a/tests/DynamoDbFeatureRequesterTest.php b/tests/DynamoDbFeatureRequesterTest.php new file mode 100644 index 000000000..776641549 --- /dev/null +++ b/tests/DynamoDbFeatureRequesterTest.php @@ -0,0 +1,120 @@ + 'http://localhost:8000', + 'region' => 'us-east-1', + 'version' => '2012-08-10' + ); + } + + protected function makeRequester() + { + $options = array( + 'dynamodb_table' => self::TABLE_NAME, + 'dynamodb_options' => self::makeDynamoDbOptions(), + 'dynamodb_prefix' => self::PREFIX, + 'logger' => new NullLogger() + ); + return new DynamoDbFeatureRequester('', '', $options); + } + + protected function putItem($namespace, $key, $version, $json) + { + self::$dynamoDbClient->putItem(array( + 'TableName' => self::TABLE_NAME, + 'Item' => array( + 'namespace' => array('S' => self::PREFIX . '/' . $namespace), + 'key' => array('S' => $key), + 'version' => array('N' => strval($version)), + 'item' => array('S' => $json) + ) + )); + } + + protected function deleteExistingData() + { + $result = self::$dynamoDbClient->scan(array( + 'TableName' => self::TABLE_NAME, + 'ConsistentRead' => true, + 'AttributesToGet' => array('namespace', 'key') + )); + $requests = array(); + foreach ($result['Items'] as $item) { + $requests[] = array( + 'DeleteRequest' => array('Key' => $item) + ); + } + if (count($requests)) { + self::$dynamoDbClient->batchWriteItem(array( + 'RequestItems' => array( + self::TABLE_NAME => $requests + ) + )); + } + } + + private static function createTableIfNecessary() + { + try { + self::$dynamoDbClient->describeTable(array('TableName' => self::TABLE_NAME)); + return; // table already exists + } catch (\Exception $e) { + } + self::$dynamoDbClient->createTable(array( + 'TableName' => self::TABLE_NAME, + 'AttributeDefinitions' => array( + array( + 'AttributeName' => 'namespace', + 'AttributeType' => 'S' + ), + array( + 'AttributeName' => 'key', + 'AttributeType' => 'S' + ) + ), + 'KeySchema' => array( + array( + 'AttributeName' => 'namespace', + 'KeyType' => 'HASH' + ), + array( + 'AttributeName' => 'key', + 'KeyType' => 'RANGE' + ) + ), + 'ProvisionedThroughput' => array( + 'ReadCapacityUnits' => 1, + 'WriteCapacityUnits' => 1 + ) + )); + while (true) { // table may not be available immediately + try { + self::$dynamoDbClient->describeTable(array('TableName' => self::TABLE_NAME)); + return; + } catch (\Exception $e) { + } + sleep(1); + } + } +} diff --git a/tests/FeatureRequesterTestBase.php b/tests/FeatureRequesterTestBase.php new file mode 100644 index 000000000..72a4815b1 --- /dev/null +++ b/tests/FeatureRequesterTestBase.php @@ -0,0 +1,154 @@ +deleteExistingData(); + } + + protected function deleteExistingData() + { + } + + protected function makeRequester() + { + } + + protected function putItem($namespace, $key, $version, $json) + { + } + + public function testGetFeature() + { + $flagKey = 'foo'; + $flagVersion = 10; + $flagJson = self::makeFlagJson($flagKey, $flagVersion); + $this->putItem('features', $flagKey, $flagVersion, $flagJson); + + $fr = $this->makeRequester(); + $flag = $fr->getFeature($flagKey); + + $this->assertInstanceOf(FeatureFlag::class, $flag); + $this->assertEquals($flagVersion, $flag->getVersion()); + } + + public function testGetMissingFeature() + { + $fr = $this->makeRequester(); + $flag = $fr->getFeature('unavailable'); + $this->assertNull($flag); + } + + public function testGetDeletedFeature() + { + $flagKey = 'foo'; + $flagVersion = 10; + $flagJson = self::makeFlagJson($flagKey, $flagVersion, true); + $this->putItem('features', $flagKey, $flagVersion, $flagJson); + + $fr = $this->makeRequester(); + $flag = $fr->getFeature($flagKey); + + $this->assertNull($flag); + } + + public function testGetAllFeatures() + { + $flagKey1 = 'foo'; + $flagKey2 = 'bar'; + $flagKey3 = 'deleted'; + $flagVersion = 10; + $flagJson1 = self::makeFlagJson($flagKey1, $flagVersion); + $flagJson2 = self::makeFlagJson($flagKey2, $flagVersion); + $flagJson3 = self::makeFlagJson($flagKey3, $flagVersion, true); + + $this->putItem('features', $flagKey1, $flagVersion, $flagJson1); + $this->putItem('features', $flagKey2, $flagVersion, $flagJson2); + $this->putItem('features', $flagKey3, $flagVersion, $flagJson3); + + $fr = $this->makeRequester(); + $flags = $fr->getAllFeatures(); + + $this->assertEquals(2, count($flags)); + $flag1 = $flags[$flagKey1]; + $this->assertEquals($flagKey1, $flag1->getKey()); + $this->assertEquals($flagVersion, $flag1->getVersion()); + $flag2 = $flags[$flagKey2]; + $this->assertEquals($flagKey2, $flag2->getKey()); + $this->assertEquals($flagVersion, $flag2->getVersion()); + } + + public function testGetSegment() + { + $segKey = 'foo'; + $segVersion = 10; + $segJson = self::makeSegmentJson($segKey, $segVersion); + $this->putItem('segments', $segKey, $segVersion, $segJson); + + $fr = $this->makeRequester(); + $segment = $fr->getSegment($segKey); + + $this->assertInstanceOf(Segment::class, $segment); + $this->assertEquals($segVersion, $segment->getVersion()); + } + + public function testGetMissingSegment() + { + $fr = $this->makeRequester(); + $segment = $fr->getSegment('unavailable'); + $this->assertNull($segment); + } + + public function testGetDeletedSegment() + { + $segKey = 'foo'; + $segVersion = 10; + $segJson = self::makeSegmentJson($segKey, $segVersion, true); + $this->putItem('segments', $segKey, $segVersion, $segJson); + + $fr = $this->makeRequester(); + $segment = $fr->getSegment($segKey); + + $this->assertNull($segment); + } + + private static function makeFlagJson($key, $version, $deleted = false) + { + return json_encode(array( + 'key' => $key, + 'version' => $version, + 'on' => true, + 'prerequisites' => [], + 'salt' => '', + 'targets' => [], + 'rules' => [], + 'fallthrough' => [ + 'variation' => 0, + ], + 'offVariation' => null, + 'variations' => [ + true, + false, + ], + 'deleted' => $deleted + )); + } + + private static function makeSegmentJson($key, $version, $deleted = false) + { + return json_encode(array( + 'key' => $key, + 'version' => $version, + 'includes' => array(), + 'excludes' => array(), + 'rules' => [], + 'deleted' => $deleted + )); + } +} From ea3268ec9b40ea39d01ed748faa21697fa94a7ae Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 22 Jan 2019 20:46:32 -0800 Subject: [PATCH 28/72] fix test config --- tests/DynamoDbFeatureRequesterTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/DynamoDbFeatureRequesterTest.php b/tests/DynamoDbFeatureRequesterTest.php index 776641549..7dbee3f17 100644 --- a/tests/DynamoDbFeatureRequesterTest.php +++ b/tests/DynamoDbFeatureRequesterTest.php @@ -22,6 +22,7 @@ public static function setUpBeforeClass() private static function makeDynamoDbOptions() { return array( + 'credentials' => array('key' => 'x', 'secret' => 'x'), // credentials for local test instance are arbitrary 'endpoint' => 'http://localhost:8000', 'region' => 'us-east-1', 'version' => '2012-08-10' From 5da57f31e9c20b2545f3545c4ce91477db968b84 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 22 Jan 2019 20:58:12 -0800 Subject: [PATCH 29/72] fix string check --- src/LaunchDarkly/DynamoDbFeatureRequester.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/DynamoDbFeatureRequester.php b/src/LaunchDarkly/DynamoDbFeatureRequester.php index 2811f3ec9..ba4fe018d 100644 --- a/src/LaunchDarkly/DynamoDbFeatureRequester.php +++ b/src/LaunchDarkly/DynamoDbFeatureRequester.php @@ -27,7 +27,7 @@ public function __construct($baseUri, $sdkKey, $options) $this->_client = new DynamoDbClient($dynamoDbOptions); $prefix = isset($options['dynamodb_prefix']) ? $options['dynamodb_prefix'] : ''; - $this->_prefix = ($prefix != null && count($prefix) > 0) ? $prefix . '/' : ''; + $this->_prefix = ($prefix != null && $prefix != '') ? ($prefix . '/') : ''; $this->_logger = $options['logger']; } From aa62488dfd32f085692e3115f72e40317cd6cb49 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 22 Jan 2019 20:58:21 -0800 Subject: [PATCH 30/72] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72005e49a..abdfa9aef 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ With Guzzle, you could persist your cache somewhere other than the default in-me Using LD-Relay ============== -The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update a database cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a database store. The database can be Redis or DynamoDB. +The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update a database cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a database store. The database can be Redis or DynamoDB. (For more about using LaunchDarkly with Redis or DynamoDB, see the [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).) 1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis From d8bcfbbedc16057e1f7aa98f7ad6f3914055e88a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 22 Jan 2019 21:02:02 -0800 Subject: [PATCH 31/72] fix test data --- tests/FeatureRequesterTestBase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/FeatureRequesterTestBase.php b/tests/FeatureRequesterTestBase.php index 72a4815b1..342eeb7e4 100644 --- a/tests/FeatureRequesterTestBase.php +++ b/tests/FeatureRequesterTestBase.php @@ -145,9 +145,10 @@ private static function makeSegmentJson($key, $version, $deleted = false) return json_encode(array( 'key' => $key, 'version' => $version, - 'includes' => array(), - 'excludes' => array(), + 'included' => array(), + 'excluded' => array(), 'rules' => [], + 'salt' => '', 'deleted' => $deleted )); } From 895aa33408ee002c203a45316262752038270e97 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 12:50:49 -0800 Subject: [PATCH 32/72] factor out base class, implement caching, make configuration simpler --- README.md | 11 +- composer.json | 3 +- src/LaunchDarkly/DynamoDbFeatureRequester.php | 87 +-------- src/LaunchDarkly/FeatureRequesterBase.php | 176 ++++++++++++++++++ src/LaunchDarkly/LDClient.php | 25 +-- tests/FeatureRequesterTestBase.php | 2 + 6 files changed, 207 insertions(+), 97 deletions(-) create mode 100644 src/LaunchDarkly/FeatureRequesterBase.php diff --git a/README.md b/README.md index abdfa9aef..1c243c8ca 100644 --- a/README.md +++ b/README.md @@ -83,24 +83,25 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela For Redis: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', + 'feature_requester' => 'LaunchDarkly\LDDFeatureRequester', 'redis_host' => 'your.redis.host', // defaults to "localhost" if not specified 'redis_port' => 6379, // defaults to 6379 if not specified 'redis_timeout' => 5, // connection timeout in seconds; defaults to 5 - 'redis_prefix' => 'env1' // corresponds to the prefix setting in ld-relay, + 'redis_prefix' => 'env1' // corresponds to the prefix setting in ld-relay 'predis_client' => $myClient // use this if you have already configured a Predis client instance ]); For DynamoDB: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester_class' => 'LaunchDarkly\DynamoDbFeatureRequester', + 'feature_requester' => 'LaunchDarkly\DynamoDbFeatureRequester', 'dynamodb_table' => 'your.table.name', // required 'dynamodb_prefix' => 'env1', // corresponds to the prefix setting in ld-relay - 'dynamodb_options' => array() // you may pass any options supported by the AWS SDK + 'dynamodb_options' => array(), // you may pass any options supported by the AWS SDK + 'apc_expiration' => 30 // expiration time for local caching, if you have apcu installed ]); -4. If you are using DynamoDB, you must create your table manually. It must have a partition key called "namespace", and a sort key called "key" (both strings). +4. If you are using DynamoDB, you must create your table manually. It must have a partition key called "namespace", and a sort key called "key" (both strings). Note that by default the AWS SDK will attempt to get your AWS credentials and region from environment variables and/or local configuration files, but you may also specify them in `dynamodb_options`. 5. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using `GuzzleEventPublisher` with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. diff --git a/composer.json b/composer.json index 48a51ebe2..3dd862aa8 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ "suggest": { "guzzlehttp/guzzle": "(^6.2.1) Required when using GuzzleEventPublisher or the default FeatureRequester", "kevinrob/guzzle-cache-middleware": "(^1.4.1) Recommended for performance when using the default FeatureRequester", - "predis/predis": "(^1.0) Required when using LDDFeatureRequester" + "predis/predis": "(^1.0) Required when using LDDFeatureRequester", + "aws/aws-sdk-php": "(^3.86) Required when using DynamoDbFeatureRequester" }, "autoload": { "psr-4": { diff --git a/src/LaunchDarkly/DynamoDbFeatureRequester.php b/src/LaunchDarkly/DynamoDbFeatureRequester.php index ba4fe018d..550797eef 100644 --- a/src/LaunchDarkly/DynamoDbFeatureRequester.php +++ b/src/LaunchDarkly/DynamoDbFeatureRequester.php @@ -2,9 +2,8 @@ namespace LaunchDarkly; use Aws\DynamoDb\DynamoDbClient; -use Psr\Log\LoggerInterface; -class DynamoDbFeatureRequester implements FeatureRequester +class DynamoDbFeatureRequester extends FeatureRequesterBase { /** @var string */ protected $_tableName; @@ -12,11 +11,11 @@ class DynamoDbFeatureRequester implements FeatureRequester protected $_prefix; /** @var DynamoDbClient */ protected $_client; - /** @var LoggerInterface */ - private $_logger; public function __construct($baseUri, $sdkKey, $options) { + parent::__construct($baseUri, $sdkKey, $options); + if (!isset($options['dynamodb_table'])) { throw new \InvalidArgumentException('dynamodb_table must be specified'); } @@ -28,81 +27,9 @@ public function __construct($baseUri, $sdkKey, $options) $prefix = isset($options['dynamodb_prefix']) ? $options['dynamodb_prefix'] : ''; $this->_prefix = ($prefix != null && $prefix != '') ? ($prefix . '/') : ''; - - $this->_logger = $options['logger']; - } - - /** - * Gets an individual feature flag. - * - * @param $key string feature flag key - * @return FeatureFlag|null The decoded JSON feature data, or null if missing - */ - public function getFeature($key) - { - $json = $this->getJsonItem('features', $key); - if (!$json) { - $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing feature with key: " . $key); - return null; - } - $flag = FeatureFlag::decode($json); - if ($flag) { - if ($flag->isDeleted()) { - $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get deleted feature with key: " . $key); - return null; - } - return $flag; - } else { - $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing feature with key: " . $key); - return null; - } - } - - /** - * Gets an individual user segment. - * - * @param $key string segment key - * @return Segment|null The decoded JSON segment data, or null if missing - */ - public function getSegment($key) - { - $json = $this->getJsonItem('segments', $key); - if (!$json) { - $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing segment with key: " . $key); - return null; - } - $segment = Segment::decode($json); - if ($segment) { - if ($segment->isDeleted()) { - $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get deleted segment with key: " . $key); - return null; - } - return $segment; - } else { - $this->_logger->warning("DynamoDBFeatureRequester: Attempted to get missing segment with key: " . $key); - return null; - } - } - - /** - * Gets all features - * - * @return array()|null The decoded FeatureFlags, or null if missing - */ - public function getAllFeatures() - { - $jsonItems = $this->queryJsonItems('features'); - $itemsOut = array(); - foreach ($jsonItems as $json) { - $flag = FeatureFlag::decode($json); - if ($flag && !$flag->isDeleted()) { - $itemsOut[$flag->getKey()] = $flag; - } - } - return $itemsOut; } - protected function getJsonItem($namespace, $key) + protected function readItemString($namespace, $key) { $request = array( 'TableName' => $this->_tableName, @@ -121,10 +48,10 @@ protected function getJsonItem($namespace, $key) return null; } $attr = $item['item']; - return isset($attr['S']) ? json_decode($attr['S'], true) : null; + return isset($attr['S']) ? $attr['S'] : null; } - protected function queryJsonItems($namespace) + protected function readItemStringList($namespace) { $items = array(); $request = array( @@ -145,7 +72,7 @@ protected function queryJsonItems($namespace) if (isset($item['item'])) { $attr = $item['item']; if (isset($attr['S'])) { - $items[] = json_decode($attr['S'], true); + $items[] = $attr['S']; } } } diff --git a/src/LaunchDarkly/FeatureRequesterBase.php b/src/LaunchDarkly/FeatureRequesterBase.php new file mode 100644 index 000000000..32eadd405 --- /dev/null +++ b/src/LaunchDarkly/FeatureRequesterBase.php @@ -0,0 +1,176 @@ +_baseUri = $baseUri; + $this->_sdkKey = $sdkKey; + $this->_options = $options; + + if (isset($options['apc_expiration'])) { + if (!extension_loaded('apcu')) { + throw new \InvalidArgumentException('apc_expiration was specified but apcu is not installed'); + } + $this->_apcExpiration = (int)$options['apc_expiration']; + } else { + $this->_apcExpiration = 0; + } + + if (isset($options['logger']) && $options['logger']) { + $this->_logger = $options['logger']; + } else { + $this->_logger = new NullLogger(); + } + } + + /** + * Override this method to read a JSON object (as a string) from the underlying store. + * + * @param $namespace "features" or "segments" + * @param $key flag or segment key + * @return string|null the stored JSON data, or null if not found + */ + protected function readItemString($namespace, $key) + { + return null; + } + + /** + * Override this method to read a set of JSON objects (as strings) from the underlying store. + * + * @param $namespace "features" or "segments" + * @return array|null array of stored JSON strings + */ + protected function readItemStringList($namespace) + { + return array(); + } + + protected function getCachedString($namespace, $key) + { + if ($this->_apcExpiration) { + $value = \apc_fetch($this->makeCacheKey($namespace, $key)); + return $value === false ? null : $value; + } + return null; + } + + protected function putCachedString($namespace, $key, $data) + { + if ($this->_apcExpiration) { + \apc_add($this->makeCacheKey($namespace, $key), $data, $this->_apcExpiration); + } + } + + protected function makeCacheKey($namespace, $key) { + return self::CACHE_PREFIX . $namespace . ':' . $key; + } + + /** + * Gets an individual feature flag. + * + * @param $key string feature flag key + * @return FeatureFlag|null The decoded JSON feature data, or null if missing + */ + public function getFeature($key) + { + $json = $this->getJsonItem(self::FEATURES_NAMESPACE, $key); + if ($json) { + $flag = FeatureFlag::decode($json); + if ($flag->isDeleted()) { + $this->_logger->warning("FeatureRequester: Attempted to get deleted feature with key: " . $key); + return null; + } + return $flag; + } else { + $this->_logger->warning("FeatureRequester: Attempted to get missing feature with key: " . $key); + return null; + } + } + + /** + * Gets an individual user segment. + * + * @param $key string segment key + * @return Segment|null The decoded JSON segment data, or null if missing + */ + public function getSegment($key) + { + $json = $this->getJsonItem(self::SEGMENTS_NAMESPACE, $key); + if ($json) { + $segment = Segment::decode($json); + if ($segment->isDeleted()) { + $this->_logger->warning("FeatureRequester: Attempted to get deleted segment with key: " . $key); + return null; + } + return $segment; + } else { + $this->_logger->warning("FeatureRequester: Attempted to get missing segment with key: " . $key); + return null; + } + } + + /** + * Gets all features + * + * @return array()|null The decoded FeatureFlags, or null if missing + */ + public function getAllFeatures() + { + $jsonList = $this->getJsonItemList(self::FEATURES_NAMESPACE); + $itemsOut = array(); + foreach ($jsonList as $json) { + $flag = FeatureFlag::decode($json); + if ($flag && !$flag->isDeleted()) { + $itemsOut[$flag->getKey()] = $flag; + } + } + return $itemsOut; + } + + protected function getJsonItem($namespace, $key) + { + $raw = $this->getCachedString($namespace, $key); + if ($raw === null) { + $raw = $this->readItemString($namespace, $key); + $this->putCachedString($namespace, $key, $raw); + } + return ($raw === null) ? null : json_decode($raw, true); + } + + protected function getJsonItemList($namespace) + { + $raw = $this->getCachedString($namespace, self::ALL_ITEMS_KEY); + if ($raw) { + $values = json_decode($raw, true); + } else { + $values = $this->readItemStringList($namespace); + $this->putCachedString($namespace, self::ALL_ITEMS_KEY, json_encode($values)); + } + foreach ($values as $i => $s) { + $values[$i] = json_decode($s, true); + } + return $values; + } +} diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index baca95a28..f24eabf62 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -107,20 +107,23 @@ public function __construct($sdkKey, $options = array()) */ private function getFeatureRequester($sdkKey, array $options) { - if (isset($options['feature_requester']) && $options['feature_requester'] instanceof FeatureRequester) { - return $options['feature_requester']; - } - - if (isset($options['feature_requester_class'])) { - $featureRequesterClass = $options['feature_requester_class']; + if (isset($options['feature_requester']) && $options['feature_requester']) { + $fr = $options['feature_requester']; + } else if (isset($options['feature_requester_class']) && $options['feature_requester_class']) { + $fr = $options['feature_requester_class']; } else { - $featureRequesterClass = GuzzleFeatureRequester::class; + $fr = GuzzleFeatureRequester::class; } - - if (!is_a($featureRequesterClass, FeatureRequester::class, true)) { - throw new \InvalidArgumentException; + if ($fr instanceof FeatureRequester) { + return $fr; + } + if (is_callable($fr)) { + return $fr($this->_baseUri, $sdkKey, $options); + } + if (is_a($fr, FeatureRequester::class, true)) { + return new $fr($this->_baseUri, $sdkKey, $options); } - return new $featureRequesterClass($this->_baseUri, $sdkKey, $options); + throw new \InvalidArgumentException('invalid feature_requester'); } /** diff --git a/tests/FeatureRequesterTestBase.php b/tests/FeatureRequesterTestBase.php index 342eeb7e4..79f0eb08b 100644 --- a/tests/FeatureRequesterTestBase.php +++ b/tests/FeatureRequesterTestBase.php @@ -7,6 +7,8 @@ class FeatureRequesterTestBase extends \PHPUnit_Framework_TestCase { + private const CACHE_TTL = 30; + public function setUp() { $this->deleteExistingData(); From 10662d511ecddb683850eda19cc823815d22e667 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 12:56:40 -0800 Subject: [PATCH 33/72] fix comment --- src/LaunchDarkly/LDClient.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index f24eabf62..96c1b3b57 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -46,8 +46,7 @@ class LDClient * - send_events: An optional bool that can disable the sending of events to LaunchDarkly. Defaults to true. * - 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. - * - feature_requester: An optional LaunchDarkly\FeatureRequester instance. - * - feature_requester_class: An optional class implementing LaunchDarkly\FeatureRequester, if `feature_requester` is not specified. Defaults to GuzzleFeatureRequester. + * - feature_requester: An optional LaunchDarkly\FeatureRequester instance, or a class that implements LaunchDarkly\FeatureRequester. Defaults to GuzzleFeatureRequester. * - event_publisher: An optional LaunchDarkly\EventPublisher instance. * - event_publisher_class: An optional class implementing LaunchDarkly\EventPublisher, if `event_publisher` is not specified. Defaults to CurlEventPublisher. * - all_attributes_private: True if no user attributes (other than the key) should be sent back to LaunchDarkly. By default, this is false. From 2ee0e9ee3842a7077bf5d6bea0480940b190b98a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 12:58:11 -0800 Subject: [PATCH 34/72] rm unused const --- tests/FeatureRequesterTestBase.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/FeatureRequesterTestBase.php b/tests/FeatureRequesterTestBase.php index 79f0eb08b..342eeb7e4 100644 --- a/tests/FeatureRequesterTestBase.php +++ b/tests/FeatureRequesterTestBase.php @@ -7,8 +7,6 @@ class FeatureRequesterTestBase extends \PHPUnit_Framework_TestCase { - private const CACHE_TTL = 30; - public function setUp() { $this->deleteExistingData(); From 1eeb15399b13c8b44b84e434e3a4f507a973e662 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 12:59:43 -0800 Subject: [PATCH 35/72] can't scope constants --- src/LaunchDarkly/FeatureRequesterBase.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly/FeatureRequesterBase.php b/src/LaunchDarkly/FeatureRequesterBase.php index 32eadd405..061867622 100644 --- a/src/LaunchDarkly/FeatureRequesterBase.php +++ b/src/LaunchDarkly/FeatureRequesterBase.php @@ -6,10 +6,10 @@ class FeatureRequesterBase implements FeatureRequester { - protected const FEATURES_NAMESPACE = 'features'; - protected const SEGMENTS_NAMESPACE = 'segments'; - private const ALL_ITEMS_KEY = '$all'; - private const CACHE_PREFIX = 'LaunchDarkly:'; + const FEATURES_NAMESPACE = 'features'; + const SEGMENTS_NAMESPACE = 'segments'; + const ALL_ITEMS_KEY = '$all'; + const CACHE_PREFIX = 'LaunchDarkly:'; /** @var string */ protected $_baseUri; From c82b7c76096339dd165dbb78f6a7ff8f834b4b0e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 13:03:55 -0800 Subject: [PATCH 36/72] linter --- src/LaunchDarkly/FeatureRequesterBase.php | 3 ++- src/LaunchDarkly/LDClient.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/FeatureRequesterBase.php b/src/LaunchDarkly/FeatureRequesterBase.php index 061867622..122eddbc6 100644 --- a/src/LaunchDarkly/FeatureRequesterBase.php +++ b/src/LaunchDarkly/FeatureRequesterBase.php @@ -83,7 +83,8 @@ protected function putCachedString($namespace, $key, $data) } } - protected function makeCacheKey($namespace, $key) { + protected function makeCacheKey($namespace, $key) + { return self::CACHE_PREFIX . $namespace . ':' . $key; } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 96c1b3b57..960303cc1 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -108,7 +108,7 @@ private function getFeatureRequester($sdkKey, array $options) { if (isset($options['feature_requester']) && $options['feature_requester']) { $fr = $options['feature_requester']; - } else if (isset($options['feature_requester_class']) && $options['feature_requester_class']) { + } elseif (isset($options['feature_requester_class']) && $options['feature_requester_class']) { $fr = $options['feature_requester_class']; } else { $fr = GuzzleFeatureRequester::class; From 6aead6204ca6f30504c1c76569827957225685e3 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 14:25:21 -0800 Subject: [PATCH 37/72] add Consul integration --- .circleci/config.yml | 14 + README.md | 16 +- composer.json | 4 +- composer.lock | 1047 +++++++++++++------ src/LaunchDarkly/ConsulFeatureRequester.php | 70 ++ src/LaunchDarkly/FeatureRequesterBase.php | 3 + tests/ConsulFeatureRequesterTest.php | 50 + 7 files changed, 897 insertions(+), 307 deletions(-) create mode 100644 src/LaunchDarkly/ConsulFeatureRequester.php create mode 100644 tests/ConsulFeatureRequesterTest.php diff --git a/.circleci/config.yml b/.circleci/config.yml index e478c111c..49996851a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,21 +45,25 @@ jobs: docker: - image: circleci/php:5.6.34-cli-jessie - image: amazon/dynamodb-local + - image: consul test-7.0: <<: *php-docker-template docker: - image: circleci/php:7.0.28-cli-jessie - image: amazon/dynamodb-local + - image: consul test-7.1: <<: *php-docker-template docker: - image: circleci/php:7.1.15-cli-jessie - image: amazon/dynamodb-local + - image: consul test-7.2: <<: *php-docker-template docker: - image: circleci/php:7.2.3-cli-stretch - image: amazon/dynamodb-local + - image: consul test-5.5: # CircleCI doesn't provide a Docker image for 5.5 machine: @@ -93,6 +97,16 @@ jobs: name: start DynamoDB command: docker run -p 8000:8000 amazon/dynamodb-local background: true + - run: + name: download Consul + command: wget https://releases.hashicorp.com/consul/0.8.0/consul_0.8.0_linux_amd64.zip + - run: + name: extract Consul + command: unzip consul_0.8.0_linux_amd64.zip + - run: + name: start Consul + command: ./consul agent -dev + background: true - run: name: run tests command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests diff --git a/README.md b/README.md index 1c243c8ca..9734ccd57 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ With Guzzle, you could persist your cache somewhere other than the default in-me Using LD-Relay ============== -The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update a database cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a database store. The database can be Redis or DynamoDB. (For more about using LaunchDarkly with Redis or DynamoDB, see the [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).) +The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update a database cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a database store. The database can be Redis, Consul, or DynamoDB. (For more about using LaunchDarkly with databases, see the [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).) 1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis @@ -74,6 +74,10 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela php composer.phar require "predis/predis:1.0.*" + For Consul: + + php composer.phar require "sensiolabs/consul-php-sdk:2.*" + For DynamoDB: php composer.phar require "aws/aws-sdk-php:3.*" @@ -91,6 +95,16 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela 'predis_client' => $myClient // use this if you have already configured a Predis client instance ]); + For Consul: + + $client = new LaunchDarkly\LDClient("your_sdk_key", [ + 'feature_requester' => 'LaunchDarkly\ConsulFeatureRequester', + 'consul_uri' => 'http://localhost:8500', // this is the default + 'consul_prefix' => 'env1', // corresponds to the prefix setting in ld-relay + 'consul_options' => array(), // you may pass any options supported by the Guzzle client + 'apc_expiration' => 30 // expiration time for local caching, if you have apcu installed + ]); + For DynamoDB: $client = new LaunchDarkly\LDClient("your_sdk_key", [ diff --git a/composer.json b/composer.json index 3dd862aa8..202ffc9d7 100644 --- a/composer.json +++ b/composer.json @@ -26,13 +26,15 @@ "phpdocumentor/phpdocumentor": "^2.0", "phpunit/phpunit": ">=4.8.26 <5.4", "predis/predis": "^1.0", + "sensiolabs/consul-php-sdk": "^2.0", "zendframework/zend-serializer": "^2.7" }, "suggest": { "guzzlehttp/guzzle": "(^6.2.1) Required when using GuzzleEventPublisher or the default FeatureRequester", "kevinrob/guzzle-cache-middleware": "(^1.4.1) Recommended for performance when using the default FeatureRequester", "predis/predis": "(^1.0) Required when using LDDFeatureRequester", - "aws/aws-sdk-php": "(^3.86) Required when using DynamoDbFeatureRequester" + "aws/aws-sdk-php": "(^3.86) Required when using DynamoDbFeatureRequester", + "sensiolabs/consul-php-sdk": "(^2.0) Required when using ConsulFeatureRequester" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index eb1c4302a..b011c942d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "67839eb605966e8495f5c47f7fdec4d9", + "content-hash": "9c8af48dc263ef3f40fd7e3ca3c74caa", "packages": [ { "name": "monolog/monolog", - "version": "1.23.0", + "version": "1.24.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", "shasum": "" }, "require": { @@ -82,20 +82,20 @@ "logging", "psr-3" ], - "time": "2017-06-19T01:22:40+00:00" + "time": "2018-11-05T09:00:11+00:00" }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -129,22 +129,22 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-11-20T15:27:04+00:00" } ], "packages-dev": [ { "name": "aws/aws-sdk-php", - "version": "3.86.0", + "version": "3.86.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "971494dfc1ef63aaffc7e909936d19140bef9ad6" + "reference": "0f38d6b5625c4e6ea8891abd6a75e3d35e3ef465" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/971494dfc1ef63aaffc7e909936d19140bef9ad6", - "reference": "971494dfc1ef63aaffc7e909936d19140bef9ad6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0f38d6b5625c4e6ea8891abd6a75e3d35e3ef465", + "reference": "0f38d6b5625c4e6ea8891abd6a75e3d35e3ef465", "shasum": "" }, "require": { @@ -214,7 +214,7 @@ "s3", "sdk" ], - "time": "2019-01-16T18:53:53+00:00" + "time": "2019-01-21T22:52:28+00:00" }, { "name": "cilex/cilex", @@ -396,6 +396,50 @@ ], "time": "2016-08-30T16:08:34+00:00" }, + { + "name": "composer/xdebug-handler", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "dc523135366eb68f22268d069ea7749486458562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562", + "reference": "dc523135366eb68f22268d069ea7749486458562", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "time": "2018-11-29T10:59:02+00:00" + }, { "name": "container-interop/container-interop", "version": "1.2.0", @@ -429,35 +473,35 @@ }, { "name": "doctrine/annotations", - "version": "v1.2.7", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535" + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", - "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", "shasum": "" }, "require": { "doctrine/lexer": "1.*", - "php": ">=5.3.2" + "php": "^7.1" }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "4.*" + "phpunit/phpunit": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Annotations\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } }, "notification-url": "https://packagist.org/downloads/", @@ -493,36 +537,36 @@ "docblock", "parser" ], - "time": "2015-08-31T12:32:49+00:00" + "time": "2017-12-06T07:11:42+00:00" }, { "name": "doctrine/instantiator", - "version": "1.0.5", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -547,7 +591,7 @@ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2017-07-22T11:58:36+00:00" }, { "name": "doctrine/lexer", @@ -651,20 +695,21 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.2.19", + "version": "v2.2.20", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "2de0ac9c1d0d48a9b969814f43698223c6e03d6d" + "reference": "f1631f0747ad2a9dd3de8d7873b71f6573f8d0c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/2de0ac9c1d0d48a9b969814f43698223c6e03d6d", - "reference": "2de0ac9c1d0d48a9b969814f43698223c6e03d6d", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/f1631f0747ad2a9dd3de8d7873b71f6573f8d0c2", + "reference": "f1631f0747ad2a9dd3de8d7873b71f6573f8d0c2", "shasum": "" }, "require": { "composer/semver": "^1.4", + "composer/xdebug-handler": "^1.0", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", @@ -688,7 +733,7 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.0.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.0", + "keradus/cli-executor": "^1.1", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^1.0.2", "phpunit/phpunit": "^4.8.35 || ^5.4.3", @@ -696,6 +741,8 @@ }, "suggest": { "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." }, "bin": [ @@ -710,7 +757,6 @@ "tests/Test/AbstractFixerTestCase.php", "tests/Test/AbstractIntegrationCaseFactory.php", "tests/Test/AbstractIntegrationTestCase.php", - "tests/Test/Constraint/SameStringsConstraint.php", "tests/Test/IntegrationCase.php", "tests/Test/IntegrationCaseFactory.php", "tests/Test/IntegrationCaseFactoryInterface.php", @@ -733,7 +779,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2018-03-20T18:04:00+00:00" + "time": "2018-06-02T17:26:04+00:00" }, { "name": "guzzlehttp/guzzle", @@ -853,32 +899,33 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.4.2", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + "reference": "9f83dded91781a01c63574e387eaa769be769115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -908,13 +955,14 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-03-20T17:10:46+00:00" + "time": "2018-12-04T20:46:45+00:00" }, { "name": "herrera-io/json", @@ -1079,16 +1127,16 @@ }, { "name": "jms/metadata", - "version": "1.6.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/metadata.git", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", "shasum": "" }, "require": { @@ -1111,9 +1159,13 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, { "name": "Johannes M. Schmitt", "email": "schmittjoh@gmail.com" @@ -1126,7 +1178,7 @@ "xml", "yaml" ], - "time": "2016-12-05T10:18:33+00:00" + "time": "2018-10-26T12:40:10+00:00" }, { "name": "jms/parser-lib", @@ -1165,22 +1217,22 @@ }, { "name": "jms/serializer", - "version": "1.11.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "e7c53477ff55c21d1b1db7d062edc050a24f465f" + "reference": "00863e1d55b411cc33ad3e1de09a4c8d3aae793c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/e7c53477ff55c21d1b1db7d062edc050a24f465f", - "reference": "e7c53477ff55c21d1b1db7d062edc050a24f465f", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/00863e1d55b411cc33ad3e1de09a4c8d3aae793c", + "reference": "00863e1d55b411cc33ad3e1de09a4c8d3aae793c", "shasum": "" }, "require": { "doctrine/annotations": "^1.0", "doctrine/instantiator": "^1.0.3", - "jms/metadata": "~1.1", + "jms/metadata": "^1.3", "jms/parser-lib": "1.*", "php": "^5.5|^7.0", "phpcollection/phpcollection": "~0.1", @@ -1214,7 +1266,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-1.x": "1.13-dev" } }, "autoload": { @@ -1224,7 +1276,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ { @@ -1245,7 +1297,7 @@ "serialization", "xml" ], - "time": "2018-02-04T17:48:54+00:00" + "time": "2018-07-25T13:58:54+00:00" }, { "name": "justinrainbow/json-schema", @@ -1489,6 +1541,54 @@ ], "time": "2016-12-03T22:08:25+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2018-06-11T23:09:50+00:00" + }, { "name": "nikic/php-parser", "version": "v1.4.1", @@ -1536,33 +1636,29 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.12", + "version": "v9.99.99", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb" + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": "^7" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -1577,10 +1673,11 @@ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ "csprng", + "polyfill", "pseudorandom", "random" ], - "time": "2018-04-04T21:24:14+00:00" + "time": "2018-07-02T15:55:56+00:00" }, { "name": "phpcollection/phpcollection", @@ -1958,16 +2055,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.6", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { @@ -1979,12 +2076,12 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -2017,43 +2114,44 @@ "spy", "stub" ], - "time": "2018-04-18T13:57:24+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "2.2.4", + "version": "3.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + "reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/44cd8e3930e431658d1a5de7d282d5cb37837fd5", + "reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5", "shasum": "" }, "require": { - "php": ">=5.3.3", + "php": "^5.6 || ^7.0", "phpunit/php-file-iterator": "~1.3", "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", + "phpunit/php-token-stream": "^1.4.2", + "sebastian/code-unit-reverse-lookup": "~1.0", "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" + "sebastian/version": "~1.0|~2.0" }, "require-dev": { "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" + "phpunit/phpunit": "~5" }, "suggest": { "ext-dom": "*", - "ext-xdebug": ">=2.2.1", + "ext-xdebug": ">=2.4.0", "ext-xmlwriter": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "3.3.x-dev" } }, "autoload": { @@ -2079,7 +2177,7 @@ "testing", "xunit" ], - "time": "2015-10-06T15:47:00+00:00" + "time": "2016-05-27T16:24:29+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2269,16 +2367,16 @@ }, { "name": "phpunit/phpunit", - "version": "4.8.36", + "version": "5.3.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" + "reference": "08c513bfcab57f3dd72f5214c1c3940439fae7fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/08c513bfcab57f3dd72f5214c1c3940439fae7fe", + "reference": "08c513bfcab57f3dd72f5214c1c3940439fae7fe", "shasum": "" }, "require": { @@ -2287,19 +2385,22 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "php": ">=5.3.3", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", + "phpunit/php-code-coverage": ">=3.3.0,<4.0.0", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.2.2", + "phpunit/phpunit-mock-objects": ">=3.1.0,<3.2.0", + "sebastian/comparator": "~1.1", "sebastian/diff": "~1.2", "sebastian/environment": "~1.3", "sebastian/exporter": "~1.2", "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", + "sebastian/object-enumerator": "~1.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0|~2.0", "symfony/yaml": "~2.1|~3.0" }, "suggest": { @@ -2311,7 +2412,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.8.x-dev" + "dev-master": "5.3.x-dev" } }, "autoload": { @@ -2337,30 +2438,30 @@ "testing", "xunit" ], - "time": "2017-06-21T08:07:12+00:00" + "time": "2016-06-03T09:42:56+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + "reference": "151c96874bff6fe61a25039df60e776613a61489" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/151c96874bff6fe61a25039df60e776613a61489", + "reference": "151c96874bff6fe61a25039df60e776613a61489", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", + "php": ">=5.6", "phpunit/php-text-template": "~1.2", "sebastian/exporter": "~1.2" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "~5" }, "suggest": { "ext-soap": "*" @@ -2368,7 +2469,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -2393,7 +2494,7 @@ "mock", "xunit" ], - "time": "2015-10-02T06:51:40+00:00" + "time": "2016-04-20T14:39:26+00:00" }, { "name": "pimple/pimple", @@ -2491,6 +2592,52 @@ ], "time": "2016-06-16T16:22:20+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.0.0", @@ -2590,6 +2737,139 @@ ], "time": "2016-08-06T14:39:51+00:00" }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, { "name": "sebastian/comparator", "version": "1.2.4", @@ -2874,6 +3154,52 @@ ], "time": "2015-10-12T03:26:01+00:00" }, + { + "name": "sebastian/object-enumerator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2016-01-28T13:25:10+00:00" + }, { "name": "sebastian/recursion-context", "version": "1.0.5", @@ -2927,21 +3253,71 @@ "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "time": "2016-10-03T07:41:43+00:00" }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, { "name": "sebastian/version", - "version": "1.0.6", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", "shasum": "" }, + "require": { + "php": ">=5.6" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -2960,7 +3336,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" + "time": "2016-10-03T07:35:21+00:00" }, { "name": "seld/jsonlint", @@ -3011,18 +3387,60 @@ ], "time": "2018-01-24T12:46:19+00:00" }, + { + "name": "sensiolabs/consul-php-sdk", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/consul-php-sdk.git", + "reference": "2e09b2e36f937438157c2f97e51b32bbd735fd26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/consul-php-sdk/zipball/2e09b2e36f937438157c2f97e51b32bbd735fd26", + "reference": "2e09b2e36f937438157c2f97e51b32bbd735fd26", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "psr/log": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "SensioLabs\\Consul\\": "Consul" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + } + ], + "description": "SDK to talk with consul.io API", + "time": "2017-03-06T13:16:38+00:00" + }, { "name": "symfony/config", - "version": "v2.8.40", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "93bdf96d0e3c9b29740bf9050e7a996b443c8436" + "reference": "7dd5f5040dc04c118d057fb5886563963eb70011" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/93bdf96d0e3c9b29740bf9050e7a996b443c8436", - "reference": "93bdf96d0e3c9b29740bf9050e7a996b443c8436", + "url": "https://api.github.com/repos/symfony/config/zipball/7dd5f5040dc04c118d057fb5886563963eb70011", + "reference": "7dd5f5040dc04c118d057fb5886563963eb70011", "shasum": "" }, "require": { @@ -3066,20 +3484,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-05-01T22:52:40+00:00" + "time": "2018-11-26T09:38:12+00:00" }, { "name": "symfony/console", - "version": "v2.8.40", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7" + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e8e59b74ad1274714dad2748349b55e3e6e630c7", - "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7", + "url": "https://api.github.com/repos/symfony/console/zipball/cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", "shasum": "" }, "require": { @@ -3127,7 +3545,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-05-15T21:17:45+00:00" + "time": "2018-11-20T15:55:20+00:00" }, { "name": "symfony/debug", @@ -3188,16 +3606,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.40", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c" + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", - "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a77e974a5fecb4398833b0709210e3d5e334ffb0", + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0", "shasum": "" }, "require": { @@ -3244,7 +3662,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-04-06T07:35:03+00:00" + "time": "2018-11-21T14:20:20+00:00" }, { "name": "symfony/filesystem", @@ -3297,16 +3715,16 @@ }, { "name": "symfony/finder", - "version": "v2.8.40", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "79764d21163db295f0daf8bd9d9b91f97e65db6a" + "reference": "1444eac52273e345d9b95129bf914639305a9ba4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/79764d21163db295f0daf8bd9d9b91f97e65db6a", - "reference": "79764d21163db295f0daf8bd9d9b91f97e65db6a", + "url": "https://api.github.com/repos/symfony/finder/zipball/1444eac52273e345d9b95129bf914639305a9ba4", + "reference": "1444eac52273e345d9b95129bf914639305a9ba4", "shasum": "" }, "require": { @@ -3342,29 +3760,29 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-05-15T21:17:45+00:00" + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/options-resolver", - "version": "v3.4.10", + "version": "v4.2.2", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "f3109a6aedd20e35c3a33190e932c2b063b7b50e" + "reference": "fbcb106aeee72f3450298bf73324d2cc00d083d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/f3109a6aedd20e35c3a33190e932c2b063b7b50e", - "reference": "f3109a6aedd20e35c3a33190e932c2b063b7b50e", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/fbcb106aeee72f3450298bf73324d2cc00d083d1", + "reference": "fbcb106aeee72f3450298bf73324d2cc00d083d1", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3396,29 +3814,32 @@ "configuration", "options" ], - "time": "2018-01-11T07:56:07+00:00" + "time": "2019-01-03T09:07:35+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "suggest": { + "ext-ctype": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3440,7 +3861,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -3451,20 +3872,20 @@ "polyfill", "portable" ], - "time": "2018-04-30T19:57:29+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", "shasum": "" }, "require": { @@ -3476,7 +3897,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3510,20 +3931,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-09-21T13:07:52+00:00" }, { "name": "symfony/polyfill-php54", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "6c3a2b84c6025e4ea3f6a19feac35408c64b22e1" + "reference": "412977e090c6a8472dc39d50d1beb7d59495a965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/6c3a2b84c6025e4ea3f6a19feac35408c64b22e1", - "reference": "6c3a2b84c6025e4ea3f6a19feac35408c64b22e1", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/412977e090c6a8472dc39d50d1beb7d59495a965", + "reference": "412977e090c6a8472dc39d50d1beb7d59495a965", "shasum": "" }, "require": { @@ -3532,7 +3953,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3568,20 +3989,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-php55", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "a39456128377a85f2c5707fcae458678560cba46" + "reference": "42a4c00a347625ac8853c3358c47eeadc7fd4e96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/a39456128377a85f2c5707fcae458678560cba46", - "reference": "a39456128377a85f2c5707fcae458678560cba46", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/42a4c00a347625ac8853c3358c47eeadc7fd4e96", + "reference": "42a4c00a347625ac8853c3358c47eeadc7fd4e96", "shasum": "" }, "require": { @@ -3591,7 +4012,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3624,30 +4045,30 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-10-31T12:13:01+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "77454693d8f10dd23bb24955cffd2d82db1007a6" + "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/77454693d8f10dd23bb24955cffd2d82db1007a6", - "reference": "77454693d8f10dd23bb24955cffd2d82db1007a6", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/6b88000cdd431cd2e940caa2cb569201f3f84224", + "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224", "shasum": "" }, "require": { - "paragonie/random_compat": "~1.0|~2.0", + "paragonie/random_compat": "~1.0|~2.0|~9.99", "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3683,20 +4104,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-09-21T06:26:08+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.8.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "a4576e282d782ad82397f3e4ec1df8e0f0cafb46" + "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/a4576e282d782ad82397f3e4ec1df8e0f0cafb46", - "reference": "a4576e282d782ad82397f3e4ec1df8e0f0cafb46", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9050816e2ca34a8e916c3a0ae8b9c2fccf68b631", + "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631", "shasum": "" }, "require": { @@ -3705,7 +4126,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3738,20 +4159,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-09-21T13:07:52+00:00" }, { "name": "symfony/process", - "version": "v2.8.40", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28" + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/713952f2ccbcc8342ecdbe1cb313d3e2da8aad28", - "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28", + "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8", + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8", "shasum": "" }, "require": { @@ -3787,20 +4208,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-05-15T21:17:45+00:00" + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/stopwatch", - "version": "v2.8.40", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e" + "reference": "752586c80af8a85aeb74d1ae8202411c68836663" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/57021208ad9830f8f8390c1a9d7bb390f32be89e", - "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/752586c80af8a85aeb74d1ae8202411c68836663", + "reference": "752586c80af8a85aeb74d1ae8202411c68836663", "shasum": "" }, "require": { @@ -3836,7 +4257,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:36:31+00:00" + "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/translation", @@ -3904,16 +4325,16 @@ }, { "name": "symfony/validator", - "version": "v2.8.40", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "96bbfd5534d2e07ba45255bad27ee90d3bc121a3" + "reference": "d5d2090bba3139d8ddb79959fbf516e87238fe3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/96bbfd5534d2e07ba45255bad27ee90d3bc121a3", - "reference": "96bbfd5534d2e07ba45255bad27ee90d3bc121a3", + "url": "https://api.github.com/repos/symfony/validator/zipball/d5d2090bba3139d8ddb79959fbf516e87238fe3a", + "reference": "d5d2090bba3139d8ddb79959fbf516e87238fe3a", "shasum": "" }, "require": { @@ -3974,11 +4395,11 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2018-05-07T06:57:27+00:00" + "time": "2018-11-14T14:06:48+00:00" }, { "name": "symfony/yaml", - "version": "v3.3.16", + "version": "v3.3.18", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -4033,30 +4454,31 @@ }, { "name": "twig/twig", - "version": "v1.35.3", + "version": "v1.37.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f" + "reference": "66be9366c76cbf23e82e7171d47cbfa54a057a62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/66be9366c76cbf23e82e7171d47cbfa54a057a62", + "reference": "66be9366c76cbf23e82e7171d47cbfa54a057a62", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=5.4.0", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "psr/container": "^1.0", "symfony/debug": "^2.7", - "symfony/phpunit-bridge": "^3.3" + "symfony/phpunit-bridge": "^3.4.19|^4.1.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.35-dev" + "dev-master": "1.37-dev" } }, "autoload": { @@ -4085,43 +4507,50 @@ }, { "name": "Twig Team", - "homepage": "http://twig.sensiolabs.org/contributors", + "homepage": "https://twig.symfony.com/contributors", "role": "Contributors" } ], "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "http://twig.sensiolabs.org", + "homepage": "https://twig.symfony.com", "keywords": [ "templating" ], - "time": "2018-03-20T04:25:58+00:00" + "time": "2019-01-14T14:59:29+00:00" }, { "name": "zendframework/zend-cache", - "version": "2.7.2", + "version": "2.8.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-cache.git", - "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039" + "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/c98331b96d3b9d9b24cf32d02660602edb34d039", - "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039", + "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/4983dff629956490c78b88adcc8ece4711d7d8a3", + "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "zendframework/zend-eventmanager": "^2.6.3 || ^3.2", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "provide": { + "psr/cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0" }, "require-dev": { - "phpbench/phpbench": "^0.10.0", - "phpunit/phpunit": "^4.8", + "cache/integration-tests": "^0.16", + "phpbench/phpbench": "^0.13", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-serializer": "^2.6", - "zendframework/zend-session": "^2.6.2" + "zendframework/zend-session": "^2.7.4" }, "suggest": { "ext-apc": "APC or compatible extension, to use the APC storage adapter", @@ -4130,9 +4559,11 @@ "ext-memcache": "Memcache >= 2.0.0 to use the Memcache storage adapter", "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", "ext-mongo": "Mongo, to use MongoDb storage adapter", + "ext-mongodb": "MongoDB, to use the ExtMongoDb storage adapter", "ext-redis": "Redis, to use Redis storage adapter", "ext-wincache": "WinCache, to use the WinCache storage adapter", "ext-xcache": "XCache, to use the XCache storage adapter", + "mongodb/mongodb": "Required for use with the ext-mongodb adapter", "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement", "zendframework/zend-serializer": "Zend\\Serializer component", "zendframework/zend-session": "Zend\\Session component" @@ -4140,8 +4571,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.8.x-dev", + "dev-develop": "2.9.x-dev" }, "zf": { "component": "Zend\\Cache", @@ -4149,6 +4580,9 @@ } }, "autoload": { + "files": [ + "autoload/patternPluginManagerPolyfill.php" + ], "psr-4": { "Zend\\Cache\\": "src/" } @@ -4157,13 +4591,15 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a generic way to cache any data", - "homepage": "https://github.com/zendframework/zend-cache", + "description": "Caching implementation with a variety of storage options, as well as codified caching strategies for callbacks, classes, and output", "keywords": [ + "ZendFramework", "cache", - "zf2" + "psr-16", + "psr-6", + "zf" ], - "time": "2016-12-16T11:35:47+00:00" + "time": "2018-05-01T21:58:00+00:00" }, { "name": "zendframework/zend-config", @@ -4223,26 +4659,26 @@ }, { "name": "zendframework/zend-eventmanager", - "version": "3.0.1", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-eventmanager.git", - "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e" + "reference": "a5e2583a211f73604691586b8406ff7296a946dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/5c80bdee0e952be112dcec0968bad770082c3a6e", - "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/a5e2583a211f73604691586b8406ff7296a946dd", + "reference": "a5e2583a211f73604691586b8406ff7296a946dd", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0" + "php": "^5.6 || ^7.0" }, "require-dev": { "athletic/athletic": "^0.1", "container-interop/container-interop": "^1.1.0", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-stdlib": "^2.7.3 || ^3.0" }, "suggest": { @@ -4252,8 +4688,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev", - "dev-develop": "3.1-dev" + "dev-master": "3.2-dev", + "dev-develop": "3.3-dev" } }, "autoload": { @@ -4273,35 +4709,40 @@ "events", "zf2" ], - "time": "2016-02-18T20:53:00+00:00" + "time": "2018-04-25T15:33:34+00:00" }, { "name": "zendframework/zend-filter", - "version": "2.7.2", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175" + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/b8d0ff872f126631bf63a932e33aa2d22d467175", - "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "php": "^5.6 || ^7.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "conflict": { + "zendframework/zend-validator": "<2.10.1" }, "require-dev": { - "pear/archive_tar": "^1.4", - "phpunit/phpunit": "^6.0.10 || ^5.7.17", + "pear/archive_tar": "^1.4.3", + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "psr/http-factory": "^1.0", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-crypt": "^2.6 || ^3.0", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-uri": "^2.5" + "zendframework/zend-crypt": "^3.2.1", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", + "zendframework/zend-uri": "^2.6" }, "suggest": { + "psr/http-factory-implementation": "psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters", "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters", "zendframework/zend-i18n": "Zend\\I18n component for filters depending on i18n functionality", "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for using the filter chain functionality", @@ -4310,8 +4751,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Filter", @@ -4328,12 +4769,12 @@ "BSD-3-Clause" ], "description": "provides a set of commonly needed data filters", - "homepage": "https://github.com/zendframework/zend-filter", "keywords": [ + "ZendFramework", "filter", - "zf2" + "zf" ], - "time": "2017-05-17T20:56:17+00:00" + "time": "2018-12-17T16:00:04+00:00" }, { "name": "zendframework/zend-hydrator", @@ -4395,26 +4836,26 @@ }, { "name": "zendframework/zend-i18n", - "version": "2.7.3", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "b2db0d8246a865c659f93199f90f5fc2cd2f3cd8" + "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/b2db0d8246a865c659f93199f90f5fc2cd2f3cd8", - "reference": "b2db0d8246a865c659f93199f90f5fc2cd2f3cd8", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/6d69af5a04e1a4de7250043cb1322f077a0cdb7f", + "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", + "php": "^5.6 || ^7.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", "zendframework/zend-cache": "^2.6.1", + "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", "zendframework/zend-filter": "^2.6.1", @@ -4436,8 +4877,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\I18n", @@ -4453,49 +4894,45 @@ "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-i18n", + "description": "Provide translations for your application, and filter and validate internationalized values", "keywords": [ + "ZendFramework", "i18n", - "zf2" + "zf" ], - "time": "2016-06-07T21:08:30+00:00" + "time": "2018-05-16T16:39:13+00:00" }, { "name": "zendframework/zend-json", - "version": "2.6.1", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-json.git", - "reference": "4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28" + "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-json/zipball/4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28", - "reference": "4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28", + "url": "https://api.github.com/repos/zendframework/zend-json/zipball/4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", + "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0" + "php": "^5.6 || ^7.0" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "zendframework/zend-http": "^2.5.4", - "zendframework/zend-server": "^2.6.1", - "zendframework/zend-stdlib": "^2.5 || ^3.0", - "zendframework/zendxml": "^1.0.2" + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" }, "suggest": { - "zendframework/zend-http": "Zend\\Http component, required to use Zend\\Json\\Server", - "zendframework/zend-server": "Zend\\Server component, required to use Zend\\Json\\Server", - "zendframework/zend-stdlib": "Zend\\Stdlib component, for use with caching Zend\\Json\\Server responses", - "zendframework/zendxml": "To support Zend\\Json\\Json::fromXml() usage" + "zendframework/zend-json-server": "For implementing JSON-RPC servers", + "zendframework/zend-xml2json": "For converting XML documents to JSON" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev", - "dev-develop": "2.7-dev" + "dev-master": "3.1.x-dev", + "dev-develop": "3.2.x-dev" } }, "autoload": { @@ -4508,47 +4945,47 @@ "BSD-3-Clause" ], "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", - "homepage": "https://github.com/zendframework/zend-json", "keywords": [ + "ZendFramework", "json", - "zf2" + "zf" ], - "time": "2016-02-04T21:20:26+00:00" + "time": "2018-01-04T17:51:34+00:00" }, { "name": "zendframework/zend-serializer", - "version": "2.7.2", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-serializer.git", - "reference": "95385c2342fc335d5164eb95ac3ca230aa51223b" + "reference": "0172690db48d8935edaf625c4cba38b79719892c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/95385c2342fc335d5164eb95ac3ca230aa51223b", - "reference": "95385c2342fc335d5164eb95ac3ca230aa51223b", + "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/0172690db48d8935edaf625c4cba38b79719892c", + "reference": "0172690db48d8935edaf625c4cba38b79719892c", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-json": "^2.5", + "php": "^5.6 || ^7.0", + "zendframework/zend-json": "^2.5 || ^3.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "^4.5", - "zendframework/zend-math": "^2.6", + "phpunit/phpunit": "^5.7.25 || ^6.4.4", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-math": "^2.6 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { - "zendframework/zend-math": "(^2.6) To support Python Pickle serialization", - "zendframework/zend-servicemanager": "To support plugin manager support" + "zendframework/zend-math": "(^2.6 || ^3.0) To support Python Pickle serialization", + "zendframework/zend-servicemanager": "(^2.7.5 || ^3.0.3) To support plugin manager support" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Serializer", @@ -4565,25 +5002,25 @@ "BSD-3-Clause" ], "description": "provides an adapter based interface to simply generate storable representation of PHP types by different facilities, and recover", - "homepage": "https://github.com/zendframework/zend-serializer", "keywords": [ + "ZendFramework", "serializer", - "zf2" + "zf" ], - "time": "2016-05-11T16:05:56+00:00" + "time": "2018-05-14T18:45:18+00:00" }, { "name": "zendframework/zend-servicemanager", - "version": "2.7.10", + "version": "2.7.11", "source": { "type": "git", "url": "https://github.com/zendframework/zend-servicemanager.git", - "reference": "ba7069c94c9af93122be9fa31cddd37f7707d5b4" + "reference": "99ec9ed5d0f15aed9876433c74c2709eb933d4c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/ba7069c94c9af93122be9fa31cddd37f7707d5b4", - "reference": "ba7069c94c9af93122be9fa31cddd37f7707d5b4", + "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/99ec9ed5d0f15aed9876433c74c2709eb933d4c7", + "reference": "99ec9ed5d0f15aed9876433c74c2709eb933d4c7", "shasum": "" }, "require": { @@ -4622,7 +5059,7 @@ "servicemanager", "zf2" ], - "time": "2017-12-05T16:27:36+00:00" + "time": "2018-06-22T14:49:54+00:00" }, { "name": "zendframework/zend-stdlib", diff --git a/src/LaunchDarkly/ConsulFeatureRequester.php b/src/LaunchDarkly/ConsulFeatureRequester.php new file mode 100644 index 000000000..7aac2e0b7 --- /dev/null +++ b/src/LaunchDarkly/ConsulFeatureRequester.php @@ -0,0 +1,70 @@ +_kvClient = $sf->get('kv'); + + $prefix = isset($options['consul_prefix']) ? $options['consul_prefix'] : 'launchdarkly'; + $this->_prefix = $prefix . '/'; + } + + protected function readItemString($namespace, $key) + { + try { + $resp = $this->_kvClient->get($this->makeKey($namespace, $key)); + } catch (ClientException $e) { + if ($e->getCode() === 404) { + return null; + } + throw $e; + } + $results = $resp->json(); + if (count($results) != 1) { + return null; + } + return base64_decode($results[0]['Value']); + } + + protected function readItemStringList($namespace) + { + try { + $resp = $this->_kvClient->get($this->makeKey($namespace, ''), array('recurse' => true)); + } catch (ClientException $e) { + if ($e->getCode() === 404) { + return array(); + } + throw $e; + } + $results = $resp->json(); + $ret = array(); + foreach ($results as $result) { + $ret[] = base64_decode($result['Value']); + } + return $ret; + } + + private function makeKey($namespace, $key) + { + return $this->_prefix . $namespace . '/' . $key; + } +} diff --git a/src/LaunchDarkly/FeatureRequesterBase.php b/src/LaunchDarkly/FeatureRequesterBase.php index 122eddbc6..9dd2b08a8 100644 --- a/src/LaunchDarkly/FeatureRequesterBase.php +++ b/src/LaunchDarkly/FeatureRequesterBase.php @@ -167,6 +167,9 @@ protected function getJsonItemList($namespace) $values = json_decode($raw, true); } else { $values = $this->readItemStringList($namespace); + if (!$values) { + $values = array(); + } $this->putCachedString($namespace, self::ALL_ITEMS_KEY, json_encode($values)); } foreach ($values as $i => $s) { diff --git a/tests/ConsulFeatureRequesterTest.php b/tests/ConsulFeatureRequesterTest.php new file mode 100644 index 000000000..f73fa647b --- /dev/null +++ b/tests/ConsulFeatureRequesterTest.php @@ -0,0 +1,50 @@ +get('kv'); + } + + protected function makeRequester() + { + $options = array( + 'consul_prefix' => self::PREFIX + ); + return new ConsulFeatureRequester('', '', $options); + } + + protected function putItem($namespace, $key, $version, $json) + { + self::$kvClient->put(self::PREFIX . '/' . $namespace . '/' . $key, $json); + } + + protected function deleteExistingData() + { + try { + $resp = self::$kvClient->get(self::PREFIX . '/', array('keys' => true, 'recurse' => true)); + } catch (ClientException $e) { + if ($e->getCode() === 404) { + return; + } + throw $e; + } + $results = $resp->json(); + foreach ($results as $key) { + self::$kvClient->delete($key); + } + } +} From 38877323298d67c7183d5452b5f740ae7f63b976 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 16:47:50 -0800 Subject: [PATCH 38/72] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb6f5cc7e..daddf39fd 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ a Redis cache operating in your production environment. The ld-relay offers many Using flag data from a file --------------------------- -For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`FileDataFeatureRequester`](https://github.com/launchdarkly/node-client/blob/master/FileDataFeatureRequester.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). +For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`FileDataFeatureRequester`](https://github.com/launchdarkly/php-client/blob/master/FileDataFeatureRequester.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). Testing ------- From 10e0dafd0e277a599abbc09053e716d0027df21d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 16:50:24 -0800 Subject: [PATCH 39/72] regenerate lockfile using PHP 5.5 --- composer.lock | 636 ++++++++++++++------------------------------------ 1 file changed, 169 insertions(+), 467 deletions(-) diff --git a/composer.lock b/composer.lock index b011c942d..71f52d301 100644 --- a/composer.lock +++ b/composer.lock @@ -135,16 +135,16 @@ "packages-dev": [ { "name": "aws/aws-sdk-php", - "version": "3.86.3", + "version": "3.87.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "0f38d6b5625c4e6ea8891abd6a75e3d35e3ef465" + "reference": "415d86ea583aa5bb69ec6ab83f81ea501af0c4da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0f38d6b5625c4e6ea8891abd6a75e3d35e3ef465", - "reference": "0f38d6b5625c4e6ea8891abd6a75e3d35e3ef465", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/415d86ea583aa5bb69ec6ab83f81ea501af0c4da", + "reference": "415d86ea583aa5bb69ec6ab83f81ea501af0c4da", "shasum": "" }, "require": { @@ -214,7 +214,7 @@ "s3", "sdk" ], - "time": "2019-01-21T22:52:28+00:00" + "time": "2019-01-23T23:06:01+00:00" }, { "name": "cilex/cilex", @@ -473,35 +473,35 @@ }, { "name": "doctrine/annotations", - "version": "v1.6.0", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5" + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", - "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535", "shasum": "" }, "require": { "doctrine/lexer": "1.*", - "php": "^7.1" + "php": ">=5.3.2" }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "4.*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + "psr-0": { + "Doctrine\\Common\\Annotations\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", @@ -537,36 +537,36 @@ "docblock", "parser" ], - "time": "2017-12-06T07:11:42+00:00" + "time": "2015-08-31T12:32:49+00:00" }, { "name": "doctrine/instantiator", - "version": "1.1.0", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=5.3,<8.0-DEV" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -591,7 +591,7 @@ "constructor", "instantiate" ], - "time": "2017-07-22T11:58:36+00:00" + "time": "2015-06-14T21:17:01+00:00" }, { "name": "doctrine/lexer", @@ -1541,54 +1541,6 @@ ], "time": "2016-12-03T22:08:25+00:00" }, - { - "name": "myclabs/deep-copy", - "version": "1.8.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2018-06-11T23:09:50+00:00" - }, { "name": "nikic/php-parser", "version": "v1.4.1", @@ -1636,29 +1588,33 @@ }, { "name": "paragonie/random_compat", - "version": "v9.99.99", + "version": "v2.0.18", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + "reference": "0a58ef6e3146256cc3dc7cc393927bcc7d1b72db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/0a58ef6e3146256cc3dc7cc393927bcc7d1b72db", + "reference": "0a58ef6e3146256cc3dc7cc393927bcc7d1b72db", "shasum": "" }, "require": { - "php": "^7" + "php": ">=5.2.0" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" + "phpunit/phpunit": "4.*|5.*" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -1677,7 +1633,7 @@ "pseudorandom", "random" ], - "time": "2018-07-02T15:55:56+00:00" + "time": "2019-01-03T20:59:08+00:00" }, { "name": "phpcollection/phpcollection", @@ -2118,40 +2074,39 @@ }, { "name": "phpunit/php-code-coverage", - "version": "3.3.3", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5" + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/44cd8e3930e431658d1a5de7d282d5cb37837fd5", - "reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "php": ">=5.3.3", "phpunit/php-file-iterator": "~1.3", "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "^1.4.2", - "sebastian/code-unit-reverse-lookup": "~1.0", + "phpunit/php-token-stream": "~1.3", "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0|~2.0" + "sebastian/version": "~1.0" }, "require-dev": { "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~5" + "phpunit/phpunit": "~4" }, "suggest": { "ext-dom": "*", - "ext-xdebug": ">=2.4.0", + "ext-xdebug": ">=2.2.1", "ext-xmlwriter": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3.x-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { @@ -2177,7 +2132,7 @@ "testing", "xunit" ], - "time": "2016-05-27T16:24:29+00:00" + "time": "2015-10-06T15:47:00+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2367,16 +2322,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.3.5", + "version": "4.8.36", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "08c513bfcab57f3dd72f5214c1c3940439fae7fe" + "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/08c513bfcab57f3dd72f5214c1c3940439fae7fe", - "reference": "08c513bfcab57f3dd72f5214c1c3940439fae7fe", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", + "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", "shasum": "" }, "require": { @@ -2385,22 +2340,19 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", + "php": ">=5.3.3", "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": ">=3.3.0,<4.0.0", + "phpunit/php-code-coverage": "~2.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": ">=3.1.0,<3.2.0", - "sebastian/comparator": "~1.1", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.2.2", "sebastian/diff": "~1.2", "sebastian/environment": "~1.3", "sebastian/exporter": "~1.2", "sebastian/global-state": "~1.0", - "sebastian/object-enumerator": "~1.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0|~2.0", + "sebastian/version": "~1.0", "symfony/yaml": "~2.1|~3.0" }, "suggest": { @@ -2412,7 +2364,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.3.x-dev" + "dev-master": "4.8.x-dev" } }, "autoload": { @@ -2438,30 +2390,30 @@ "testing", "xunit" ], - "time": "2016-06-03T09:42:56+00:00" + "time": "2017-06-21T08:07:12+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.1.3", + "version": "2.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "151c96874bff6fe61a25039df60e776613a61489" + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/151c96874bff6fe61a25039df60e776613a61489", - "reference": "151c96874bff6fe61a25039df60e776613a61489", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", - "php": ">=5.6", + "php": ">=5.3.3", "phpunit/php-text-template": "~1.2", "sebastian/exporter": "~1.2" }, "require-dev": { - "phpunit/phpunit": "~5" + "phpunit/phpunit": "~4.4" }, "suggest": { "ext-soap": "*" @@ -2469,7 +2421,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "2.3.x-dev" } }, "autoload": { @@ -2494,7 +2446,7 @@ "mock", "xunit" ], - "time": "2016-04-20T14:39:26+00:00" + "time": "2015-10-02T06:51:40+00:00" }, { "name": "pimple/pimple", @@ -2592,52 +2544,6 @@ ], "time": "2016-06-16T16:22:20+00:00" }, - { - "name": "psr/cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "time": "2016-08-06T20:24:11+00:00" - }, { "name": "psr/container", "version": "1.0.0", @@ -2737,54 +2643,6 @@ ], "time": "2016-08-06T14:39:51+00:00" }, - { - "name": "psr/simple-cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\SimpleCache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interfaces for simple caching", - "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" - ], - "time": "2017-10-23T01:57:42+00:00" - }, { "name": "ralouphie/getallheaders", "version": "2.0.5", @@ -2825,51 +2683,6 @@ "description": "A polyfill for getallheaders.", "time": "2016-02-11T07:05:27+00:00" }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" - }, { "name": "sebastian/comparator", "version": "1.2.4", @@ -3154,52 +2967,6 @@ ], "time": "2015-10-12T03:26:01+00:00" }, - { - "name": "sebastian/object-enumerator", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2016-01-28T13:25:10+00:00" - }, { "name": "sebastian/recursion-context", "version": "1.0.5", @@ -3253,71 +3020,21 @@ "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "time": "2016-10-03T07:41:43+00:00" }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" - }, { "name": "sebastian/version", - "version": "2.0.1", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", "shasum": "" }, - "require": { - "php": ">=5.6" - }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -3336,7 +3053,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" + "time": "2015-06-21T13:59:46+00:00" }, { "name": "seld/jsonlint", @@ -3764,25 +3481,25 @@ }, { "name": "symfony/options-resolver", - "version": "v4.2.2", + "version": "v3.4.21", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "fbcb106aeee72f3450298bf73324d2cc00d083d1" + "reference": "8a10e36ffd04c0c551051594952304d34ecece71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/fbcb106aeee72f3450298bf73324d2cc00d083d1", - "reference": "fbcb106aeee72f3450298bf73324d2cc00d083d1", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/8a10e36ffd04c0c551051594952304d34ecece71", + "reference": "8a10e36ffd04c0c551051594952304d34ecece71", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -3814,7 +3531,7 @@ "configuration", "options" ], - "time": "2019-01-03T09:07:35+00:00" + "time": "2019-01-01T13:45:19+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4520,37 +4237,30 @@ }, { "name": "zendframework/zend-cache", - "version": "2.8.2", + "version": "2.7.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-cache.git", - "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3" + "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/4983dff629956490c78b88adcc8ece4711d7d8a3", - "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3", + "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/c98331b96d3b9d9b24cf32d02660602edb34d039", + "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", - "zendframework/zend-eventmanager": "^2.6.3 || ^3.2", - "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", - "zendframework/zend-stdlib": "^2.7.7 || ^3.1" - }, - "provide": { - "psr/cache-implementation": "1.0", - "psr/simple-cache-implementation": "1.0" + "php": "^5.5 || ^7.0", + "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", + "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "cache/integration-tests": "^0.16", - "phpbench/phpbench": "^0.13", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "phpbench/phpbench": "^0.10.0", + "phpunit/phpunit": "^4.8", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-serializer": "^2.6", - "zendframework/zend-session": "^2.7.4" + "zendframework/zend-session": "^2.6.2" }, "suggest": { "ext-apc": "APC or compatible extension, to use the APC storage adapter", @@ -4559,11 +4269,9 @@ "ext-memcache": "Memcache >= 2.0.0 to use the Memcache storage adapter", "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", "ext-mongo": "Mongo, to use MongoDb storage adapter", - "ext-mongodb": "MongoDB, to use the ExtMongoDb storage adapter", "ext-redis": "Redis, to use Redis storage adapter", "ext-wincache": "WinCache, to use the WinCache storage adapter", "ext-xcache": "XCache, to use the XCache storage adapter", - "mongodb/mongodb": "Required for use with the ext-mongodb adapter", "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement", "zendframework/zend-serializer": "Zend\\Serializer component", "zendframework/zend-session": "Zend\\Session component" @@ -4571,8 +4279,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" }, "zf": { "component": "Zend\\Cache", @@ -4580,9 +4288,6 @@ } }, "autoload": { - "files": [ - "autoload/patternPluginManagerPolyfill.php" - ], "psr-4": { "Zend\\Cache\\": "src/" } @@ -4591,15 +4296,13 @@ "license": [ "BSD-3-Clause" ], - "description": "Caching implementation with a variety of storage options, as well as codified caching strategies for callbacks, classes, and output", + "description": "provides a generic way to cache any data", + "homepage": "https://github.com/zendframework/zend-cache", "keywords": [ - "ZendFramework", "cache", - "psr-16", - "psr-6", - "zf" + "zf2" ], - "time": "2018-05-01T21:58:00+00:00" + "time": "2016-12-16T11:35:47+00:00" }, { "name": "zendframework/zend-config", @@ -4659,26 +4362,26 @@ }, { "name": "zendframework/zend-eventmanager", - "version": "3.2.1", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-eventmanager.git", - "reference": "a5e2583a211f73604691586b8406ff7296a946dd" + "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/a5e2583a211f73604691586b8406ff7296a946dd", - "reference": "a5e2583a211f73604691586b8406ff7296a946dd", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/5c80bdee0e952be112dcec0968bad770082c3a6e", + "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.5 || ^7.0" }, "require-dev": { "athletic/athletic": "^0.1", "container-interop/container-interop": "^1.1.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", - "zendframework/zend-coding-standard": "~1.0.0", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "^2.0", "zendframework/zend-stdlib": "^2.7.3 || ^3.0" }, "suggest": { @@ -4688,8 +4391,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev", - "dev-develop": "3.3-dev" + "dev-master": "3.0-dev", + "dev-develop": "3.1-dev" } }, "autoload": { @@ -4709,40 +4412,35 @@ "events", "zf2" ], - "time": "2018-04-25T15:33:34+00:00" + "time": "2016-02-18T20:53:00+00:00" }, { "name": "zendframework/zend-filter", - "version": "2.9.1", + "version": "2.7.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f" + "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", - "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/b8d0ff872f126631bf63a932e33aa2d22d467175", + "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "zendframework/zend-stdlib": "^2.7.7 || ^3.1" - }, - "conflict": { - "zendframework/zend-validator": "<2.10.1" + "php": "^5.5 || ^7.0", + "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "pear/archive_tar": "^1.4.3", - "phpunit/phpunit": "^5.7.23 || ^6.4.3", - "psr/http-factory": "^1.0", + "pear/archive_tar": "^1.4", + "phpunit/phpunit": "^6.0.10 || ^5.7.17", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-crypt": "^3.2.1", - "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", - "zendframework/zend-uri": "^2.6" + "zendframework/zend-crypt": "^2.6 || ^3.0", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", + "zendframework/zend-uri": "^2.5" }, "suggest": { - "psr/http-factory-implementation": "psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters", "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters", "zendframework/zend-i18n": "Zend\\I18n component for filters depending on i18n functionality", "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for using the filter chain functionality", @@ -4751,8 +4449,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.9.x-dev", - "dev-develop": "2.10.x-dev" + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" }, "zf": { "component": "Zend\\Filter", @@ -4769,12 +4467,12 @@ "BSD-3-Clause" ], "description": "provides a set of commonly needed data filters", + "homepage": "https://github.com/zendframework/zend-filter", "keywords": [ - "ZendFramework", "filter", - "zf" + "zf2" ], - "time": "2018-12-17T16:00:04+00:00" + "time": "2017-05-17T20:56:17+00:00" }, { "name": "zendframework/zend-hydrator", @@ -4836,26 +4534,26 @@ }, { "name": "zendframework/zend-i18n", - "version": "2.9.0", + "version": "2.7.3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f" + "reference": "b2db0d8246a865c659f93199f90f5fc2cd2f3cd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/6d69af5a04e1a4de7250043cb1322f077a0cdb7f", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/b2db0d8246a865c659f93199f90f5fc2cd2f3cd8", + "reference": "b2db0d8246a865c659f93199f90f5fc2cd2f3cd8", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "php": "^5.5 || ^7.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0", "zendframework/zend-cache": "^2.6.1", - "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", "zendframework/zend-filter": "^2.6.1", @@ -4877,8 +4575,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.9.x-dev", - "dev-develop": "2.10.x-dev" + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" }, "zf": { "component": "Zend\\I18n", @@ -4894,45 +4592,49 @@ "license": [ "BSD-3-Clause" ], - "description": "Provide translations for your application, and filter and validate internationalized values", + "homepage": "https://github.com/zendframework/zend-i18n", "keywords": [ - "ZendFramework", "i18n", - "zf" + "zf2" ], - "time": "2018-05-16T16:39:13+00:00" + "time": "2016-06-07T21:08:30+00:00" }, { "name": "zendframework/zend-json", - "version": "3.1.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-json.git", - "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c" + "reference": "4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-json/zipball/4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", - "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", + "url": "https://api.github.com/repos/zendframework/zend-json/zipball/4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28", + "reference": "4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.5 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3", - "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0", + "zendframework/zend-http": "^2.5.4", + "zendframework/zend-server": "^2.6.1", + "zendframework/zend-stdlib": "^2.5 || ^3.0", + "zendframework/zendxml": "^1.0.2" }, "suggest": { - "zendframework/zend-json-server": "For implementing JSON-RPC servers", - "zendframework/zend-xml2json": "For converting XML documents to JSON" + "zendframework/zend-http": "Zend\\Http component, required to use Zend\\Json\\Server", + "zendframework/zend-server": "Zend\\Server component, required to use Zend\\Json\\Server", + "zendframework/zend-stdlib": "Zend\\Stdlib component, for use with caching Zend\\Json\\Server responses", + "zendframework/zendxml": "To support Zend\\Json\\Json::fromXml() usage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev", - "dev-develop": "3.2.x-dev" + "dev-master": "2.6-dev", + "dev-develop": "2.7-dev" } }, "autoload": { @@ -4945,47 +4647,47 @@ "BSD-3-Clause" ], "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", + "homepage": "https://github.com/zendframework/zend-json", "keywords": [ - "ZendFramework", "json", - "zf" + "zf2" ], - "time": "2018-01-04T17:51:34+00:00" + "time": "2016-02-04T21:20:26+00:00" }, { "name": "zendframework/zend-serializer", - "version": "2.9.0", + "version": "2.7.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-serializer.git", - "reference": "0172690db48d8935edaf625c4cba38b79719892c" + "reference": "95385c2342fc335d5164eb95ac3ca230aa51223b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/0172690db48d8935edaf625c4cba38b79719892c", - "reference": "0172690db48d8935edaf625c4cba38b79719892c", + "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/95385c2342fc335d5164eb95ac3ca230aa51223b", + "reference": "95385c2342fc335d5164eb95ac3ca230aa51223b", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "zendframework/zend-json": "^2.5 || ^3.0", + "php": "^5.5 || ^7.0", + "zendframework/zend-json": "^2.5", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.25 || ^6.4.4", - "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-math": "^2.6 || ^3.0", + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "^4.5", + "zendframework/zend-math": "^2.6", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { - "zendframework/zend-math": "(^2.6 || ^3.0) To support Python Pickle serialization", - "zendframework/zend-servicemanager": "(^2.7.5 || ^3.0.3) To support plugin manager support" + "zendframework/zend-math": "(^2.6) To support Python Pickle serialization", + "zendframework/zend-servicemanager": "To support plugin manager support" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.9.x-dev", - "dev-develop": "2.10.x-dev" + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" }, "zf": { "component": "Zend\\Serializer", @@ -5002,12 +4704,12 @@ "BSD-3-Clause" ], "description": "provides an adapter based interface to simply generate storable representation of PHP types by different facilities, and recover", + "homepage": "https://github.com/zendframework/zend-serializer", "keywords": [ - "ZendFramework", "serializer", - "zf" + "zf2" ], - "time": "2018-05-14T18:45:18+00:00" + "time": "2016-05-11T16:05:56+00:00" }, { "name": "zendframework/zend-servicemanager", From 200229cd5a365e3075db346fd14fc17da829de88 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 17:22:21 -0800 Subject: [PATCH 40/72] we need at least version 2.1 of the Consul package --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 202ffc9d7..6b89f748b 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "phpdocumentor/phpdocumentor": "^2.0", "phpunit/phpunit": ">=4.8.26 <5.4", "predis/predis": "^1.0", - "sensiolabs/consul-php-sdk": "^2.0", + "sensiolabs/consul-php-sdk": ">=2.1 <3.0", "zendframework/zend-serializer": "^2.7" }, "suggest": { @@ -34,7 +34,7 @@ "kevinrob/guzzle-cache-middleware": "(^1.4.1) Recommended for performance when using the default FeatureRequester", "predis/predis": "(^1.0) Required when using LDDFeatureRequester", "aws/aws-sdk-php": "(^3.86) Required when using DynamoDbFeatureRequester", - "sensiolabs/consul-php-sdk": "(^2.0) Required when using ConsulFeatureRequester" + "sensiolabs/consul-php-sdk": "(>=2.1 <3.0) Required when using ConsulFeatureRequester" }, "autoload": { "psr-4": { From 1a7cd195c79033e20d57361cfef5451caeaf5312 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 17:27:17 -0800 Subject: [PATCH 41/72] update lockfile --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 71f52d301..32c268446 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "9c8af48dc263ef3f40fd7e3ca3c74caa", + "content-hash": "3564a364234f8f8f69e273002225f9b0", "packages": [ { "name": "monolog/monolog", From 313a4730f948ad1c74e5f0c04f437e197c1e86ef Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 17:47:50 -0800 Subject: [PATCH 42/72] rm comment --- src/LaunchDarkly/ConsulFeatureRequester.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/LaunchDarkly/ConsulFeatureRequester.php b/src/LaunchDarkly/ConsulFeatureRequester.php index 7aac2e0b7..daa0454c7 100644 --- a/src/LaunchDarkly/ConsulFeatureRequester.php +++ b/src/LaunchDarkly/ConsulFeatureRequester.php @@ -15,8 +15,6 @@ public function __construct($baseUri, $sdkKey, $options) { parent::__construct($baseUri, $sdkKey, $options); - // TODO: config - $consulOpts = isset($options['consul_options']) ? $options['consul_options'] : array(); if (isset($options['consul_uri'])) { $consulOpts['base_uri'] = $options['consul_uri']; From 2580f6fca69c32adcff1edb11991c21842e5e5eb Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 17:50:09 -0800 Subject: [PATCH 43/72] remove lockfile --- .circleci/config.yml | 9 +- composer.lock | 4673 ------------------------------------------ 2 files changed, 3 insertions(+), 4679 deletions(-) delete mode 100644 composer.lock diff --git a/.circleci/config.yml b/.circleci/config.yml index af78ad3e9..5319ef246 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,20 +18,17 @@ php-docker-template: &php-docker-template name: validate composer.json command: composer validate - run: - name: install current dependencies + name: install dependencies command: composer install --no-progress - run: - name: run tests with current dependency versions + name: run tests with highest compatible 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 + name: run tests with lowest compatible 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: | diff --git a/composer.lock b/composer.lock deleted file mode 100644 index d7eb9bb67..000000000 --- a/composer.lock +++ /dev/null @@ -1,4673 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "b7b9d1818e9117bf34ae546c08c5593e", - "packages": [ - { - "name": "monolog/monolog", - "version": "1.23.0", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" - }, - "provide": { - "psr/log-implementation": "1.0.0" - }, - "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", - "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "jakub-onderka/php-parallel-lint": "0.9", - "php-amqplib/php-amqplib": "~2.4", - "php-console/php-console": "^3.1.3", - "phpunit/phpunit": "~4.5", - "phpunit/phpunit-mock-objects": "2.3.0", - "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "^5.3|^6.0" - }, - "suggest": { - "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", - "doctrine/couchdb": "Allow sending log messages to a CouchDB server", - "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", - "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "php-console/php-console": "Allow sending log messages to Google Chrome", - "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Monolog\\": "src/Monolog" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "http://github.com/Seldaek/monolog", - "keywords": [ - "log", - "logging", - "psr-3" - ], - "time": "2017-06-19T01:22:40+00:00" - }, - { - "name": "psr/log", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "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", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2016-10-10T12:19:37+00:00" - } - ], - "packages-dev": [ - { - "name": "cilex/cilex", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/Cilex/Cilex.git", - "reference": "7acd965a609a56d0345e8b6071c261fbdb926cb5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Cilex/Cilex/zipball/7acd965a609a56d0345e8b6071c261fbdb926cb5", - "reference": "7acd965a609a56d0345e8b6071c261fbdb926cb5", - "shasum": "" - }, - "require": { - "cilex/console-service-provider": "1.*", - "php": ">=5.3.3", - "pimple/pimple": "~1.0", - "symfony/finder": "~2.1", - "symfony/process": "~2.1" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*", - "symfony/validator": "~2.1" - }, - "suggest": { - "monolog/monolog": ">=1.0.0", - "symfony/validator": ">=1.0.0", - "symfony/yaml": ">=1.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "Cilex": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "description": "The PHP micro-framework for Command line tools based on the Symfony2 Components", - "homepage": "http://cilex.github.com", - "keywords": [ - "cli", - "microframework" - ], - "time": "2014-03-29T14:03:13+00:00" - }, - { - "name": "cilex/console-service-provider", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/Cilex/console-service-provider.git", - "reference": "25ee3d1875243d38e1a3448ff94bdf944f70d24e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Cilex/console-service-provider/zipball/25ee3d1875243d38e1a3448ff94bdf944f70d24e", - "reference": "25ee3d1875243d38e1a3448ff94bdf944f70d24e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "pimple/pimple": "1.*@dev", - "symfony/console": "~2.1" - }, - "require-dev": { - "cilex/cilex": "1.*@dev", - "silex/silex": "1.*@dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "Cilex\\Provider\\Console": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Beau Simensen", - "email": "beau@dflydev.com", - "homepage": "http://beausimensen.com" - }, - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "description": "Console Service Provider", - "keywords": [ - "cilex", - "console", - "pimple", - "service-provider", - "silex" - ], - "time": "2012-12-19T10:50:58+00:00" - }, - { - "name": "composer/semver", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "time": "2016-08-30T16:08:34+00:00" - }, - { - "name": "container-interop/container-interop", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/container-interop/container-interop.git", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "shasum": "" - }, - "require": { - "psr/container": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Interop\\Container\\": "src/Interop/Container/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", - "homepage": "https://github.com/container-interop/container-interop", - "time": "2017-02-14T19:40:03+00:00" - }, - { - "name": "doctrine/annotations", - "version": "v1.2.7", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", - "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "require-dev": { - "doctrine/cache": "1.*", - "phpunit/phpunit": "4.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Annotations\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "time": "2015-08-31T12:32:49+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", - "shasum": "" - }, - "require": { - "php": ">=5.3,<8.0-DEV" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2015-06-14T21:17:01+00:00" - }, - { - "name": "doctrine/lexer", - "version": "v1.0.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Lexer\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "lexer", - "parser" - ], - "time": "2014-09-09T13:34:57+00:00" - }, - { - "name": "erusev/parsedown", - "version": "1.7.1", - "source": { - "type": "git", - "url": "https://github.com/erusev/parsedown.git", - "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1", - "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35" - }, - "type": "library", - "autoload": { - "psr-0": { - "Parsedown": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Emanuil Rusev", - "email": "hello@erusev.com", - "homepage": "http://erusev.com" - } - ], - "description": "Parser for Markdown.", - "homepage": "http://parsedown.org", - "keywords": [ - "markdown", - "parser" - ], - "time": "2018-03-08T01:11:30+00:00" - }, - { - "name": "friendsofphp/php-cs-fixer", - "version": "v2.2.19", - "source": { - "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "2de0ac9c1d0d48a9b969814f43698223c6e03d6d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/2de0ac9c1d0d48a9b969814f43698223c6e03d6d", - "reference": "2de0ac9c1d0d48a9b969814f43698223c6e03d6d", - "shasum": "" - }, - "require": { - "composer/semver": "^1.4", - "doctrine/annotations": "^1.2", - "ext-json": "*", - "ext-tokenizer": "*", - "php": "^5.3.6 || >=7.0 <7.3", - "sebastian/diff": "^1.4", - "symfony/console": "^2.4 || ^3.0 || ^4.0", - "symfony/event-dispatcher": "^2.1 || ^3.0 || ^4.0", - "symfony/filesystem": "^2.4 || ^3.0 || ^4.0", - "symfony/finder": "^2.2 || ^3.0 || ^4.0", - "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0", - "symfony/polyfill-php54": "^1.0", - "symfony/polyfill-php55": "^1.3", - "symfony/polyfill-php70": "^1.0", - "symfony/polyfill-php72": "^1.4", - "symfony/process": "^2.3 || ^3.0 || ^4.0", - "symfony/stopwatch": "^2.5 || ^3.0 || ^4.0" - }, - "conflict": { - "hhvm": "<3.18" - }, - "require-dev": { - "johnkary/phpunit-speedtrap": "^1.0.1 || ^2.0 || ^3.0", - "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.0", - "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^1.0.2", - "phpunit/phpunit": "^4.8.35 || ^5.4.3", - "symfony/phpunit-bridge": "^3.2.2 || ^4.0" - }, - "suggest": { - "ext-mbstring": "For handling non-UTF8 characters in cache signature.", - "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." - }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "autoload": { - "psr-4": { - "PhpCsFixer\\": "src/" - }, - "classmap": [ - "tests/Test/AbstractFixerTestCase.php", - "tests/Test/AbstractIntegrationCaseFactory.php", - "tests/Test/AbstractIntegrationTestCase.php", - "tests/Test/Constraint/SameStringsConstraint.php", - "tests/Test/IntegrationCase.php", - "tests/Test/IntegrationCaseFactory.php", - "tests/Test/IntegrationCaseFactoryInterface.php", - "tests/Test/InternalIntegrationCaseFactory.php", - "tests/TestCase.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "A tool to automatically fix PHP code style", - "time": "2018-03-20T18:04:00+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "6.3.3", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "shasum": "" - }, - "require": { - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", - "php": ">=5.5" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.3-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2018-04-22T15:46:56+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "time": "2016-12-20T10:07:11+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Schultze", - "homepage": "https://github.com/Tobion" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "request", - "response", - "stream", - "uri", - "url" - ], - "time": "2017-03-20T17:10:46+00:00" - }, - { - "name": "herrera-io/json", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/kherge-php/json.git", - "reference": "60c696c9370a1e5136816ca557c17f82a6fa83f1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kherge-php/json/zipball/60c696c9370a1e5136816ca557c17f82a6fa83f1", - "reference": "60c696c9370a1e5136816ca557c17f82a6fa83f1", - "shasum": "" - }, - "require": { - "ext-json": "*", - "justinrainbow/json-schema": ">=1.0,<2.0-dev", - "php": ">=5.3.3", - "seld/jsonlint": ">=1.0,<2.0-dev" - }, - "require-dev": { - "herrera-io/phpunit-test-case": "1.*", - "mikey179/vfsstream": "1.1.0", - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "files": [ - "src/lib/json_version.php" - ], - "psr-0": { - "Herrera\\Json": "src/lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Herrera", - "email": "kevin@herrera.io", - "homepage": "http://kevin.herrera.io" - } - ], - "description": "A library for simplifying JSON linting and validation.", - "homepage": "http://herrera-io.github.com/php-json", - "keywords": [ - "json", - "lint", - "schema", - "validate" - ], - "abandoned": "kherge/json", - "time": "2013-10-30T16:51:34+00:00" - }, - { - "name": "herrera-io/phar-update", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/kherge-abandoned/php-phar-update.git", - "reference": "00a79e1d5b8cf3c080a2e3becf1ddf7a7fea025b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kherge-abandoned/php-phar-update/zipball/00a79e1d5b8cf3c080a2e3becf1ddf7a7fea025b", - "reference": "00a79e1d5b8cf3c080a2e3becf1ddf7a7fea025b", - "shasum": "" - }, - "require": { - "herrera-io/json": "1.*", - "kherge/version": "1.*", - "php": ">=5.3.3" - }, - "require-dev": { - "herrera-io/phpunit-test-case": "1.*", - "mikey179/vfsstream": "1.1.0", - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "files": [ - "src/lib/constants.php" - ], - "psr-0": { - "Herrera\\Phar\\Update": "src/lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Herrera", - "email": "kevin@herrera.io", - "homepage": "http://kevin.herrera.io" - } - ], - "description": "A library for self-updating Phars.", - "homepage": "http://herrera-io.github.com/php-phar-update", - "keywords": [ - "phar", - "update" - ], - "abandoned": true, - "time": "2013-10-30T17:23:01+00:00" - }, - { - "name": "ircmaxell/password-compat", - "version": "v1.0.4", - "source": { - "type": "git", - "url": "https://github.com/ircmaxell/password_compat.git", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", - "shasum": "" - }, - "require-dev": { - "phpunit/phpunit": "4.*" - }, - "type": "library", - "autoload": { - "files": [ - "lib/password.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Anthony Ferrara", - "email": "ircmaxell@php.net", - "homepage": "http://blog.ircmaxell.com" - } - ], - "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", - "homepage": "https://github.com/ircmaxell/password_compat", - "keywords": [ - "hashing", - "password" - ], - "time": "2014-11-20T16:49:30+00:00" - }, - { - "name": "jms/metadata", - "version": "1.6.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/metadata.git", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "doctrine/cache": "~1.0", - "symfony/cache": "~3.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5.x-dev" - } - }, - "autoload": { - "psr-0": { - "Metadata\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Class/method/property metadata management in PHP", - "keywords": [ - "annotations", - "metadata", - "xml", - "yaml" - ], - "time": "2016-12-05T10:18:33+00:00" - }, - { - "name": "jms/parser-lib", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/parser-lib.git", - "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", - "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", - "shasum": "" - }, - "require": { - "phpoption/phpoption": ">=0.9,<2.0-dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "JMS\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "description": "A library for easily creating recursive-descent parsers.", - "time": "2012-11-18T18:08:43+00:00" - }, - { - "name": "jms/serializer", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/serializer.git", - "reference": "e7c53477ff55c21d1b1db7d062edc050a24f465f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/e7c53477ff55c21d1b1db7d062edc050a24f465f", - "reference": "e7c53477ff55c21d1b1db7d062edc050a24f465f", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.0", - "doctrine/instantiator": "^1.0.3", - "jms/metadata": "~1.1", - "jms/parser-lib": "1.*", - "php": "^5.5|^7.0", - "phpcollection/phpcollection": "~0.1", - "phpoption/phpoption": "^1.1" - }, - "conflict": { - "twig/twig": "<1.12" - }, - "require-dev": { - "doctrine/orm": "~2.1", - "doctrine/phpcr-odm": "^1.3|^2.0", - "ext-pdo_sqlite": "*", - "jackalope/jackalope-doctrine-dbal": "^1.1.5", - "phpunit/phpunit": "^4.8|^5.0", - "propel/propel1": "~1.7", - "psr/container": "^1.0", - "symfony/dependency-injection": "^2.7|^3.3|^4.0", - "symfony/expression-language": "^2.6|^3.0", - "symfony/filesystem": "^2.1", - "symfony/form": "~2.1|^3.0", - "symfony/translation": "^2.1|^3.0", - "symfony/validator": "^2.2|^3.0", - "symfony/yaml": "^2.1|^3.0", - "twig/twig": "~1.12|~2.0" - }, - "suggest": { - "doctrine/cache": "Required if you like to use cache functionality.", - "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", - "symfony/yaml": "Required if you'd like to serialize data to YAML format." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11-dev" - } - }, - "autoload": { - "psr-0": { - "JMS\\Serializer": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - }, - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", - "homepage": "http://jmsyst.com/libs/serializer", - "keywords": [ - "deserialization", - "jaxb", - "json", - "serialization", - "xml" - ], - "time": "2018-02-04T17:48:54+00:00" - }, - { - "name": "justinrainbow/json-schema", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "cc84765fb7317f6b07bd8ac78364747f95b86341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/cc84765fb7317f6b07bd8ac78364747f95b86341", - "reference": "cc84765fb7317f6b07bd8ac78364747f95b86341", - "shasum": "" - }, - "require": { - "php": ">=5.3.29" - }, - "require-dev": { - "json-schema/json-schema-test-suite": "1.1.0", - "phpdocumentor/phpdocumentor": "~2", - "phpunit/phpunit": "~3.7" - }, - "bin": [ - "bin/validate-json" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, - "autoload": { - "psr-4": { - "JsonSchema\\": "src/JsonSchema/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" - } - ], - "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", - "keywords": [ - "json", - "schema" - ], - "time": "2016-01-25T15:43:01+00:00" - }, - { - "name": "kevinrob/guzzle-cache-middleware", - "version": "v1.5.2", - "source": { - "type": "git", - "url": "https://github.com/Kevinrob/guzzle-cache-middleware.git", - "reference": "2893fff87ef9f7f2c669957f5e446beea48d7a1d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Kevinrob/guzzle-cache-middleware/zipball/2893fff87ef9f7f2c669957f5e446beea48d7a1d", - "reference": "2893fff87ef9f7f2c669957f5e446beea48d7a1d", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "require-dev": { - "cache/array-adapter": "^0.4", - "doctrine/cache": "^1.0", - "guzzlehttp/guzzle": "^6.0", - "illuminate/cache": "^5.0", - "league/flysystem": "^1.0", - "phpunit/phpunit": "^4.0 || ^5.0", - "psr/cache": "^1.0" - }, - "suggest": { - "doctrine/cache": "This library have a lot of ready-to-use cache storage (to be use with Kevinrob\\GuzzleCache\\Storage\\DoctrineCacheStorage)", - "guzzlehttp/guzzle": "For using this library. It was created for Guzzle6. (but you can use it with any PSR-7 HTTP Client)", - "laravel/framework": "To be use with Kevinrob\\GuzzleCache\\Storage\\LaravelCacheStorage", - "league/flysystem": "To be use with Kevinrob\\GuzzleCache\\Storage\\FlysystemStorage", - "psr/cache": "To be use with Kevinrob\\GuzzleCache\\Storage\\Psr6CacheStorage" - }, - "type": "library", - "autoload": { - "psr-4": { - "Kevinrob\\GuzzleCache\\": [ - "src/", - "tests/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Robatel", - "email": "kevinrob2@gmail.com", - "homepage": "https://github.com/Kevinrob" - } - ], - "description": "A HTTP/1.1 Cache for Guzzle 6. It's a simple Middleware to be added in the HandlerStack. (RFC 7234)", - "homepage": "https://github.com/Kevinrob/guzzle-cache-middleware", - "keywords": [ - "Etag", - "Flysystem", - "Guzzle", - "cache", - "cache-control", - "doctrine", - "expiration", - "guzzle6", - "handler", - "http", - "http 1.1", - "middleware", - "performance", - "php", - "promise", - "psr6", - "psr7", - "rfc7234", - "validation" - ], - "time": "2017-01-16T07:02:13+00:00" - }, - { - "name": "kherge/version", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/kherge-abandoned/Version.git", - "reference": "f07cf83f8ce533be8f93d2893d96d674bbeb7e30" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kherge-abandoned/Version/zipball/f07cf83f8ce533be8f93d2893d96d674bbeb7e30", - "reference": "f07cf83f8ce533be8f93d2893d96d674bbeb7e30", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "KevinGH\\Version": "src/lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Herrera", - "email": "me@kevingh.com" - } - ], - "description": "A parsing and comparison library for semantic versioning.", - "homepage": "http://github.com/kherge/Version", - "abandoned": true, - "time": "2012-08-16T17:13:03+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v1.4.1", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51", - "reference": "f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "files": [ - "lib/bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "time": "2015-09-19T14:15:08+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v2.0.12", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb", - "shasum": "" - }, - "require": { - "php": ">=5.2.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "pseudorandom", - "random" - ], - "time": "2018-04-04T21:24:14+00:00" - }, - { - "name": "phpcollection/phpcollection", - "version": "0.5.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/php-collection.git", - "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", - "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", - "shasum": "" - }, - "require": { - "phpoption/phpoption": "1.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.4-dev" - } - }, - "autoload": { - "psr-0": { - "PhpCollection": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "General-Purpose Collection Library for PHP", - "keywords": [ - "collection", - "list", - "map", - "sequence", - "set" - ], - "time": "2015-05-17T12:39:23+00:00" - }, - { - "name": "phpdocumentor/fileset", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/Fileset.git", - "reference": "bfa78d8fa9763dfce6d0e5d3730c1d8ab25d34b0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/Fileset/zipball/bfa78d8fa9763dfce6d0e5d3730c1d8ab25d34b0", - "reference": "bfa78d8fa9763dfce6d0e5d3730c1d8ab25d34b0", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "symfony/finder": "~2.1" - }, - "require-dev": { - "phpunit/phpunit": "~3.7" - }, - "type": "library", - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/", - "tests/unit/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Fileset component for collecting a set of files given directories and file paths", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "files", - "fileset", - "phpdoc" - ], - "time": "2013-08-06T21:07:42+00:00" - }, - { - "name": "phpdocumentor/graphviz", - "version": "1.0.4", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/GraphViz.git", - "reference": "a906a90a9f230535f25ea31caf81b2323956283f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/GraphViz/zipball/a906a90a9f230535f25ea31caf81b2323956283f", - "reference": "a906a90a9f230535f25ea31caf81b2323956283f", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/", - "tests/unit" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "time": "2016-02-02T13:00:08+00:00" - }, - { - "name": "phpdocumentor/phpdocumentor", - "version": "v2.9.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/phpDocumentor2.git", - "reference": "be607da0eef9b9249c43c5b4820d25d631c73667" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/phpDocumentor2/zipball/be607da0eef9b9249c43c5b4820d25d631c73667", - "reference": "be607da0eef9b9249c43c5b4820d25d631c73667", - "shasum": "" - }, - "require": { - "cilex/cilex": "~1.0", - "erusev/parsedown": "~1.0", - "herrera-io/phar-update": "1.0.3", - "jms/serializer": ">=0.12", - "monolog/monolog": "~1.6", - "php": ">=5.3.3", - "phpdocumentor/fileset": "~1.0", - "phpdocumentor/graphviz": "~1.0", - "phpdocumentor/reflection": "^3.0", - "phpdocumentor/reflection-docblock": "~2.0", - "symfony/config": "~2.3", - "symfony/console": "~2.3", - "symfony/event-dispatcher": "~2.1", - "symfony/process": "~2.0", - "symfony/stopwatch": "~2.3", - "symfony/validator": "~2.2", - "twig/twig": "~1.3", - "zendframework/zend-cache": "~2.1", - "zendframework/zend-config": "~2.1", - "zendframework/zend-filter": "~2.1", - "zendframework/zend-i18n": "~2.1", - "zendframework/zend-serializer": "~2.1", - "zendframework/zend-servicemanager": "~2.1", - "zendframework/zend-stdlib": "~2.1", - "zetacomponents/document": ">=1.3.1" - }, - "require-dev": { - "behat/behat": "~3.0", - "mikey179/vfsstream": "~1.2", - "mockery/mockery": "~0.9@dev", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.4", - "symfony/expression-language": "~2.4" - }, - "suggest": { - "ext-twig": "Enabling the twig extension improves the generation of twig based templates.", - "ext-xslcache": "Enabling the XSLCache extension improves the generation of xml based templates." - }, - "bin": [ - "bin/phpdoc.php", - "bin/phpdoc" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-develop": "2.9-dev" - } - }, - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/", - "tests/unit/" - ], - "Cilex\\Provider": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Documentation Generator for PHP", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "api", - "application", - "dga", - "documentation", - "phpdoc" - ], - "time": "2016-05-22T09:50:56+00:00" - }, - { - "name": "phpdocumentor/reflection", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/Reflection.git", - "reference": "793bfd92d9a0fc96ae9608fb3e947c3f59fb3a0d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/793bfd92d9a0fc96ae9608fb3e947c3f59fb3a0d", - "reference": "793bfd92d9a0fc96ae9608fb3e947c3f59fb3a0d", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^1.0", - "php": ">=5.3.3", - "phpdocumentor/reflection-docblock": "~2.0", - "psr/log": "~1.0" - }, - "require-dev": { - "behat/behat": "~2.4", - "mockery/mockery": "~0.8", - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/", - "tests/unit/", - "tests/mocks/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Reflection library to do Static Analysis for PHP Projects", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2016-05-21T08:42:32+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e6a969a640b00d8daa3c66518b0405fb41ae0c4b", - "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "time": "2016-01-25T08:17:30+00:00" - }, - { - "name": "phpoption/phpoption", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/php-option.git", - "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed", - "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "4.7.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-0": { - "PhpOption\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Option Type for PHP", - "keywords": [ - "language", - "option", - "php", - "type" - ], - "time": "2015-07-25T16:39:46+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.7.6", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.7.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2018-04-18T13:57:24+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "2.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" - }, - "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.2.1", - "ext-xmlwriter": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2015-10-06T15:47:00+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2017-11-27T13:52:08+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "1.4.12", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2017-12-04T08:55:13+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "4.8.36", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.2.2", - "sebastian/diff": "~1.2", - "sebastian/environment": "~1.3", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", - "symfony/yaml": "~2.1|~3.0" - }, - "suggest": { - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.8.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2017-06-21T08:07:12+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", - "phpunit/php-text-template": "~1.2", - "sebastian/exporter": "~1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2015-10-02T06:51:40+00:00" - }, - { - "name": "pimple/pimple", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/silexphp/Pimple.git", - "reference": "2019c145fe393923f3441b23f29bbdfaa5c58c4d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silexphp/Pimple/zipball/2019c145fe393923f3441b23f29bbdfaa5c58c4d", - "reference": "2019c145fe393923f3441b23f29bbdfaa5c58c4d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-0": { - "Pimple": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Pimple is a simple Dependency Injection Container for PHP 5.3", - "homepage": "http://pimple.sensiolabs.org", - "keywords": [ - "container", - "dependency injection" - ], - "time": "2013-11-22T08:30:29+00:00" - }, - { - "name": "predis/predis", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/nrk/predis.git", - "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1", - "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "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 Redis client for PHP and HHVM", - "homepage": "http://github.com/nrk/predis", - "keywords": [ - "nosql", - "predis", - "redis" - ], - "time": "2016-06-16T16:22:20+00:00" - }, - { - "name": "psr/container", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "time": "2017-02-14T16:28:37+00:00" - }, - { - "name": "psr/http-message", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "time": "2016-08-06T14:39:51+00:00" - }, - { - "name": "sebastian/comparator", - "version": "1.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2017-01-29T09:50:25+00:00" - }, - { - "name": "sebastian/diff", - "version": "1.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-05-22T07:24:03+00:00" - }, - { - "name": "sebastian/environment", - "version": "1.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-08-18T05:49:44+00:00" - }, - { - "name": "sebastian/exporter", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2016-06-17T09:04:28+00:00" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12T03:26:01+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-10-03T07:41:43+00:00" - }, - { - "name": "sebastian/version", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" - }, - { - "name": "seld/jsonlint", - "version": "1.7.1", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38", - "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38", - "shasum": "" - }, - "require": { - "php": "^5.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "bin": [ - "bin/jsonlint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "JSON Linter", - "keywords": [ - "json", - "linter", - "parser", - "validator" - ], - "time": "2018-01-24T12:46:19+00:00" - }, - { - "name": "symfony/config", - "version": "v2.8.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "93bdf96d0e3c9b29740bf9050e7a996b443c8436" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/93bdf96d0e3c9b29740bf9050e7a996b443c8436", - "reference": "93bdf96d0e3c9b29740bf9050e7a996b443c8436", - "shasum": "" - }, - "require": { - "php": ">=5.3.9", - "symfony/filesystem": "~2.3|~3.0.0", - "symfony/polyfill-ctype": "~1.8" - }, - "require-dev": { - "symfony/yaml": "~2.7|~3.0.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Config\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Config Component", - "homepage": "https://symfony.com", - "time": "2018-05-01T22:52:40+00:00" - }, - { - "name": "symfony/console", - "version": "v2.8.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e8e59b74ad1274714dad2748349b55e3e6e630c7", - "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7", - "shasum": "" - }, - "require": { - "php": ">=5.3.9", - "symfony/debug": "^2.7.2|~3.0.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1|~3.0.0", - "symfony/process": "~2.1|~3.0.0" - }, - "suggest": { - "psr/log-implementation": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2018-05-15T21:17:45+00:00" - }, - { - "name": "symfony/debug", - "version": "v3.0.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a", - "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a", - "shasum": "" - }, - "require": { - "php": ">=5.5.9", - "psr/log": "~1.0" - }, - "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" - }, - "require-dev": { - "symfony/class-loader": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Debug Component", - "homepage": "https://symfony.com", - "time": "2016-07-30T07:22:48+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v2.8.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", - "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^2.0.5|~3.0.0", - "symfony/dependency-injection": "~2.6|~3.0.0", - "symfony/expression-language": "~2.6|~3.0.0", - "symfony/stopwatch": "~2.3|~3.0.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "https://symfony.com", - "time": "2018-04-06T07:35:03+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v3.0.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b2da5009d9bacbd91d83486aa1f44c793a8c380d", - "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2016-07-20T05:43:46+00:00" - }, - { - "name": "symfony/finder", - "version": "v2.8.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "79764d21163db295f0daf8bd9d9b91f97e65db6a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/79764d21163db295f0daf8bd9d9b91f97e65db6a", - "reference": "79764d21163db295f0daf8bd9d9b91f97e65db6a", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "https://symfony.com", - "time": "2018-05-15T21:17:45+00:00" - }, - { - "name": "symfony/options-resolver", - "version": "v3.4.10", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "f3109a6aedd20e35c3a33190e932c2b063b7b50e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/f3109a6aedd20e35c3a33190e932c2b063b7b50e", - "reference": "f3109a6aedd20e35c3a33190e932c2b063b7b50e", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony OptionsResolver Component", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "time": "2018-01-11T07:56:07+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2018-04-30T19:57:29+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/polyfill-php54", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "6c3a2b84c6025e4ea3f6a19feac35408c64b22e1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/6c3a2b84c6025e4ea3f6a19feac35408c64b22e1", - "reference": "6c3a2b84c6025e4ea3f6a19feac35408c64b22e1", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php54\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 5.4+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/polyfill-php55", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "a39456128377a85f2c5707fcae458678560cba46" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/a39456128377a85f2c5707fcae458678560cba46", - "reference": "a39456128377a85f2c5707fcae458678560cba46", - "shasum": "" - }, - "require": { - "ircmaxell/password-compat": "~1.0", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php55\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "77454693d8f10dd23bb24955cffd2d82db1007a6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/77454693d8f10dd23bb24955cffd2d82db1007a6", - "reference": "77454693d8f10dd23bb24955cffd2d82db1007a6", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "~1.0|~2.0", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "a4576e282d782ad82397f3e4ec1df8e0f0cafb46" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/a4576e282d782ad82397f3e4ec1df8e0f0cafb46", - "reference": "a4576e282d782ad82397f3e4ec1df8e0f0cafb46", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/process", - "version": "v2.8.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/713952f2ccbcc8342ecdbe1cb313d3e2da8aad28", - "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2018-05-15T21:17:45+00:00" - }, - { - "name": "symfony/stopwatch", - "version": "v2.8.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/57021208ad9830f8f8390c1a9d7bb390f32be89e", - "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Stopwatch Component", - "homepage": "https://symfony.com", - "time": "2018-01-03T07:36:31+00:00" - }, - { - "name": "symfony/translation", - "version": "v3.0.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/translation.git", - "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/eee6c664853fd0576f21ae25725cfffeafe83f26", - "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26", - "shasum": "" - }, - "require": { - "php": ">=5.5.9", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/config": "<2.8" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0", - "symfony/intl": "~2.8|~3.0", - "symfony/yaml": "~2.8|~3.0" - }, - "suggest": { - "psr/log": "To use logging capability in translator", - "symfony/config": "", - "symfony/yaml": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Translation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Translation Component", - "homepage": "https://symfony.com", - "time": "2016-07-30T07:22:48+00:00" - }, - { - "name": "symfony/validator", - "version": "v2.8.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/validator.git", - "reference": "96bbfd5534d2e07ba45255bad27ee90d3bc121a3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/96bbfd5534d2e07ba45255bad27ee90d3bc121a3", - "reference": "96bbfd5534d2e07ba45255bad27ee90d3bc121a3", - "shasum": "" - }, - "require": { - "php": ">=5.3.9", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation": "~2.4|~3.0.0" - }, - "require-dev": { - "doctrine/annotations": "~1.0", - "doctrine/cache": "~1.0", - "egulias/email-validator": "^1.2.1", - "symfony/config": "~2.2|~3.0.0", - "symfony/expression-language": "~2.4|~3.0.0", - "symfony/http-foundation": "~2.3|~3.0.0", - "symfony/intl": "~2.7.25|^2.8.18|~3.2.5", - "symfony/property-access": "~2.3|~3.0.0", - "symfony/yaml": "^2.0.5|~3.0.0" - }, - "suggest": { - "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", - "doctrine/cache": "For using the default cached annotation reader and metadata cache.", - "egulias/email-validator": "Strict (RFC compliant) email validation", - "symfony/config": "", - "symfony/expression-language": "For using the 2.4 Expression validator", - "symfony/http-foundation": "", - "symfony/intl": "", - "symfony/property-access": "For using the 2.4 Validator API", - "symfony/yaml": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Validator\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Validator Component", - "homepage": "https://symfony.com", - "time": "2018-05-07T06:57:27+00:00" - }, - { - "name": "symfony/yaml", - "version": "v3.3.16", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "af615970e265543a26ee712c958404eb9b7ac93d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/af615970e265543a26ee712c958404eb9b7ac93d", - "reference": "af615970e265543a26ee712c958404eb9b7ac93d", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "require-dev": { - "symfony/console": "~2.8|~3.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2018-01-20T15:04:53+00:00" - }, - { - "name": "twig/twig", - "version": "v1.35.3", - "source": { - "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", - "reference": "b48680b6eb7d16b5025b9bfc4108d86f6b8af86f", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "psr/container": "^1.0", - "symfony/debug": "^2.7", - "symfony/phpunit-bridge": "^3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.35-dev" - } - }, - "autoload": { - "psr-0": { - "Twig_": "lib/" - }, - "psr-4": { - "Twig\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com", - "role": "Project Founder" - }, - { - "name": "Twig Team", - "homepage": "http://twig.sensiolabs.org/contributors", - "role": "Contributors" - } - ], - "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "http://twig.sensiolabs.org", - "keywords": [ - "templating" - ], - "time": "2018-03-20T04:25:58+00:00" - }, - { - "name": "zendframework/zend-cache", - "version": "2.7.2", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-cache.git", - "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/c98331b96d3b9d9b24cf32d02660602edb34d039", - "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-stdlib": "^2.7 || ^3.0" - }, - "require-dev": { - "phpbench/phpbench": "^0.10.0", - "phpunit/phpunit": "^4.8", - "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-serializer": "^2.6", - "zendframework/zend-session": "^2.6.2" - }, - "suggest": { - "ext-apc": "APC or compatible extension, to use the APC storage adapter", - "ext-apcu": "APCU >= 5.1.0, to use the APCu storage adapter", - "ext-dba": "DBA, to use the DBA storage adapter", - "ext-memcache": "Memcache >= 2.0.0 to use the Memcache storage adapter", - "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", - "ext-mongo": "Mongo, to use MongoDb storage adapter", - "ext-redis": "Redis, to use Redis storage adapter", - "ext-wincache": "WinCache, to use the WinCache storage adapter", - "ext-xcache": "XCache, to use the XCache storage adapter", - "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement", - "zendframework/zend-serializer": "Zend\\Serializer component", - "zendframework/zend-session": "Zend\\Session component" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" - }, - "zf": { - "component": "Zend\\Cache", - "config-provider": "Zend\\Cache\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Zend\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "provides a generic way to cache any data", - "homepage": "https://github.com/zendframework/zend-cache", - "keywords": [ - "cache", - "zf2" - ], - "time": "2016-12-16T11:35:47+00:00" - }, - { - "name": "zendframework/zend-config", - "version": "2.6.0", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-config.git", - "reference": "2920e877a9f6dca9fa8f6bd3b1ffc2e19bb1e30d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-config/zipball/2920e877a9f6dca9fa8f6bd3b1ffc2e19bb1e30d", - "reference": "2920e877a9f6dca9fa8f6bd3b1ffc2e19bb1e30d", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" - }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "zendframework/zend-filter": "^2.6", - "zendframework/zend-i18n": "^2.5", - "zendframework/zend-json": "^2.6.1", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" - }, - "suggest": { - "zendframework/zend-filter": "Zend\\Filter component", - "zendframework/zend-i18n": "Zend\\I18n component", - "zendframework/zend-json": "Zend\\Json to use the Json reader or writer classes", - "zendframework/zend-servicemanager": "Zend\\ServiceManager for use with the Config Factory to retrieve reader and writer instances" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev", - "dev-develop": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\Config\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "provides a nested object property based user interface for accessing this configuration data within application code", - "homepage": "https://github.com/zendframework/zend-config", - "keywords": [ - "config", - "zf2" - ], - "time": "2016-02-04T23:01:10+00:00" - }, - { - "name": "zendframework/zend-eventmanager", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-eventmanager.git", - "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/5c80bdee0e952be112dcec0968bad770082c3a6e", - "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0" - }, - "require-dev": { - "athletic/athletic": "^0.1", - "container-interop/container-interop": "^1.1.0", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.0", - "zendframework/zend-stdlib": "^2.7.3 || ^3.0" - }, - "suggest": { - "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", - "zendframework/zend-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev", - "dev-develop": "3.1-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\EventManager\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Trigger and listen to events within a PHP application", - "homepage": "https://github.com/zendframework/zend-eventmanager", - "keywords": [ - "event", - "eventmanager", - "events", - "zf2" - ], - "time": "2016-02-18T20:53:00+00:00" - }, - { - "name": "zendframework/zend-filter", - "version": "2.7.2", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-filter.git", - "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/b8d0ff872f126631bf63a932e33aa2d22d467175", - "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" - }, - "require-dev": { - "pear/archive_tar": "^1.4", - "phpunit/phpunit": "^6.0.10 || ^5.7.17", - "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-crypt": "^2.6 || ^3.0", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-uri": "^2.5" - }, - "suggest": { - "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters", - "zendframework/zend-i18n": "Zend\\I18n component for filters depending on i18n functionality", - "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for using the filter chain functionality", - "zendframework/zend-uri": "Zend\\Uri component, for the UriNormalize filter" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" - }, - "zf": { - "component": "Zend\\Filter", - "config-provider": "Zend\\Filter\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Zend\\Filter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "provides a set of commonly needed data filters", - "homepage": "https://github.com/zendframework/zend-filter", - "keywords": [ - "filter", - "zf2" - ], - "time": "2017-05-17T20:56:17+00:00" - }, - { - "name": "zendframework/zend-hydrator", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-hydrator.git", - "reference": "22652e1661a5a10b3f564cf7824a2206cf5a4a65" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/22652e1661a5a10b3f564cf7824a2206cf5a4a65", - "reference": "22652e1661a5a10b3f564cf7824a2206cf5a4a65", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.0@dev", - "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-filter": "^2.6", - "zendframework/zend-inputfilter": "^2.6", - "zendframework/zend-serializer": "^2.6.1", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" - }, - "suggest": { - "zendframework/zend-eventmanager": "^2.6.2 || ^3.0, to support aggregate hydrator usage", - "zendframework/zend-filter": "^2.6, to support naming strategy hydrator usage", - "zendframework/zend-serializer": "^2.6.1, to use the SerializableStrategy", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3, to support hydrator plugin manager usage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-release-1.0": "1.0-dev", - "dev-release-1.1": "1.1-dev", - "dev-master": "2.0-dev", - "dev-develop": "2.1-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\Hydrator\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "homepage": "https://github.com/zendframework/zend-hydrator", - "keywords": [ - "hydrator", - "zf2" - ], - "time": "2016-02-18T22:38:26+00:00" - }, - { - "name": "zendframework/zend-i18n", - "version": "2.7.3", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "b2db0d8246a865c659f93199f90f5fc2cd2f3cd8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/b2db0d8246a865c659f93199f90f5fc2cd2f3cd8", - "reference": "b2db0d8246a865c659f93199f90f5fc2cd2f3cd8", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" - }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "zendframework/zend-cache": "^2.6.1", - "zendframework/zend-config": "^2.6", - "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-filter": "^2.6.1", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-validator": "^2.6", - "zendframework/zend-view": "^2.6.3" - }, - "suggest": { - "ext-intl": "Required for most features of Zend\\I18n; included in default builds of PHP", - "zendframework/zend-cache": "Zend\\Cache component", - "zendframework/zend-config": "Zend\\Config component", - "zendframework/zend-eventmanager": "You should install this package to use the events in the translator", - "zendframework/zend-filter": "You should install this package to use the provided filters", - "zendframework/zend-i18n-resources": "Translation resources", - "zendframework/zend-servicemanager": "Zend\\ServiceManager component", - "zendframework/zend-validator": "You should install this package to use the provided validators", - "zendframework/zend-view": "You should install this package to use the provided view helpers" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" - }, - "zf": { - "component": "Zend\\I18n", - "config-provider": "Zend\\I18n\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Zend\\I18n\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "homepage": "https://github.com/zendframework/zend-i18n", - "keywords": [ - "i18n", - "zf2" - ], - "time": "2016-06-07T21:08:30+00:00" - }, - { - "name": "zendframework/zend-json", - "version": "2.6.1", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-json.git", - "reference": "4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-json/zipball/4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28", - "reference": "4c8705dbe4ad7d7e51b2876c5b9eea0ef916ba28", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0" - }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "zendframework/zend-http": "^2.5.4", - "zendframework/zend-server": "^2.6.1", - "zendframework/zend-stdlib": "^2.5 || ^3.0", - "zendframework/zendxml": "^1.0.2" - }, - "suggest": { - "zendframework/zend-http": "Zend\\Http component, required to use Zend\\Json\\Server", - "zendframework/zend-server": "Zend\\Server component, required to use Zend\\Json\\Server", - "zendframework/zend-stdlib": "Zend\\Stdlib component, for use with caching Zend\\Json\\Server responses", - "zendframework/zendxml": "To support Zend\\Json\\Json::fromXml() usage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev", - "dev-develop": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\Json\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", - "homepage": "https://github.com/zendframework/zend-json", - "keywords": [ - "json", - "zf2" - ], - "time": "2016-02-04T21:20:26+00:00" - }, - { - "name": "zendframework/zend-serializer", - "version": "2.7.2", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-serializer.git", - "reference": "95385c2342fc335d5164eb95ac3ca230aa51223b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/95385c2342fc335d5164eb95ac3ca230aa51223b", - "reference": "95385c2342fc335d5164eb95ac3ca230aa51223b", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-json": "^2.5", - "zendframework/zend-stdlib": "^2.7 || ^3.0" - }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "^4.5", - "zendframework/zend-math": "^2.6", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" - }, - "suggest": { - "zendframework/zend-math": "(^2.6) To support Python Pickle serialization", - "zendframework/zend-servicemanager": "To support plugin manager support" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" - }, - "zf": { - "component": "Zend\\Serializer", - "config-provider": "Zend\\Serializer\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Zend\\Serializer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "provides an adapter based interface to simply generate storable representation of PHP types by different facilities, and recover", - "homepage": "https://github.com/zendframework/zend-serializer", - "keywords": [ - "serializer", - "zf2" - ], - "time": "2016-05-11T16:05:56+00:00" - }, - { - "name": "zendframework/zend-servicemanager", - "version": "2.7.10", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-servicemanager.git", - "reference": "ba7069c94c9af93122be9fa31cddd37f7707d5b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/ba7069c94c9af93122be9fa31cddd37f7707d5b4", - "reference": "ba7069c94c9af93122be9fa31cddd37f7707d5b4", - "shasum": "" - }, - "require": { - "container-interop/container-interop": "~1.0", - "php": "^5.5 || ^7.0" - }, - "require-dev": { - "athletic/athletic": "dev-master", - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "zendframework/zend-di": "~2.5", - "zendframework/zend-mvc": "~2.5" - }, - "suggest": { - "ocramius/proxy-manager": "ProxyManager 0.5.* to handle lazy initialization of services", - "zendframework/zend-di": "Zend\\Di component" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\ServiceManager\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "homepage": "https://github.com/zendframework/zend-servicemanager", - "keywords": [ - "servicemanager", - "zf2" - ], - "time": "2017-12-05T16:27:36+00:00" - }, - { - "name": "zendframework/zend-stdlib", - "version": "2.7.7", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-stdlib.git", - "reference": "0e44eb46788f65e09e077eb7f44d2659143bcc1f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/0e44eb46788f65e09e077eb7f44d2659143bcc1f", - "reference": "0e44eb46788f65e09e077eb7f44d2659143bcc1f", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-hydrator": "~1.1" - }, - "require-dev": { - "athletic/athletic": "~0.1", - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0", - "zendframework/zend-config": "~2.5", - "zendframework/zend-eventmanager": "~2.5", - "zendframework/zend-filter": "~2.5", - "zendframework/zend-inputfilter": "~2.5", - "zendframework/zend-serializer": "~2.5", - "zendframework/zend-servicemanager": "~2.5" - }, - "suggest": { - "zendframework/zend-eventmanager": "To support aggregate hydrator usage", - "zendframework/zend-filter": "To support naming strategy hydrator usage", - "zendframework/zend-serializer": "Zend\\Serializer component", - "zendframework/zend-servicemanager": "To support hydrator plugin manager usage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-release-2.7": "2.7-dev", - "dev-master": "3.0-dev", - "dev-develop": "3.1-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\Stdlib\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "homepage": "https://github.com/zendframework/zend-stdlib", - "keywords": [ - "stdlib", - "zf2" - ], - "time": "2016-04-12T21:17:31+00:00" - }, - { - "name": "zetacomponents/base", - "version": "1.9.1", - "source": { - "type": "git", - "url": "https://github.com/zetacomponents/Base.git", - "reference": "489e20235989ddc97fdd793af31ac803972454f1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zetacomponents/Base/zipball/489e20235989ddc97fdd793af31ac803972454f1", - "reference": "489e20235989ddc97fdd793af31ac803972454f1", - "shasum": "" - }, - "require-dev": { - "phpunit/phpunit": "~5.7", - "zetacomponents/unit-test": "*" - }, - "type": "library", - "autoload": { - "classmap": [ - "src" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Sergey Alexeev" - }, - { - "name": "Sebastian Bergmann" - }, - { - "name": "Jan Borsodi" - }, - { - "name": "Raymond Bosman" - }, - { - "name": "Frederik Holljen" - }, - { - "name": "Kore Nordmann" - }, - { - "name": "Derick Rethans" - }, - { - "name": "Vadym Savchuk" - }, - { - "name": "Tobias Schlitt" - }, - { - "name": "Alexandru Stanoi" - } - ], - "description": "The Base package provides the basic infrastructure that all packages rely on. Therefore every component relies on this package.", - "homepage": "https://github.com/zetacomponents", - "time": "2017-11-28T11:30:00+00:00" - }, - { - "name": "zetacomponents/document", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/zetacomponents/Document.git", - "reference": "688abfde573cf3fe0730f82538fbd7aa9fc95bc8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zetacomponents/Document/zipball/688abfde573cf3fe0730f82538fbd7aa9fc95bc8", - "reference": "688abfde573cf3fe0730f82538fbd7aa9fc95bc8", - "shasum": "" - }, - "require": { - "zetacomponents/base": "*" - }, - "require-dev": { - "zetacomponents/unit-test": "dev-master" - }, - "type": "library", - "autoload": { - "classmap": [ - "src" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Sebastian Bergmann" - }, - { - "name": "Kore Nordmann" - }, - { - "name": "Derick Rethans" - }, - { - "name": "Tobias Schlitt" - }, - { - "name": "Alexandru Stanoi" - } - ], - "description": "The Document components provides a general conversion framework for different semantic document markup languages like XHTML, Docbook, RST and similar.", - "homepage": "https://github.com/zetacomponents", - "time": "2013-12-19T11:40:00+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.5" - }, - "platform-dev": [] -} From 72f0ac8a1d2bda2f6e2f0bd8623394595fa55e78 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 23 Jan 2019 17:55:59 -0800 Subject: [PATCH 44/72] add delay for creating test table --- tests/DynamoDbFeatureRequesterTest.php | 62 +++++++++++++++----------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/tests/DynamoDbFeatureRequesterTest.php b/tests/DynamoDbFeatureRequesterTest.php index 7dbee3f17..e78f8f749 100644 --- a/tests/DynamoDbFeatureRequesterTest.php +++ b/tests/DynamoDbFeatureRequesterTest.php @@ -82,33 +82,41 @@ private static function createTableIfNecessary() return; // table already exists } catch (\Exception $e) { } - self::$dynamoDbClient->createTable(array( - 'TableName' => self::TABLE_NAME, - 'AttributeDefinitions' => array( - array( - 'AttributeName' => 'namespace', - 'AttributeType' => 'S' - ), - array( - 'AttributeName' => 'key', - 'AttributeType' => 'S' - ) - ), - 'KeySchema' => array( - array( - 'AttributeName' => 'namespace', - 'KeyType' => 'HASH' - ), - array( - 'AttributeName' => 'key', - 'KeyType' => 'RANGE' - ) - ), - 'ProvisionedThroughput' => array( - 'ReadCapacityUnits' => 1, - 'WriteCapacityUnits' => 1 - ) - )); + while (true) { + // We may need to retry this because in the CI build, the local DynamoDB may not have finished starting yet. + try { + self::$dynamoDbClient->createTable(array( + 'TableName' => self::TABLE_NAME, + 'AttributeDefinitions' => array( + array( + 'AttributeName' => 'namespace', + 'AttributeType' => 'S' + ), + array( + 'AttributeName' => 'key', + 'AttributeType' => 'S' + ) + ), + 'KeySchema' => array( + array( + 'AttributeName' => 'namespace', + 'KeyType' => 'HASH' + ), + array( + 'AttributeName' => 'key', + 'KeyType' => 'RANGE' + ) + ), + 'ProvisionedThroughput' => array( + 'ReadCapacityUnits' => 1, + 'WriteCapacityUnits' => 1 + ) + )); + break; + } catch (\Exception $e) { + sleep(1); + } + } while (true) { // table may not be available immediately try { self::$dynamoDbClient->describeTable(array('TableName' => self::TABLE_NAME)); From fd536a59eabe6f5a64c7567e6587e268675f14c6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 24 Jan 2019 14:56:38 -0800 Subject: [PATCH 45/72] move feature requester code into Integrations namespace --- .circleci/config.yml | 5 + src/LaunchDarkly/ApcLDDFeatureRequester.php | 6 +- src/LaunchDarkly/ApcuLDDFeatureRequester.php | 5 + .../Integrations}/ConsulFeatureRequester.php | 2 +- .../DynamoDbFeatureRequester.php | 2 +- .../Integrations}/FeatureRequesterBase.php | 6 +- .../FileDataFeatureRequester.php | 25 +-- .../Integrations/RedisFeatureRequester.php | 59 ++++++ src/LaunchDarkly/Integrations/Consul.php | 38 ++++ src/LaunchDarkly/Integrations/DynamoDb.php | 37 ++++ src/LaunchDarkly/Integrations/Files.php | 33 ++++ src/LaunchDarkly/Integrations/Redis.php | 39 ++++ src/LaunchDarkly/LDDFeatureRequester.php | 185 +----------------- tests/ConsulFeatureRequesterTest.php | 5 +- tests/DynamoDbFeatureRequesterTest.php | 5 +- tests/FileDataFeatureRequesterTest.php | 8 +- tests/LDDFeatureRequesterTest.php | 72 ------- tests/RedisFeatureRequesterTest.php | 34 ++++ 18 files changed, 282 insertions(+), 284 deletions(-) rename src/LaunchDarkly/{ => Impl/Integrations}/ConsulFeatureRequester.php (97%) rename src/LaunchDarkly/{ => Impl/Integrations}/DynamoDbFeatureRequester.php (98%) rename src/LaunchDarkly/{ => Impl/Integrations}/FeatureRequesterBase.php (97%) rename src/LaunchDarkly/{ => Impl/Integrations}/FileDataFeatureRequester.php (77%) create mode 100644 src/LaunchDarkly/Impl/Integrations/RedisFeatureRequester.php create mode 100644 src/LaunchDarkly/Integrations/Consul.php create mode 100644 src/LaunchDarkly/Integrations/DynamoDb.php create mode 100644 src/LaunchDarkly/Integrations/Files.php create mode 100644 src/LaunchDarkly/Integrations/Redis.php delete mode 100644 tests/LDDFeatureRequesterTest.php create mode 100644 tests/RedisFeatureRequesterTest.php diff --git a/.circleci/config.yml b/.circleci/config.yml index 49996851a..eea4f57b5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,24 +46,28 @@ jobs: - image: circleci/php:5.6.34-cli-jessie - image: amazon/dynamodb-local - image: consul + - image: redis test-7.0: <<: *php-docker-template docker: - image: circleci/php:7.0.28-cli-jessie - image: amazon/dynamodb-local - image: consul + - image: redis test-7.1: <<: *php-docker-template docker: - image: circleci/php:7.1.15-cli-jessie - image: amazon/dynamodb-local - image: consul + - image: redis test-7.2: <<: *php-docker-template docker: - image: circleci/php:7.2.3-cli-stretch - image: amazon/dynamodb-local - image: consul + - image: redis test-5.5: # CircleCI doesn't provide a Docker image for 5.5 machine: @@ -83,6 +87,7 @@ jobs: sudo apt-get -q update && sudo apt-cache policy docker-ce && sudo apt-get -qy install docker-ce + - run: sudo apt-get -qy install redis-server - checkout - run: name: validate composer.json diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index 6d850cf5b..222faf554 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -2,10 +2,12 @@ namespace LaunchDarkly; /** - * Feature requester from an LDD-populated redis, with APC caching + * Deprecated feature requester from an LDD-populated Redis, with APC caching. + * * @deprecated Per the docs (http://php.net/manual/en/intro.apc.php): * "This extension (APC) is considered unmaintained and dead". - * Install APCu and use \LaunchDarkly\ApcuLDDFeatureRequester instead! + * + * Install APCu and use {@link \LaunchDarkly\Integrations\Redis::newFeatureRequester()} instead! * * @package LaunchDarkly */ diff --git a/src/LaunchDarkly/ApcuLDDFeatureRequester.php b/src/LaunchDarkly/ApcuLDDFeatureRequester.php index 30f2fb2c9..f48d623cf 100644 --- a/src/LaunchDarkly/ApcuLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcuLDDFeatureRequester.php @@ -7,6 +7,11 @@ * * Unlike APC, APCu is actively maintained and is available from php53 to php7. * + * This class is deprecated. Use {@link \LaunchDarkly\Integrations\Redis::newFeatureRequester()} + * and set the `apc_expiration` option. + * + * @deprecated Use {@link \LaunchDarkly\Integrations\Redis::newFeatureRequester()} and set the `apc_expiration` option. + * * @package LaunchDarkly */ class ApcuLDDFeatureRequester extends ApcLDDFeatureRequester diff --git a/src/LaunchDarkly/ConsulFeatureRequester.php b/src/LaunchDarkly/Impl/Integrations/ConsulFeatureRequester.php similarity index 97% rename from src/LaunchDarkly/ConsulFeatureRequester.php rename to src/LaunchDarkly/Impl/Integrations/ConsulFeatureRequester.php index daa0454c7..1eb57a06a 100644 --- a/src/LaunchDarkly/ConsulFeatureRequester.php +++ b/src/LaunchDarkly/Impl/Integrations/ConsulFeatureRequester.php @@ -1,5 +1,5 @@ - * To use this component, create an instance of this class, passing the path(s) of your data - * file(s). Then place the resulting object in your LaunchDarkly client configuration with the - * key "feature_requester". - *

- *     $file_data = new FileDataFeatureRequester("./testData/flags.json");
- *     $config = array("feature_requester" => $file_data, "send_events" => false);
- *     $client = new LDClient("sdk_key", $config);
- * 
- *

- * This will cause the client not to connect to LaunchDarkly to get feature flags. (Note - * that in this example, send_events is also set to false so that it will not - * connect to LaunchDarkly to send analytics events either.) - *

- */ class FileDataFeatureRequester implements FeatureRequester { /** @var array */ diff --git a/src/LaunchDarkly/Impl/Integrations/RedisFeatureRequester.php b/src/LaunchDarkly/Impl/Integrations/RedisFeatureRequester.php new file mode 100644 index 000000000..a5eefcfd8 --- /dev/null +++ b/src/LaunchDarkly/Impl/Integrations/RedisFeatureRequester.php @@ -0,0 +1,59 @@ +_prefix = isset($options['redis_prefix']) ? $options['redis_prefix'] : 'launchdarkly'; + + if (isset($this->_options['predis_client']) && $this->_options['predis_client'] instanceof ClientInterface) { + $this->_connection = $this->_options['predis_client']; + } else { + $this->_redisOptions = array( + "scheme" => "tcp", + "timeout" => isset($options['redis_timeout']) ? $options['redis_timeout'] : 5, + "host" => isset($options['redis_host']) ? $options['redis_host'] : 'localhost', + "port" => isset($options['redis_port']) ? $options['redis_port'] : 6379 + ); + } + } + + protected function readItemString($namespace, $key) + { + $redis = $this->getConnection(); + return $redis->hget("$this->_prefix:$namespace", $key); + } + + protected function readItemStringList($namespace) + { + $redis = $this->getConnection(); + $raw = $redis->hgetall("$this->_prefix:$namespace"); + return $raw ? array_values($raw) : null; + } + + /** + * @return ClientInterface + */ + protected function getConnection() + { + if ($this->_connection instanceof ClientInterface) { + return $this->_connection; + } + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + return $this->_connection = new \Predis\Client($this->_redisOptions); + } +} diff --git a/src/LaunchDarkly/Integrations/Consul.php b/src/LaunchDarkly/Integrations/Consul.php new file mode 100644 index 000000000..9140aaa93 --- /dev/null +++ b/src/LaunchDarkly/Integrations/Consul.php @@ -0,0 +1,38 @@ + "env1" ]); + * $config = [ "feature_requester" => $fr ]; + * $client = new LDClient("sdk_key", $config); + * + * For more about using LaunchDarkly with databases, see the + * [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store). + * + * @param array $options Configuration settings (can also be passed in the main client configuration): + * - `consul_uri`: URI of the Consul host; defaults to `http://localhost:8500` + * - `consul_options`: array of settings that the Consul client will pass to Guzzle + * - `consul_prefix`: a string to be prepended to all database keys; corresponds to the prefix + * setting in ld-relay + * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed + * @return object an object to be stored in the `feature_requester` configuration property + */ + public static function newFeatureRequester($options = array()) { + return function($baseUri, $sdkKey, $baseOptions) use ($options) { + return new \LaunchDarkly\Impl\Integrations\ConsulFeatureRequester($baseUri, $sdkKey, + array_merge($baseOptions, $options)); + }; + } +} diff --git a/src/LaunchDarkly/Integrations/DynamoDb.php b/src/LaunchDarkly/Integrations/DynamoDb.php new file mode 100644 index 000000000..39f23cd4b --- /dev/null +++ b/src/LaunchDarkly/Integrations/DynamoDb.php @@ -0,0 +1,37 @@ + "my-table" ]); + * $config = [ "feature_requester" => $fr ]; + * $client = new LDClient("sdk_key", $config); + * + * For more about using LaunchDarkly with databases, see the + * [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store). + * + * @param array $options Configuration settings (can also be passed in the main client configuration): + * - `dynamodb_table`: (required) name of an existing table in DynamoDB. + * - `dynamodb_options`: can include any settings supported by the AWS SDK client + * - `dynamodb_prefix`: a string to be prepended to all database keys; corresponds to the prefix + * setting in ld-relay + * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed + * @return object an object to be stored in the `feature_requester` configuration property + */ + public static function newFeatureRequester($options = array()) { + return function($baseUri, $sdkKey, $baseOptions) use ($options) { + return new \LaunchDarkly\Impl\Integrations\DynamoDbFeatureRequester($baseUri, $sdkKey, + array_merge($baseOptions, $options)); + }; + } +} diff --git a/src/LaunchDarkly/Integrations/Files.php b/src/LaunchDarkly/Integrations/Files.php new file mode 100644 index 000000000..8a66b3bdf --- /dev/null +++ b/src/LaunchDarkly/Integrations/Files.php @@ -0,0 +1,33 @@ + $fr, "send_events" => false ]; + * $client = new LDClient("sdk_key", $config); + * + * This will cause the client _not_ to connect to LaunchDarkly to get feature flags. (Note + * that in this example, `send_events` is also set to false so that it will not connect to + * LaunchDarkly to send analytics events either.) + * + * @param array $filePaths relative or absolute paths to the data files + * @return + */ + public static function newFeatureRequester($filePaths) { + return new \LaunchDarkly\Impl\Integrations\FileDataFeatureRequester($filePaths); + } +} diff --git a/src/LaunchDarkly/Integrations/Redis.php b/src/LaunchDarkly/Integrations/Redis.php new file mode 100644 index 000000000..d8e0f0c75 --- /dev/null +++ b/src/LaunchDarkly/Integrations/Redis.php @@ -0,0 +1,39 @@ + "env1" ]); + * $config = [ "feature_requester" => $fr ]; + * $client = new LDClient("sdk_key", $config); + * + * For more about using LaunchDarkly with databases, see the + * [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store). + * + * @param array $options Configuration settings (can also be passed in the main client configuration): + * - `redis_host`: hostname of the Redis server; defaults to `localhost` + * - `redis_port`: port of the Redis server; defaults to 6379 + * - `redis_timeout`: connection timeout in seconds; defaults to 5 + * - `redis_prefix`: a string to be prepended to all database keys; corresponds to the prefix + * setting in ld-relay + * - `predis_client`: an already-configured Predis client instance if you wish to reuse one + * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed + * @return object an object to be stored in the `feature_requester` configuration property + */ + public static function newFeatureRequester($options = array()) { + return function($baseUri, $sdkKey, $baseOptions) use ($options) { + return new \LaunchDarkly\Impl\Integrations\RedisFeatureRequester($baseUri, $sdkKey, + array_merge($baseOptions, $options)); + }; + } +} diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index a0095686c..18f1c8b21 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -1,183 +1,12 @@ _baseUri = $baseUri; - $this->_sdkKey = $sdkKey; - if (!isset($options['redis_host'])) { - $options['redis_host'] = 'localhost'; - } - if (!isset($options['redis_port'])) { - $options['redis_port'] = 6379; - } - if (!isset($options['redis_timeout'])) { - $options['redis_timeout'] = 5; - } - - $this->_options = $options; - - $prefix = "launchdarkly"; - if (isset($options['redis_prefix'])) { - $prefix = $options['redis_prefix']; - } - $this->_features_key = "$prefix:features"; - $this->_segments_key = "$prefix:segments"; - $this->_logger = $options['logger']; - - if (isset($this->_options['predis_client']) && $this->_options['predis_client'] instanceof ClientInterface) { - $this->_connection = $this->_options['predis_client']; - } - } - - /** - * @return ClientInterface - */ - protected function get_connection() - { - if ($this->_connection instanceof ClientInterface) { - return $this->_connection; - } - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - return $this->_connection = new \Predis\Client(array( - "scheme" => "tcp", - "timeout" => $this->_options['redis_timeout'], - "host" => $this->_options['redis_host'], - "port" => $this->_options['redis_port'] - )); - } - - /** - * Gets feature data from a likely cached store - * - * @param $key string feature key - * @return FeatureFlag|null The decoded JSON feature data, or null if missing - */ - public function getFeature($key) - { - $raw = $this->get_from_cache($this->_features_key, $key); - if ($raw === null) { - $redis = $this->get_connection(); - $raw = $redis->hget($this->_features_key, $key); - if ($raw) { - $this->store_in_cache($this->_features_key, $key, $raw); - } - } - if ($raw) { - $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; - } - } - - /** - * Gets segment data from a likely cached store - * - * @param $key string segment key - * @return Segment|null The decoded JSON segment data, or null if missing - */ - public function getSegment($key) - { - $raw = $this->get_from_cache($this->_segments_key, $key); - if ($raw === null) { - $redis = $this->get_connection(); - $raw = $redis->hget($this->_segments_key, $key); - if ($raw) { - $this->store_in_cache($this->_segments_key, $key, $raw); - } - } - if ($raw) { - $segment = Segment::decode(json_decode($raw, true)); - if ($segment->isDeleted()) { - $this->_logger->warning("LDDFeatureRequester: Attempted to get deleted segment with key: " . $key); - return null; - } - return $segment; - } else { - $this->_logger->warning("LDDFeatureRequester: Attempted to get missing segment with key: " . $key); - return null; - } - } - - /** - * Gets the value from local cache. No-op by default. - * @param $namespace string that denotes features or segments - * @param $key string The feature or segment key - * @return null|array The feature or segment data or null if missing - */ - protected function get_from_cache($namespace, $key) - { - return null; - } - - /** - * Stores the feature or segment data into the local cache. No-op by default. - * @param $namespace string that denotes features or segments - * @param $key string The feature or segment key - * @param $val array The feature or segment data - */ - protected function store_in_cache($namespace, $key, $val) - { - } - - /** - * Gets all features - * - * @return array()|null The decoded FeatureFlags, or null if missing - */ - public function getAllFeatures() - { - $redis = $this->get_connection(); - $raw = $redis->hgetall($this->_features_key); - if ($raw) { - $allFlags = array_map(FeatureFlag::getDecoder(), $this->decodeFeatures($raw)); - /** - * @param $flag FeatureFlag - * @return bool - */ - $isNotDeleted = function ($flag) { - return !$flag->isDeleted(); - }; - return array_filter($allFlags, $isNotDeleted); - } else { - $this->_logger->warning("LDDFeatureRequester: Attempted to get all features, instead got nothing."); - return null; - } - } - - /** - * @param array $features - * - * @return array - */ - private function decodeFeatures(array $features) - { - foreach ($features as $featureKey => $feature) { - $features[$featureKey] = json_decode($feature, true); - } - - return $features; - } } diff --git a/tests/ConsulFeatureRequesterTest.php b/tests/ConsulFeatureRequesterTest.php index f73fa647b..14d18b502 100644 --- a/tests/ConsulFeatureRequesterTest.php +++ b/tests/ConsulFeatureRequesterTest.php @@ -2,7 +2,7 @@ namespace LaunchDarkly\Tests; -use LaunchDarkly\ConsulFeatureRequester; +use \LaunchDarkly\Integrations\Consul; use SensioLabs\Consul\Exception\ClientException; use SensioLabs\Consul\ServiceFactory; @@ -24,7 +24,8 @@ protected function makeRequester() $options = array( 'consul_prefix' => self::PREFIX ); - return new ConsulFeatureRequester('', '', $options); + $factory = Consul::newFeatureRequester(); + return $factory('', '', $options); } protected function putItem($namespace, $key, $version, $json) diff --git a/tests/DynamoDbFeatureRequesterTest.php b/tests/DynamoDbFeatureRequesterTest.php index e78f8f749..5e7921b8a 100644 --- a/tests/DynamoDbFeatureRequesterTest.php +++ b/tests/DynamoDbFeatureRequesterTest.php @@ -3,7 +3,7 @@ namespace LaunchDarkly\Tests; use Aws\DynamoDb\DynamoDbClient; -use LaunchDarkly\DynamoDbFeatureRequester; +use LaunchDarkly\Integrations\DynamoDb; use Psr\Log\NullLogger; class DynamoDbFeatureRequesterTest extends FeatureRequesterTestBase @@ -37,7 +37,8 @@ protected function makeRequester() 'dynamodb_prefix' => self::PREFIX, 'logger' => new NullLogger() ); - return new DynamoDbFeatureRequester('', '', $options); + $factory = DynamoDb::newFeatureRequester(); + return $factory('', '', $options); } protected function putItem($namespace, $key, $version, $json) diff --git a/tests/FileDataFeatureRequesterTest.php b/tests/FileDataFeatureRequesterTest.php index 87e4b2a0d..12cf28cf6 100644 --- a/tests/FileDataFeatureRequesterTest.php +++ b/tests/FileDataFeatureRequesterTest.php @@ -1,14 +1,14 @@ getFeature("flag1"); $this->assertEquals("flag1", $flag1->getKey()); $flag2 = $fr->getFeature("flag2"); @@ -19,7 +19,7 @@ public function testLoadsFile() public function testLoadsMultipleFiles() { - $fr = new FileDataFeatureRequester(array("./tests/filedata/flag-only.json", + $fr = Files::newFeatureRequester(array("./tests/filedata/flag-only.json", "./tests/filedata/segment-only.json")); $flag1 = $fr->getFeature("flag1"); $this->assertEquals("flag1", $flag1->getKey()); @@ -29,7 +29,7 @@ public function testLoadsMultipleFiles() public function testShortcutFlagCanBeEvaluated() { - $fr = new FileDataFeatureRequester("./tests/filedata/all-properties.json"); + $fr = Files::newFeatureRequester("./tests/filedata/all-properties.json"); $flag2 = $fr->getFeature("flag2"); $this->assertEquals("flag2", $flag2->getKey()); $result = $flag2->evaluate(new LDUser("user"), null); diff --git a/tests/LDDFeatureRequesterTest.php b/tests/LDDFeatureRequesterTest.php deleted file mode 100644 index e4663b233..000000000 --- a/tests/LDDFeatureRequesterTest.php +++ /dev/null @@ -1,72 +0,0 @@ -logger = new NullLogger(); - - $this->predisClient = $this->getMockBuilder(ClientInterface::class) - ->setMethods(['hget']) - ->getMockForAbstractClass(); - } - - public function testGetFeature() - { - $sut = new LDDFeatureRequester('example.com', 'MySdkKey', [ - 'logger' => $this->logger, - 'predis_client' => $this->predisClient, - ]); - - $this->predisClient->method('hget')->with('launchdarkly:features', 'foo') - ->willReturn(json_encode([ - 'key' => 'foo', - 'version' => 14, - 'on' => true, - 'prerequisites' => [], - 'salt' => 'c3lzb3BzLXRlc3Q=', - 'sel' => '8ed13de1bfb14507ba7e6dde01f3e035', - 'targets' => [ - [ - 'values' => [], - 'variation' => 0, - ], - [ - 'values' => [], - 'variation' => 1, - ], - ], - 'rules' => [], - 'fallthrough' => [ - 'variation' => 0, - ], - 'offVariation' => null, - 'variations' => [ - true, - false, - ], - 'deleted' => false, - ])); - - $featureFlag = $sut->getFeature('foo'); - - self::assertInstanceOf(FeatureFlag::class, $featureFlag); - self::assertTrue($featureFlag->isOn()); - } -} diff --git a/tests/RedisFeatureRequesterTest.php b/tests/RedisFeatureRequesterTest.php new file mode 100644 index 000000000..27fcb5077 --- /dev/null +++ b/tests/RedisFeatureRequesterTest.php @@ -0,0 +1,34 @@ + self::PREFIX)); + } + + protected function putItem($namespace, $key, $version, $json) + { + self::$predisClient->hset(self::PREFIX . ":$namespace", $key, $json); + } + + protected function deleteExistingData() + { + } +} From a8c1647f0aa75135c0666c2752ac8eb7365148d6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 24 Jan 2019 15:02:25 -0800 Subject: [PATCH 46/72] linter --- src/LaunchDarkly/Integrations/Consul.php | 8 +++++--- src/LaunchDarkly/Integrations/DynamoDb.php | 8 +++++--- src/LaunchDarkly/Integrations/Files.php | 14 ++++++++------ src/LaunchDarkly/Integrations/Redis.php | 8 +++++--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/LaunchDarkly/Integrations/Consul.php b/src/LaunchDarkly/Integrations/Consul.php index 9140aaa93..57e4ff35e 100644 --- a/src/LaunchDarkly/Integrations/Consul.php +++ b/src/LaunchDarkly/Integrations/Consul.php @@ -6,7 +6,8 @@ * Integration with a Consul data store. * @since 3.5.0 */ -class Consul { +class Consul +{ /** * Configures an adapter for reading feature flag data from Consul. * @@ -29,8 +30,9 @@ class Consul { * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($options = array()) { - return function($baseUri, $sdkKey, $baseOptions) use ($options) { + public static function newFeatureRequester($options = array()) + { + return function ($baseUri, $sdkKey, $baseOptions) use ($options) { return new \LaunchDarkly\Impl\Integrations\ConsulFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options)); }; diff --git a/src/LaunchDarkly/Integrations/DynamoDb.php b/src/LaunchDarkly/Integrations/DynamoDb.php index 39f23cd4b..1aeb3fa32 100644 --- a/src/LaunchDarkly/Integrations/DynamoDb.php +++ b/src/LaunchDarkly/Integrations/DynamoDb.php @@ -6,7 +6,8 @@ * Integration with a DynamoDB data store. * @since 3.5.0 */ -class DynamoDb { +class DynamoDb +{ /** * Configures an adapter for reading feature flag data from DynamoDB. * @@ -28,8 +29,9 @@ class DynamoDb { * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($options = array()) { - return function($baseUri, $sdkKey, $baseOptions) use ($options) { + public static function newFeatureRequester($options = array()) + { + return function ($baseUri, $sdkKey, $baseOptions) use ($options) { return new \LaunchDarkly\Impl\Integrations\DynamoDbFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options)); }; diff --git a/src/LaunchDarkly/Integrations/Files.php b/src/LaunchDarkly/Integrations/Files.php index 8a66b3bdf..18f9930f2 100644 --- a/src/LaunchDarkly/Integrations/Files.php +++ b/src/LaunchDarkly/Integrations/Files.php @@ -6,28 +6,30 @@ * Integration with filesystem data. * @since 3.5.0 */ -class Files { +class Files +{ /** * This component allows you to use local files as a source of feature flag state. This would * typically be used in a test environment, to operate using a predetermined feature flag state * without an actual LaunchDarkly connection. - * + * * To use this component, create an instance of this class, passing the path(s) of your data * file(s). Then place the resulting object in your LaunchDarkly client configuration with the * key `feature_requester`. - * + * * $fr = LaunchDarkly\Integrations\Files::newFeatureRequester("./testData/flags.json"); * $config = [ "feature_requester" => $fr, "send_events" => false ]; * $client = new LDClient("sdk_key", $config); - * + * * This will cause the client _not_ to connect to LaunchDarkly to get feature flags. (Note * that in this example, `send_events` is also set to false so that it will not connect to * LaunchDarkly to send analytics events either.) * * @param array $filePaths relative or absolute paths to the data files - * @return + * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($filePaths) { + public static function newFeatureRequester($filePaths) + { return new \LaunchDarkly\Impl\Integrations\FileDataFeatureRequester($filePaths); } } diff --git a/src/LaunchDarkly/Integrations/Redis.php b/src/LaunchDarkly/Integrations/Redis.php index d8e0f0c75..aa821eff9 100644 --- a/src/LaunchDarkly/Integrations/Redis.php +++ b/src/LaunchDarkly/Integrations/Redis.php @@ -6,7 +6,8 @@ * Integration with a Redis data store. * @since 3.5.0 */ -class Redis { +class Redis +{ /** * Configures an adapter for reading feature flag data from Redis. * @@ -30,8 +31,9 @@ class Redis { * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($options = array()) { - return function($baseUri, $sdkKey, $baseOptions) use ($options) { + public static function newFeatureRequester($options = array()) + { + return function ($baseUri, $sdkKey, $baseOptions) use ($options) { return new \LaunchDarkly\Impl\Integrations\RedisFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options)); }; From f192e9a5ff817f6cf8b3db8a9174adfc28bc515a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 24 Jan 2019 15:15:27 -0800 Subject: [PATCH 47/72] misc test fixes --- integration-tests/LDDFeatureRequesterTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/integration-tests/LDDFeatureRequesterTest.php b/integration-tests/LDDFeatureRequesterTest.php index 541d1bbc6..0938d1528 100644 --- a/integration-tests/LDDFeatureRequesterTest.php +++ b/integration-tests/LDDFeatureRequesterTest.php @@ -102,7 +102,7 @@ public function testGetAllWithoutFeatures() $user = new LDUser(static::API_KEY); $allFlags = $client->allFlags($user); - $this->assertNull($allFlags); + $this->assertEquals(array(), $allFlags); } public function testGetAll() @@ -137,8 +137,6 @@ private function gen_feature($key, $val) $val, false, ], - 'commitDate' => '2015-09-08T21:24:16.712Z', - 'creationDate' => '2015-09-08T21:06:16.527Z', 'version' => 4, 'prerequisites' => [], 'targets' => [ From c7f98fdb7f32e2e5e29cd73b4801416112287259 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 24 Jan 2019 17:31:27 -0800 Subject: [PATCH 48/72] fix apc/apcu calls --- src/LaunchDarkly/ApcLDDFeatureRequester.php | 2 +- src/LaunchDarkly/ApcuLDDFeatureRequester.php | 2 +- src/LaunchDarkly/FeatureRequesterBase.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index 6d850cf5b..e5e548ef7 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -51,7 +51,7 @@ protected function get_from_cache($namespace, $key) */ protected function add($key, $var, $ttl = 0) { - return \apc_add($key, $var, $ttl); + return \apc_store($key, $var, $ttl); } protected function store_in_cache($namespace, $key, $val) diff --git a/src/LaunchDarkly/ApcuLDDFeatureRequester.php b/src/LaunchDarkly/ApcuLDDFeatureRequester.php index 30f2fb2c9..8fc93040a 100644 --- a/src/LaunchDarkly/ApcuLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcuLDDFeatureRequester.php @@ -29,6 +29,6 @@ protected function fetch($key, &$success = null) */ protected function add($key, $var, $ttl = 0) { - return \apcu_add($key, $var, $ttl); + return \apcu_store($key, $var, $ttl); } } diff --git a/src/LaunchDarkly/FeatureRequesterBase.php b/src/LaunchDarkly/FeatureRequesterBase.php index 122eddbc6..896489aa6 100644 --- a/src/LaunchDarkly/FeatureRequesterBase.php +++ b/src/LaunchDarkly/FeatureRequesterBase.php @@ -70,7 +70,7 @@ protected function readItemStringList($namespace) protected function getCachedString($namespace, $key) { if ($this->_apcExpiration) { - $value = \apc_fetch($this->makeCacheKey($namespace, $key)); + $value = \apcu_fetch($this->makeCacheKey($namespace, $key)); return $value === false ? null : $value; } return null; @@ -79,7 +79,7 @@ protected function getCachedString($namespace, $key) protected function putCachedString($namespace, $key, $data) { if ($this->_apcExpiration) { - \apc_add($this->makeCacheKey($namespace, $key), $data, $this->_apcExpiration); + \apcu_store($this->makeCacheKey($namespace, $key), $data, $this->_apcExpiration); } } From a49b3c151b644a7139a864292997b87dfd6ac561 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 24 Jan 2019 17:41:30 -0800 Subject: [PATCH 49/72] fix comment --- src/LaunchDarkly/ConsulFeatureRequester.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/ConsulFeatureRequester.php b/src/LaunchDarkly/ConsulFeatureRequester.php index daa0454c7..130c59b71 100644 --- a/src/LaunchDarkly/ConsulFeatureRequester.php +++ b/src/LaunchDarkly/ConsulFeatureRequester.php @@ -8,7 +8,7 @@ class ConsulFeatureRequester extends FeatureRequesterBase { /** @var string */ protected $_prefix; - /** @var DynamoDbClient */ + /** @var \SensioLabs\Consul\Services\KV */ protected $_kvClient; public function __construct($baseUri, $sdkKey, $options) From 24cf9e06d860dbf424342f8ba765ad3b3febba3c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 24 Jan 2019 17:51:22 -0800 Subject: [PATCH 50/72] fix deprecated caching store classes --- src/LaunchDarkly/ApcLDDFeatureRequester.php | 44 ++++--------------- src/LaunchDarkly/ApcuLDDFeatureRequester.php | 25 +++-------- .../Integrations/FeatureRequesterBase.php | 28 ++++++------ 3 files changed, 30 insertions(+), 67 deletions(-) diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index 18d1e39e4..de7292df7 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -24,45 +24,19 @@ public function __construct($baseUri, $sdkKey, $options) } } - /** - * @param $key - * @param $success - * @return mixed - */ - protected function fetch($key, &$success = null) + protected function getCachedString($cacheKey) { - return \apc_fetch($key, $success); - } - - protected function get_from_cache($namespace, $key) - { - $key = self::make_cache_key($namespace, $key); - $enabled = $this->fetch($key); - if ($enabled === false) { - return null; - } else { - return $enabled; + if ($this->_expiration) { + $value = \apc_fetch($cacheKey); + return $value === false ? null : $value; } + return null; } - /** - * @param $key - * @param $var - * @param int $ttl - * @return mixed - */ - protected function add($key, $var, $ttl = 0) - { - return \apc_store($key, $var, $ttl); - } - - protected function store_in_cache($namespace, $key, $val) + protected function putCachedString($cacheKey, $data) { - $this->add($this->make_cache_key($namespace, $key), $val, $this->_expiration); - } - - private function make_cache_key($namespace, $name) - { - return $namespace.'.'.$name; + if ($this->_expiration) { + \apc_store($cacheKey, $data, $this->_expiration); + } } } diff --git a/src/LaunchDarkly/ApcuLDDFeatureRequester.php b/src/LaunchDarkly/ApcuLDDFeatureRequester.php index 70dd589a4..032d224e0 100644 --- a/src/LaunchDarkly/ApcuLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcuLDDFeatureRequester.php @@ -14,26 +14,13 @@ * * @package LaunchDarkly */ -class ApcuLDDFeatureRequester extends ApcLDDFeatureRequester +class ApcuLDDFeatureRequester extends LDDFeatureRequester { - /** - * @param $key - * @param null $success - * @return mixed - */ - protected function fetch($key, &$success = null) + public function __construct($baseUri, $sdkKey, $options) { - return \apcu_fetch($key, $success); - } - - /** - * @param $key - * @param $var - * @param int $ttl - * @return bool - */ - protected function add($key, $var, $ttl = 0) - { - return \apcu_store($key, $var, $ttl); + if (!isset($options['apc_expiration'])) { + $options['apc_expiration'] = 30; + } + parent::__construct($baseUri, $sdkKey, $options); } } diff --git a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php index a62d097a3..cf0daaebb 100644 --- a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php +++ b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php @@ -69,27 +69,22 @@ protected function readItemStringList($namespace) return array(); } - protected function getCachedString($namespace, $key) + protected function getCachedString($cacheKey) { if ($this->_apcExpiration) { - $value = \apcu_fetch($this->makeCacheKey($namespace, $key)); + $value = \apcu_fetch($cacheKey); return $value === false ? null : $value; } return null; } - protected function putCachedString($namespace, $key, $data) + protected function putCachedString($cacheKey, $data) { if ($this->_apcExpiration) { - \apcu_store($this->makeCacheKey($namespace, $key), $data, $this->_apcExpiration); + \apcu_store($cacheKey, $data, $this->_apcExpiration); } } - protected function makeCacheKey($namespace, $key) - { - return self::CACHE_PREFIX . $namespace . ':' . $key; - } - /** * Gets an individual feature flag. * @@ -154,17 +149,19 @@ public function getAllFeatures() protected function getJsonItem($namespace, $key) { - $raw = $this->getCachedString($namespace, $key); + $cacheKey = $this->makeCacheKey($namespace, $key); + $raw = $this->getCachedString($cacheKey); if ($raw === null) { $raw = $this->readItemString($namespace, $key); - $this->putCachedString($namespace, $key, $raw); + $this->putCachedString($cacheKey, $raw); } return ($raw === null) ? null : json_decode($raw, true); } protected function getJsonItemList($namespace) { - $raw = $this->getCachedString($namespace, self::ALL_ITEMS_KEY); + $cacheKey = $this->makeCacheKey($namespace, self::ALL_ITEMS_KEY); + $raw = $this->getCachedString($cacheKey); if ($raw) { $values = json_decode($raw, true); } else { @@ -172,11 +169,16 @@ protected function getJsonItemList($namespace) if (!$values) { $values = array(); } - $this->putCachedString($namespace, self::ALL_ITEMS_KEY, json_encode($values)); + $this->putCachedString($cacheKey, json_encode($values)); } foreach ($values as $i => $s) { $values[$i] = json_decode($s, true); } return $values; } + + private function makeCacheKey($namespace, $key) + { + return self::CACHE_PREFIX . $namespace . ':' . $key; + } } From a9100fce2109620e9b1955c22d34fb51614966f7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 25 Jan 2019 13:55:54 -0800 Subject: [PATCH 51/72] better caching abstraction --- src/LaunchDarkly/ApcLDDFeatureRequester.php | 33 +++---------- src/LaunchDarkly/ApcuLDDFeatureRequester.php | 14 +++--- .../Integrations/ApcFeatureRequesterCache.php | 30 ++++++++++++ .../ApcuFeatureRequesterCache.php | 31 ++++++++++++ .../Integrations/FeatureRequesterBase.php | 47 ++++++++----------- .../Integrations/FeatureRequesterCache.php | 21 +++++++++ src/LaunchDarkly/LDDFeatureRequester.php | 8 +++- 7 files changed, 122 insertions(+), 62 deletions(-) create mode 100644 src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php create mode 100644 src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php create mode 100644 src/LaunchDarkly/Impl/Integrations/FeatureRequesterCache.php diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index de7292df7..3010ae097 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -1,6 +1,9 @@ _expiration = (int)$options['apc_expiration']; - } - } - - protected function getCachedString($cacheKey) - { - if ($this->_expiration) { - $value = \apc_fetch($cacheKey); - return $value === false ? null : $value; - } - return null; - } - - protected function putCachedString($cacheKey, $data) - { - if ($this->_expiration) { - \apc_store($cacheKey, $data, $this->_expiration); - } + protected function createCache($options) { + $expiration = isset($options['apc_expiration']) ? (int)$options['apc_expiration'] : 30; + return new ApcFeatureRequesterCache($expiration); } } diff --git a/src/LaunchDarkly/ApcuLDDFeatureRequester.php b/src/LaunchDarkly/ApcuLDDFeatureRequester.php index 032d224e0..44f554e3b 100644 --- a/src/LaunchDarkly/ApcuLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcuLDDFeatureRequester.php @@ -2,6 +2,9 @@ namespace LaunchDarkly; +use LaunchDarkly\Impl\Integrations\ApcuFeatureRequesterCache; +use LaunchDarkly\Impl\Integrations\RedisFeatureRequester; + /** * Feature requester from an LDD-populated redis, with APCu caching. * @@ -14,13 +17,10 @@ * * @package LaunchDarkly */ -class ApcuLDDFeatureRequester extends LDDFeatureRequester +class ApcuLDDFeatureRequester extends RedisFeatureRequester { - public function __construct($baseUri, $sdkKey, $options) - { - if (!isset($options['apc_expiration'])) { - $options['apc_expiration'] = 30; - } - parent::__construct($baseUri, $sdkKey, $options); + protected function createCache($options) { + $expiration = isset($options['apc_expiration']) ? (int)$options['apc_expiration'] : 30; + return new ApcuFeatureRequesterCache($expiration); } } diff --git a/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php new file mode 100644 index 000000000..f0c72e95a --- /dev/null +++ b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php @@ -0,0 +1,30 @@ +_expiration = $expiration; + } + + public function getCachedString($cacheKey) { + $value = \apc_fetch($cacheKey); + return $value === false ? null : $value; + } + + public function putCachedString($cacheKey, $data) { + \apc_store($cacheKey, $data, $this->_expiration); + } +} diff --git a/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php new file mode 100644 index 000000000..f43bd063c --- /dev/null +++ b/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php @@ -0,0 +1,31 @@ +_expiration = $expiration; + } + + public function getCachedString($cacheKey) { + $value = \apcu_fetch($cacheKey); + return $value === false ? null : $value; + } + + public function putCachedString($cacheKey, $data) { + \apcu_store($cacheKey, $data, $this->_expiration); + } +} diff --git a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php index cf0daaebb..0bafc4ba7 100644 --- a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php +++ b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php @@ -19,8 +19,8 @@ class FeatureRequesterBase implements \LaunchDarkly\FeatureRequester protected $_sdkKey; /** @var array */ protected $_options; - /** @var int */ - protected $_apcExpiration; + /** @var FeatureRequesterCache */ + protected $_cache; /** @var LoggerInterface */ protected $_logger; @@ -29,15 +29,7 @@ protected function __construct($baseUri, $sdkKey, $options) $this->_baseUri = $baseUri; $this->_sdkKey = $sdkKey; $this->_options = $options; - - if (isset($options['apc_expiration'])) { - if (!extension_loaded('apcu')) { - throw new \InvalidArgumentException('apc_expiration was specified but apcu is not installed'); - } - $this->_apcExpiration = (int)$options['apc_expiration']; - } else { - $this->_apcExpiration = 0; - } + $this->_cache = $this->createCache($options); if (isset($options['logger']) && $options['logger']) { $this->_logger = $options['logger']; @@ -69,20 +61,15 @@ protected function readItemStringList($namespace) return array(); } - protected function getCachedString($cacheKey) - { - if ($this->_apcExpiration) { - $value = \apcu_fetch($cacheKey); - return $value === false ? null : $value; - } - return null; - } - - protected function putCachedString($cacheKey, $data) + /** + * Determines the caching implementation to use, if any. + * + * @return FeatureRequesterCache a cache implementation, or null + */ + protected function createCache($options) { - if ($this->_apcExpiration) { - \apcu_store($cacheKey, $data, $this->_apcExpiration); - } + $expiration = isset($options['apc_expiration']) ? (int)$options['apc_expiration'] : 0; + return (expiration > 0) ? new ApcuFeatureRequesterCache($expiration) : null; } /** @@ -150,10 +137,12 @@ public function getAllFeatures() protected function getJsonItem($namespace, $key) { $cacheKey = $this->makeCacheKey($namespace, $key); - $raw = $this->getCachedString($cacheKey); + $raw = $this->_cache ? $this->_cache->getCachedString($cacheKey) : null; if ($raw === null) { $raw = $this->readItemString($namespace, $key); - $this->putCachedString($cacheKey, $raw); + if ($this->_cache) { + $this->_cache->putCachedString($cacheKey, $raw); + } } return ($raw === null) ? null : json_decode($raw, true); } @@ -161,7 +150,7 @@ protected function getJsonItem($namespace, $key) protected function getJsonItemList($namespace) { $cacheKey = $this->makeCacheKey($namespace, self::ALL_ITEMS_KEY); - $raw = $this->getCachedString($cacheKey); + $raw = $this->_cache ? $this->_cache->getCachedString($cacheKey) : null; if ($raw) { $values = json_decode($raw, true); } else { @@ -169,7 +158,9 @@ protected function getJsonItemList($namespace) if (!$values) { $values = array(); } - $this->putCachedString($cacheKey, json_encode($values)); + if ($this->_cache) { + $this->_cache->putCachedString($cacheKey, json_encode(values)); + } } foreach ($values as $i => $s) { $values[$i] = json_decode($s, true); diff --git a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterCache.php new file mode 100644 index 000000000..ac3342d71 --- /dev/null +++ b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterCache.php @@ -0,0 +1,21 @@ + Date: Fri, 25 Jan 2019 13:58:04 -0800 Subject: [PATCH 52/72] typo --- src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php index 0bafc4ba7..a0f76eda3 100644 --- a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php +++ b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php @@ -69,7 +69,7 @@ protected function readItemStringList($namespace) protected function createCache($options) { $expiration = isset($options['apc_expiration']) ? (int)$options['apc_expiration'] : 0; - return (expiration > 0) ? new ApcuFeatureRequesterCache($expiration) : null; + return ($expiration > 0) ? new ApcuFeatureRequesterCache($expiration) : null; } /** From 47c1bc2e65d250157b4233f0638a84e6ce63389e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 25 Jan 2019 14:03:15 -0800 Subject: [PATCH 53/72] linter --- src/LaunchDarkly/ApcLDDFeatureRequester.php | 3 ++- src/LaunchDarkly/ApcuLDDFeatureRequester.php | 3 ++- .../Impl/Integrations/ApcFeatureRequesterCache.php | 9 ++++++--- .../Impl/Integrations/ApcuFeatureRequesterCache.php | 9 ++++++--- src/LaunchDarkly/LDDFeatureRequester.php | 9 +++++---- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index 3010ae097..c2909523d 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -16,7 +16,8 @@ */ class ApcLDDFeatureRequester extends RedisFeatureRequester { - protected function createCache($options) { + protected function createCache($options) + { $expiration = isset($options['apc_expiration']) ? (int)$options['apc_expiration'] : 30; return new ApcFeatureRequesterCache($expiration); } diff --git a/src/LaunchDarkly/ApcuLDDFeatureRequester.php b/src/LaunchDarkly/ApcuLDDFeatureRequester.php index 44f554e3b..d42ef6c5b 100644 --- a/src/LaunchDarkly/ApcuLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcuLDDFeatureRequester.php @@ -19,7 +19,8 @@ */ class ApcuLDDFeatureRequester extends RedisFeatureRequester { - protected function createCache($options) { + protected function createCache($options) + { $expiration = isset($options['apc_expiration']) ? (int)$options['apc_expiration'] : 30; return new ApcuFeatureRequesterCache($expiration); } diff --git a/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php index f0c72e95a..68dd8cf59 100644 --- a/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php +++ b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php @@ -12,19 +12,22 @@ class ApcFeatureRequesterCache implements FeatureRequesterCache /** @var int */ private $_expiration; - public function __construct($expiration) { + public function __construct($expiration) + { if (!extension_loaded('apcu')) { throw new \InvalidArgumentException('apc_expiration was specified but apcu is not installed'); } $this->_expiration = $expiration; } - public function getCachedString($cacheKey) { + public function getCachedString($cacheKey) + { $value = \apc_fetch($cacheKey); return $value === false ? null : $value; } - public function putCachedString($cacheKey, $data) { + public function putCachedString($cacheKey, $data) + { \apc_store($cacheKey, $data, $this->_expiration); } } diff --git a/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php index f43bd063c..f48a63e35 100644 --- a/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php +++ b/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php @@ -13,19 +13,22 @@ class ApcuFeatureRequesterCache implements FeatureRequesterCache /** @var int */ private $_expiration; - public function __construct($expiration) { + public function __construct($expiration) + { if (!extension_loaded('apcu')) { throw new \InvalidArgumentException('apc_expiration was specified but apcu is not installed'); } $this->_expiration = $expiration; } - public function getCachedString($cacheKey) { + public function getCachedString($cacheKey) + { $value = \apcu_fetch($cacheKey); return $value === false ? null : $value; } - public function putCachedString($cacheKey, $data) { + public function putCachedString($cacheKey, $data) + { \apcu_store($cacheKey, $data, $this->_expiration); } } diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index 05d969400..e7bb1cf91 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -11,8 +11,9 @@ */ class LDDFeatureRequester extends RedisFeatureRequester { - protected function createCache($options) { - // The new base class has optional caching behavior, but this deprecated class doesn't. - return null; - } + protected function createCache($options) + { + // The new base class has optional caching behavior, but this deprecated class doesn't. + return null; + } } From f6cf050081905b5445aa3956e9437c2a169613fe Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 25 Jan 2019 14:04:19 -0800 Subject: [PATCH 54/72] fix namespaces --- src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php | 2 +- .../Impl/Integrations/ApcuFeatureRequesterCache.php | 2 +- src/LaunchDarkly/Impl/Integrations/FeatureRequesterCache.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php index 68dd8cf59..beb6a6516 100644 --- a/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php +++ b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php @@ -1,5 +1,5 @@ Date: Fri, 25 Jan 2019 14:28:48 -0800 Subject: [PATCH 55/72] fix tests --- integration-tests/LDDFeatureRequesterTest.php | 4 ++-- src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-tests/LDDFeatureRequesterTest.php b/integration-tests/LDDFeatureRequesterTest.php index 0938d1528..edf5aea40 100644 --- a/integration-tests/LDDFeatureRequesterTest.php +++ b/integration-tests/LDDFeatureRequesterTest.php @@ -52,7 +52,7 @@ public function testGetApc() $redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "baz")); $this->assertEquals("bar", $client->variation('foo', $user, 'jim')); - apc_delete("launchdarkly:features.foo"); + apc_delete("launchdarkly:features:foo"); $this->assertEquals("baz", $client->variation('foo', $user, 'jim')); } @@ -85,7 +85,7 @@ public function testGetApcu() $redis->hset('launchdarkly:features', 'fiz', $this->gen_feature('fiz', 'bob')); $this->assertEquals('buz', $client->variation('fiz', $user, 'alice')); - \apcu_delete('launchdarkly:features.fiz'); + \apcu_delete('launchdarkly:features:fiz'); $this->assertEquals('bob', $client->variation('fiz', $user, 'alice')); } diff --git a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php index a0f76eda3..9a85e21de 100644 --- a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php +++ b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php @@ -11,7 +11,7 @@ class FeatureRequesterBase implements \LaunchDarkly\FeatureRequester const FEATURES_NAMESPACE = 'features'; const SEGMENTS_NAMESPACE = 'segments'; const ALL_ITEMS_KEY = '$all'; - const CACHE_PREFIX = 'LaunchDarkly:'; + const CACHE_PREFIX = 'launchdarkly:'; /** @var string */ protected $_baseUri; From 528fc91d2d38cc37f9f4f80d701347f0ae07e12e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 25 Jan 2019 15:42:45 -0800 Subject: [PATCH 56/72] doc fixes --- README.md | 8 ++++---- src/LaunchDarkly/Integrations/Files.php | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7bcff3eb5..49a402f5e 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela For Redis: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => 'LaunchDarkly\LDDFeatureRequester', + 'feature_requester' => \LaunchDarkly\Integrations\Redis::newFeatureRequester(), 'redis_host' => 'your.redis.host', // defaults to "localhost" if not specified 'redis_port' => 6379, // defaults to 6379 if not specified 'redis_timeout' => 5, // connection timeout in seconds; defaults to 5 @@ -98,7 +98,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela For Consul: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => 'LaunchDarkly\ConsulFeatureRequester', + 'feature_requester' => \LaunchDarkly\Integrations\Consul::newFeatureRequester(), 'consul_uri' => 'http://localhost:8500', // this is the default 'consul_prefix' => 'env1', // corresponds to the prefix setting in ld-relay 'consul_options' => array(), // you may pass any options supported by the Guzzle client @@ -108,7 +108,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela For DynamoDB: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => 'LaunchDarkly\DynamoDbFeatureRequester', + 'feature_requester' => \LaunchDarkly\Integrations\DynamoDb::newFeatureRequester(), 'dynamodb_table' => 'your.table.name', // required 'dynamodb_prefix' => 'env1', // corresponds to the prefix setting in ld-relay 'dynamodb_options' => array(), // you may pass any options supported by the AWS SDK @@ -127,7 +127,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela Using flag data from a file --------------------------- -For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`FileDataFeatureRequester`](https://github.com/launchdarkly/php-client/blob/master/FileDataFeatureRequester.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). +For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`LaunchDarkly\Integrations\Files`](https://github.com/launchdarkly/php-client/blob/master/src/LaunchDarkly/Integrations/Files.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). Testing ------- diff --git a/src/LaunchDarkly/Integrations/Files.php b/src/LaunchDarkly/Integrations/Files.php index 18f9930f2..780086668 100644 --- a/src/LaunchDarkly/Integrations/Files.php +++ b/src/LaunchDarkly/Integrations/Files.php @@ -25,6 +25,9 @@ class Files * that in this example, `send_events` is also set to false so that it will not connect to * LaunchDarkly to send analytics events either.) * + * For more information about using this component, and the format of data files, see + * the SDK reference guide on ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). + * * @param array $filePaths relative or absolute paths to the data files * @return object an object to be stored in the `feature_requester` configuration property */ From 68c81fbbb647176775df7c559530487ce9f7c152 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 25 Jan 2019 16:40:21 -0800 Subject: [PATCH 57/72] move a bunch more stuff --- README.md | 10 +- src/LaunchDarkly/ApcLDDFeatureRequester.php | 2 +- src/LaunchDarkly/ApcuLDDFeatureRequester.php | 4 +- src/LaunchDarkly/CurlEventPublisher.php | 72 +--------- src/LaunchDarkly/EventProcessor.php | 25 ++-- src/LaunchDarkly/GuzzleEventPublisher.php | 68 +-------- src/LaunchDarkly/GuzzleFeatureRequester.php | 125 +---------------- .../Integrations/ApcFeatureRequesterCache.php | 3 - .../ApcuFeatureRequesterCache.php | 3 - .../Impl/Integrations/CurlEventPublisher.php | 72 ++++++++++ .../Integrations/GuzzleEventPublisher.php | 66 +++++++++ .../Integrations/GuzzleFeatureRequester.php | 129 ++++++++++++++++++ src/LaunchDarkly/Integrations/Consul.php | 4 +- src/LaunchDarkly/Integrations/Curl.php | 37 +++++ src/LaunchDarkly/Integrations/DynamoDb.php | 4 +- src/LaunchDarkly/Integrations/Files.php | 4 +- src/LaunchDarkly/Integrations/Guzzle.php | 62 +++++++++ src/LaunchDarkly/Integrations/Redis.php | 4 +- src/LaunchDarkly/LDClient.php | 11 +- src/LaunchDarkly/LDDFeatureRequester.php | 4 +- tests/ConsulFeatureRequesterTest.php | 2 +- tests/DynamoDbFeatureRequesterTest.php | 2 +- tests/FileDataFeatureRequesterTest.php | 6 +- tests/RedisFeatureRequesterTest.php | 2 +- 24 files changed, 427 insertions(+), 294 deletions(-) create mode 100644 src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php create mode 100644 src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php create mode 100644 src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php create mode 100644 src/LaunchDarkly/Integrations/Curl.php create mode 100644 src/LaunchDarkly/Integrations/Guzzle.php diff --git a/README.md b/README.md index 49a402f5e..099958854 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela For Redis: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => \LaunchDarkly\Integrations\Redis::newFeatureRequester(), + 'feature_requester' => LaunchDarkly\Integrations\Redis::featureRequester(), 'redis_host' => 'your.redis.host', // defaults to "localhost" if not specified 'redis_port' => 6379, // defaults to 6379 if not specified 'redis_timeout' => 5, // connection timeout in seconds; defaults to 5 @@ -98,7 +98,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela For Consul: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => \LaunchDarkly\Integrations\Consul::newFeatureRequester(), + 'feature_requester' => LaunchDarkly\Integrations\Consul::featureRequester(), 'consul_uri' => 'http://localhost:8500', // this is the default 'consul_prefix' => 'env1', // corresponds to the prefix setting in ld-relay 'consul_options' => array(), // you may pass any options supported by the Guzzle client @@ -108,7 +108,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela For DynamoDB: $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => \LaunchDarkly\Integrations\DynamoDb::newFeatureRequester(), + 'feature_requester' => LaunchDarkly\Integrations\DynamoDb::featureRequester(), 'dynamodb_table' => 'your.table.name', // required 'dynamodb_prefix' => 'env1', // corresponds to the prefix setting in ld-relay 'dynamodb_options' => array(), // you may pass any options supported by the AWS SDK @@ -117,11 +117,11 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela 4. If you are using DynamoDB, you must create your table manually. It must have a partition key called "namespace", and a sort key called "key" (both strings). Note that by default the AWS SDK will attempt to get your AWS credentials and region from environment variables and/or local configuration files, but you may also specify them in `dynamodb_options`. -5. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using `GuzzleEventPublisher` with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. +5. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using the `Guzzle` implementation of event publishing with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. To forward events, add the following configuration properties to the configuration shown above: - 'event_publisher_class' => 'LaunchDarkly\GuzzleEventPublisher', + 'event_publisher' => LaunchDarkly\Integrations\Guzzle::eventPublisher(), 'events_uri' => 'http://your-ldrelay-host:8030' Using flag data from a file diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index c2909523d..b0d9c7b88 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -10,7 +10,7 @@ * @deprecated Per the docs (http://php.net/manual/en/intro.apc.php): * "This extension (APC) is considered unmaintained and dead". * - * Install APCu and use {@link \LaunchDarkly\Integrations\Redis::newFeatureRequester()} instead! + * Install APCu and use {@link \LaunchDarkly\Integrations\Redis::featureRequester()} instead! * * @package LaunchDarkly */ diff --git a/src/LaunchDarkly/ApcuLDDFeatureRequester.php b/src/LaunchDarkly/ApcuLDDFeatureRequester.php index d42ef6c5b..12e5b7a01 100644 --- a/src/LaunchDarkly/ApcuLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcuLDDFeatureRequester.php @@ -10,10 +10,10 @@ * * Unlike APC, APCu is actively maintained and is available from php53 to php7. * - * This class is deprecated. Use {@link \LaunchDarkly\Integrations\Redis::newFeatureRequester()} + * This class is deprecated. Use {@link \LaunchDarkly\Integrations\Redis::featureRequester()} * and set the `apc_expiration` option. * - * @deprecated Use {@link \LaunchDarkly\Integrations\Redis::newFeatureRequester()} and set the `apc_expiration` option. + * @deprecated Use {@link \LaunchDarkly\Integrations\Redis::featureRequester()} and set the `apc_expiration` option. * * @package LaunchDarkly */ diff --git a/src/LaunchDarkly/CurlEventPublisher.php b/src/LaunchDarkly/CurlEventPublisher.php index 1151b0494..7eea0d0bb 100644 --- a/src/LaunchDarkly/CurlEventPublisher.php +++ b/src/LaunchDarkly/CurlEventPublisher.php @@ -2,75 +2,11 @@ namespace LaunchDarkly; /** - * Sends events to the LaunchDarkly service using the `curl` command line tool. - * The `curl` requests are executed as background processes in order to - * minimize overhead to the PHP request. This `EventPublisher` implementation - * is the default for `LDClient`. + * Deprecated implementation class for using curl to send analytics events. + * Replaced by {@link \LaunchDarkly\Integrations\Curl::eventPublisher()}. * - * `curl` must be installed in the environment's search path, or otherwise the - * absolute path to the executable must be specified using the `'curl'` option - * for `LDClient`. + * @deprecated Use {@link \LaunchDarkly\Integrations\Curl::eventPublisher()} */ -class CurlEventPublisher implements EventPublisher +class CurlEventPublisher extends \LaunchDarkly\Impl\Integrations\CurlEventPublisher { - private $_sdkKey; - private $_host; - private $_port; - private $_ssl; - private $_curl = '/usr/bin/env curl'; - - public function __construct($sdkKey, array $options = array()) - { - $this->_sdkKey = $sdkKey; - - $eventsUri = LDClient::DEFAULT_EVENTS_URI; - if (isset($options['events_uri'])) { - $eventsUri = $options['events_uri']; - } - $url = parse_url(rtrim($eventsUri, '/')); - $this->_host = $url['host']; - $this->_ssl = $url['scheme'] === 'https'; - if (isset($url['port'])) { - $this->_port = $url['port']; - } else { - $this->_port = $this->_ssl ? 443 : 80; - } - if (isset($url['path'])) { - $this->_path = $url['path']; - } else { - $this->_path = ''; - } - - if (array_key_exists('curl', $options)) { - $this->_curl = $options['curl']; - } - } - - public function publish($payload) - { - $args = $this->createArgs($payload); - - return $this->makeRequest($args); - } - - private function createArgs($payload) - { - $scheme = $this->_ssl ? "https://" : "http://"; - $args = " -X POST"; - $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"); - return $args; - } - - private function makeRequest($args) - { - $cmd = $this->_curl . " " . $args . ">> /dev/null 2>&1 &"; - shell_exec($cmd); - return true; - } } diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index c2600422a..abd9c4b32 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -1,6 +1,8 @@ _sdkKey = $sdkKey; - $this->_logger = $options['logger']; - if (isset($options['events_uri'])) { - $this->_eventsUri = $options['events_uri']; - } else { - $this->_eventsUri = LDClient::DEFAULT_EVENTS_URI; - } - $this->_requestOptions = [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => $this->_sdkKey, - 'User-Agent' => 'PHPClient/' . LDClient::VERSION, - 'Accept' => 'application/json', - 'X-LaunchDarkly-Event-Schema' => strval(EventPublisher::CURRENT_SCHEMA_VERSION) - ], - 'timeout' => $options['timeout'], - 'connect_timeout' => $options['connect_timeout'] - ]; - } - - public function publish($payload) - { - $client = new Client(['base_uri' => $this->_eventsUri]); - $response = null; - - try { - $options = $this->_requestOptions; - $options['body'] = $payload; - $response = $client->request('POST', '/bulk', $options); - } catch (\Exception $e) { - $this->_logger->warning("GuzzleEventPublisher::publish caught $e"); - return false; - } - if ($response && ($response->getStatusCode() >= 300)) { - $this->_logger->error(Util::httpErrorMessage($response->getStatusCode(), 'event posting', 'some events were dropped')); - if (!Util::isHttpErrorRecoverable($response->getStatusCode())) { - throw new UnrecoverableHTTPStatusException($response->getStatusCode()); - } - return false; - } - return $response != null; - } } diff --git a/src/LaunchDarkly/GuzzleFeatureRequester.php b/src/LaunchDarkly/GuzzleFeatureRequester.php index efd40c59c..fed7e5f59 100644 --- a/src/LaunchDarkly/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/GuzzleFeatureRequester.php @@ -1,123 +1,12 @@ _baseUri = $baseUri; - $this->_logger = $options['logger']; - $stack = HandlerStack::create(); - if (class_exists('Kevinrob\GuzzleCache\CacheMiddleware')) { - $stack->push(new CacheMiddleware(new PublicCacheStrategy(isset($options['cache']) ? $options['cache'] : null)), 'cache'); - } elseif (!$this->_loggedCacheNotice) { - $this->_logger->info("GuzzleFeatureRequester is not using an HTTP cache because Kevinrob\GuzzleCache\CacheMiddleware was not installed"); - $this->_loggedCacheNotice = true; - } - - $this->_defaults = array( - 'headers' => array( - 'Authorization' => $sdkKey, - 'Content-Type' => 'application/json', - 'User-Agent' => 'PHPClient/' . LDClient::VERSION - ), - 'timeout' => $options['timeout'], - 'connect_timeout' => $options['connect_timeout'] - ); - $this->_client = new Client(['handler' => $stack, 'debug' => false]); - } - - /** - * Gets feature data from a likely cached store - * - * @param $key string feature key - * @return FeatureFlag|null The decoded FeatureFlag, or null if missing - */ - public function getFeature($key) - { - try { - $uri = $this->_baseUri . self::SDK_FLAGS . "/" . $key; - $response = $this->_client->get($uri, $this->_defaults); - $body = $response->getBody(); - return FeatureFlag::decode(json_decode($body, true)); - } catch (BadResponseException $e) { - $code = $e->getResponse()->getStatusCode(); - if ($code == 404) { - $this->_logger->warning("GuzzleFeatureRequester::get returned 404. Feature flag does not exist for key: " . $key); - } else { - $this->handleUnexpectedStatus($code, "GuzzleFeatureRequester::get"); - } - return null; - } - } - - /** - * Gets segment data from a likely cached store - * - * @param $key string segment key - * @return Segment|null The decoded Segment, or null if missing - */ - public function getSegment($key) - { - try { - $uri = $this->_baseUri . self::SDK_SEGMENTS . "/" . $key; - $response = $this->_client->get($uri, $this->_defaults); - $body = $response->getBody(); - return Segment::decode(json_decode($body, true)); - } catch (BadResponseException $e) { - $code = $e->getResponse()->getStatusCode(); - if ($code == 404) { - $this->_logger->warning("GuzzleFeatureRequester::get returned 404. Segment does not exist for key: " . $key); - } else { - $this->handleUnexpectedStatus($code, "GuzzleFeatureRequester::get"); - } - return null; - } - } - - /** - * Gets all features from a likely cached store - * - * @return array()|null The decoded FeatureFlags, or null if missing - */ - public function getAllFeatures() - { - try { - $uri = $this->_baseUri . self::SDK_FLAGS; - $response = $this->_client->get($uri, $this->_defaults); - $body = $response->getBody(); - return array_map(FeatureFlag::getDecoder(), json_decode($body, true)); - } catch (BadResponseException $e) { - $this->handleUnexpectedStatus($e->getResponse()->getStatusCode(), "GuzzleFeatureRequester::getAll"); - return null; - } - } - - private function handleUnexpectedStatus($code, $method) - { - $this->_logger->error(Util::httpErrorMessage($code, $method, 'default value was returned')); - if (!Util::isHttpErrorRecoverable($code)) { - throw new UnrecoverableHTTPStatusException($code); - } - } } diff --git a/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php index beb6a6516..7ad04f884 100644 --- a/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php +++ b/src/LaunchDarkly/Impl/Integrations/ApcFeatureRequesterCache.php @@ -3,9 +3,6 @@ /** * Deprecated caching implementation based on the APC extension. - * - * @deprecated Per the docs (http://php.net/manual/en/intro.apc.php): - * "This extension (APC) is considered unmaintained and dead". */ class ApcFeatureRequesterCache implements FeatureRequesterCache { diff --git a/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php b/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php index 7e2eb4626..330b66ae5 100644 --- a/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php +++ b/src/LaunchDarkly/Impl/Integrations/ApcuFeatureRequesterCache.php @@ -4,9 +4,6 @@ /** * Caching implementation based on the APCu extension. This is used by default by all database feature * requester implementations if the 'apc_expiration' property is set. - * - * @deprecated Per the docs (http://php.net/manual/en/intro.apc.php): - * "This extension (APC) is considered unmaintained and dead". */ class ApcuFeatureRequesterCache implements FeatureRequesterCache { diff --git a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php new file mode 100644 index 000000000..31e16ef45 --- /dev/null +++ b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php @@ -0,0 +1,72 @@ +_sdkKey = $sdkKey; + + $eventsUri = LDClient::DEFAULT_EVENTS_URI; + if (isset($options['events_uri'])) { + $eventsUri = $options['events_uri']; + } + $url = parse_url(rtrim($eventsUri, '/')); + $this->_host = $url['host']; + $this->_ssl = $url['scheme'] === 'https'; + if (isset($url['port'])) { + $this->_port = $url['port']; + } else { + $this->_port = $this->_ssl ? 443 : 80; + } + if (isset($url['path'])) { + $this->_path = $url['path']; + } else { + $this->_path = ''; + } + + if (array_key_exists('curl', $options)) { + $this->_curl = $options['curl']; + } + } + + public function publish($payload) + { + $args = $this->createArgs($payload); + + return $this->makeRequest($args); + } + + private function createArgs($payload) + { + $scheme = $this->_ssl ? "https://" : "http://"; + $args = " -X POST"; + $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"); + return $args; + } + + private function makeRequest($args) + { + $cmd = $this->_curl . " " . $args . ">> /dev/null 2>&1 &"; + shell_exec($cmd); + return true; + } +} diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php new file mode 100644 index 000000000..5779211d9 --- /dev/null +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php @@ -0,0 +1,66 @@ +_sdkKey = $sdkKey; + $this->_logger = $options['logger']; + if (isset($options['events_uri'])) { + $this->_eventsUri = $options['events_uri']; + } else { + $this->_eventsUri = LDClient::DEFAULT_EVENTS_URI; + } + $this->_requestOptions = [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => $this->_sdkKey, + 'User-Agent' => 'PHPClient/' . LDClient::VERSION, + 'Accept' => 'application/json', + 'X-LaunchDarkly-Event-Schema' => strval(EventPublisher::CURRENT_SCHEMA_VERSION) + ], + 'timeout' => $options['timeout'], + 'connect_timeout' => $options['connect_timeout'] + ]; + } + + public function publish($payload) + { + $client = new Client(['base_uri' => $this->_eventsUri]); + $response = null; + + try { + $options = $this->_requestOptions; + $options['body'] = $payload; + $response = $client->request('POST', '/bulk', $options); + } catch (\Exception $e) { + $this->_logger->warning("GuzzleEventPublisher::publish caught $e"); + return false; + } + if ($response && ($response->getStatusCode() >= 300)) { + $this->_logger->error(Util::httpErrorMessage($response->getStatusCode(), 'event posting', 'some events were dropped')); + if (!Util::isHttpErrorRecoverable($response->getStatusCode())) { + throw new UnrecoverableHTTPStatusException($response->getStatusCode()); + } + return false; + } + return $response != null; + } +} diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php new file mode 100644 index 000000000..51074f354 --- /dev/null +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php @@ -0,0 +1,129 @@ +_baseUri = $baseUri; + $this->_logger = $options['logger']; + $stack = HandlerStack::create(); + if (class_exists('Kevinrob\GuzzleCache\CacheMiddleware')) { + $stack->push(new CacheMiddleware(new PublicCacheStrategy(isset($options['cache']) ? $options['cache'] : null)), 'cache'); + } elseif (!$this->_loggedCacheNotice) { + $this->_logger->info("GuzzleFeatureRequester is not using an HTTP cache because Kevinrob\GuzzleCache\CacheMiddleware was not installed"); + $this->_loggedCacheNotice = true; + } + + $this->_defaults = array( + 'headers' => array( + 'Authorization' => $sdkKey, + 'Content-Type' => 'application/json', + 'User-Agent' => 'PHPClient/' . LDClient::VERSION + ), + 'timeout' => $options['timeout'], + 'connect_timeout' => $options['connect_timeout'] + ); + $this->_client = new Client(['handler' => $stack, 'debug' => false]); + } + + /** + * Gets feature data from a likely cached store + * + * @param $key string feature key + * @return FeatureFlag|null The decoded FeatureFlag, or null if missing + */ + public function getFeature($key) + { + try { + $uri = $this->_baseUri . self::SDK_FLAGS . "/" . $key; + $response = $this->_client->get($uri, $this->_defaults); + $body = $response->getBody(); + return FeatureFlag::decode(json_decode($body, true)); + } catch (BadResponseException $e) { + $code = $e->getResponse()->getStatusCode(); + if ($code == 404) { + $this->_logger->warning("GuzzleFeatureRequester::get returned 404. Feature flag does not exist for key: " . $key); + } else { + $this->handleUnexpectedStatus($code, "GuzzleFeatureRequester::get"); + } + return null; + } + } + + /** + * Gets segment data from a likely cached store + * + * @param $key string segment key + * @return Segment|null The decoded Segment, or null if missing + */ + public function getSegment($key) + { + try { + $uri = $this->_baseUri . self::SDK_SEGMENTS . "/" . $key; + $response = $this->_client->get($uri, $this->_defaults); + $body = $response->getBody(); + return Segment::decode(json_decode($body, true)); + } catch (BadResponseException $e) { + $code = $e->getResponse()->getStatusCode(); + if ($code == 404) { + $this->_logger->warning("GuzzleFeatureRequester::get returned 404. Segment does not exist for key: " . $key); + } else { + $this->handleUnexpectedStatus($code, "GuzzleFeatureRequester::get"); + } + return null; + } + } + + /** + * Gets all features from a likely cached store + * + * @return array()|null The decoded FeatureFlags, or null if missing + */ + public function getAllFeatures() + { + try { + $uri = $this->_baseUri . self::SDK_FLAGS; + $response = $this->_client->get($uri, $this->_defaults); + $body = $response->getBody(); + return array_map(FeatureFlag::getDecoder(), json_decode($body, true)); + } catch (BadResponseException $e) { + $this->handleUnexpectedStatus($e->getResponse()->getStatusCode(), "GuzzleFeatureRequester::getAll"); + return null; + } + } + + private function handleUnexpectedStatus($code, $method) + { + $this->_logger->error(Util::httpErrorMessage($code, $method, 'default value was returned')); + if (!Util::isHttpErrorRecoverable($code)) { + throw new UnrecoverableHTTPStatusException($code); + } + } +} diff --git a/src/LaunchDarkly/Integrations/Consul.php b/src/LaunchDarkly/Integrations/Consul.php index 57e4ff35e..e1ee5e418 100644 --- a/src/LaunchDarkly/Integrations/Consul.php +++ b/src/LaunchDarkly/Integrations/Consul.php @@ -15,7 +15,7 @@ class Consul * After calling this method, store its return value in the `feature_requester` property of * your client configuration: * - * $fr = LaunchDarkly\Integrations\Consul::newFeatureRequester([ "consul_prefix" => "env1" ]); + * $fr = LaunchDarkly\Integrations\Consul::featureRequester([ "consul_prefix" => "env1" ]); * $config = [ "feature_requester" => $fr ]; * $client = new LDClient("sdk_key", $config); * @@ -30,7 +30,7 @@ class Consul * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($options = array()) + public static function featureRequester($options = array()) { return function ($baseUri, $sdkKey, $baseOptions) use ($options) { return new \LaunchDarkly\Impl\Integrations\ConsulFeatureRequester($baseUri, $sdkKey, diff --git a/src/LaunchDarkly/Integrations/Curl.php b/src/LaunchDarkly/Integrations/Curl.php new file mode 100644 index 000000000..7d306eb97 --- /dev/null +++ b/src/LaunchDarkly/Integrations/Curl.php @@ -0,0 +1,37 @@ + $ep ]; + * $client = new LDClient("sdk_key", $config); + * + * This implementation forks a process for each event payload. Alternatively, you can use + * {@link \LaunchDarkly\Integrations\Guzzle::eventPublisher()}, which makes synchronous requests. + * + * @param array $options Configuration settings (can also be passed in the main client configuration): + * - `curl`: command for executing `curl`; defaults to `/usr/bin/env curl` + * @return object an object to be stored in the `event_publisher` configuration property + */ + public static function eventPublisher($options = array()) + { + return function ($sdkKey, $baseOptions) use ($options) { + return new \LaunchDarkly\Impl\Integrations\CurlEventPublisher($sdkKey, + array_merge($baseOptions, $options)); + }; + } +} diff --git a/src/LaunchDarkly/Integrations/DynamoDb.php b/src/LaunchDarkly/Integrations/DynamoDb.php index 1aeb3fa32..cb2977c33 100644 --- a/src/LaunchDarkly/Integrations/DynamoDb.php +++ b/src/LaunchDarkly/Integrations/DynamoDb.php @@ -14,7 +14,7 @@ class DynamoDb * To use this method, you must have installed the package `aws/aws-sdk-php`. After calling this * method, store its return value in the `feature_requester` property of your client configuration: * - * $fr = LaunchDarkly\Integrations\DynamoDb::newFeatureRequester([ "dynamodb_table" => "my-table" ]); + * $fr = LaunchDarkly\Integrations\DynamoDb::featureRequester([ "dynamodb_table" => "my-table" ]); * $config = [ "feature_requester" => $fr ]; * $client = new LDClient("sdk_key", $config); * @@ -29,7 +29,7 @@ class DynamoDb * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($options = array()) + public static function featureRequester($options = array()) { return function ($baseUri, $sdkKey, $baseOptions) use ($options) { return new \LaunchDarkly\Impl\Integrations\DynamoDbFeatureRequester($baseUri, $sdkKey, diff --git a/src/LaunchDarkly/Integrations/Files.php b/src/LaunchDarkly/Integrations/Files.php index 780086668..030849c13 100644 --- a/src/LaunchDarkly/Integrations/Files.php +++ b/src/LaunchDarkly/Integrations/Files.php @@ -17,7 +17,7 @@ class Files * file(s). Then place the resulting object in your LaunchDarkly client configuration with the * key `feature_requester`. * - * $fr = LaunchDarkly\Integrations\Files::newFeatureRequester("./testData/flags.json"); + * $fr = LaunchDarkly\Integrations\Files::featureRequester("./testData/flags.json"); * $config = [ "feature_requester" => $fr, "send_events" => false ]; * $client = new LDClient("sdk_key", $config); * @@ -31,7 +31,7 @@ class Files * @param array $filePaths relative or absolute paths to the data files * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($filePaths) + public static function featureRequester($filePaths) { return new \LaunchDarkly\Impl\Integrations\FileDataFeatureRequester($filePaths); } diff --git a/src/LaunchDarkly/Integrations/Guzzle.php b/src/LaunchDarkly/Integrations/Guzzle.php new file mode 100644 index 000000000..6e06448ed --- /dev/null +++ b/src/LaunchDarkly/Integrations/Guzzle.php @@ -0,0 +1,62 @@ + $ep ]; + * $client = new LDClient("sdk_key", $config); + * + * Unlike the curl implementation, which forks processes, this implementation executes synchronously in + * the request handler. In order to minimize request overhead, we recommend that you set up `ld-relay` + * in your production environment and configure the `events_uri` option for `LDClient` to publish to + * `ld-relay`. + * + * @param array $options Configuration settings (can also be passed in the main client configuration): + * - `events_uri`: URI of the server that will receive events, if it is `ld-relay` instead of LaunchDarkly + * - `connect_timeout`: connection timeout in seconds; defaults to 3 + * - `timeout`: read timeout in seconds; defaults to 3 + * @return object an object to be stored in the `event_publisher` configuration property + */ + public static function eventPublisher($options = array()) + { + return function ($sdkKey, $baseOptions) use ($options) { + return new \LaunchDarkly\Impl\Integrations\GuzzleEventPublisher($sdkKey, + array_merge($baseOptions, $options)); + }; + } +} diff --git a/src/LaunchDarkly/Integrations/Redis.php b/src/LaunchDarkly/Integrations/Redis.php index aa821eff9..a21711875 100644 --- a/src/LaunchDarkly/Integrations/Redis.php +++ b/src/LaunchDarkly/Integrations/Redis.php @@ -14,7 +14,7 @@ class Redis * To use this method, you must have installed the package `predis/predis`. After calling this * method, store its return value in the `feature_requester` property of your client configuration: * - * $fr = LaunchDarkly\Integrations\Redis::newFeatureRequester([ "redis_prefix" => "env1" ]); + * $fr = LaunchDarkly\Integrations\Redis::featureRequester([ "redis_prefix" => "env1" ]); * $config = [ "feature_requester" => $fr ]; * $client = new LDClient("sdk_key", $config); * @@ -31,7 +31,7 @@ class Redis * - `apc_expiration`: expiration time in seconds for local caching, if `APCu` is installed * @return object an object to be stored in the `feature_requester` configuration property */ - public static function newFeatureRequester($options = array()) + public static function featureRequester($options = array()) { return function ($baseUri, $sdkKey, $baseOptions) use ($options) { return new \LaunchDarkly\Impl\Integrations\RedisFeatureRequester($baseUri, $sdkKey, diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 960303cc1..4ae5a31f0 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -1,6 +1,7 @@ self::PREFIX ); - $factory = Consul::newFeatureRequester(); + $factory = Consul::featureRequester(); return $factory('', '', $options); } diff --git a/tests/DynamoDbFeatureRequesterTest.php b/tests/DynamoDbFeatureRequesterTest.php index 5e7921b8a..e68ee342b 100644 --- a/tests/DynamoDbFeatureRequesterTest.php +++ b/tests/DynamoDbFeatureRequesterTest.php @@ -37,7 +37,7 @@ protected function makeRequester() 'dynamodb_prefix' => self::PREFIX, 'logger' => new NullLogger() ); - $factory = DynamoDb::newFeatureRequester(); + $factory = DynamoDb::featureRequester(); return $factory('', '', $options); } diff --git a/tests/FileDataFeatureRequesterTest.php b/tests/FileDataFeatureRequesterTest.php index 12cf28cf6..1e0e51a2b 100644 --- a/tests/FileDataFeatureRequesterTest.php +++ b/tests/FileDataFeatureRequesterTest.php @@ -8,7 +8,7 @@ class FileDataFeatureRequesterTest extends \PHPUnit_Framework_TestCase { public function testLoadsFile() { - $fr = Files::newFeatureRequester("./tests/filedata/all-properties.json"); + $fr = Files::featureRequester("./tests/filedata/all-properties.json"); $flag1 = $fr->getFeature("flag1"); $this->assertEquals("flag1", $flag1->getKey()); $flag2 = $fr->getFeature("flag2"); @@ -19,7 +19,7 @@ public function testLoadsFile() public function testLoadsMultipleFiles() { - $fr = Files::newFeatureRequester(array("./tests/filedata/flag-only.json", + $fr = Files::featureRequester(array("./tests/filedata/flag-only.json", "./tests/filedata/segment-only.json")); $flag1 = $fr->getFeature("flag1"); $this->assertEquals("flag1", $flag1->getKey()); @@ -29,7 +29,7 @@ public function testLoadsMultipleFiles() public function testShortcutFlagCanBeEvaluated() { - $fr = Files::newFeatureRequester("./tests/filedata/all-properties.json"); + $fr = Files::featureRequester("./tests/filedata/all-properties.json"); $flag2 = $fr->getFeature("flag2"); $this->assertEquals("flag2", $flag2->getKey()); $result = $flag2->evaluate(new LDUser("user"), null); diff --git a/tests/RedisFeatureRequesterTest.php b/tests/RedisFeatureRequesterTest.php index 27fcb5077..d05462213 100644 --- a/tests/RedisFeatureRequesterTest.php +++ b/tests/RedisFeatureRequesterTest.php @@ -19,7 +19,7 @@ public static function setUpBeforeClass() protected function makeRequester() { - $factory = Redis::newFeatureRequester(); + $factory = Redis::featureRequester(); return $factory('', '', array('redis_prefix' => self::PREFIX)); } From 613f88151bf658e1acf7db4edd6639612d9cd1c9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 25 Jan 2019 16:58:22 -0800 Subject: [PATCH 58/72] misc cleanup --- src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php | 3 ++- tests/ConsulFeatureRequesterTest.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php index 9a85e21de..805302674 100644 --- a/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php +++ b/src/LaunchDarkly/Impl/Integrations/FeatureRequesterBase.php @@ -2,11 +2,12 @@ namespace LaunchDarkly\Impl\Integrations; use LaunchDarkly\FeatureFlag; +use LaunchDarkly\FeatureRequester; use LaunchDarkly\Segment; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -class FeatureRequesterBase implements \LaunchDarkly\FeatureRequester +class FeatureRequesterBase implements FeatureRequester { const FEATURES_NAMESPACE = 'features'; const SEGMENTS_NAMESPACE = 'segments'; diff --git a/tests/ConsulFeatureRequesterTest.php b/tests/ConsulFeatureRequesterTest.php index 7adfbe94f..5a3abd6fa 100644 --- a/tests/ConsulFeatureRequesterTest.php +++ b/tests/ConsulFeatureRequesterTest.php @@ -2,7 +2,7 @@ namespace LaunchDarkly\Tests; -use \LaunchDarkly\Integrations\Consul; +use LaunchDarkly\Integrations\Consul; use SensioLabs\Consul\Exception\ClientException; use SensioLabs\Consul\ServiceFactory; From 6d37fdd13ecb05313de356175d61728e0013e8b9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 25 Jan 2019 17:05:50 -0800 Subject: [PATCH 59/72] add test for not having any flags --- tests/FeatureRequesterTestBase.php | 7 +++++++ tests/RedisFeatureRequesterTest.php | 1 + 2 files changed, 8 insertions(+) diff --git a/tests/FeatureRequesterTestBase.php b/tests/FeatureRequesterTestBase.php index 342eeb7e4..430135527 100644 --- a/tests/FeatureRequesterTestBase.php +++ b/tests/FeatureRequesterTestBase.php @@ -84,6 +84,13 @@ public function testGetAllFeatures() $this->assertEquals($flagVersion, $flag2->getVersion()); } + public function testAllFeaturesWithEmptyStore() + { + $fr = $this->makeRequester(); + $flags = $fr->getAllFeatures(); + $this->assertEquals(array(), $flags); + } + public function testGetSegment() { $segKey = 'foo'; diff --git a/tests/RedisFeatureRequesterTest.php b/tests/RedisFeatureRequesterTest.php index d05462213..a6e507b39 100644 --- a/tests/RedisFeatureRequesterTest.php +++ b/tests/RedisFeatureRequesterTest.php @@ -30,5 +30,6 @@ protected function putItem($namespace, $key, $version, $json) protected function deleteExistingData() { + self::$predisClient->flushdb(); } } From 4014e19104f53328c4d6ace4719435c33367e376 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 30 Jan 2019 17:40:04 -0800 Subject: [PATCH 60/72] restore documentation for deprecated properties --- src/LaunchDarkly/LDClient.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 4ae5a31f0..fc668b97f 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -48,7 +48,9 @@ class LDClient * - 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. * - feature_requester: An optional LaunchDarkly\FeatureRequester instance, or a class or factory for one. Defaults to {@link \LaunchDarkly\Integrations\Guzzle::featureRequester()}. + * - feature_requester_class: Deprecated, equivalent to `feature_requester`. * - event_publisher: An optional LaunchDarkly\EventPublisher instance, or a class or factory for one. Defaults to {@link \LaunchDarkly\Integrations\Curl::eventPublisher()}. + * - event_publisher_class: Deprecated, equivalent to `event_publisher`. * - all_attributes_private: True if no user attributes (other than the key) should be sent back to LaunchDarkly. By default, this is false. * - private_attribute_names: An optional array of user attribute names to be marked private. Any users sent to LaunchDarkly with this configuration active will have attributes with these names removed. You can also set private attributes on a per-user basis in LDUserBuilder. * - Other options may be available depending on which features you are using from {@link \LaunchDarkly\Integrations}. From 86bc30f295ec3e535103182d11b45a23c49fa355 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 30 Jan 2019 17:49:41 -0800 Subject: [PATCH 61/72] version 3.5.0 --- CHANGELOG.md | 14 ++++++++++++++ VERSION | 2 +- src/LaunchDarkly/LDClient.php | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9f4613e..429fb0480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [3.5.0] - 2019-01-30 +### Added: +- It is now possible to use Consul or DynamoDB as a data store with `ld-relay`, similar to the existing Redis integration. See `LaunchDarkly\Integrations\Consul` and `LaunchDarkly\Integrations\DynamoDb`, and the reference guide [Using a persistent feature store](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store). +- When using the Redis integration, you can specify a Redis connection timeout different from the default of 5 seconds by setting the option `redis_timeout` to the desired number of seconds. (Thanks, [jjozefowicz](https://github.com/launchdarkly/php-client/pull/113)!) +- It is now possible to inject feature flags into the client from local JSON files, replacing the normal LaunchDarkly connection. This would typically be for testing purposes. See `LaunchDarkly\Integrations\Files`. +- The `allFlagsState` method now accepts a new option, `detailsOnlyForTrackedFlags`, which reduces the size of the JSON representation of the flag state by omitting some metadata. Specifically, it omits any data that is normally used for generating detailed evaluation events if a flag does not have event tracking or debugging turned on. + +### Changed: +- The `feature_requester` and `event_publisher` configuration options now work differently: you can still set them to an instance of an implementation object, but you can also set them to a class or a class name (i.e. the same as the `feature_requester_class` option), or a factory function. Therefore, the `_class` versions of these options are no longer necessary. However, the old semantics still work, so you can for instance set `event_publisher_class` to `"LaunchDarkly\GuzzleEventPublisher"`, even though the new preferred way is to set `event_publisher` to `LaunchDarkly\Integrations\Guzzle::featureRequester()`. + +### Fixed: +- JSON data from `allFlagsState` is now slightly smaller even if you do not use the new option described above, because it omits the flag property for event tracking unless that property is true. +- The `$_anonymous` property of the `LDUser` class was showing up as public rather than protected. (Thanks, [dstockto](https://github.com/launchdarkly/php-client/pull/114)!) + ## [3.4.1] - 2018-09-25 ### Fixed: - Improved the performance of `allFlags`/`allFlagsState` by not making redundant individual requests for prerequisite flags, when a flag is being evaluated that has prerequisites. Instead it will reuse the same flag data that it already obtained from LaunchDarkly in the "get all the flags" request. diff --git a/VERSION b/VERSION index 47b322c97..1545d9665 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.1 +3.5.0 diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index fc668b97f..4e2fbc9f3 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -13,7 +13,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '3.4.1'; + const VERSION = '3.5.0'; /** @var string */ protected $_sdkKey; From 9c589d15ecd4f079c6d44a3263620ccf285eb941 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 26 Feb 2019 16:34:45 -0800 Subject: [PATCH 62/72] revert accidental commit --- tests/LDClientTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 26b1900ea..25921a2cf 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -176,21 +176,21 @@ public function testVariationSendsEvent() public function testVariationDetailSendsEvent() { - $flag = $this->makeOffFlagWithValue('FUCKINGWEIRDflagkey', 'flagvalue'); - MockFeatureRequester::$flags = array('FUCKINGWEIRDflagkey' => $flag); + $flag = $this->makeOffFlagWithValue('flagkey', 'flagvalue'); + MockFeatureRequester::$flags = array('flagkey' => $flag); $client = new LDClient("someKey", array( 'feature_requester_class' => MockFeatureRequester::class, 'events' => true )); $user = new LDUser('userkey'); - $client->variationDetail('FUCKINGWEIRDflagkey', $user, 'default'); + $client->variationDetail('flagkey', $user, 'default'); $proc = $this->getPrivateField($client, '_eventProcessor'); $queue = $this->getPrivateField($proc, '_queue'); $this->assertEquals(1, sizeof($queue)); $event = $queue[0]; $this->assertEquals('feature', $event['kind']); - $this->assertEquals('FUCKINGWEIRDflagkey', $event['key']); + $this->assertEquals('flagkey', $event['key']); $this->assertEquals($flag->getVersion(), $event['version']); $this->assertEquals('flagvalue', $event['value']); $this->assertEquals(1, $event['variation']); From 87dadb35ff894407d7245a4fd5c464edfa5d2e78 Mon Sep 17 00:00:00 2001 From: hroederld <46500128+hroederld@users.noreply.github.com> Date: Tue, 26 Mar 2019 11:09:38 -0700 Subject: [PATCH 63/72] Hr/ch34492/waitonconsul (#39) * add step to wait on Consul --- .circleci/config.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index afd5166b2..44c2500d6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -84,7 +84,7 @@ jobs: sudo apt-get -q update && sudo apt-cache policy docker-ce && sudo apt-get -qy install docker-ce - - run: sudo apt-get -qy install redis-server + - run: sudo apt-get -qy install redis-server curl - checkout - run: name: validate composer.json @@ -109,6 +109,13 @@ jobs: name: start Consul command: ./consul agent -dev background: true + - run: + name: wait on Consul + command: | + until $(curl --output /dev/null --silent --fail --request PUT --data 'test' http://localhost:8500/v1/kv/initchecker); do + echo 'still waiting on Consul...'; sleep 2; + done + timeout: 50 - run: name: run tests command: vendor/bin/phpunit --log-junit ~/phpunit/junit.xml --coverage-text tests From 30f10c81975bc59177478384c3be0bc2777e56a8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 2 Apr 2019 13:41:49 -0700 Subject: [PATCH 64/72] coerce user attributes to strings when necessary, don't send events without valid users --- src/LaunchDarkly/EventSerializer.php | 16 +++---- src/LaunchDarkly/LDClient.php | 4 +- src/LaunchDarkly/VariationOrRollout.php | 2 +- tests/EventSerializerTest.php | 56 ++++++++++++++++++++----- tests/FeatureFlagTest.php | 31 ++++++++++++++ 5 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/LaunchDarkly/EventSerializer.php b/src/LaunchDarkly/EventSerializer.php index 85b6c616f..6afea6f5d 100644 --- a/src/LaunchDarkly/EventSerializer.php +++ b/src/LaunchDarkly/EventSerializer.php @@ -37,7 +37,7 @@ private function filterEvent($e) return $ret; } - private function filterAttrs($attrs, &$json, $userPrivateAttrs, &$allPrivateAttrs) + private function filterAttrs($attrs, &$json, $userPrivateAttrs, &$allPrivateAttrs, $stringify) { foreach ($attrs as $key => $value) { if ($value != null) { @@ -46,7 +46,7 @@ private function filterAttrs($attrs, &$json, $userPrivateAttrs, &$allPrivateAttr array_search($key, $this->_privateAttrNames) !== false) { $allPrivateAttrs[$key] = true; } else { - $json[$key] = $value; + $json[$key] = $stringify ? strval($value) : $value; } } } @@ -54,7 +54,7 @@ private function filterAttrs($attrs, &$json, $userPrivateAttrs, &$allPrivateAttr private function serializeUser($user) { - $json = array("key" => $user->getKey()); + $json = array("key" => strval($user->getKey())); $userPrivateAttrs = $user->getPrivateAttributeNames(); $allPrivateAttrs = array(); @@ -66,13 +66,15 @@ private function serializeUser($user) 'name' => $user->getName(), 'avatar' => $user->getAvatar(), 'firstName' => $user->getFirstName(), - 'lastName' => $user->getLastName(), - 'anonymous' => $user->getAnonymous() + 'lastName' => $user->getLastName() ); - $this->filterAttrs($attrs, $json, $userPrivateAttrs, $allPrivateAttrs); + $this->filterAttrs($attrs, $json, $userPrivateAttrs, $allPrivateAttrs, true); + if ($user->getAnonymous()) { + $json['anonymous'] = true; + } if (!empty($user->getCustom())) { $customs = array(); - $this->filterAttrs($user->getCustom(), $customs, $userPrivateAttrs, $allPrivateAttrs); + $this->filterAttrs($user->getCustom(), $customs, $userPrivateAttrs, $allPrivateAttrs, false); if ($customs) { // if this is empty, we will return a json array for 'custom' instead of an object $json['custom'] = $customs; } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 4e2fbc9f3..7c45fab5e 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -270,6 +270,7 @@ public function track($eventName, $user, $data) } if (is_null($user) || $user->isKeyBlank()) { $this->_logger->warning("Track called with null user or null/empty user key!"); + return; } $event = array(); @@ -293,13 +294,14 @@ public function identify($user) } if (is_null($user) || $user->isKeyBlank()) { $this->_logger->warning("Track called with null user or null/empty user key!"); + return; } $event = array(); $event['user'] = $user; $event['kind'] = "identify"; $event['creationDate'] = Util::currentTimeUnixMillis(); - $event['key'] = $user->getKey(); + $event['key'] = strval($user->getKey()); $this->_eventProcessor->enqueue($event); } diff --git a/src/LaunchDarkly/VariationOrRollout.php b/src/LaunchDarkly/VariationOrRollout.php index df9407ea8..cf33b84ea 100644 --- a/src/LaunchDarkly/VariationOrRollout.php +++ b/src/LaunchDarkly/VariationOrRollout.php @@ -84,7 +84,7 @@ public static function bucketUser($user, $_key, $attr, $_salt) if (is_string($userValue)) { $idHash = $userValue; if ($user->getSecondary() !== null) { - $idHash = $idHash . "." . $user->getSecondary(); + $idHash = $idHash . "." . strval($user->getSecondary()); } $hash = substr(sha1($_key . "." . $_salt . "." . $idHash), 0, 15); $longVal = base_convert($hash, 16, 10); diff --git a/tests/EventSerializerTest.php b/tests/EventSerializerTest.php index 6ccdc2ee2..084392e0e 100644 --- a/tests/EventSerializerTest.php +++ b/tests/EventSerializerTest.php @@ -126,7 +126,7 @@ public function testUserKey() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("foo@bar.com", $json['key']); + $this->assertSame("foo@bar.com", $json['key']); } public function testEmptyCustom() @@ -150,7 +150,7 @@ public function testUserSecondary() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->secondary("secondary")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("secondary", $json['secondary']); + $this->assertSame("secondary", $json['secondary']); } public function testUserIP() @@ -158,7 +158,7 @@ public function testUserIP() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->ip("127.0.0.1")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("127.0.0.1", $json['ip']); + $this->assertSame("127.0.0.1", $json['ip']); } public function testUserCountry() @@ -166,7 +166,7 @@ public function testUserCountry() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->country("US")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("US", $json['country']); + $this->assertSame("US", $json['country']); } public function testUserEmail() @@ -174,7 +174,7 @@ public function testUserEmail() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->email("foo+test@bar.com")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("foo+test@bar.com", $json['email']); + $this->assertSame("foo+test@bar.com", $json['email']); } public function testUserName() @@ -182,7 +182,7 @@ public function testUserName() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->name("Foo Bar")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("Foo Bar", $json['name']); + $this->assertSame("Foo Bar", $json['name']); } public function testUserAvatar() @@ -190,7 +190,7 @@ public function testUserAvatar() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->avatar("http://www.gravatar.com/avatar/1")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("http://www.gravatar.com/avatar/1", $json['avatar']); + $this->assertSame("http://www.gravatar.com/avatar/1", $json['avatar']); } public function testUserFirstName() @@ -198,7 +198,7 @@ public function testUserFirstName() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->firstName("Foo")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("Foo", $json['firstName']); + $this->assertSame("Foo", $json['firstName']); } public function testUserLastName() @@ -206,7 +206,7 @@ public function testUserLastName() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->lastName("Bar")->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals("Bar", $json['lastName']); + $this->assertSame("Bar", $json['lastName']); } public function testUserAnonymous() @@ -214,6 +214,42 @@ public function testUserAnonymous() $builder = new LDUserBuilder("foo@bar.com"); $user = $builder->anonymous(true)->build(); $json = $this->getJsonForUserBySerializingEvent($user); - $this->assertEquals(true, $json['anonymous']); + $this->assertSame(true, $json['anonymous']); + } + + public function testUserNotAnonymous() + { + $builder = new LDUserBuilder("foo@bar.com"); + $user = $builder->anonymous(false)->build(); + $json = $this->getJsonForUserBySerializingEvent($user); + $this->assertFalse(isset($json['anonymous'])); // omitted rather than set to false, for efficiency + } + + public function testNonStringAttributes() + { + $builder = new LDUserBuilder(1); + $user = $builder->secondary(2) + ->ip(3) + ->country(4) + ->email(5) + ->name(6) + ->avatar(7) + ->firstName(8) + ->lastName(9) + ->anonymous(true) + ->customAttribute('foo', 10) + ->build(); + $json = $this->getJsonForUserBySerializingEvent($user); + $this->assertSame('1', $json['key']); + $this->assertSame('2', $json['secondary']); + $this->assertSame('3', $json['ip']); + $this->assertSame('4', $json['country']); + $this->assertSame('5', $json['email']); + $this->assertSame('6', $json['name']); + $this->assertSame('7', $json['avatar']); + $this->assertSame('8', $json['firstName']); + $this->assertSame('9', $json['lastName']); + $this->assertSame(true, $json['anonymous']); + $this->assertSame(10, $json['custom']['foo']); } } diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index f0f09f02e..6546f1868 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -615,6 +615,37 @@ public function testFlagReturnsErrorIfRuleHasRolloutWithNoVariations() self::assertEquals(array(), $result->getPrerequisiteEvents()); } + public function testSecondaryKeyIsCoercedToStringForRolloutCalculation() + { + // We can't really verify that the rollout calculation works correctly, but we can at least + // make sure it doesn't error out if there's a non-string secondary value (ch35189) + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'rollout' => array( + 'salt' => '', + 'variations' => array( + array( + 'weight' => 100000, + 'variation' => 1 + ) + ) + ) + ) + )); + $ub = new LDUserBuilder('userkey'); + $ub->secondary(999); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, 'ruleid')); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + public function clauseCanMatchBuiltInAttribute() { $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => false); From 2d3120baea5941cacfc1c430ab17f73ac61c8c8b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 2 Apr 2019 17:30:23 -0700 Subject: [PATCH 65/72] explanatory comments --- tests/EventSerializerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/EventSerializerTest.php b/tests/EventSerializerTest.php index 084392e0e..7d8a82f42 100644 --- a/tests/EventSerializerTest.php +++ b/tests/EventSerializerTest.php @@ -249,7 +249,7 @@ public function testNonStringAttributes() $this->assertSame('7', $json['avatar']); $this->assertSame('8', $json['firstName']); $this->assertSame('9', $json['lastName']); - $this->assertSame(true, $json['anonymous']); - $this->assertSame(10, $json['custom']['foo']); + $this->assertSame(true, $json['anonymous']); // We do NOT want "anonymous" to be stringified + $this->assertSame(10, $json['custom']['foo']); // We do NOT want custom attribute values to be stringified } } From 6d5ed05914559e16be97ccb494dc771845e45cfe Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 11 Apr 2019 14:00:49 -0700 Subject: [PATCH 66/72] add release script (version update only) --- scripts/release.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100755 scripts/release.sh diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 000000000..0babf1c15 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# This script updates the version in the client code. It does not need to do anything else to release +# a new version, because Packagist will pick up the new version automatically by watching our public +# repository. + +# It takes exactly one argument: the new version. +# It should be run from the root of this git repo like this: +# ./scripts/release.sh 4.0.9 + +# When done you should commit and push the changes made. + +set -uxe +echo "Starting php-client release (version update only)" + +VERSION=$1 + +echo $VERSION >./VERSION + +# Update version in LDClient class +LDCLIENT_PHP=src/LaunchDarkly/LDClient.php +LDCLIENT_PHP_TEMP=./LDClient.php.tmp +sed "s/const VERSION = '.*'/const VERSION = '${VERSION}'/g" $LDCLIENT_PHP > $LDCLIENT_PHP_TEMP +mv $LDCLIENT_PHP_TEMP $LDCLIENT_PHP + +echo "Done with php-client release (version update only)" From aa29430d98b5bdb4c209207f61b8ab51ff799c7f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 11 Apr 2019 16:55:55 -0700 Subject: [PATCH 67/72] use newer readme footer format --- README.md | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 099958854..6c0303c92 100644 --- a/README.md +++ b/README.md @@ -152,21 +152,10 @@ About LaunchDarkly * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -* LaunchDarkly provides feature flag SDKs for - * [Java](http://docs.launchdarkly.com/docs/java-sdk-reference "Java SDK") - * [JavaScript](http://docs.launchdarkly.com/docs/js-sdk-reference "LaunchDarkly JavaScript SDK") - * [PHP](http://docs.launchdarkly.com/docs/php-sdk-reference "LaunchDarkly PHP SDK") - * [Python](http://docs.launchdarkly.com/docs/python-sdk-reference "LaunchDarkly Python SDK") - * [Go](http://docs.launchdarkly.com/docs/go-sdk-reference "LaunchDarkly Go SDK") - * [Node.JS](http://docs.launchdarkly.com/docs/node-sdk-reference "LaunchDarkly Node SDK") - * [Electron](http://docs.launchdarkly.com/docs/electron-sdk-reference "LaunchDarkly Electron SDK") - * [.NET](http://docs.launchdarkly.com/docs/dotnet-sdk-reference "LaunchDarkly .Net SDK") - * [Ruby](http://docs.launchdarkly.com/docs/ruby-sdk-reference "LaunchDarkly Ruby SDK") - * [iOS](http://docs.launchdarkly.com/docs/ios-sdk-reference "LaunchDarkly iOS SDK") - * [Android](http://docs.launchdarkly.com/docs/android-sdk-reference "LaunchDarkly Android SDK") +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. * Explore LaunchDarkly - * [launchdarkly.com](http://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information - * [docs.launchdarkly.com](http://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDKs - * [apidocs.launchdarkly.com](http://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation - * [blog.launchdarkly.com](http://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies From 3e46f1a4a18b6cc8b6d4b2623932e67c5cfa062c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 25 Apr 2019 13:10:20 -0700 Subject: [PATCH 68/72] add param to skip db tests, update docs for new repo name --- CONTRIBUTING.md | 41 ++++++++++++++++++++++++-- README.md | 8 ++--- composer.json | 2 +- scripts/release.sh | 4 +-- tests/ConsulFeatureRequesterTest.php | 11 +++++-- tests/DynamoDbFeatureRequesterTest.php | 11 +++++-- tests/FeatureRequesterTestBase.php | 16 +++++++++- tests/RedisFeatureRequesterTest.php | 9 +++++- 8 files changed, 86 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 064e6e159..9aa5ec660 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,39 @@ -Contributing to the LaunchDarkly SDK for PHP -================================================ +# Contributing to the LaunchDarkly SDK FOR PHP -We encourage pull-requests and other contributions from the community. We've also published an [SDK contributor's guide](http://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. \ No newline at end of file +LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. + +## Submitting bug reports and feature requests + +The LaunchDarkly SDK team monitors the issue tracker associated with in the `launchdarkly/php-server-sdk` SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. + +## Submitting pull requests + +We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. + +## Build instructions + +### Prerequisites + +The project uses [Composer](https://getcomposer.org/). + +If you will be running the full test suite that includes the database integrations, you will need to have instances of Consul, DynamoDB, and Redis running locally. See below for how to skip these tests. + +### Installing dependencies + +From the project root directory: + +``` +composer install +``` + +### Testing + +To run all unit tests: + +``` +phpunit +``` + +By default, the full unit test suite includes live tests of the integrations for Consul, DynamoDB, and Redis. To skip these, set the environment variable `LD_SKIP_DATABASE_TESTS=1` before running the tests. + +It is preferable to run tests against all supported minor versions of PHP (as described in `README.md` under Requirements), or at least the lowest and highest versions, prior to submitting a pull request. However, LaunchDarkly's CI tests will run automatically against all supported versions. diff --git a/README.md b/README.md index 6c0303c92..0c9c2c691 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ LaunchDarkly SDK for PHP =========================== -[![Circle CI](https://circleci.com/gh/launchdarkly/php-client.svg?style=svg)](https://circleci.com/gh/launchdarkly/php-client) +[![Circle CI](https://circleci.com/gh/launchdarkly/php-server-sdk.svg?style=svg)](https://circleci.com/gh/launchdarkly/php-server-sdk) Requirements ------------ @@ -56,7 +56,7 @@ Require Guzzle as a dependency: It will then be used as the default way of fetching flags. -With Guzzle, you could persist your cache somewhere other than the default in-memory store, like Memcached or Redis. You could then specify your cache when initializing the client with the [cache option](https://github.com/launchdarkly/php-client/blob/master/src/LaunchDarkly/LDClient.php#L44). +With Guzzle, you could persist your cache somewhere other than the default in-memory store, like Memcached or Redis. You could then specify your cache when initializing the client with the [cache option](https://github.com/launchdarkly/php-server-sdk/blob/master/src/LaunchDarkly/LDClient.php#L44). $client = new LaunchDarkly\LDClient("YOUR_SDK_KEY", array("cache" => $cacheStorage)); @@ -127,7 +127,7 @@ The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-rela Using flag data from a file --------------------------- -For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`LaunchDarkly\Integrations\Files`](https://github.com/launchdarkly/php-client/blob/master/src/LaunchDarkly/Integrations/Files.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). +For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`LaunchDarkly\Integrations\Files`](https://github.com/launchdarkly/php-server-sdk/blob/master/src/LaunchDarkly/Integrations/Files.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). Testing ------- @@ -142,7 +142,7 @@ Check out our [documentation](http://docs.launchdarkly.com) for in-depth instruc Contributing ------------ -We encourage pull-requests and other contributions from the community. We've also published an [SDK contributor's guide](http://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. +We encourage pull-requests and other contributions from the community. We've also published an [SDK contributor's guide](http://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See [CONTRIBUTING](CONTRIBUTING.md) for more developer information about this project. About LaunchDarkly ------------------ diff --git a/composer.json b/composer.json index 6b89f748b..7d6ab72f6 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "launchdarkly", "launchdarkly php" ], - "homepage": "https://github.com/launchdarkly/php-client", + "homepage": "https://github.com/launchdarkly/php-server-sdk", "license": "Apache-2.0", "authors": [ { diff --git a/scripts/release.sh b/scripts/release.sh index 0babf1c15..fe523f7a3 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -10,7 +10,7 @@ # When done you should commit and push the changes made. set -uxe -echo "Starting php-client release (version update only)" +echo "Starting php-server-sdk release (version update only)" VERSION=$1 @@ -22,4 +22,4 @@ LDCLIENT_PHP_TEMP=./LDClient.php.tmp sed "s/const VERSION = '.*'/const VERSION = '${VERSION}'/g" $LDCLIENT_PHP > $LDCLIENT_PHP_TEMP mv $LDCLIENT_PHP_TEMP $LDCLIENT_PHP -echo "Done with php-client release (version update only)" +echo "Done with php-server-sdk release (version update only)" diff --git a/tests/ConsulFeatureRequesterTest.php b/tests/ConsulFeatureRequesterTest.php index 5a3abd6fa..fb5ff1542 100644 --- a/tests/ConsulFeatureRequesterTest.php +++ b/tests/ConsulFeatureRequesterTest.php @@ -15,8 +15,15 @@ class ConsulFeatureRequesterTest extends FeatureRequesterTestBase public static function setUpBeforeClass() { - $sf = new ServiceFactory(); - self::$kvClient = $sf->get('kv'); + if (!static::isSkipDatabaseTests()) { + $sf = new ServiceFactory(); + self::$kvClient = $sf->get('kv'); + } + } + + protected function isDatabaseTest() + { + return true; } protected function makeRequester() diff --git a/tests/DynamoDbFeatureRequesterTest.php b/tests/DynamoDbFeatureRequesterTest.php index e68ee342b..588c0ccea 100644 --- a/tests/DynamoDbFeatureRequesterTest.php +++ b/tests/DynamoDbFeatureRequesterTest.php @@ -15,8 +15,15 @@ class DynamoDbFeatureRequesterTest extends FeatureRequesterTestBase public static function setUpBeforeClass() { - self::$dynamoDbClient = new DynamoDbClient(self::makeDynamoDbOptions()); - self::createTableIfNecessary(); + if (!static::isSkipDatabaseTests()) { + self::$dynamoDbClient = new DynamoDbClient(self::makeDynamoDbOptions()); + self::createTableIfNecessary(); + } + } + + protected function isDatabaseTest() + { + return true; } private static function makeDynamoDbOptions() diff --git a/tests/FeatureRequesterTestBase.php b/tests/FeatureRequesterTestBase.php index 430135527..be434852f 100644 --- a/tests/FeatureRequesterTestBase.php +++ b/tests/FeatureRequesterTestBase.php @@ -9,7 +9,21 @@ class FeatureRequesterTestBase extends \PHPUnit_Framework_TestCase { public function setUp() { - $this->deleteExistingData(); + if ($this->isDatabaseTest() && static::isSkipDatabaseTests()) { + $this->markTestSkipped('skipping database tests'); + } else { + $this->deleteExistingData(); + } + } + + protected static function isSkipDatabaseTests() + { + return isset($_ENV['LD_SKIP_DATABASE_TESTS']) && $_ENV['LD_SKIP_DATABASE_TESTS']; + } + + protected function isDatabaseTest() + { + return false; } protected function deleteExistingData() diff --git a/tests/RedisFeatureRequesterTest.php b/tests/RedisFeatureRequesterTest.php index a6e507b39..3c706450d 100644 --- a/tests/RedisFeatureRequesterTest.php +++ b/tests/RedisFeatureRequesterTest.php @@ -14,7 +14,14 @@ class RedisFeatureRequesterTest extends FeatureRequesterTestBase public static function setUpBeforeClass() { - self::$predisClient = new Client(array()); + if (!static::isDatabaseTest()) { + self::$predisClient = new Client(array()); + } + } + + protected function isDatabaseTest() + { + return true; } protected function makeRequester() From ff1a19d9aedd5c9c2b7d66f030b935abe8231884 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 25 Apr 2019 13:12:46 -0700 Subject: [PATCH 69/72] wrong method name --- tests/RedisFeatureRequesterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RedisFeatureRequesterTest.php b/tests/RedisFeatureRequesterTest.php index 3c706450d..05ca388cd 100644 --- a/tests/RedisFeatureRequesterTest.php +++ b/tests/RedisFeatureRequesterTest.php @@ -14,7 +14,7 @@ class RedisFeatureRequesterTest extends FeatureRequesterTestBase public static function setUpBeforeClass() { - if (!static::isDatabaseTest()) { + if (!static::isSkipDatabaseTests()) { self::$predisClient = new Client(array()); } } From 86eefb37c03a1ebe605001de81fce479af2eec43 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 25 Apr 2019 16:01:36 -0700 Subject: [PATCH 70/72] add tests for rollout calculations, + misc test cleanup --- tests/FeatureFlagTest.php | 305 +++++++++++++++++++------------------- tests/SegmentTest.php | 123 +++++++-------- 2 files changed, 215 insertions(+), 213 deletions(-) diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 6546f1868..19c22e324 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -8,6 +8,54 @@ use LaunchDarkly\LDUserBuilder; use LaunchDarkly\Segment; +const RULE_ID = 'ruleid'; + +$defaultUser = (new LDUserBuilder('foo'))->build(); + +function makeBooleanFlagWithRules(array $rules) +{ + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => $rules, + 'offVariation' => 0, + 'fallthrough' => array('variation' => 0), + 'variations' => array(false, true), + 'salt' => '' + ); + return FeatureFlag::decode($flagJson); +} + +function makeBooleanFlagWithClauses($clauses) +{ + return makeBooleanFlagWithRules(array(array('clauses' => $clauses, 'variation' => 1))); +} + +function makeRuleMatchingUser($user, $ruleAttrs = array()) +{ + $clause = array('attribute' => 'key', 'op' => 'in', 'values' => array($user->getKey()), 'negate' => false); + return array_merge(array('id' => RULE_ID, 'clauses' => array($clause)), $ruleAttrs); +} + +function makeSegmentMatchClause($segmentKey) +{ + return array('attribute' => '', 'op' => 'segmentMatch', 'values' => array($segmentKey), 'negate' => false); +} + +// This is our way of verifying that the bucket value for a rollout is within 1.0 of the expected value. +function makeRolloutVariations($targetValue, $targetVariation, $otherVariation) +{ + return array( + array('weight' => $targetValue, 'variation' => $otherVariation), + array('weight' => 1, 'variation' => $targetVariation), + array('weight' => 100000 - ($targetValue + 1), 'variation' => $otherVariation) + ); +} + class FeatureFlagTest extends \PHPUnit_Framework_TestCase { private static $json1 = "{ @@ -498,59 +546,23 @@ public function testFlagMatchesUserFromTargets() self::assertEquals(array(), $result->getPrerequisiteEvents()); } - private function makeBooleanFlagWithRules(array $rules) - { - $flagJson = array( - 'key' => 'feature', - 'version' => 1, - 'deleted' => false, - 'on' => true, - 'targets' => array(), - 'prerequisites' => array(), - 'rules' => $rules, - 'offVariation' => 0, - 'fallthrough' => array('variation' => 0), - 'variations' => array(false, true), - 'salt' => '' - ); - return FeatureFlag::decode($flagJson); - } - public function testFlagMatchesUserFromRules() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'variation' => 1 - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => 1)))); - $result = $flag->evaluate($user, null); - $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, 'ruleid')); + $result = $flag->evaluate($defaultUser, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } public function testFlagReturnsErrorIfRuleVariationIsTooHigh() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'variation' => 999 - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => 999)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($defaultUser, null); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -558,19 +570,10 @@ public function testFlagReturnsErrorIfRuleVariationIsTooHigh() public function testFlagReturnsErrorIfRuleVariationIsNegative() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'variation' => -1 - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => -1)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($defaultUser, null); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -578,18 +581,10 @@ public function testFlagReturnsErrorIfRuleVariationIsNegative() public function testFlagReturnsErrorIfRuleHasNoVariationOrRollout() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ) - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array()))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($defaultUser, null); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -597,118 +592,154 @@ public function testFlagReturnsErrorIfRuleHasNoVariationOrRollout() public function testFlagReturnsErrorIfRuleHasRolloutWithNoVariations() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'rollout' => array('variations' => array()) - ) - )); + global $defaultUser; + $rollout = array('variations' => array()); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('rollout' => $rollout)))); + + $result = $flag->evaluate($defaultUser, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testRolloutCalculationBucketsByUserKeyByDefault() + { $ub = new LDUserBuilder('userkey'); $user = $ub->build(); + $expectedBucketValue = 22464; + $rollout = array( + 'salt' => '', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); $result = $flag->evaluate($user, null); - $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); + self::assertEquals($detail, $result->getDetail()); + } + + public function testRolloutCalculationCanBucketBySpecificAttribute() + { + $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); + $user = $ub->build(); + $expectedBucketValue = 95913; + $rollout = array( + 'salt' => '', + 'bucketBy' => 'name', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); + self::assertEquals($detail, $result->getDetail()); + } + + public function testRolloutCalculationIncludesSecondaryKey() + { + $ub = new LDUserBuilder('userkey'); + $ub->secondary('999'); + $user = $ub->build(); + $expectedBucketValue = 31179; + $rollout = array( + 'salt' => '', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } - public function testSecondaryKeyIsCoercedToStringForRolloutCalculation() - { - // We can't really verify that the rollout calculation works correctly, but we can at least - // make sure it doesn't error out if there's a non-string secondary value (ch35189) - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'rollout' => array( - 'salt' => '', - 'variations' => array( - array( - 'weight' => 100000, - 'variation' => 1 - ) - ) - ) - ) - )); + public function testRolloutCalculationCoercesSecondaryKeyToString() + { + // This should produce the same result as the previous test, and should not cause an error (ch35189). $ub = new LDUserBuilder('userkey'); $ub->secondary(999); $user = $ub->build(); + $expectedBucketValue = 31179; + $rollout = array( + 'salt' => '', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); $result = $flag->evaluate($user, null); - $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, 'ruleid')); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } - public function clauseCanMatchBuiltInAttribute() + public function testClauseCanMatchBuiltInAttribute() { $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(true, $result->getValue()); + self::assertEquals(true, $result->getDetail()->getValue()); } - public function clauseCanMatchCustomAttribute() + public function testClauseCanMatchCustomAttribute() { $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); $ub->customAttribute('legs', 4); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(true, $result->getValue()); + self::assertEquals(true, $result->getDetail()->getValue()); } - public function clauseReturnsFalseForMissingAttribute() + public function testClauseReturnsFalseForMissingAttribute() { $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(false, $result->getValue()); + self::assertEquals(false, $result->getDetail()->getValue()); } - public function clauseCanBeNegated() + public function testClauseCanBeNegated() { $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => true); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(false, $result->getValue()); + self::assertEquals(false, $result->getDetail()->getValue()); } - public function clauseWithUnknownOperatorDoesNotMatch() + public function testClauseWithUnknownOperatorDoesNotMatch() { $clause = array('attribute' => 'name', 'op' => 'doesSomethingUnsupported', 'values' => array('Bob'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(false, $result->getValue()); + self::assertEquals(false, $result->getDetail()->getValue()); } public function testSegmentMatchClauseRetrievesSegmentFromStore() { + global $defaultUser; $segmentJson = array( 'key' => 'segkey', 'version' => 1, 'deleted' => false, - 'included' => array('foo'), + 'included' => array($defaultUser->getKey()), 'excluded' => array(), 'rules' => array(), 'salt' => '' @@ -719,58 +750,22 @@ public function testSegmentMatchClauseRetrievesSegmentFromStore() $requester->key = 'segkey'; $requester->val = $segment; - $feature = $this->makeBooleanFeatureWithSegmentMatch('segkey'); - - $ub = new LDUserBuilder('foo'); - $user = $ub->build(); + $feature = makeBooleanFlagWithClauses(array(makeSegmentMatchClause('segkey'))); - $result = $feature->evaluate($user, $requester); + $result = $feature->evaluate($defaultUser, $requester); self::assertTrue($result->getDetail()->getValue()); } public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound() { + global $defaultUser; $requester = new MockFeatureRequesterForSegment(); - $feature = $this->makeBooleanFeatureWithSegmentMatch('segkey'); - - $ub = new LDUserBuilder('foo'); - $user = $ub->build(); + $feature = makeBooleanFlagWithClauses(array(makeSegmentMatchClause('segkey'))); - $result = $feature->evaluate($user, $requester); + $result = $feature->evaluate($defaultUser, $requester); self::assertFalse($result->getDetail()->getValue()); } - - private function booleanFlagWithClauses($clauses) - { - $featureJson = array( - 'key' => 'test', - 'version' => 1, - 'deleted' => false, - 'on' => true, - 'variations' => array(false, true), - 'fallthrough' => array('variation' => 0), - 'rules' => array( - array('clauses' => $clauses, 'variation' => 1) - ), - 'offVariation' => 0, - 'prerequisites' => array(), - 'targets' => array(), - 'salt' => '' - ); - return FeatureFlag::decode($featureJson); - } - - private function makeBooleanFeatureWithSegmentMatch($segmentKey) - { - $clause = array( - 'attribute' => '', - 'op' => 'segmentMatch', - 'values' => array($segmentKey), - 'negate' => false - ); - return $this->booleanFlagWithClauses(array($clause)); - } } diff --git a/tests/SegmentTest.php b/tests/SegmentTest.php index 60331d9dc..c9e6af39a 100644 --- a/tests/SegmentTest.php +++ b/tests/SegmentTest.php @@ -4,13 +4,32 @@ use LaunchDarkly\LDUserBuilder; use LaunchDarkly\Segment; +$defaultUser = (new LDUserBuilder('foo'))->build(); + +function makeSegmentMatchingUser($user, $ruleAttrs = array()) +{ + $clause = array('attribute' => 'key', 'op' => 'in', 'values' => array($user->getKey()), 'negate' => false); + $rule = array_merge(array('clauses' => array($clause)), $ruleAttrs); + $json = array( + 'key' => 'test', + 'included' => array(), + 'excluded' => array(), + 'salt' => 'salt', + 'rules' => array($rule), + 'version' => 1, + 'deleted' => false + ); + return Segment::decode($json); +} + class SegmentTest extends \PHPUnit_Framework_TestCase { public function testExplicitIncludeUser() { + global $defaultUser; $json = array( 'key' => 'test', - 'included' => array('foo'), + 'included' => array($defaultUser->getKey()), 'excluded' => array(), 'rules' => array(), 'salt' => 'salt', @@ -18,32 +37,32 @@ public function testExplicitIncludeUser() 'deleted' => false ); $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $this->assertTrue($segment->matchesUser($ub->build())); + $this->assertTrue($segment->matchesUser($defaultUser)); } public function testExplicitExcludeUser() { + global $defaultUser; $json = array( 'key' => 'test', 'included' => array(), - 'excluded' => array('foo'), + 'excluded' => array($defaultUser->getKey()), 'rules' => array(), 'salt' => 'salt', 'version' => 1, 'deleted' => false ); $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $this->assertFalse($segment->matchesUser($ub->build())); + $this->assertFalse($segment->matchesUser($defaultUser)); } public function testExplicitIncludePasPrecedence() { + global $defaultUser; $json = array( 'key' => 'test', - 'included' => array('foo'), - 'excluded' => array('foo'), + 'included' => array($defaultUser->getKey()), + 'excluded' => array($defaultUser->getKey()), 'rules' => array(), 'salt' => 'salt', 'version' => 1, @@ -56,60 +75,48 @@ public function testExplicitIncludePasPrecedence() public function testMatchingRuleWithFullRollout() { - $json = array( - 'key' => 'test', - 'included' => array(), - 'excluded' => array(), - 'salt' => 'salt', - 'rules' => array( - array( - 'clauses' => array( - array( - 'attribute' => 'email', - 'op' => 'in', - 'values' => array('test@example.com'), - 'negate' => false - ) - ), - 'weight' => 100000 - ) - ), - 'version' => 1, - 'deleted' => false - ); - $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $ub->email('test@example.com'); - $this->assertTrue($segment->matchesUser($ub->build())); + global $defaultUser; + $segment = makeSegmentMatchingUser($defaultUser, array('weight' => 100000)); + $this->assertTrue($segment->matchesUser($defaultUser)); } public function testMatchingRuleWithZeroRollout() { - $json = array( - 'key' => 'test', - 'included' => array(), - 'excluded' => array(), - 'salt' => 'salt', - 'rules' => array( - array( - 'clauses' => array( - array( - 'attribute' => 'email', - 'op' => 'in', - 'values' => array('test@example.com'), - 'negate' => false - ) - ), - 'weight' => 0 - ) - ), - 'version' => 1, - 'deleted' => false - ); - $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $ub->email('test@example.com'); - $this->assertFalse($segment->matchesUser($ub->build())); + global $defaultUser; + $segment = makeSegmentMatchingUser($defaultUser, array('weight' => 0)); + $this->assertFalse($segment->matchesUser($defaultUser)); + } + + public function testRolloutCalculationCanBucketByKey() + { + $user = (new LDUserBuilder('userkey'))->name('Bob')->build(); + $this->verifyRollout($user, 12551); + } + + public function testRolloutCalculationIncludesSecondaryKey() + { + $user = (new LDUserBuilder('userkey'))->secondary('999')->build(); + $this->verifyRollout($user, 81650); + } + + public function testRolloutCalculationCoercesSecondaryKeyToString() + { + $user = (new LDUserBuilder('userkey'))->secondary(999)->build(); + $this->verifyRollout($user, 81650); + } + + public function testRolloutCalculationCanBucketBySpecificAttribute() + { + $user = (new LDUserBuilder('userkey'))->name('Bob')->build(); + $this->verifyRollout($user, 61691, array('bucketBy' => 'name')); + } + + private function verifyRollout($user, $expectedBucketValue, $rolloutAttrs = array()) + { + $segment0 = makeSegmentMatchingUser($user, array_merge(array('weight' => $expectedBucketValue + 1), $rolloutAttrs)); + $this->assertTrue($segment0->matchesUser($user)); + $segment1 = makeSegmentMatchingUser($user, array_merge(array('weight' => $expectedBucketValue), $rolloutAttrs)); + $this->assertFalse($segment1->matchesUser($user)); } public function testMatchingRuleWithMultipleClauses() From 0e212b7dbd2f3c77f51daac4e5167455faad797e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 25 Apr 2019 20:44:53 -0700 Subject: [PATCH 71/72] misc doc fixes --- CONTRIBUTING.md | 6 +- README.md | 149 ++++++------------------------------------------ 2 files changed, 18 insertions(+), 137 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9aa5ec660..ac726c0b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkl ## Submitting bug reports and feature requests -The LaunchDarkly SDK team monitors the issue tracker associated with in the `launchdarkly/php-server-sdk` SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. +The LaunchDarkly SDK team monitors the issue tracker associated with the `launchdarkly/php-server-sdk` SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. ## Submitting pull requests @@ -16,8 +16,6 @@ We encourage pull requests and other contributions from the community. Before su The project uses [Composer](https://getcomposer.org/). -If you will be running the full test suite that includes the database integrations, you will need to have instances of Consul, DynamoDB, and Redis running locally. See below for how to skip these tests. - ### Installing dependencies From the project root directory: @@ -34,6 +32,6 @@ To run all unit tests: phpunit ``` -By default, the full unit test suite includes live tests of the integrations for Consul, DynamoDB, and Redis. To skip these, set the environment variable `LD_SKIP_DATABASE_TESTS=1` before running the tests. +By default, the full unit test suite includes live tests of the integrations for Consul, DynamoDB, and Redis. Those tests expect you to have instances of all of those databases running locally. To skip them, set the environment variable `LD_SKIP_DATABASE_TESTS=1` before running the tests. It is preferable to run tests against all supported minor versions of PHP (as described in `README.md` under Requirements), or at least the lowest and highest versions, prior to submitting a pull request. However, LaunchDarkly's CI tests will run automatically against all supported versions. diff --git a/README.md b/README.md index 0c9c2c691..020b6a742 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,34 @@ -LaunchDarkly SDK for PHP -=========================== +# LaunchDarkly Server-side SDK for PHP -[![Circle CI](https://circleci.com/gh/launchdarkly/php-server-sdk.svg?style=svg)](https://circleci.com/gh/launchdarkly/php-server-sdk) +[![Circle CI](https://img.shields.io/circleci/project/launchdarkly/php-server-sdk.png)](https://circleci.com/gh/launchdarkly/php-server-sdk) -Requirements ------------- -1. PHP 5.5 or higher. +## LaunchDarkly overview -Quick setup ------------ +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) -1. Install the PHP SDK and monolog for logging with [Composer](https://getcomposer.org/) +## Supported PHP versions - php composer.phar require launchdarkly/launchdarkly-php +This version of the LaunchDarkly SDK is compatible with PHP 5.5 and higher. -1. After installing, require Composer's autoloader: +## Getting started - require 'vendor/autoload.php'; +Refer to the [SDK reference guide](https://docs.launchdarkly.com/docs/php-sdk-reference) for instructions on getting started with using the SDK. -1. Create a new LDClient with your SDK key: +## Learn more - $client = new LaunchDarkly\LDClient("your_sdk_key"); - -Your first feature flag ------------------------ - -1. Create a new feature flag on your [dashboard](https://app.launchdarkly.com) - -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->variation("your.flag.key", $user)) { - # application code to show the feature - } else { - # the code to run if the feature is off - } - -Fetching flags --------------- - -There are two distinct methods of integrating LaunchDarkly in a PHP environment. - -* [Guzzle Cache Middleware](https://github.com/Kevinrob/guzzle-cache-middleware) to request and cache HTTP responses in an in-memory array (default) -* [ld-relay](https://github.com/launchdarkly/ld-relay) to retrieve and store flags in Redis (recommended) - -We strongly recommend using the ld-relay. Per-flag caching (Guzzle method) is only intended for low-throughput environments. - -Using Guzzle -============ - -Require Guzzle as a dependency: - - php composer.phar require "guzzlehttp/guzzle:6.2.1" - php composer.phar require "kevinrob/guzzle-cache-middleware:1.4.1" - -It will then be used as the default way of fetching flags. - -With Guzzle, you could persist your cache somewhere other than the default in-memory store, like Memcached or Redis. You could then specify your cache when initializing the client with the [cache option](https://github.com/launchdarkly/php-server-sdk/blob/master/src/LaunchDarkly/LDClient.php#L44). - - $client = new LaunchDarkly\LDClient("YOUR_SDK_KEY", array("cache" => $cacheStorage)); - - -Using LD-Relay -============== - -The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update a database cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a database store. The database can be Redis, Consul, or DynamoDB. (For more about using LaunchDarkly with databases, see the [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).) - -1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis - -2. Add the necessary dependency for the chosen database. - - For Redis: - - php composer.phar require "predis/predis:1.0.*" - - For Consul: - - php composer.phar require "sensiolabs/consul-php-sdk:2.*" - - For DynamoDB: - - php composer.phar require "aws/aws-sdk-php:3.*" - -3. Create the LDClient with the appropriate parameters for the chosen database. These examples show all of the available options. - - For Redis: - - $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => LaunchDarkly\Integrations\Redis::featureRequester(), - 'redis_host' => 'your.redis.host', // defaults to "localhost" if not specified - 'redis_port' => 6379, // defaults to 6379 if not specified - 'redis_timeout' => 5, // connection timeout in seconds; defaults to 5 - 'redis_prefix' => 'env1' // corresponds to the prefix setting in ld-relay - 'predis_client' => $myClient // use this if you have already configured a Predis client instance - ]); - - For Consul: - - $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => LaunchDarkly\Integrations\Consul::featureRequester(), - 'consul_uri' => 'http://localhost:8500', // this is the default - 'consul_prefix' => 'env1', // corresponds to the prefix setting in ld-relay - 'consul_options' => array(), // you may pass any options supported by the Guzzle client - 'apc_expiration' => 30 // expiration time for local caching, if you have apcu installed - ]); - - For DynamoDB: - - $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => LaunchDarkly\Integrations\DynamoDb::featureRequester(), - 'dynamodb_table' => 'your.table.name', // required - 'dynamodb_prefix' => 'env1', // corresponds to the prefix setting in ld-relay - 'dynamodb_options' => array(), // you may pass any options supported by the AWS SDK - 'apc_expiration' => 30 // expiration time for local caching, if you have apcu installed - ]); - -4. If you are using DynamoDB, you must create your table manually. It must have a partition key called "namespace", and a sort key called "key" (both strings). Note that by default the AWS SDK will attempt to get your AWS credentials and region from environment variables and/or local configuration files, but you may also specify them in `dynamodb_options`. - -5. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using the `Guzzle` implementation of event publishing with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. - - To forward events, add the following configuration properties to the configuration shown above: - - 'event_publisher' => LaunchDarkly\Integrations\Guzzle::eventPublisher(), - 'events_uri' => 'http://your-ldrelay-host:8030' - -Using flag data from a file ---------------------------- - -For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`LaunchDarkly\Integrations\Files`](https://github.com/launchdarkly/php-server-sdk/blob/master/src/LaunchDarkly/Integrations/Files.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). +Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](http://docs.launchdarkly.com/docs/php-sdk-reference). -Testing -------- +## Testing We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly. -Learn more ------------ - -Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](http://docs.launchdarkly.com/docs/php-sdk-reference). - -Contributing ------------- +## Contributing -We encourage pull-requests and other contributions from the community. We've also published an [SDK contributor's guide](http://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See [CONTRIBUTING](CONTRIBUTING.md) for more developer information about this project. +We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. -About LaunchDarkly ------------------- +## About LaunchDarkly * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. From 9b99cd7569348ff64f1f5d09397e21f70370e3c5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 26 Apr 2019 11:49:38 -0700 Subject: [PATCH 72/72] misc doc fixes --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac726c0b7..c010aa5b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -# Contributing to the LaunchDarkly SDK FOR PHP +# Contributing to the LaunchDarkly Server-Side SDK for PHP LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. ## Submitting bug reports and feature requests -The LaunchDarkly SDK team monitors the issue tracker associated with the `launchdarkly/php-server-sdk` SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. +The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/php-server-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. ## Submitting pull requests