diff --git a/CHANGELOG.md b/CHANGELOG.md index dd9beb412..8cd2db97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.2.0] - 2017-06-05 +### Added +- Support for [publishing events via ld-relay](README.md#using-ld-relay) +- Allow `EventPublisher` to be injected into the client. +- `GuzzleEventPublisher` as a synchronous, in-process alternative to publishing events via background processes. + ## [2.1.2] - 2017-04-27 ### Changed - Relaxed the requirement on `kevinrob/guzzle-cache-middleware` for the default `GuzzleFeatureRequester`. diff --git a/README.md b/README.md index db889dd62..368d8ed30 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,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#L42). +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). $client = new LaunchDarkly\LDClient("YOUR_SDK_KEY", array("cache" => $cacheStorage)); @@ -66,15 +66,32 @@ With Guzzle, you could persist your cache somewhere other than the default in-me Using LD-Relay ============== -* Setup [ld-relay](https://github.com/launchdarkly/ld-relay) in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis +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. -* Require Predis as a dependency: +1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis - php composer.phar require "predis/predis:1.0.*" +2. Require Predis as a dependency: -* Create the LDClient with the Redis feature requester as an option: + php composer.phar require "predis/predis:1.0.*" - $client = new LaunchDarkly\LDClient("your_sdk_key", ['feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', 'redis_host' => 'your.redis.host', 'redis_port' => 6379]); +3. Create the LDClient with the Redis feature requester as an option: + + $client = new LaunchDarkly\LDClient("your_sdk_key", [ + 'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', + 'redis_host' => 'your.redis.host', + 'redis_port' => 6379 + ]); + +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. + + $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 + ]); Testing ------- diff --git a/VERSION b/VERSION index eca07e4c1..ccbccc3dc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.2 +2.2.0 diff --git a/composer.json b/composer.json index 2ed48998a..28d09d16e 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "zendframework/zend-serializer": "^2.7" }, "suggest": { - "guzzlehttp/guzzle": "(^6.2.1) Required when using the default FeatureRequester", + "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" }, diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index ad4a36af7..d6d56e005 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -13,8 +13,8 @@ class ApcLDDFeatureRequester extends LDDFeatureRequester { protected $_expiration = 30; - function __construct($baseUri, $apiKey, $options) { - parent::__construct($baseUri, $apiKey, $options); + function __construct($baseUri, $sdkKey, $options) { + parent::__construct($baseUri, $sdkKey, $options); if (isset($options['apc_expiration'])) { $this->_expiration = (int)$options['apc_expiration']; diff --git a/src/LaunchDarkly/CurlEventPublisher.php b/src/LaunchDarkly/CurlEventPublisher.php new file mode 100644 index 000000000..96131ed55 --- /dev/null +++ b/src/LaunchDarkly/CurlEventPublisher.php @@ -0,0 +1,73 @@ +_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 '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; + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index cfa74043e..af6ea81ab 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -6,44 +6,13 @@ */ class EventProcessor { - private $_sdkKey; + private $_eventPublisher; private $_queue; private $_capacity; private $_timeout; - private $_host; - private $_port; - private $_ssl; - private $_curl = '/usr/bin/env curl'; - - public function __construct($apiKey, $options = array()) { - $this->_sdkKey = $apiKey; - if (!isset($options['events_uri'])) { - $this->_host = 'events.launchdarkly.com'; - $this->_port = 443; - $this->_ssl = true; - $this->_path = ''; - } - else { - $url = parse_url(rtrim($options['events_uri'],'/')); - $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 __construct($sdkKey, $options = array()) { + $this->_eventPublisher = $this->getEventPublisher($sdkKey, $options); $this->_capacity = $options['capacity']; $this->_timeout = $options['timeout']; @@ -76,28 +45,29 @@ protected function flush() { $payload = json_encode($this->_queue); - $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 'Accept: application/json'"; - $args.= " -d " . escapeshellarg($payload); - $args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); - return $args; + return $this->_eventPublisher->publish($payload); } - private function makeRequest($args) { - $cmd = $this->_curl . " " . $args . ">> /dev/null 2>&1 &"; - shell_exec($cmd); - return true; - } + /** + * @param string $sdkKey + * @param mixed[] $options + * @return EventPublisher + */ + private function getEventPublisher($sdkKey, array $options) + { + if (isset($options['event_publisher']) && $options['event_publisher'] instanceof EventPublisher) { + return $options['event_publisher']; + } + if (isset($options['event_publisher_class'])) { + $eventPublisherClass = $options['event_publisher_class']; + } else { + $eventPublisherClass = CurlEventPublisher::class; + } + if (!is_a($eventPublisherClass, EventPublisher::class, true)) { + throw new \InvalidArgumentException; + } + return new $eventPublisherClass($sdkKey, $options); + } } \ No newline at end of file diff --git a/src/LaunchDarkly/EventPublisher.php b/src/LaunchDarkly/EventPublisher.php new file mode 100644 index 000000000..0b21d6fb7 --- /dev/null +++ b/src/LaunchDarkly/EventPublisher.php @@ -0,0 +1,21 @@ +_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' + ], + 'timeout' => $options['timeout'], + 'connect_timeout' => $options['connect_timeout'] + ]; + } + + public function publish($payload) { + $client = new Client(['base_uri' => $this->_eventsUri]); + + try { + $options = $this->_requestOptions; + $options['body'] = $payload; + $response = $client->request('POST', '/bulk', $options); + + return $response->getStatusCode() < 300; + } catch (\Exception $e) { + $this->_logger->warning("GuzzleEventPublisher::publish caught $e"); + return false; + } + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index a10031a6c..becd4e6a6 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -10,12 +10,15 @@ */ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; - const VERSION = '2.1.2'; + const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; + const VERSION = '2.2.0'; /** @var string */ protected $_sdkKey; /** @var string */ protected $_baseUri; + /** @var string */ + protected $_eventsUri; /** @var EventProcessor */ protected $_eventProcessor; /** @var bool */ @@ -26,8 +29,7 @@ class LDClient { protected $_defaults = array(); /** @var LoggerInterface */ protected $_logger; - - /** @var FeatureRequester */ + /** @var FeatureRequester */ protected $_featureRequester; /** @@ -43,6 +45,10 @@ class LDClient { * - send_events: An optional bool that can disable the sending of events to LaunchDarkly. Defaults to false. * - logger: An optional Psr\Log\LoggerInterface. Defaults to a Monolog\Logger sending all messages to the php error_log. * - offline: An optional boolean which will disable all network calls and always return the default value. Defaults to false. + * - 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. + * - 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. */ public function __construct($sdkKey, $options = array()) { $this->_sdkKey = $sdkKey; @@ -51,6 +57,11 @@ public function __construct($sdkKey, $options = array()) { } else { $this->_baseUri = rtrim($options['base_uri'], '/'); } + if (!isset($options['events_uri'])) { + $this->_eventsUri = self::DEFAULT_EVENTS_URI; + } else { + $this->_eventsUri = rtrim($options['events_uri'], '/'); + } if (isset($options['send_events'])) { $this->_send_events = $options['send_events']; } @@ -82,15 +93,15 @@ public function __construct($sdkKey, $options = array()) { $this->_eventProcessor = new EventProcessor($sdkKey, $options); - $this->_featureRequester = $this->getFeatureRequester($options, $sdkKey); + $this->_featureRequester = $this->getFeatureRequester($sdkKey, $options); } /** - * @param mixed[] $options * @param string $sdkKey + * @param mixed[] $options * @return FeatureRequester */ - private function getFeatureRequester(array $options, $sdkKey) + private function getFeatureRequester($sdkKey, array $options) { if (isset($options['feature_requester']) && $options['feature_requester'] instanceof FeatureRequester) { return $options['feature_requester']; diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index 67142b538..dd5ec7262 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -7,7 +7,7 @@ class LDDFeatureRequester implements FeatureRequester { protected $_baseUri; - protected $_apiKey; + protected $_sdkKey; protected $_options; protected $_features_key; /** @var LoggerInterface */ @@ -15,9 +15,9 @@ class LDDFeatureRequester implements FeatureRequester { /** @var ClientInterface */ private $_connection; - function __construct($baseUri, $apiKey, $options) { + function __construct($baseUri, $sdkKey, $options) { $this->_baseUri = $baseUri; - $this->_apiKey = $apiKey; + $this->_sdkKey = $sdkKey; if (!isset($options['redis_host'])) { $options['redis_host'] = 'localhost'; } diff --git a/tests/LDDFeatureRequesterTest.php b/tests/LDDFeatureRequesterTest.php index 97bfcc428..2fb2d7d07 100644 --- a/tests/LDDFeatureRequesterTest.php +++ b/tests/LDDFeatureRequesterTest.php @@ -29,7 +29,7 @@ protected function setUp() public function testGet() { - $sut = new LDDFeatureRequester('example.com', 'MyApiKey', [ + $sut = new LDDFeatureRequester('example.com', 'MySdkKey', [ 'logger' => $this->logger, 'predis_client' => $this->predisClient, ]);