Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,40 @@ 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));


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
-------
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.2
2.2.0
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 2 additions & 2 deletions src/LaunchDarkly/ApcLDDFeatureRequester.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
73 changes: 73 additions & 0 deletions src/LaunchDarkly/CurlEventPublisher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
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`.
*
* `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`.
*/
class CurlEventPublisher implements EventPublisher
{
private $_sdkKey;
private $_host;
private $_port;
private $_ssl;
private $_curl = '/usr/bin/env curl';

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 '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;
}
}
80 changes: 25 additions & 55 deletions src/LaunchDarkly/EventProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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);
}
}
21 changes: 21 additions & 0 deletions src/LaunchDarkly/EventPublisher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
namespace LaunchDarkly;

/**
* Provides a transport mechanism for sending events to the LaunchDarkly service.
*/
interface EventPublisher {
/**
* @param string $sdkKey The SDK key for your account
* @param mixed[] $options Client configuration settings
*/
public function __construct($sdkKey, array $options);

/**
* Publish events to LaunchDarkly
*
* @param $payload string Event payload
* @return bool Whether the events were successfully published
*/
public function publish($payload);
}
62 changes: 62 additions & 0 deletions src/LaunchDarkly/GuzzleEventPublisher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php
namespace LaunchDarkly;

use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;

/**
* Sends events to the LaunchDarkly service using the GuzzleHttp client.
* This `EventPublisher` implement provides an in-process alternative to
* the default `CurlEventPublisher` implementation which forks processes.
*
* Note that 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`.
*/
class GuzzleEventPublisher implements EventPublisher
{
/** @var string */
private $_sdkKey;
/** @var string */
private $_eventsUri;
/** @var LoggerInterface */
private $_logger;
/** @var mixed[] */
private $_requestOptions;

function __construct($sdkKey, array $options = array()) {
$this->_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;
}
}
}
Loading