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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.0.0] - 2018-02-21
### Added
- Support for a new LaunchDarkly feature: reusable user segments.

## [2.5.0] - 2018-02-13
### Added
- Adds support for a future LaunchDarkly feature, coming soon: semantic version user attributes.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.5.0
3.0.0
12 changes: 6 additions & 6 deletions src/LaunchDarkly/ApcLDDFeatureRequester.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ protected function fetch($key, &$success = null)
return \apc_fetch($key, $success);
}

protected function get_from_cache($key)
protected function get_from_cache($namespace, $key)
{
$key = self::make_cache_key($key);
$key = self::make_cache_key($namespace, $key);
$enabled = $this->fetch($key);
if ($enabled === false) {
return null;
Expand All @@ -54,13 +54,13 @@ protected function add($key, $var, $ttl = 0)
return \apc_add($key, $var, $ttl);
}

protected function store_in_cache($key, $val)
protected function store_in_cache($namespace, $key, $val)
{
$this->add($this->make_cache_key($key), $val, $this->_expiration);
$this->add($this->make_cache_key($namespace, $key), $val, $this->_expiration);
}

private function make_cache_key($name)
private function make_cache_key($namespace, $name)
{
return $this->_features_key.'.'.$name;
return $namespace.'.'.$name;
}
}
23 changes: 22 additions & 1 deletion src/LaunchDarkly/Clause.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,28 @@ public static function getDecoder()
* @param $user LDUser
* @return bool
*/
public function matchesUser($user)
public function matchesUser($user, $featureRequester)
{
if ($this->_op === 'segmentMatch') {
foreach ($this->_values as $value) {
$segment = $featureRequester->getSegment($value);
if ($segment) {
if ($segment->matchesUser($user)) {
return $this->_maybeNegate(true);
}
}
}
return $this->_maybeNegate(false);
} else {
return $this->matchesUserNoSegments($user);
}
}

/**
* @param $user LDUser
* @return bool
*/
public function matchesUserNoSegments($user)
{
$userValue = $user->getValueForEvaluation($this->_attribute);
if ($userValue === null) {
Expand Down
8 changes: 4 additions & 4 deletions src/LaunchDarkly/FeatureFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ private function _evaluate($user, $featureRequester, &$events)
foreach ($this->_prerequisites as $prereq) {
try {
$prereqEvalResult = null;
$prereqFeatureFlag = $featureRequester->get($prereq->getKey());
$prereqFeatureFlag = $featureRequester->getFeature($prereq->getKey());
if ($prereqFeatureFlag == null) {
return null;
} elseif ($prereqFeatureFlag->isOn()) {
Expand All @@ -134,7 +134,7 @@ private function _evaluate($user, $featureRequester, &$events)
}
}
if ($prereqOk) {
return $this->getVariation($this->evaluateIndex($user));
return $this->getVariation($this->evaluateIndex($user, $featureRequester));
}
return null;
}
Expand All @@ -143,7 +143,7 @@ private function _evaluate($user, $featureRequester, &$events)
* @param $user LDUser
* @return int|null
*/
private function evaluateIndex($user)
private function evaluateIndex($user, $featureRequester)
{
// Check to see if targets match
if ($this->_targets != null) {
Expand All @@ -158,7 +158,7 @@ private function evaluateIndex($user)
// Now walk through the rules and see if any match
if ($this->_rules != null) {
foreach ($this->_rules as $rule) {
if ($rule->matchesUser($user)) {
if ($rule->matchesUser($user, $featureRequester)) {
return $rule->variationIndexForUser($user, $this->_key, $this->_salt);
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/LaunchDarkly/FeatureRequester.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ interface FeatureRequester
* @param $key string feature key
* @return FeatureFlag|null The decoded FeatureFlag, or null if missing
*/
public function get($key);
public function getFeature($key);

/**
* 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);

/**
* Gets all features.
*
* @return array()|null The decoded FeatureFlags, or null if missing
*/
public function getAll();
public function getAllFeatures();
}
30 changes: 27 additions & 3 deletions src/LaunchDarkly/GuzzleFeatureRequester.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
class GuzzleFeatureRequester implements FeatureRequester
{
const SDK_FLAGS = "/sdk/flags";
const SDK_SEGMENTS = "/sdk/segments";
/** @var Client */
private $_client;
/** @var string */
Expand Down Expand Up @@ -46,14 +47,13 @@ public function __construct($baseUri, $sdkKey, $options)
$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 get($key)
public function getFeature($key)
{
try {
$uri = $this->_baseUri . self::SDK_FLAGS . "/" . $key;
Expand All @@ -71,12 +71,36 @@ public function get($key)
}
}

/**
* 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 getAll()
public function getAllFeatures()
{
try {
$uri = $this->_baseUri . self::SDK_FLAGS;
Expand Down
6 changes: 3 additions & 3 deletions src/LaunchDarkly/LDClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class LDClient
{
const DEFAULT_BASE_URI = 'https://app.launchdarkly.com';
const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com';
const VERSION = '2.5.0';
const VERSION = '3.0.0';

/** @var string */
protected $_sdkKey;
Expand Down Expand Up @@ -157,7 +157,7 @@ public function variation($key, $user, $default = false)
$this->_logger->warning("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly.");
}
try {
$flag = $this->_featureRequester->get($key);
$flag = $this->_featureRequester->getFeature($key);
} catch (InvalidSDKKeyException $e) {
$this->handleInvalidSDKKey();
return $default;
Expand Down Expand Up @@ -278,7 +278,7 @@ public function allFlags($user)
return null;
}
try {
$flags = $this->_featureRequester->getAll();
$flags = $this->_featureRequester->getAllFeatures();
} catch (InvalidSDKKeyException $e) {
$this->handleInvalidSDKKey();
return null;
Expand Down
56 changes: 44 additions & 12 deletions src/LaunchDarkly/LDDFeatureRequester.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class LDDFeatureRequester implements FeatureRequester
protected $_sdkKey;
protected $_options;
protected $_features_key;
protected $_segments_key;
/** @var LoggerInterface */
private $_logger;
/** @var ClientInterface */
Expand All @@ -33,6 +34,7 @@ public function __construct($baseUri, $sdkKey, $options)
$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) {
Expand All @@ -56,21 +58,20 @@ protected function get_connection()
"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 get($key)
public function getFeature($key)
{
$raw = $this->get_from_cache($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($key, $raw);
$this->store_in_cache($this->_features_key, $key, $raw);
}
}
if ($raw) {
Expand All @@ -86,22 +87,53 @@ public function get($key)
}
}

/**
* 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->_features_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 $key string The feature key
* @return null|array The feature data or null if missing
* @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($key)
protected function get_from_cache($namespace, $key)
{
return null;
}

/**
* Stores the feature data into the local cache. No-op by default.
* @param $key string The feature key
* @param $val array The feature data
* 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($key, $val)
protected function store_in_cache($namespace, $key, $val)
{
}

Expand All @@ -110,7 +142,7 @@ protected function store_in_cache($key, $val)
*
* @return array()|null The decoded FeatureFlags, or null if missing
*/
public function getAll()
public function getAllFeatures()
{
$redis = $this->get_connection();
$raw = $redis->hgetall($this->_features_key);
Expand Down
4 changes: 2 additions & 2 deletions src/LaunchDarkly/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ public static function getDecoder()
* @param $user LDUser
* @return bool
*/
public function matchesUser($user)
public function matchesUser($user, $featureRequester)
{
foreach ($this->_clauses as $clause) {
if (!$clause->matchesUser($user)) {
if (!$clause->matchesUser($user, $featureRequester)) {
return false;
}
}
Expand Down
Loading