Skip to content

Commit b9cdcc2

Browse files
mfahadahmedaliabbasrizvi
authored andcommitted
feat(datafile-management): Adds HTTPProjectConfigManager and integrated it with Optimizely class (#180)
1 parent fc77081 commit b9cdcc2

File tree

7 files changed

+730
-36
lines changed

7 files changed

+730
-36
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/**
3+
* Copyright 2019, 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\Enums;
19+
20+
class ProjectConfigManagerConstants
21+
{
22+
/**
23+
* @const int Time in seconds to wait before timing out.
24+
*/
25+
const TIMEOUT = 10;
26+
27+
/**
28+
* @const String Default URL Template to use if only SDK key is provided.
29+
*/
30+
const DEFAULT_URL_TEMPLATE = "https://cdn.optimizely.com/datafiles/%s.json";
31+
32+
/**
33+
* @const String to use while fetching the datafile.
34+
*/
35+
const IF_MODIFIED_SINCE = "If-Modified-Since";
36+
37+
/**
38+
* @const String to use while handling the response.
39+
*/
40+
const LAST_MODIFIED = "Last-Modified";
41+
}

src/Optimizely/Optimizely.php

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@
3434
use Optimizely\Event\Dispatcher\EventDispatcherInterface;
3535
use Optimizely\Logger\LoggerInterface;
3636
use Optimizely\Logger\NoOpLogger;
37-
use Optimizely\ProjectConfigManager\StaticProjectConfigManager;
3837
use Optimizely\Notification\NotificationCenter;
3938
use Optimizely\Notification\NotificationType;
39+
use Optimizely\ProjectConfigManager\ProjectConfigManagerInterface;
40+
use Optimizely\ProjectConfigManager\StaticProjectConfigManager;
4041
use Optimizely\UserProfile\UserProfileServiceInterface;
4142
use Optimizely\Utils\Errors;
4243
use Optimizely\Utils\Validator;
@@ -56,6 +57,11 @@ class Optimizely
5657
const VARIABLE_KEY = 'Variable Key';
5758
const VARIATION_KEY = 'Variation Key';
5859

60+
/**
61+
* @var DatafileProjectConfig
62+
*/
63+
private $_config;
64+
5965
/**
6066
* @var DecisionService
6167
*/
@@ -87,7 +93,7 @@ class Optimizely
8793
private $_logger;
8894

8995
/**
90-
* @var ProjectConfigManager
96+
* @var ProjectConfigManagerInterface
9197
*/
9298
private $_projectConfigManager;
9399

@@ -112,16 +118,21 @@ public function __construct(
112118
LoggerInterface $logger = null,
113119
ErrorHandlerInterface $errorHandler = null,
114120
$skipJsonValidation = false,
115-
UserProfileServiceInterface $userProfileService = null
121+
UserProfileServiceInterface $userProfileService = null,
122+
ProjectConfigManagerInterface $configManager = null
116123
) {
117124
$this->_isValid = true;
118125
$this->_eventDispatcher = $eventDispatcher ?: new DefaultEventDispatcher();
119126
$this->_logger = $logger ?: new NoOpLogger();
120127
$this->_errorHandler = $errorHandler ?: new NoOpErrorHandler();
121128
$this->_eventBuilder = new EventBuilder($this->_logger);
122-
$this->_projectConfigManager = new StaticProjectConfigManager($datafile, $skipJsonValidation, $this->_logger, $this->_errorHandler);
123129
$this->_decisionService = new DecisionService($this->_logger, $userProfileService);
124130
$this->notificationCenter = new NotificationCenter($this->_logger, $this->_errorHandler);
131+
$this->_projectConfigManager = $configManager;
132+
133+
if ($this->_projectConfigManager === null) {
134+
$this->_projectConfigManager = new StaticProjectConfigManager($datafile, $skipJsonValidation, $this->_logger, $this->_errorHandler);
135+
}
125136
}
126137

127138
/**
@@ -166,17 +177,14 @@ private function validateUserInputs($attributes, $eventTags = null)
166177
}
167178

168179
/**
169-
* @param string Experiment key
170-
* @param string Variation key
171-
* @param string User ID
172-
* @param array Associative array of user attributes
180+
* @param string Experiment key
181+
* @param string Variation key
182+
* @param string User ID
183+
* @param array Associative array of user attributes
184+
* @param DatafileProjectConfig DatafileProjectConfig instance
173185
*/
174-
protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes)
186+
protected function sendImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes)
175187
{
176-
// TODO: Config should be passed as param when this is called from activate but
177-
// since PHP is single-threaded we can leave this for now.
178-
$config = $this->getConfig();
179-
180188
$impressionEvent = $this->_eventBuilder
181189
->createImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes);
182190
$this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey));
@@ -254,7 +262,7 @@ public function activate($experimentKey, $userId, $attributes = null)
254262
return null;
255263
}
256264

257-
$this->sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes);
265+
$this->sendImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes);
258266

259267
return $variationKey;
260268
}
@@ -527,7 +535,7 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null)
527535
'variationKey'=> $variationKey
528536
);
529537

530-
$this->sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes);
538+
$this->sendImpressionEvent($config, $experimentKey, $variationKey, $userId, $attributes);
531539
} else {
532540
$this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'.");
533541
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php
2+
/**
3+
* Copyright 2019, Optimizely Inc and Contributors
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\ProjectConfigManager;
19+
20+
use Exception;
21+
use GuzzleHttp\Client as HttpClient;
22+
use Monolog\Logger;
23+
use Optimizely\Config\DatafileProjectConfig;
24+
use Optimizely\Enums\ProjectConfigManagerConstants;
25+
use Optimizely\ErrorHandler\NoOpErrorHandler;
26+
use Optimizely\Logger\NoOpLogger;
27+
use Optimizely\Utils\Validator;
28+
29+
class HTTPProjectConfigManager implements ProjectConfigManagerInterface
30+
{
31+
/**
32+
* @var \GuzzleHttp\Client Guzzle HTTP client to send requests.
33+
*/
34+
private $httpClient;
35+
36+
/**
37+
* @var DatafileProjectConfig
38+
*/
39+
private $_config;
40+
41+
/**
42+
* @var String Datafile URL.
43+
*/
44+
private $_url;
45+
46+
/**
47+
* @var boolean Flag indicates that skip JSON validation of datafile.
48+
*/
49+
private $_skipJsonValidation;
50+
51+
/**
52+
* @var String datafile last modified time.
53+
*/
54+
private $_lastModifiedSince;
55+
56+
/**
57+
* @var LoggerInterface Logger instance.
58+
*/
59+
private $_logger;
60+
61+
/**
62+
* @var ErrorHandlerInterface ErrorHandler instance.
63+
*/
64+
private $_errorHandler;
65+
66+
public function __construct(
67+
$sdkKey = null,
68+
$url = null,
69+
$urlTemplate = null,
70+
$fetchOnInit = true,
71+
$datafile = null,
72+
$skipJsonValidation = false,
73+
$logger = null,
74+
$errorHandler = null
75+
) {
76+
$this->_skipJsonValidation = $skipJsonValidation;
77+
$this->_logger = $logger;
78+
$this->_errorHandler = $errorHandler;
79+
$this->httpClient = new HttpClient();
80+
81+
if ($this->_logger === null) {
82+
$this->_logger = new NoOpLogger();
83+
}
84+
85+
if ($this->_errorHandler === null) {
86+
$this->_errorHandler = new NoOpErrorHandler();
87+
}
88+
89+
$this->_url = $this->getUrl($sdkKey, $url, $urlTemplate);
90+
91+
if ($datafile !== null) {
92+
$this->_config = DatafileProjectConfig::createProjectConfigFromDatafile(
93+
$datafile,
94+
$skipJsonValidation,
95+
$this->_logger,
96+
$this->_errorHandler
97+
);
98+
}
99+
100+
// Update config on initialization.
101+
if ($fetchOnInit === true) {
102+
$this->fetch();
103+
}
104+
}
105+
106+
/**
107+
* Helper function to return URL based on params passed.
108+
*
109+
* @param $sdkKey string SDK key.
110+
* @param $url string URL for datafile.
111+
* @param $urlTemplate string Template to be used with SDK key to fetch datafile.
112+
*
113+
* @return string URL for datafile.
114+
*/
115+
protected function getUrl($sdkKey, $url, $urlTemplate)
116+
{
117+
if (Validator::validateNonEmptyString($url)) {
118+
return $url;
119+
}
120+
121+
if (!Validator::validateNonEmptyString($sdkKey)) {
122+
$exception = new Exception("One of the SDK key or URL must be provided.");
123+
$this->_errorHandler->handleError($exception);
124+
throw $exception;
125+
}
126+
127+
if (!Validator::validateNonEmptyString($urlTemplate)) {
128+
$urlTemplate = ProjectConfigManagerConstants::DEFAULT_URL_TEMPLATE;
129+
}
130+
131+
$url = sprintf($urlTemplate, $sdkKey);
132+
133+
return $url;
134+
}
135+
136+
/**
137+
* Function to fetch latest datafile.
138+
*
139+
* @return boolean flag to indicate if datafile is updated.
140+
*/
141+
public function fetch()
142+
{
143+
$datafile = $this->fetchDatafile();
144+
145+
if ($datafile === null) {
146+
return false;
147+
}
148+
149+
return true;
150+
}
151+
152+
/**
153+
* Helper function to fetch datafile and handle response if datafile is modified.
154+
*
155+
* @return null|datafile.
156+
*/
157+
protected function fetchDatafile()
158+
{
159+
$headers = null;
160+
161+
// Add If-Modified-Since header.
162+
if (Validator::validateNonEmptyString($this->_lastModifiedSince)) {
163+
$headers = array(ProjectConfigManagerConstants::IF_MODIFIED_SINCE => $this->_lastModifiedSince);
164+
}
165+
166+
$options = [
167+
'headers' => $headers,
168+
'timeout' => ProjectConfigManagerConstants::TIMEOUT,
169+
'connect_timeout' => ProjectConfigManagerConstants::TIMEOUT
170+
];
171+
172+
try {
173+
$response = $this->httpClient->get($this->_url, $options);
174+
} catch (Exception $exception) {
175+
$this->_logger->log(Logger::ERROR, 'Unexpected response when trying to fetch datafile, status code: ' . $exception->getCode());
176+
return null;
177+
}
178+
179+
$status = $response->getStatusCode();
180+
181+
// Datafile not updated.
182+
if ($status === 304) {
183+
$this->_logger->log(Logger::DEBUG, 'Not updating ProjectConfig as datafile has not updated since ' . $this->_lastModifiedSince);
184+
return null;
185+
}
186+
187+
// Datafile retrieved successfully.
188+
if ($status >= 200 && $status < 300) {
189+
if ($response->hasHeader(ProjectConfigManagerConstants::LAST_MODIFIED)) {
190+
$this->_lastModifiedSince = $response->getHeader(ProjectConfigManagerConstants::LAST_MODIFIED)[0];
191+
}
192+
193+
$datafile = $response->getBody();
194+
195+
if ($this->handleResponse($datafile) === true) {
196+
return $datafile;
197+
}
198+
199+
return null;
200+
}
201+
202+
// Failed to retrieve datafile from Url.
203+
$this->_logger->log(Logger::ERROR, 'Unexpected response when trying to fetch datafile, status code: ' . $status);
204+
return null;
205+
}
206+
207+
/**
208+
* Helper function to create config from datafile.
209+
*
210+
* @param string $datafile
211+
* @return boolean flag to indicate if config is updated.
212+
*/
213+
protected function handleResponse($datafile)
214+
{
215+
if ($datafile === null) {
216+
return false;
217+
}
218+
219+
$config = DatafileProjectConfig::createProjectConfigFromDatafile($datafile, $this->_skipJsonValidation, $this->_logger, $this->_errorHandler);
220+
if ($config === null) {
221+
return false;
222+
}
223+
224+
$previousRevision = null;
225+
if ($this->_config !== null) {
226+
$previousRevision = $this->_config->getRevision();
227+
}
228+
229+
if ($previousRevision === $config->getRevision()) {
230+
return false;
231+
}
232+
233+
$this->_config = $config;
234+
return true;
235+
}
236+
237+
/**
238+
* Returns instance of DatafileProjectConfig.
239+
* @return null|DatafileProjectConfig DatafileProjectConfig instance.
240+
*/
241+
public function getConfig()
242+
{
243+
return $this->_config;
244+
}
245+
}

0 commit comments

Comments
 (0)