Skip to content

Commit 118d4aa

Browse files
committed
re-add LDUser, allow SDK to accept it interchangeably with LDContext
1 parent ba5947c commit 118d4aa

File tree

8 files changed

+814
-18
lines changed

8 files changed

+814
-18
lines changed

Makefile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log
1111

1212
# TEST_HARNESS_PARAMS can be set to add -skip parameters for any contract tests that cannot yet pass
1313
# Explanation of current skips:
14-
# - "evaluation/bucketing/secondary": The "secondary" behavior needs to be removed from contract tests.
1514
# - "evaluation/parameterized/attribute references/array index is not supported": Due to how PHP
1615
# arrays work, there's no way to disallow an array index lookup without breaking object property
1716
# lookups for properties that are numeric strings.
1817
TEST_HARNESS_PARAMS := $(TEST_HARNESS_PARAMS) \
19-
-skip 'evaluation/bucketing/secondary' \
2018
-skip 'evaluation/parameterized/attribute references/array index is not supported'
2119

2220
build-contract-tests:

src/LaunchDarkly/LDClient.php

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,12 @@ private function getFeatureRequester(string $sdkKey, array $options): FeatureReq
178178
* does not match any existing flag), `$defaultValue` is returned.
179179
*
180180
* @param string $key the unique key for the feature flag
181-
* @param LDContext $context the evaluation context
181+
* @param LDContext|LDUser $context the evaluation context
182182
* @param mixed $defaultValue the default value of the flag
183183
* @return mixed the variation for the given context, or `$defaultValue` if the flag cannot be evaluated
184184
* @see \LaunchDarkly\LDClient::variationDetail()
185185
*/
186-
public function variation(string $key, LDContext $context, mixed $defaultValue = false): mixed
186+
public function variation(string $key, LDContext|LDUser $context, mixed $defaultValue = false): mixed
187187
{
188188
$detail = $this->variationDetailInternal($key, $context, $defaultValue, $this->_eventFactoryDefault);
189189
return $detail->getValue();
@@ -197,27 +197,28 @@ public function variation(string $key, LDContext $context, mixed $defaultValue =
197197
* detailed event data for this flag.
198198
*
199199
* @param string $key the unique key for the feature flag
200-
* @param LDContext $context the evaluation context
200+
* @param LDContext|LDUser $context the evaluation context
201201
* @param mixed $defaultValue the default value of the flag
202202
*
203203
* @return EvaluationDetail an EvaluationDetail object that includes the feature flag value
204204
* and evaluation reason
205205
*/
206-
public function variationDetail(string $key, LDContext $context, mixed $defaultValue = false): EvaluationDetail
206+
public function variationDetail(string $key, LDContext|LDUser $context, mixed $defaultValue = false): EvaluationDetail
207207
{
208208
return $this->variationDetailInternal($key, $context, $defaultValue, $this->_eventFactoryWithReasons);
209209
}
210210

211211
/**
212212
* @param string $key
213-
* @param LDContext $context
213+
* @param LDContext|LDUser $contextOrUser
214214
* @param mixed $default
215215
* @param EventFactory $eventFactory
216216
*
217217
* @return EvaluationDetail
218218
*/
219-
private function variationDetailInternal(string $key, LDContext $context, mixed $default, EventFactory $eventFactory): EvaluationDetail
219+
private function variationDetailInternal(string $key, LDContext|LDUser $contextOrUser, mixed $default, EventFactory $eventFactory): EvaluationDetail
220220
{
221+
$context = $contextOrUser instanceof LDUser ? LDContext::fromUser($contextOrUser) : $contextOrUser;
221222
$default = $this->_get_default($key, $default);
222223

223224
$errorDetail = fn (string $errorKind): EvaluationDetail =>
@@ -297,14 +298,15 @@ public function isOffline(): bool
297298
* Tracks that a user performed an event.
298299
*
299300
* @param string $eventName The name of the event
300-
* @param LDContext $context The user that performed the event
301+
* @param LDContext|LDUser $context The evaluation context associated with the event
301302
* @param mixed $data Optional additional information to associate with the event
302303
* @param int|float|null $metricValue A numeric value used by the LaunchDarkly experimentation feature in
303304
* numeric custom metrics. Can be omitted if this event is used by only non-numeric metrics. This
304305
* field will also be returned as part of the custom event for Data Export.
305306
*/
306-
public function track(string $eventName, LDContext $context, mixed $data = null, int|float|null $metricValue = null): void
307+
public function track(string $eventName, LDContext|LDUser $context, mixed $data = null, int|float|null $metricValue = null): void
307308
{
309+
$context = $context instanceof LDUser ? LDContext::fromUser($context) : $context;
308310
if (!$context->isValid()) {
309311
$this->_logger->warning("Track called with null/empty user key!");
310312
return;
@@ -318,11 +320,12 @@ public function track(string $eventName, LDContext $context, mixed $data = null,
318320
* This simply registers the given user properties with LaunchDarkly without evaluating a feature flag.
319321
* This also happens automatically when you evaluate a flag.
320322
*
321-
* @param LDContext $context The user properties
323+
* @param LDContext|LDUser $context The user properties
322324
* @return void
323325
*/
324-
public function identify(LDContext $context): void
326+
public function identify(LDContext|LDUser $context): void
325327
{
328+
$context = $context instanceof LDUser ? LDContext::fromUser($context) : $context;
326329
if (!$context->isValid()) {
327330
$this->_logger->warning("Identify called with null/empty user key!");
328331
return;
@@ -339,7 +342,7 @@ public function identify(LDContext $context): void
339342
*
340343
* This method does not send analytics events back to LaunchDarkly.
341344
*
342-
* @param LDContext $context the evalation context
345+
* @param LDContext|LDUser $context the evalation context
343346
* @param array $options Optional properties affecting how the state is computed:
344347
* - `clientSideOnly`: Set this to true to specify that only flags marked for client-side use
345348
* should be included; by default, all flags are included
@@ -351,8 +354,9 @@ public function identify(LDContext $context): void
351354
*
352355
* @return FeatureFlagsState a FeatureFlagsState object (will never be null)
353356
*/
354-
public function allFlagsState(LDContext $context, array $options = []): FeatureFlagsState
357+
public function allFlagsState(LDContext|LDUser $context, array $options = []): FeatureFlagsState
355358
{
359+
$context = $context instanceof LDUser ? LDContext::fromUser($context) : $context;
356360
if (!$context->isValid()) {
357361
$error = $context->getError();
358362
$this->_logger->warning("Invalid context for allFlagsState ($error); returning empty state");
@@ -395,11 +399,12 @@ public function allFlagsState(LDContext $context, array $options = []): FeatureF
395399
*
396400
* See: [Secure mode](https://docs.launchdarkly.com/sdk/features/secure-mode)
397401
*
398-
* @param LDContext $context the evaluation context
402+
* @param LDContext|LDUser $context the evaluation context
399403
* @return string the hash value
400404
*/
401-
public function secureModeHash(LDContext $context): string
405+
public function secureModeHash(LDContext|LDUser $context): string
402406
{
407+
$context = $context instanceof LDUser ? LDContext::fromUser($context) : $context;
403408
if (!$context->isValid()) {
404409
return "";
405410
}

src/LaunchDarkly/LDContext.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
* A collection of attributes that can be referenced in flag evaluations and analytics events.
1111
* This entity is also called an "evaluation context."
1212
*
13+
* LDContext is the newer replacement for the previous, less flexible {@see \LaunchDarkly\LDUser} type.
14+
* The current SDK still supports LDUser, but LDContext is now the preferred model and may entirely
15+
* replace User in the future.
16+
*
1317
* To create an LDContext of a single kind, such as a user, you may use
1418
* {@see \LaunchDarkly\LDContext::create()} when only the key and the kind are relevant; or, to
1519
* specify other attributes, use {@see \LaunchDarkly\LDContext::builder()}.
@@ -197,6 +201,58 @@ public static function createMulti(LDContext ...$contexts): LDContext
197201
return $b->build();
198202
}
199203

204+
/**
205+
* @param LDUser $user
206+
* @return LDContext
207+
*/
208+
public static function fromUser(LDUser $user): LDContext
209+
{
210+
$attrs = null;
211+
self::maybeAddAttr($attrs, "avatar", $user->getAvatar());
212+
self::maybeAddAttr($attrs, "country", $user->getCountry());
213+
self::maybeAddAttr($attrs, "email", $user->getEmail());
214+
self::maybeAddAttr($attrs, "firstName", $user->getFirstName());
215+
self::maybeAddAttr($attrs, "ip", $user->getIP());
216+
self::maybeAddAttr($attrs, "lastName", $user->getLastName());
217+
$userCustom = $user->getCustom();
218+
if ($userCustom !== null && count($userCustom) !== 0) {
219+
if ($attrs === null) {
220+
$attrs = [];
221+
}
222+
foreach ($userCustom as $k => $v) {
223+
$attrs[$k] = $v;
224+
}
225+
}
226+
$privateAttrs = null;
227+
$userPrivate = $user->getPrivateAttributeNames();
228+
if ($userPrivate !== null && count($userPrivate) !== 0) {
229+
$privateAttrs = [];
230+
foreach ($userPrivate as $pa) {
231+
$privateAttrs[] = AttributeReference::fromLiteral($pa);
232+
}
233+
}
234+
return new LDContext(
235+
self::DEFAULT_KIND,
236+
$user->getKey(),
237+
$user->getName(),
238+
$user->getAnonymous() ?? false,
239+
$attrs,
240+
$privateAttrs,
241+
null,
242+
null
243+
);
244+
}
245+
246+
private static function maybeAddAttr(?array &$attrsOut, string $name, ?string $value): void
247+
{
248+
if ($value !== null) {
249+
if ($attrsOut === null) {
250+
$attrsOut = [];
251+
}
252+
$attrsOut[$name] = $value;
253+
}
254+
}
255+
200256
/**
201257
* Creates a builder for building an LDContext.
202258
*

src/LaunchDarkly/LDUser.php

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaunchDarkly;
6+
7+
/**
8+
* Contains specific attributes of a user browsing your site.
9+
*
10+
* LDUser supports only a subset of the behaviors that are available with the newer {@see \LaunchDarkly\LDContext}
11+
* type. An LDUser is equivalent to an individual LDContext that has a `kind` of {@see \LaunchDarkly\LDContext::DEFAULT_KIND};
12+
* it also has more constraints on attribute values than LDContext does (for instance, built-in attributes such as
13+
* {@see \LaunchDarkly\LDUserBuilder::email()} can only have string values). Older LaunchDarkly SDKs only had the
14+
* LDUser model, and the LDUser type has been retained for backward compatibility, but it may be removed in a
15+
* future SDK version. Therefore, developers are recommended to migrate toward using LDContext.
16+
*
17+
* The only mandatory property property is the key, which must uniquely identify each user. For authenticated users,
18+
* this may be a username or e-mail address. For anonymous users, it could be an IP address or session ID.
19+
*
20+
* Use {@see \LaunchDarkly\LDUserBuilder} to construct instances of this class.
21+
*/
22+
class LDUser
23+
{
24+
protected string $_key;
25+
protected ?string $_ip = null;
26+
protected ?string $_country = null;
27+
protected ?string $_email = null;
28+
protected ?string $_name = null;
29+
protected ?string $_avatar = null;
30+
protected ?string $_firstName = null;
31+
protected ?string $_lastName = null;
32+
protected ?bool $_anonymous = false;
33+
protected ?array $_custom = [];
34+
protected ?array $_privateAttributeNames = [];
35+
36+
/**
37+
* Constructor for directly creating an instance.
38+
*
39+
* It is preferable to use {@see LDUserBuilder} instead of this constructor.
40+
*
41+
* @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.
42+
* @param string|null $secondary Obsolete parameter that is ignored if present, retained to avoid breaking code that called this constructor
43+
* @param string|null $ip The user's IP address (optional)
44+
* @param string|null $country The user's country, as an ISO 3166-1 alpha-2 code (e.g. 'US') (optional)
45+
* @param string|null $email The user's e-mail address (optional)
46+
* @param string|null $name The user's full name (optional)
47+
* @param string|null $avatar A URL pointing to the user's avatar image (optional)
48+
* @param string|null $firstName The user's first name (optional)
49+
* @param string|null $lastName The user's last name (optional)
50+
* @param bool|null $anonymous Whether this is an anonymous user
51+
* @param array|null $custom Other custom attributes that can be used to create custom rules
52+
* @return LDUser
53+
*/
54+
public function __construct(
55+
string $key,
56+
?string $secondary = null,
57+
?string $ip = null,
58+
?string $country = null,
59+
?string $email = null,
60+
?string $name = null,
61+
?string $avatar = null,
62+
?string $firstName = null,
63+
?string $lastName = null,
64+
?bool $anonymous = null,
65+
?array $custom = [],
66+
?array $privateAttributeNames = []
67+
) {
68+
$this->_key = $key;
69+
$this->_ip = $ip;
70+
$this->_country = $country;
71+
$this->_email = $email;
72+
$this->_name = $name;
73+
$this->_avatar = $avatar;
74+
$this->_firstName = $firstName;
75+
$this->_lastName = $lastName;
76+
$this->_anonymous = $anonymous;
77+
$this->_custom = $custom;
78+
$this->_privateAttributeNames = $privateAttributeNames;
79+
}
80+
81+
/**
82+
* Used internally in flag evaluation.
83+
* @ignore
84+
* @return mixed
85+
*/
86+
public function getValueForEvaluation(?string $attr): mixed
87+
{
88+
if (is_null($attr)) {
89+
return null;
90+
}
91+
switch ($attr) {
92+
case "key":
93+
return $this->_key;
94+
case "ip":
95+
return $this->_ip;
96+
case "country":
97+
return $this->_country;
98+
case "email":
99+
return $this->_email;
100+
case "name":
101+
return $this->_name;
102+
case "avatar":
103+
return $this->_avatar;
104+
case "firstName":
105+
return $this->_firstName;
106+
case "lastName":
107+
return $this->_lastName;
108+
case "anonymous":
109+
return $this->_anonymous;
110+
default:
111+
if ($this->_custom === null) {
112+
return null;
113+
}
114+
return $this->_custom[$attr] ?? null;
115+
}
116+
}
117+
118+
public function getCountry(): ?string
119+
{
120+
return $this->_country;
121+
}
122+
123+
public function getCustom(): ?array
124+
{
125+
return $this->_custom;
126+
}
127+
128+
public function getIP(): ?string
129+
{
130+
return $this->_ip;
131+
}
132+
133+
public function getKey(): string
134+
{
135+
return $this->_key;
136+
}
137+
138+
public function getEmail(): ?string
139+
{
140+
return $this->_email;
141+
}
142+
143+
public function getName(): ?string
144+
{
145+
return $this->_name;
146+
}
147+
148+
public function getAvatar(): ?string
149+
{
150+
return $this->_avatar;
151+
}
152+
153+
public function getFirstName(): ?string
154+
{
155+
return $this->_firstName;
156+
}
157+
158+
public function getLastName(): ?string
159+
{
160+
return $this->_lastName;
161+
}
162+
163+
public function getAnonymous(): ?bool
164+
{
165+
return $this->_anonymous;
166+
}
167+
168+
public function getPrivateAttributeNames(): ?array
169+
{
170+
return $this->_privateAttributeNames;
171+
}
172+
173+
public function isKeyBlank(): bool
174+
{
175+
return empty($this->_key);
176+
}
177+
}

0 commit comments

Comments
 (0)