Skip to content

Commit 212c0d7

Browse files
Adding audience evaluation (#5)
1 parent de31675 commit 212c0d7

File tree

11 files changed

+464
-33
lines changed

11 files changed

+464
-33
lines changed

src/Optimizely/Entity/Audience.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ class Audience
3434
*/
3535
private $_conditions;
3636

37+
/**
38+
* @var array De-serialized audience conditions
39+
*/
40+
private $_conditionsList;
41+
3742

3843
public function __construct($id = null, $name = null, $conditions = null) {
3944
$this->_id = $id;
@@ -88,4 +93,20 @@ public function setConditions($conditions)
8893
{
8994
$this->_conditions = $conditions;
9095
}
96+
97+
/**
98+
* @return array De-serialized audience conditions.
99+
*/
100+
public function getConditionsList()
101+
{
102+
return $this->_conditionsList;
103+
}
104+
105+
/**
106+
* @param $conditionsList array De-serialized audience conditions.
107+
*/
108+
public function setConditionsList($conditionsList)
109+
{
110+
$this->_conditionsList = $conditionsList;
111+
}
91112
}

src/Optimizely/Entity/Experiment.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ public function getForcedVariations()
214214
*/
215215
public function setForcedVariations($forcedVariations)
216216
{
217-
$this->_forcedVariations = get_object_vars($forcedVariations);
217+
$this->_forcedVariations = $forcedVariations;
218218
}
219219

220220
/**

src/Optimizely/Optimizely.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ private function validatePreconditions($experiment, $userId, $attributes)
129129
return true;
130130
}
131131

132-
//@TODO(ali): Insert audience check
132+
if (!Validator::isUserInExperiment($this->_config, $experiment, $attributes)) {
133+
return false;
134+
}
133135

134136
return true;
135137
}

src/Optimizely/ProjectConfig.php

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Optimizely\Entity\Experiment;
2424
use Optimizely\Entity\Group;
2525
use Optimizely\Entity\Variation;
26+
use Optimizely\Utils\ConditionDecoder;
2627
use Optimizely\Utils\ConfigParser;
2728

2829
/**
@@ -100,17 +101,17 @@ class ProjectConfig
100101
*/
101102
public function __construct($datafile)
102103
{
103-
$config = json_decode($datafile);
104-
$this->_version = $config->{'version'};
105-
$this->_accountId = $config->{'accountId'};
106-
$this->_projectId = $config->{'projectId'};
107-
$this->_revision = $config->{'revision'};
108-
109-
$groups = $config->{'groups'};
110-
$experiments = $config->{'experiments'};
111-
$events = $config->{'events'};
112-
$attributes = $config->{'attributes'};
113-
$audiences = $config->{'audiences'};
104+
$config = json_decode($datafile, true);
105+
$this->_version = $config['version'];
106+
$this->_accountId = $config['accountId'];
107+
$this->_projectId = $config['projectId'];
108+
$this->_revision = $config['revision'];
109+
110+
$groups = $config['groups'];
111+
$experiments = $config['experiments'];
112+
$events = $config['events'];
113+
$attributes = $config['attributes'];
114+
$audiences = $config['audiences'];
114115

115116
$this->_groupIdMap = ConfigParser::generateMap($groups, 'id', Group::class);
116117
$this->_experimentKeyMap = ConfigParser::generateMap($experiments, 'key', Experiment::class);
@@ -137,6 +138,12 @@ public function __construct($datafile)
137138
$this->_variationIdMap[$experiment->getKey()][$variation->getId()] = $variation;
138139
}
139140
}
141+
142+
$conditionDecoder = new ConditionDecoder();
143+
forEach(array_values($this->_audienceIdMap) as $audience) {
144+
$conditionDecoder->deserializeAudienceConditions($audience->getConditions());
145+
$audience->setConditionsList($conditionDecoder->getConditionsList());
146+
}
140147
}
141148

142149
/**
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
/**
3+
* Copyright 2016, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Optimizely\Utils;
19+
20+
class ConditionDecoder
21+
{
22+
/**
23+
* @var array JSON decoded audience conditions.
24+
*/
25+
private $_conditionsList;
26+
27+
/**
28+
* Deserialize audience conditions into a list of structure and conditions.
29+
*
30+
* @param $conditions string Audience conditions.
31+
*/
32+
public function deserializeAudienceConditions($conditions)
33+
{
34+
$this->_conditionsList = json_decode($conditions);
35+
}
36+
37+
/**
38+
* @return array JSON decoded audience conditions.
39+
*/
40+
public function getConditionsList()
41+
{
42+
return $this->_conditionsList;
43+
}
44+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
/**
3+
* Copyright 2016, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Optimizely\Utils;
19+
20+
class ConditionEvaluator
21+
{
22+
/**
23+
* const string Representing AND operator.
24+
*/
25+
const AND_OPERATOR = 'and';
26+
27+
/**
28+
* const string Representing OR operator.
29+
*/
30+
const OR_OPERATOR = 'or';
31+
32+
/**
33+
* const string Representing NOT operator.
34+
*/
35+
const NOT_OPERATOR = 'not';
36+
37+
/**
38+
* @param $conditions array Audience conditions list.
39+
* @param $userAttributes array Associative array of user attributes to values.
40+
*
41+
* @return boolean True if all conditions evaluate to True.
42+
*/
43+
private function andEvaluator($conditions, $userAttributes)
44+
{
45+
forEach ($conditions as $condition) {
46+
$result = $this->evaluate($condition, $userAttributes);
47+
if (!$result) {
48+
return false;
49+
}
50+
}
51+
52+
return true;
53+
}
54+
55+
/**
56+
* @param $conditions array Audience conditions list.
57+
* @param $userAttributes array Associative array of user attributes to values.
58+
*
59+
* @return boolean True if any one of the conditions evaluate to True.
60+
*/
61+
private function orEvaluator($conditions, $userAttributes)
62+
{
63+
forEach ($conditions as $condition) {
64+
$result = $this->evaluate($condition, $userAttributes);
65+
if ($result) {
66+
return true;
67+
}
68+
}
69+
70+
return false;
71+
}
72+
73+
/**
74+
* @param $condition array Audience conditions list consisting of single condition.
75+
* @param $userAttributes array Associative array of user attributes to values.
76+
*
77+
* @return boolean True if the condition evaluates to False.
78+
*/
79+
private function notEvaluator($condition, $userAttributes)
80+
{
81+
if (count($condition) != 1) {
82+
return false;
83+
}
84+
85+
return !$this->evaluate($condition[0], $userAttributes);
86+
}
87+
88+
/**
89+
* Function to evaluate audience conditions against user's attributes.
90+
*
91+
* @param $conditions array Nested array of and/or/not conditions representing the audience conditions.
92+
* @param $userAttributes array Associative array of user attributes to values.
93+
*
94+
* @return boolean Representing if audience conditions are satisfied or not.
95+
*/
96+
public function evaluate($conditions, $userAttributes)
97+
{
98+
if (is_array($conditions)) {
99+
switch ($conditions[0]) {
100+
case self::AND_OPERATOR:
101+
array_shift($conditions);
102+
return $this->andEvaluator($conditions, $userAttributes);
103+
case self::OR_OPERATOR:
104+
array_shift($conditions);
105+
return $this->orEvaluator($conditions, $userAttributes);
106+
case self::NOT_OPERATOR:
107+
array_shift($conditions);
108+
return $this->notEvaluator($conditions, $userAttributes);
109+
default:
110+
return false;
111+
}
112+
113+
}
114+
115+
$conditionName = $conditions->{'name'};
116+
if (!isset($userAttributes[$conditionName])) {
117+
return false;
118+
}
119+
return $userAttributes[$conditionName] == $conditions->{'value'};
120+
}
121+
}

src/Optimizely/Utils/Validator.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
namespace Optimizely\Utils;
1818

1919
use JsonSchema;
20+
use Optimizely\Entity\Experiment;
21+
use Optimizely\ProjectConfig;
2022

2123

2224
class Validator
@@ -46,4 +48,38 @@ public static function areAttributesValid($attributes)
4648
//TODO(ali): Implement me
4749
return True;
4850
}
51+
52+
/**
53+
* @param $config ProjectConfig Configuration for the project.
54+
* @param $experiment Experiment Entity representing the experiment.
55+
* @param $userAttributes array Attributes of the user.
56+
*
57+
* @return boolean Representing whether user meets audience conditions to be in experiment or not.
58+
*/
59+
public static function isUserInExperiment($config, $experiment, $userAttributes)
60+
{
61+
$audienceIds = $experiment->getAudienceIds();
62+
63+
// Return true if experiment is not targeted to any audience.
64+
if (empty($audienceIds)) {
65+
return true;
66+
}
67+
68+
// Return false if there is audience, but no user attributes.
69+
if (empty($userAttributes)) {
70+
return false;
71+
}
72+
73+
// Return true if conditions for any audience are met.
74+
$conditionEvaluator = new ConditionEvaluator();
75+
forEach ($audienceIds as $audienceId) {
76+
$audience = $config->getAudience($audienceId);
77+
$result = $conditionEvaluator->evaluate($audience->getConditionsList(), $userAttributes);
78+
if ($result) {
79+
return true;
80+
}
81+
}
82+
83+
return false;
84+
}
4985
}

0 commit comments

Comments
 (0)