Skip to content

Commit 79d69bc

Browse files
authored
Introduce decision service (#48)
1 parent c3b182a commit 79d69bc

File tree

6 files changed

+383
-132
lines changed

6 files changed

+383
-132
lines changed

src/Optimizely/Bucketer.php

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -170,32 +170,4 @@ public function bucket(ProjectConfig $config, Experiment $experiment, $userId)
170170
$this->_logger->log(Logger::INFO, sprintf('User "%s" is in no variation.', $userId));
171171
return new Variation();
172172
}
173-
174-
/**
175-
* Determine variation the user has been forced into.
176-
*
177-
* @param $config ProjectConfig Configuration for the project.
178-
* @param $experiment Experiment Experiment in which user is to be bucketed.
179-
* @param $userId string User identifier.
180-
*
181-
* @return null|Variation Representing the variation the user is forced into.
182-
*/
183-
public function getForcedVariation($config, $experiment, $userId)
184-
{
185-
// Check if user is whitelisted for a variation.
186-
$forcedVariations = $experiment->getForcedVariations();
187-
if (!is_null($forcedVariations) && isset($forcedVariations[$userId])) {
188-
$variationKey = $forcedVariations[$userId];
189-
$variation = $config->getVariationFromKey($experiment->getKey(), $variationKey);
190-
if ($variationKey) {
191-
$this->_logger->log(
192-
Logger::INFO,
193-
sprintf('User "%s" is forced in variation "%s" of experiment "%s".', $userId, $variationKey, $experiment->getKey())
194-
);
195-
}
196-
return $variation;
197-
}
198-
199-
return null;
200-
}
201173
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
/**
3+
* Copyright 2017, 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+
namespace Optimizely\DecisionService;
18+
19+
use Monolog\Logger;
20+
use Optimizely\Bucketer;
21+
use Optimizely\Entity\Experiment;
22+
use Optimizely\Entity\Variation;
23+
use Optimizely\Logger\LoggerInterface;
24+
use Optimizely\ProjectConfig;
25+
use Optimizely\Utils\Validator;
26+
27+
/**
28+
* Optimizely's decision service that determines which variation of an experiment the user will be allocated to.
29+
*
30+
* The decision service contains all logic around how a user decision is made. This includes all of the following (in order):
31+
* 1. Checking experiment status
32+
* 2. Checking whitelisting
33+
* 3. Checking audience targeting
34+
* 4. Using Murmurhash3 to bucket the user.
35+
*
36+
* @package Optimizely
37+
*/
38+
class DecisionService
39+
{
40+
/**
41+
* @var LoggerInterface
42+
*/
43+
private $_logger;
44+
45+
/**
46+
* @var ProjectConfig
47+
*/
48+
private $_projectConfig;
49+
50+
/**
51+
* @var Bucketer
52+
*/
53+
private $_bucketer;
54+
55+
/**
56+
* DecisionService constructor.
57+
* @param LoggerInterface $logger
58+
* @param ProjectConfig $projectConfig
59+
*/
60+
public function __construct(LoggerInterface $logger, ProjectConfig $projectConfig)
61+
{
62+
$this->_logger = $logger;
63+
$this->_projectConfig = $projectConfig;
64+
$this->_bucketer = new Bucketer($logger);
65+
}
66+
67+
/**
68+
* Determine which variation to show the user.
69+
*
70+
* @param $experiment Experiment Experiment to get the variation for.
71+
* @param $userId string User identifier.
72+
* @param $attributes array Attributes of the user.
73+
*
74+
* @return Variation Variation which the user is bucketed into.
75+
*/
76+
public function getVariation(Experiment $experiment, $userId, $attributes = null)
77+
{
78+
if (!$experiment->isExperimentRunning()) {
79+
$this->_logger->log(Logger::INFO, sprintf('Experiment "%s" is not running.', $experiment->getKey()));
80+
return null;
81+
}
82+
83+
$variation = $this->getWhitelistedVariation($experiment, $userId);
84+
if (!is_null($variation)) {
85+
return $variation;
86+
}
87+
88+
if (!Validator::isUserInExperiment($this->_projectConfig, $experiment, $attributes)) {
89+
$this->_logger->log(
90+
Logger::INFO,
91+
sprintf('User "%s" does not meet conditions to be in experiment "%s".', $userId, $experiment->getKey())
92+
);
93+
return null;
94+
}
95+
96+
$variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $userId);
97+
return $variation;
98+
}
99+
100+
/**
101+
* Determine variation the user has been forced into.
102+
*
103+
* @param $experiment Experiment Experiment in which user is to be bucketed.
104+
* @param $userId string string
105+
*
106+
* @return null|Variation Representing the variation the user is forced into.
107+
*/
108+
private function getWhitelistedVariation(Experiment $experiment, $userId)
109+
{
110+
// Check if user is whitelisted for a variation.
111+
$forcedVariations = $experiment->getForcedVariations();
112+
if (!is_null($forcedVariations) && isset($forcedVariations[$userId])) {
113+
$variationKey = $forcedVariations[$userId];
114+
$variation = $this->_projectConfig->getVariationFromKey($experiment->getKey(), $variationKey);
115+
if ($variationKey) {
116+
$this->_logger->log(
117+
Logger::INFO,
118+
sprintf('User "%s" is forced in variation "%s" of experiment "%s".', $userId, $variationKey, $experiment->getKey())
119+
);
120+
}
121+
return $variation;
122+
}
123+
return null;
124+
}
125+
}

src/Optimizely/Optimizely.php

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Optimizely\Exceptions\InvalidEventTagException;
2222
use Throwable;
2323
use Monolog\Logger;
24+
use Optimizely\DecisionService\DecisionService;
2425
use Optimizely\Entity\Experiment;
2526
use Optimizely\Logger\DefaultLogger;
2627
use Optimizely\ErrorHandler\ErrorHandlerInterface;
@@ -41,29 +42,24 @@
4142
class Optimizely
4243
{
4344
/**
44-
* @var EventDispatcherInterface
45+
* @var ProjectConfig
4546
*/
46-
private $_eventDispatcher;
47+
private $_config;
4748

4849
/**
49-
* @var LoggerInterface
50+
* @var DecisionService
5051
*/
51-
private $_logger;
52+
private $_decisionService;
5253

5354
/**
5455
* @var ErrorHandlerInterface
5556
*/
5657
private $_errorHandler;
5758

5859
/**
59-
* @var ProjectConfig
60-
*/
61-
private $_config;
62-
63-
/**
64-
* @var Bucketer
60+
* @var EventDispatcherInterface
6561
*/
66-
private $_bucketer;
62+
private $_eventDispatcher;
6763

6864
/**
6965
* @var EventBuilder
@@ -75,6 +71,11 @@ class Optimizely
7571
*/
7672
private $_isValid;
7773

74+
/**
75+
* @var LoggerInterface
76+
*/
77+
private $_logger;
78+
7879
/**
7980
* Optimizely constructor for managing Full Stack PHP projects.
8081
*
@@ -118,8 +119,8 @@ public function __construct($datafile,
118119
return;
119120
}
120121

121-
$this->_bucketer = new Bucketer($this->_logger);
122-
$this->_eventBuilder = new EventBuilder($this->_bucketer);
122+
$this->_eventBuilder = new EventBuilder();
123+
$this->_decisionService = new DecisionService($this->_logger, $this->_config);
123124
}
124125

125126
/**
@@ -137,29 +138,6 @@ private function validateDatafile($datafile, $skipJsonValidation)
137138
return true;
138139
}
139140

140-
/**
141-
* Helper function to validate all required conditions before performing activate or track.
142-
*
143-
* @param $experiment Experiment Object representing experiment.
144-
* @param $userId string ID for user.
145-
* @param $attributes array User attributes.
146-
*
147-
* @return boolean Representing whether all conditions are met or not.
148-
*/
149-
private function validatePreconditions($experiment, $userId, $attributes)
150-
{
151-
if (!$this->validateUserInputs($attributes)) {
152-
return false;
153-
}
154-
155-
if (!$experiment->isExperimentRunning()) {
156-
$this->_logger->log(Logger::INFO, sprintf('Experiment "%s" is not running.', $experiment->getKey()));
157-
return false;
158-
}
159-
160-
return true;
161-
}
162-
163141
/**
164142
* Helper function to validate user inputs into the API methods.
165143
*
@@ -363,25 +341,15 @@ public function getVariation($experimentKey, $userId, $attributes = null)
363341
return null;
364342
}
365343

366-
if (!$this->validatePreconditions($experiment, $userId, $attributes)) {
344+
if (!$this->validateUserInputs($attributes)) {
367345
return null;
368346
}
369347

370-
$variation = $this->_bucketer->getForcedVariation($this->_config, $experiment, $userId);
371-
if (!is_null($variation)) {
372-
return $variation->getKey();
373-
}
374-
375-
if (!Validator::isUserInExperiment($this->_config, $experiment, $attributes)) {
376-
$this->_logger->log(
377-
Logger::INFO,
378-
sprintf('User "%s" does not meet conditions to be in experiment "%s".', $userId, $experiment->getKey())
379-
);
348+
$variation = $this->_decisionService->getVariation($experiment, $userId, $attributes);
349+
if (is_null($variation)) {
380350
return null;
381351
}
382352

383-
$variation = $this->_bucketer->bucket($this->_config, $experiment, $userId);
384-
385353
return $variation->getKey();
386354
}
387355
}

tests/BucketerTest.php

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -281,30 +281,4 @@ public function testBucketInvalidExperiment()
281281
$bucketer->bucket($this->config, new Experiment(), $this->testUserId)
282282
);
283283
}
284-
285-
public function testGetForcedVariationExperimentNotInGroupUserInForcedVariation()
286-
{
287-
$bucketer = new Bucketer($this->loggerMock);
288-
$this->loggerMock->expects($this->once())
289-
->method('log')
290-
->with(Logger::INFO, 'User "user1" is forced in variation "control" of experiment "test_experiment".');
291-
292-
$this->assertEquals(
293-
new Variation('7722370027', 'control'),
294-
$bucketer->getForcedVariation($this->config, $this->config->getExperimentFromKey('test_experiment'), 'user1')
295-
);
296-
}
297-
298-
public function testGetForcedVariationExperimentInGroupUserInForcedVariation()
299-
{
300-
$bucketer = new Bucketer($this->loggerMock);
301-
$this->loggerMock->expects($this->once())
302-
->method('log')
303-
->with(Logger::INFO, 'User "user1" is forced in variation "group_exp_1_var_1" of experiment "group_experiment_1".');
304-
305-
$this->assertEquals(
306-
new Variation('7722260071', 'group_exp_1_var_1'),
307-
$bucketer->getForcedVariation($this->config, $this->config->getExperimentFromKey('group_experiment_1'), 'user1')
308-
);
309-
}
310284
}

0 commit comments

Comments
 (0)