Skip to content

Commit 36aaa08

Browse files
authored
Release 2.4.0 (#89)
* Support for private user attributes * Stop retrying HTTP requests if the API key has been invalidated. * User bucketing supports integer attributes. Thanks @mlund01! * Source code complies with the PSR-2 standard. Thanks @valerianpereira! * Fix PSR-4 autoloading. Thanks @jenssegers!
1 parent 0dc8a7e commit 36aaa08

16 files changed

+675
-123
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [2.4.0] - 2018-01-04
6+
### Added
7+
- Support for [private user attributes](https://docs.launchdarkly.com/docs/private-user-attributes).
8+
9+
### Changed
10+
- Stop retrying HTTP requests if the API key has been invalidated.
11+
- User bucketing supports integer attributes. Thanks @mlund01!
12+
- Source code complies with the PSR-2 standard. Thanks @valerianpereira!
13+
14+
### Fixed
15+
- The PSR-4 autoloading specification is now correct. Thanks @jenssegers!
16+
517
## [2.3.0] - 2017-10-06
618
### Added
719
- New `flush` method forces events to be published to the LaunchDarkly service. This can be useful if `LDClient` is not automatically destroyed at the end of a request. Thanks @foxted!

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.3.0
1+
2.4.0

circle.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ test:
2828

2929
- docker run -it -v `pwd`:/php-client nyanpass/php5.5:5.5-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update && vendor/bin/phpunit"
3030
- docker run -it -v `pwd`:/php-client nyanpass/php5.5:5.5-alpine sh -c "curl -s https://getcomposer.org/installer | php && cd /php-client && /composer.phar update --prefer-lowest && vendor/bin/phpunit"
31+

src/LaunchDarkly/EventProcessor.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@
77
class EventProcessor
88
{
99
private $_eventPublisher;
10+
private $_eventSerializer;
1011
private $_queue;
1112
private $_capacity;
1213
private $_timeout;
1314

14-
public function __construct($sdkKey, $options = array())
15-
{
16-
$this->_eventPublisher = $this->getEventPublisher($sdkKey, $options);
17-
18-
$this->_capacity = $options['capacity'];
19-
$this->_timeout = $options['timeout'];
15+
public function __construct($sdkKey, $options = array()) {
16+
$this->_eventPublisher = $this->getEventPublisher($sdkKey, $options);
17+
$this->_eventSerializer = new EventSerializer($options);
18+
19+
$this->_capacity = $options['capacity'];
20+
$this->_timeout = $options['timeout'];
2021

21-
$this->_queue = array();
22+
$this->_queue = array();
2223
}
2324

2425
public function __destruct()
@@ -52,7 +53,10 @@ public function flush()
5253
return null;
5354
}
5455

55-
$payload = json_encode($this->_queue);
56+
$payload = $this->_eventSerializer->serializeEvents($this->_queue);
57+
58+
// We don't expect flush to be called more than once per request cycle, but let's empty the queue just in case
59+
$this->_queue = array();
5660

5761
return $this->_eventPublisher->publish($payload);
5862
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
/**
5+
* @internal
6+
*/
7+
class EventSerializer
8+
{
9+
10+
private $_allAttrsPrivate;
11+
private $_privateAttrNames;
12+
13+
public function __construct($options)
14+
{
15+
$this->_allAttrsPrivate = isset($options['all_attributes_private']) && $options['all_attributes_private'];
16+
$this->_privateAttrNames = isset($options['private_attribute_names']) ? $options['private_attribute_names'] : array();
17+
}
18+
19+
public function serializeEvents($events)
20+
{
21+
$filtered = array();
22+
foreach ($events as $e) {
23+
array_push($filtered, $this->filterEvent($e));
24+
}
25+
return json_encode($filtered);
26+
}
27+
28+
private function filterEvent($e)
29+
{
30+
$ret = array();
31+
foreach ($e as $key => $value) {
32+
if ($key == 'user') {
33+
$ret[$key] = $this->serializeUser($value);
34+
}
35+
else {
36+
$ret[$key] = $value;
37+
}
38+
}
39+
return $ret;
40+
}
41+
42+
private function filterAttrs($attrs, &$json, $userPrivateAttrs, &$allPrivateAttrs)
43+
{
44+
foreach ($attrs as $key => $value) {
45+
if ($value != null) {
46+
if ($this->_allAttrsPrivate ||
47+
array_search($key, $userPrivateAttrs) !== FALSE ||
48+
array_search($key, $this->_privateAttrNames) !== FALSE) {
49+
$allPrivateAttrs[$key] = true;
50+
}
51+
else {
52+
$json[$key] = $value;
53+
}
54+
}
55+
}
56+
}
57+
58+
private function serializeUser($user)
59+
{
60+
$json = array("key" => $user->getKey());
61+
$userPrivateAttrs = $user->getPrivateAttributeNames();
62+
$allPrivateAttrs = array();
63+
64+
$attrs = array(
65+
'secondary' => $user->getSecondary(),
66+
'ip' => $user->getIP(),
67+
'country' => $user->getCountry(),
68+
'email' => $user->getEmail(),
69+
'name' => $user->getName(),
70+
'avatar' => $user->getAvatar(),
71+
'firstName' => $user->getFirstName(),
72+
'lastName' => $user->getLastName(),
73+
'anonymous' => $user->getAnonymous()
74+
);
75+
$this->filterAttrs($attrs, $json, $userPrivateAttrs, $allPrivateAttrs);
76+
if (!empty($user->getCustom())) {
77+
$customs = array();
78+
$this->filterAttrs($user->getCustom(), $customs, $userPrivateAttrs, $allPrivateAttrs);
79+
if ($customs) { // if this is empty, we will return a json array for 'custom' instead of an object
80+
$json['custom'] = $customs;
81+
}
82+
}
83+
if (count($allPrivateAttrs)) {
84+
$pa = array_keys($allPrivateAttrs);
85+
sort($pa);
86+
$json['privateAttrs'] = $pa;
87+
}
88+
return $json;
89+
}
90+
}

src/LaunchDarkly/GuzzleEventPublisher.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,19 @@ public function __construct($sdkKey, array $options = array())
4949
public function publish($payload)
5050
{
5151
$client = new Client(['base_uri' => $this->_eventsUri]);
52+
$response = null;
5253

5354
try {
5455
$options = $this->_requestOptions;
5556
$options['body'] = $payload;
5657
$response = $client->request('POST', '/bulk', $options);
57-
58-
return $response->getStatusCode() < 300;
5958
} catch (\Exception $e) {
6059
$this->_logger->warning("GuzzleEventPublisher::publish caught $e");
6160
return false;
6261
}
62+
if ($response && ($response->getStatusCode() == 401)) {
63+
throw new InvalidSDKKeyException();
64+
}
65+
return $response && ($response->getStatusCode() < 300);
6366
}
6467
}

src/LaunchDarkly/GuzzleFeatureRequester.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function get($key)
6565
if ($code == 404) {
6666
$this->_logger->warning("GuzzleFeatureRequester::get returned 404. Feature flag does not exist for key: " . $key);
6767
} else {
68-
$this->_logger->error("GuzzleFeatureRequester::get received an unexpected HTTP status code $code");
68+
$this->handleUnexpectedStatus($code, "GuzzleFeatureRequester::get");
6969
}
7070
return null;
7171
}
@@ -84,9 +84,15 @@ public function getAll()
8484
$body = $response->getBody();
8585
return array_map(FeatureFlag::getDecoder(), json_decode($body, true));
8686
} catch (BadResponseException $e) {
87-
$code = $e->getResponse()->getStatusCode();
88-
$this->_logger->error("GuzzleFeatureRequester::getAll received an unexpected HTTP status code $code");
87+
$this->handleUnexpectedStatus($e->getResponse()->getStatusCode(), "GuzzleFeatureRequester::getAll");
8988
return null;
9089
}
9190
}
91+
92+
private function handleUnexpectedStatus($code, $method) {
93+
$this->_logger->error("$method received an unexpected HTTP status code $code");
94+
if ($code == 401) {
95+
throw new InvalidSDKKeyException();
96+
}
97+
}
9298
}

src/LaunchDarkly/LDClient.php

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@
55
use Monolog\Logger;
66
use Psr\Log\LoggerInterface;
77

8+
/**
9+
* Used internally.
10+
*/
11+
class InvalidSDKKeyException extends \Exception
12+
{
13+
}
14+
815
/**
916
* A client for the LaunchDarkly API.
1017
*/
1118
class LDClient
1219
{
1320
const DEFAULT_BASE_URI = 'https://app.launchdarkly.com';
1421
const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com';
15-
const VERSION = '2.3.0';
22+
const VERSION = '2.4.0';
1623

1724
/** @var string */
1825
protected $_sdkKey;
@@ -50,6 +57,8 @@ class LDClient
5057
* - feature_requester_class: An optional class implementing LaunchDarkly\FeatureRequester, if `feature_requester` is not specified. Defaults to GuzzleFeatureRequester.
5158
* - event_publisher: An optional LaunchDarkly\EventPublisher instance.
5259
* - event_publisher_class: An optional class implementing LaunchDarkly\EventPublisher, if `event_publisher` is not specified. Defaults to CurlEventPublisher.
60+
* - all_attributes_private: True if no user attributes (other than the key) should be sent back to LaunchDarkly. By default, this is false.
61+
* - 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.
5362
*/
5463
public function __construct($sdkKey, $options = array())
5564
{
@@ -147,7 +156,12 @@ public function variation($key, $user, $default = false)
147156
if ($user->isKeyBlank()) {
148157
$this->_logger->warning("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly.");
149158
}
150-
$flag = $this->_featureRequester->get($key);
159+
try {
160+
$flag = $this->_featureRequester->get($key);
161+
} catch (InvalidSDKKeyException $e) {
162+
$this->handleInvalidSDKKey();
163+
return $default;
164+
}
151165

152166
if (is_null($flag)) {
153167
$this->_sendFlagRequestEvent($key, $user, $default, $default);
@@ -213,7 +227,7 @@ public function track($eventName, $user, $data)
213227
}
214228

215229
$event = array();
216-
$event['user'] = $user->toJSON();
230+
$event['user'] = $user;
217231
$event['kind'] = "custom";
218232
$event['creationDate'] = Util::currentTimeUnixMillis();
219233
$event['key'] = $eventName;
@@ -236,7 +250,7 @@ public function identify($user)
236250
}
237251

238252
$event = array();
239-
$event['user'] = $user->toJSON();
253+
$event['user'] = $user;
240254
$event['kind'] = "identify";
241255
$event['creationDate'] = Util::currentTimeUnixMillis();
242256
$event['key'] = $user->getKey();
@@ -260,7 +274,15 @@ public function allFlags($user)
260274
$this->_logger->warn("allFlags called with null user or null/empty user key! Returning null");
261275
return null;
262276
}
263-
$flags = $this->_featureRequester->getAll();
277+
if ($this->isOffline()) {
278+
return null;
279+
}
280+
try {
281+
$flags = $this->_featureRequester->getAll();
282+
} catch (InvalidSDKKeyException $e) {
283+
$this->handleInvalidSDKKey();
284+
return null;
285+
}
264286
if ($flags === null) {
265287
return null;
266288
}
@@ -294,7 +316,11 @@ public function secureModeHash($user)
294316
*/
295317
public function flush()
296318
{
297-
return $this->_eventProcessor->flush();
319+
try {
320+
return $this->_eventProcessor->flush();
321+
} catch (InvalidSDKKeyException $e) {
322+
$this->handleInvalidSDKKey();
323+
}
298324
}
299325

300326
/**
@@ -321,4 +347,9 @@ protected function _get_default($key, $default)
321347
return $default;
322348
}
323349
}
350+
351+
protected function handleInvalidSDKKey() {
352+
$this->_logger->error("Received 401 error, no further HTTP requests will be made during lifetime of LDClient since SDK key is invalid");
353+
$this->_offline = true;
354+
}
324355
}

src/LaunchDarkly/LDUser.php

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class LDUser
1919
protected $_lastName = null;
2020
protected $_anonyomus = false;
2121
protected $_custom = array();
22+
protected $_privateAttributeNames = array();
2223

2324
/**
2425
* @param string $key Unique key for the user. For authenticated users, this may be a username or e-mail address. For anonymous users, this could be an IP address or session ID.
@@ -33,7 +34,7 @@ class LDUser
3334
* @param boolean|null $anonymous Whether this is an anonymous user
3435
* @param array|null $custom Other custom attributes that can be used to create custom rules
3536
*/
36-
public function __construct($key, $secondary = null, $ip = null, $country = null, $email = null, $name = null, $avatar = null, $firstName = null, $lastName = null, $anonymous = null, $custom = array())
37+
public function __construct($key, $secondary = null, $ip = null, $country = null, $email = null, $name = null, $avatar = null, $firstName = null, $lastName = null, $anonymous = null, $custom = array(), $privateAttributeNames = array())
3738
{
3839
if ($key !== null) {
3940
$this->_key = strval($key);
@@ -48,6 +49,7 @@ public function __construct($key, $secondary = null, $ip = null, $country = null
4849
$this->_lastName = $lastName;
4950
$this->_anonymous = $anonymous;
5051
$this->_custom = $custom;
52+
$this->_privateAttributeNames = $privateAttributeNames;
5153
}
5254

5355
public function getValueForEvaluation($attr)
@@ -143,45 +145,13 @@ public function getAnonymous()
143145
return $this->_anonymous;
144146
}
145147

146-
public function isKeyBlank()
148+
public function getPrivateAttributeNames()
147149
{
148-
return isset($this->_key) && empty($this->_key);
150+
return $this->_privateAttributeNames;
149151
}
150-
151-
public function toJSON()
152+
153+
public function isKeyBlank()
152154
{
153-
$json = array("key" => $this->_key);
154-
155-
if (isset($this->_secondary)) {
156-
$json['secondary'] = $this->_secondary;
157-
}
158-
if (isset($this->_ip)) {
159-
$json['ip'] = $this->_ip;
160-
}
161-
if (isset($this->_country)) {
162-
$json['country'] = $this->_country;
163-
}
164-
if (isset($this->_email)) {
165-
$json['email'] = $this->_email;
166-
}
167-
if (isset($this->_name)) {
168-
$json['name'] = $this->_name;
169-
}
170-
if (isset($this->_avatar)) {
171-
$json['avatar'] = $this->_avatar;
172-
}
173-
if (isset($this->_firstName)) {
174-
$json['firstName'] = $this->_firstName;
175-
}
176-
if (isset($this->_lastName)) {
177-
$json['lastName'] = $this->_lastName;
178-
}
179-
if (isset($this->_custom) && !empty($this->_custom)) {
180-
$json['custom'] = $this->_custom;
181-
}
182-
if (isset($this->_anonymous)) {
183-
$json['anonymous'] = $this->_anonymous;
184-
}
185-
return $json;
155+
return isset($this->_key) && empty($this->_key);
186156
}
187157
}

0 commit comments

Comments
 (0)