Skip to content

Commit d617d18

Browse files
oakbanialiabbasrizvi
authored andcommitted
feat (audience match types): Update audience evaluator and project config for new audience match types (#145)
1 parent 6093028 commit d617d18

13 files changed

+576
-153
lines changed

src/Optimizely/ProjectConfig.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,15 @@ public function __construct($datafile, $logger, $errorHandler)
214214
$events = $config['events'] ?: [];
215215
$attributes = $config['attributes'] ?: [];
216216
$audiences = $config['audiences'] ?: [];
217+
$typedAudiences = isset($config['typedAudiences']) ? $config['typedAudiences']: [];
217218
$rollouts = isset($config['rollouts']) ? $config['rollouts'] : [];
218219
$featureFlags = isset($config['featureFlags']) ? $config['featureFlags']: [];
219220

220221
$this->_groupIdMap = ConfigParser::generateMap($groups, 'id', Group::class);
221222
$this->_experimentKeyMap = ConfigParser::generateMap($experiments, 'key', Experiment::class);
222223
$this->_eventKeyMap = ConfigParser::generateMap($events, 'key', Event::class);
223224
$this->_attributeKeyMap = ConfigParser::generateMap($attributes, 'key', Attribute::class);
225+
$typedAudienceIdMap = ConfigParser::generateMap($typedAudiences, 'id', Audience::class);
224226
$this->_audienceIdMap = ConfigParser::generateMap($audiences, 'id', Audience::class);
225227
$this->_rollouts = ConfigParser::generateMap($rollouts, null, Rollout::class);
226228
$this->_featureFlags = ConfigParser::generateMap($featureFlags, null, FeatureFlag::class);
@@ -249,12 +251,19 @@ public function __construct($datafile, $logger, $errorHandler)
249251
}
250252
}
251253

252-
$conditionDecoder = new ConditionDecoder();
253254
foreach (array_values($this->_audienceIdMap) as $audience) {
254-
$conditionDecoder->deserializeAudienceConditions($audience->getConditions());
255-
$audience->setConditionsList($conditionDecoder->getConditionsList());
255+
$audience->setConditionsList(json_decode($audience->getConditions(), true));
256256
}
257257

258+
// Conditions in typedAudiences are not expected to be string-encoded so they don't need
259+
// to be decoded unlike audiences.
260+
foreach (array_values($typedAudienceIdMap) as $typedAudience) {
261+
$typedAudience->setConditionsList($typedAudience->getConditions());
262+
}
263+
264+
// Overwrite audiences by typedAudiences.
265+
$this->_audienceIdMap = array_replace($this->_audienceIdMap, $typedAudienceIdMap);
266+
258267
$rolloutVariationIdMap = [];
259268
$rolloutVariationKeyMap = [];
260269
foreach ($this->_rollouts as $rollout) {

src/Optimizely/Utils/ConditionDecoder.php

Lines changed: 0 additions & 44 deletions
This file was deleted.

src/Optimizely/Utils/ConditionTreeEvaluator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ protected function notEvaluator(array $condition, callable $leafEvaluator)
140140
*/
141141
public function evaluate($conditions, callable $leafEvaluator)
142142
{
143-
if (is_array($conditions)) {
143+
if (!Validator::doesArrayContainOnlyStringKeys($conditions)) {
144144

145145
if(in_array($conditions[0], $this->getOperators())) {
146146
$operator = array_shift($conditions);

src/Optimizely/Utils/CustomAttributeConditionEvaluator.php

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ public function __construct(array $userAttributes)
4444
$this->userAttributes = $userAttributes;
4545
}
4646

47+
/**
48+
* Sets null for missing keys in a leaf condition.
49+
*
50+
* @param array $leafCondition The leaf condition node of an audience.
51+
*/
52+
protected function setNullForMissingKeys(array $leafCondition)
53+
{
54+
$keys = ['type', 'match', 'value'];
55+
foreach($keys as $key) {
56+
$leafCondition[$key] = isset($leafCondition[$key]) ? $leafCondition[$key]: null;
57+
}
58+
59+
return $leafCondition;
60+
}
61+
4762
/**
4863
* Gets the supported match types for condition evaluation.
4964
*
@@ -103,8 +118,8 @@ protected function isValueValidForExactConditions($value)
103118
*/
104119
protected function exactEvaluator($condition)
105120
{
106-
$conditionName = $condition->{'name'};
107-
$conditionValue = $condition->{'value'};
121+
$conditionName = $condition['name'];
122+
$conditionValue = $condition['value'];
108123
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
109124

110125
if(!$this->isValueValidForExactConditions($userValue) ||
@@ -128,7 +143,7 @@ protected function exactEvaluator($condition)
128143
*/
129144
protected function existsEvaluator($condition)
130145
{
131-
$conditionName = $condition->{'name'};
146+
$conditionName = $condition['name'];
132147
return isset($this->userAttributes[$conditionName]);
133148
}
134149

@@ -144,8 +159,8 @@ protected function existsEvaluator($condition)
144159
*/
145160
protected function greaterThanEvaluator($condition)
146161
{
147-
$conditionName = $condition->{'name'};
148-
$conditionValue = $condition->{'value'};
162+
$conditionName = $condition['name'];
163+
$conditionValue = $condition['value'];
149164
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
150165

151166
if(!Validator::isFiniteNumber($userValue) || !Validator::isFiniteNumber($conditionValue)) {
@@ -167,8 +182,8 @@ protected function greaterThanEvaluator($condition)
167182
*/
168183
protected function lessThanEvaluator($condition)
169184
{
170-
$conditionName = $condition->{'name'};
171-
$conditionValue = $condition->{'value'};
185+
$conditionName = $condition['name'];
186+
$conditionValue = $condition['value'];
172187
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
173188

174189
if(!Validator::isFiniteNumber($userValue) || !Validator::isFiniteNumber($conditionValue)) {
@@ -190,8 +205,8 @@ protected function lessThanEvaluator($condition)
190205
*/
191206
protected function substringEvaluator($condition)
192207
{
193-
$conditionName = $condition->{'name'};
194-
$conditionValue = $condition->{'value'};
208+
$conditionName = $condition['name'];
209+
$conditionValue = $condition['value'];
195210
$userValue = isset($this->userAttributes[$conditionName]) ? $this->userAttributes[$conditionName]: null;
196211

197212
if(!is_string($userValue) || !is_string($conditionValue)) {
@@ -211,14 +226,16 @@ protected function substringEvaluator($condition)
211226
*/
212227
public function evaluate($leafCondition)
213228
{
214-
if($leafCondition->{'type'} !== self::CUSTOM_ATTRIBUTE_CONDITION_TYPE) {
229+
$leafCondition = $this->setNullForMissingKeys($leafCondition);
230+
231+
if($leafCondition['type'] !== self::CUSTOM_ATTRIBUTE_CONDITION_TYPE) {
215232
return null;
216233
}
217234

218-
if(!isset($leafCondition->{'match'})) {
235+
if(($leafCondition['match']) === null) {
219236
$conditionMatch = self::EXACT_MATCH_TYPE;
220237
} else {
221-
$conditionMatch = $leafCondition->{'match'};
238+
$conditionMatch = $leafCondition['match'];
222239
}
223240

224241
if(!in_array($conditionMatch, $this->getMatchTypes())) {

src/Optimizely/Utils/Validator.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,8 @@ public static function isUserInExperiment($config, $experiment, $userAttributes)
111111
return true;
112112
}
113113

114-
// Return false if there is audience, but no user attributes.
115-
if (empty($userAttributes)) {
116-
return false;
114+
if ($userAttributes == null) {
115+
$userAttributes = [];
117116
}
118117

119118
$customAttrCondEval = new CustomAttributeConditionEvaluator($userAttributes);
@@ -223,4 +222,18 @@ public static function areValuesSameType($firstVal, $secondVal)
223222

224223
return $firstValType == $secondValType;
225224
}
225+
226+
/**
227+
* Returns true only if given input is an array with all of it's keys of type string.
228+
* @param mixed $arr
229+
* @return bool True if array contains all string keys. Otherwise, false.
230+
*/
231+
public static function doesArrayContainOnlyStringKeys($arr)
232+
{
233+
if(!is_array($arr) || empty($arr)) {
234+
return false;
235+
}
236+
237+
return count(array_filter(array_keys($arr), 'is_string')) == count(array_keys($arr));
238+
}
226239
}

tests/DecisionServiceTests/DecisionServiceTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,7 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar
11381138
$experiment2 = $rollout->getExperiments()[2];
11391139

11401140
// Set an AudienceId for everyone else/last rule so that user does not qualify for audience
1141-
$experiment2->setAudienceIds(["11154"]);
1141+
$experiment2->setAudienceIds(["11155"]);
11421142
$expected_variation = $experiment2->getVariations()[0];
11431143

11441144
// Provide null attributes so that user does not qualify for audience

0 commit comments

Comments
 (0)