Skip to content

Commit 4cb2c35

Browse files
oakbanimikeproeng37
authored andcommitted
Feature Notification Center (#73)
1 parent d4a4aad commit 4cb2c35

File tree

10 files changed

+1534
-120
lines changed

10 files changed

+1534
-120
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
18+
namespace Optimizely\Exceptions;
19+
20+
21+
class InvalidCallbackArgumentCountException extends OptimizelyException
22+
{
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
18+
namespace Optimizely\Exceptions;
19+
20+
21+
class InvalidNotificationTypeException extends OptimizelyException
22+
{
23+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
/**
3+
* Copyright 2017, 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+
namespace Optimizely\Notification;
18+
19+
use ArgumentCountError;
20+
use Exception;
21+
use Throwable;
22+
23+
use Monolog\Logger;
24+
25+
use Optimizely\ErrorHandler\ErrorHandlerInterface;
26+
use Optimizely\Exceptions\InvalidCallbackArgumentCountException;
27+
use Optimizely\Exceptions\InvalidNotificationTypeException;
28+
use Optimizely\Logger\LoggerInterface;
29+
use Optimizely\Logger\NoOpLogger;
30+
31+
class NotificationCenter
32+
{
33+
private $_notificationId;
34+
35+
private $_notifications;
36+
37+
private $_logger;
38+
39+
private $_errorHandler;
40+
41+
public function __construct(LoggerInterface $logger, ErrorHandlerInterface $errorHandler)
42+
{
43+
$this->_notificationId = 1;
44+
$this->_notifications = [];
45+
foreach (array_values(NotificationType::getAll()) as $type) {
46+
$this->_notifications[$type] = [];
47+
}
48+
49+
$this->_logger = $logger;
50+
$this->_errorHandler = $errorHandler;
51+
}
52+
53+
public function getNotifications()
54+
{
55+
return $this->_notifications;
56+
}
57+
58+
/**
59+
* Adds a notification callback for a notification type to the notification center
60+
* @param string $notification_type One of the constants defined in NotificationType
61+
* @param string $notification_callback A valid PHP callback
62+
*
63+
* @return null Given invalid notification type/callback
64+
* -1 Given callback has been already added
65+
* int Notification ID for the added callback
66+
*/
67+
public function addNotificationListener($notification_type, $notification_callback)
68+
{
69+
if (!NotificationType::isNotificationTypeValid($notification_type)) {
70+
$this->_logger->log(Logger::ERROR, "Invalid notification type.");
71+
$this->_errorHandler->handleError(new InvalidNotificationTypeException('Invalid notification type.'));
72+
return null;
73+
}
74+
75+
if (!is_callable($notification_callback)) {
76+
$this->_logger->log(Logger::ERROR, "Invalid notification callback.");
77+
return null;
78+
}
79+
80+
foreach (array_values($this->_notifications[$notification_type]) as $callback) {
81+
if ($notification_callback == $callback) {
82+
// Note: anonymous methods sent with the same body will be re-added.
83+
// Only variable and object methods can be checked for duplication
84+
$this->_logger->log(Logger::DEBUG, "Callback already added for notification type '{$notification_type}'.");
85+
return -1;
86+
}
87+
}
88+
89+
$this->_notifications[$notification_type][$this->_notificationId] = $notification_callback;
90+
$this->_logger->log(Logger::INFO, "Callback added for notification type '{$notification_type}'.");
91+
$returnVal = $this->_notificationId++;
92+
return $returnVal;
93+
}
94+
95+
/**
96+
* Removes notification callback from the notification center
97+
* @param int $notification_id notification IT
98+
*
99+
* @return true When callback removed
100+
* false When no callback found for the given notification ID
101+
*/
102+
public function removeNotificationListener($notification_id)
103+
{
104+
foreach ($this->_notifications as $notification_type => $notifications) {
105+
foreach (array_keys($notifications) as $id) {
106+
if ($notification_id == $id) {
107+
unset($this->_notifications[$notification_type][$id]);
108+
$this->_logger->log(Logger::INFO, "Callback with notification ID '{$notification_id}' has been removed.");
109+
return true;
110+
}
111+
}
112+
}
113+
114+
$this->_logger->log(Logger::DEBUG, "No Callback found with notification ID '{$notification_id}'.");
115+
return false;
116+
}
117+
118+
/**
119+
* Removes all notification callbacks for the given notification type
120+
* @param string $notification_type One of the constants defined in NotificationType
121+
*
122+
*/
123+
public function clearNotifications($notification_type)
124+
{
125+
if (!NotificationType::isNotificationTypeValid($notification_type)) {
126+
$this->_logger->log(Logger::ERROR, "Invalid notification type.");
127+
$this->_errorHandler->handleError(new InvalidNotificationTypeException('Invalid notification type.'));
128+
return;
129+
}
130+
131+
$this->_notifications[$notification_type] = [];
132+
$this->_logger->log(Logger::INFO, "All callbacks for notification type '{$notification_type}' have been removed.");
133+
}
134+
135+
/**
136+
* Removes all notifications for all notification types
137+
* from the notification center
138+
*
139+
*/
140+
public function cleanAllNotifications()
141+
{
142+
foreach (array_values(NotificationType::getAll()) as $type) {
143+
$this->_notifications[$type] = [];
144+
}
145+
}
146+
147+
/**
148+
* Executes all registered callbacks for the given notification type
149+
* @param [type] $notification_type One of the constants defined in NotificationType
150+
* @param array $args Array of items to pass as arguments to the callback
151+
*
152+
*/
153+
public function sendNotifications($notification_type, array $args = [])
154+
{
155+
if (!isset($this->_notifications[$notification_type])) {
156+
// No exception thrown and error logged since this method will be called from
157+
// within the SDK
158+
return;
159+
}
160+
161+
/**
162+
* Note: Before PHP 7, if the callback in call_user_func is called with less number of arguments than expected,
163+
* a warning is issued but the method is still executed with assigning null to the remaining
164+
* arguments. From PHP 7, ArgumentCountError is thrown in such case and the method isn't executed.
165+
* Therefore, we set error handler for warnings so that we raise an exception and notify the
166+
* user that the registered callback has more number of arguments than
167+
* expected. This should be done to keep a consistent behavior across all PHP versions.
168+
*/
169+
170+
set_error_handler(array($this, 'reportArgumentCountError'), E_WARNING);
171+
172+
foreach (array_values($this->_notifications[$notification_type]) as $callback) {
173+
try {
174+
call_user_func_array($callback, $args);
175+
} catch (ArgumentCountError $e) {
176+
$this->reportArgumentCountError();
177+
} catch (Exception $e) {
178+
$this->_logger->log(Logger::ERROR, "Problem calling notify callback.");
179+
}
180+
}
181+
182+
restore_error_handler();
183+
}
184+
185+
/**
186+
* Logs and raises an exception when registered callback expects more number of arguments when executed
187+
*
188+
*/
189+
public function reportArgumentCountError()
190+
{
191+
$this->_logger->log(Logger::ERROR, "Problem calling notify callback.");
192+
$this->_errorHandler->handleError(
193+
new InvalidCallbackArgumentCountException('Registered callback expects more number of arguments than the actual number')
194+
);
195+
}
196+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
/**
3+
* Copyright 2017, 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+
namespace Optimizely\Notification;
18+
19+
class NotificationType
20+
{
21+
// format is EVENT: list of parameters to callback.
22+
const ACTIVATE = "ACTIVATE:experiment, user_id, attributes, variation, event";
23+
const TRACK = "TRACK:event_key, user_id, attributes, event_tags, event";
24+
25+
public static function isNotificationTypeValid($notification_type)
26+
{
27+
$oClass = new \ReflectionClass(__CLASS__);
28+
$notificationTypeList = array_values($oClass->getConstants());
29+
30+
return in_array($notification_type, $notificationTypeList);
31+
}
32+
33+
public static function getAll()
34+
{
35+
$oClass = new \ReflectionClass(__CLASS__);
36+
return $oClass->getConstants();
37+
}
38+
}

src/Optimizely/Optimizely.php

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
use Optimizely\Event\Dispatcher\EventDispatcherInterface;
3636
use Optimizely\Logger\LoggerInterface;
3737
use Optimizely\Logger\NoOpLogger;
38+
use Optimizely\Notification\NotificationCenter;
39+
use Optimizely\Notification\NotificationType;
3840
use Optimizely\UserProfile\UserProfileServiceInterface;
3941
use Optimizely\Utils\EventTagUtils;
4042
use Optimizely\Utils\Validator;
@@ -82,6 +84,11 @@ class Optimizely
8284
*/
8385
private $_logger;
8486

87+
/**
88+
* @var NotificationCenter
89+
*/
90+
private $_notificationCenter;
91+
8592
/**
8693
* Optimizely constructor for managing Full Stack PHP projects.
8794
*
@@ -129,6 +136,7 @@ public function __construct($datafile,
129136

130137
$this->_eventBuilder = new EventBuilder();
131138
$this->_decisionService = new DecisionService($this->_logger, $this->_config, $userProfileService);
139+
$this->_notificationCenter = new NotificationCenter($this->_logger, $this->_errorHandler);
132140
}
133141

134142
/**
@@ -169,6 +177,7 @@ private function validateUserInputs($attributes, $eventTags = null) {
169177
$this->_errorHandler->handleError(
170178
new InvalidEventTagException('Provided event tags are in an invalid format.')
171179
);
180+
return false;
172181
}
173182
}
174183

@@ -180,7 +189,7 @@ private function validateUserInputs($attributes, $eventTags = null) {
180189
* is one that is in "Running" state and into which the user has been bucketed.
181190
*
182191
* @param $event string Event key representing the event which needs to be recorded.
183-
* @param $userId string ID for user.
192+
* @param $user string ID for user.
184193
* @param $attributes array Attributes of the user.
185194
*
186195
* @return Array Of objects where each object contains the ID of the experiment to track and the ID of the variation the user is bucketed into.
@@ -238,6 +247,17 @@ protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $
238247
$exception->getMessage()
239248
));
240249
}
250+
251+
$this->_notificationCenter->sendNotifications(
252+
NotificationType::ACTIVATE,
253+
array(
254+
$this->_config->getExperimentFromKey($experimentKey),
255+
$userId,
256+
$attributes,
257+
$this->_config->getVariationFromKey($experimentKey, $variationKey),
258+
$impressionEvent
259+
)
260+
);
241261
}
242262

243263
/**
@@ -334,6 +354,17 @@ public function track($eventKey, $userId, $attributes = null, $eventTags = null)
334354
'Unable to dispatch conversion event. Error %s', $exception->getMessage()));
335355
}
336356

357+
$this->_notificationCenter->sendNotifications(
358+
NotificationType::TRACK,
359+
array(
360+
$eventKey,
361+
$userId,
362+
$attributes,
363+
$eventTags,
364+
$conversionEvent
365+
)
366+
);
367+
337368
} else {
338369
$this->_logger->log(
339370
Logger::INFO,
@@ -452,18 +483,21 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null)
452483
return false;
453484
}
454485

486+
$experiment_id = $decision->getExperimentId();
487+
$variation_id = $decision->getVariationId();
488+
455489
if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_EXPERIMENT) {
456-
$experiment_id = $decision->getExperimentId();
457-
$variation_id = $decision->getVariationId();
458490
$experiment = $this->_config->getExperimentFromId($experiment_id);
459491
$variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id);
460492

461493
$this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes);
494+
462495
} else {
463496
$this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'.");
464497
}
465498

466499
$this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'.");
500+
467501
return true;
468502
}
469503

0 commit comments

Comments
 (0)