Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* Copyright 2017, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Optimizely\Exceptions;


class InvalidCallbackArgumentCountException extends OptimizelyException
{
}
23 changes: 23 additions & 0 deletions src/Optimizely/Exceptions/InvalidNotificationTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* Copyright 2017, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Optimizely\Exceptions;


class InvalidNotificationTypeException extends OptimizelyException
{
}
196 changes: 196 additions & 0 deletions src/Optimizely/Notification/NotificationCenter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php
/**
* Copyright 2017, Optimizely Inc and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Optimizely\Notification;

use ArgumentCountError;
use Exception;
use Throwable;

use Monolog\Logger;

use Optimizely\ErrorHandler\ErrorHandlerInterface;
use Optimizely\Exceptions\InvalidCallbackArgumentCountException;
use Optimizely\Exceptions\InvalidNotificationTypeException;
use Optimizely\Logger\LoggerInterface;
use Optimizely\Logger\NoOpLogger;

class NotificationCenter
{
private $_notificationId;

private $_notifications;

private $_logger;

private $_errorHandler;

public function __construct(LoggerInterface $logger, ErrorHandlerInterface $errorHandler)
{
$this->_notificationId = 1;
$this->_notifications = [];
foreach (array_values(NotificationType::getAll()) as $type) {
$this->_notifications[$type] = [];
}

$this->_logger = $logger;
$this->_errorHandler = $errorHandler;
}

public function getNotifications()
{
return $this->_notifications;
}

/**
* Adds a notification callback for a notification type to the notification center
* @param string $notification_type One of the constants defined in NotificationType
* @param string $notification_callback A valid PHP callback
*
* @return null Given invalid notification type/callback
* -1 Given callback has been already added
* int Notification ID for the added callback
*/
public function addNotificationListener($notification_type, $notification_callback)
{
if (!NotificationType::isNotificationTypeValid($notification_type)) {
$this->_logger->log(Logger::ERROR, "Invalid notification type.");
$this->_errorHandler->handleError(new InvalidNotificationTypeException('Invalid notification type.'));
return null;
}

if (!is_callable($notification_callback)) {
$this->_logger->log(Logger::ERROR, "Invalid notification callback.");
return null;
}

foreach (array_values($this->_notifications[$notification_type]) as $callback) {
if ($notification_callback == $callback) {
// Note: anonymous methods sent with the same body will be re-added.
// Only variable and object methods can be checked for duplication
$this->_logger->log(Logger::DEBUG, "Callback already added for notification type '{$notification_type}'.");
return -1;
}
}

$this->_notifications[$notification_type][$this->_notificationId] = $notification_callback;
$this->_logger->log(Logger::INFO, "Callback added for notification type '{$notification_type}'.");
$returnVal = $this->_notificationId++;
return $returnVal;
}

/**
* Removes notification callback from the notification center
* @param int $notification_id notification IT
*
* @return true When callback removed
* false When no callback found for the given notification ID
*/
public function removeNotificationListener($notification_id)
{
foreach ($this->_notifications as $notification_type => $notifications) {
foreach (array_keys($notifications) as $id) {
if ($notification_id == $id) {
unset($this->_notifications[$notification_type][$id]);
$this->_logger->log(Logger::INFO, "Callback with notification ID '{$notification_id}' has been removed.");
return true;
}
}
}

$this->_logger->log(Logger::DEBUG, "No Callback found with notification ID '{$notification_id}'.");
return false;
}

/**
* Removes all notification callbacks for the given notification type
* @param string $notification_type One of the constants defined in NotificationType
*
*/
public function clearNotifications($notification_type)
{
if (!NotificationType::isNotificationTypeValid($notification_type)) {
$this->_logger->log(Logger::ERROR, "Invalid notification type.");
$this->_errorHandler->handleError(new InvalidNotificationTypeException('Invalid notification type.'));
return;
}

$this->_notifications[$notification_type] = [];
$this->_logger->log(Logger::INFO, "All callbacks for notification type '{$notification_type}' have been removed.");
}

/**
* Removes all notifications for all notification types
* from the notification center
*
*/
public function cleanAllNotifications()
{
foreach (array_values(NotificationType::getAll()) as $type) {
$this->_notifications[$type] = [];
}
}

/**
* Executes all registered callbacks for the given notification type
* @param [type] $notification_type One of the constants defined in NotificationType
* @param array $args Array of items to pass as arguments to the callback
*
*/
public function sendNotifications($notification_type, array $args = [])
{
if (!isset($this->_notifications[$notification_type])) {
// No exception thrown and error logged since this method will be called from
// within the SDK
return;
}

/**
* Note: Before PHP 7, if the callback in call_user_func is called with less number of arguments than expected,
* a warning is issued but the method is still executed with assigning null to the remaining
* arguments. From PHP 7, ArgumentCountError is thrown in such case and the method isn't executed.
* Therefore, we set error handler for warnings so that we raise an exception and notify the
* user that the registered callback has more number of arguments than
* expected. This should be done to keep a consistent behavior across all PHP versions.
*/

set_error_handler(array($this, 'reportArgumentCountError'), E_WARNING);

foreach (array_values($this->_notifications[$notification_type]) as $callback) {
try {
call_user_func_array($callback, $args);
} catch (ArgumentCountError $e) {
$this->reportArgumentCountError();
} catch (Exception $e) {
$this->_logger->log(Logger::ERROR, "Problem calling notify callback.");
}
}

restore_error_handler();
}

/**
* Logs and raises an exception when registered callback expects more number of arguments when executed
*
*/
public function reportArgumentCountError()
{
$this->_logger->log(Logger::ERROR, "Problem calling notify callback.");
$this->_errorHandler->handleError(
new InvalidCallbackArgumentCountException('Registered callback expects more number of arguments than the actual number')
);
}
}
38 changes: 38 additions & 0 deletions src/Optimizely/Notification/NotificationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* Copyright 2017, Optimizely Inc and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Optimizely\Notification;

class NotificationType
{
// format is EVENT: list of parameters to callback.
const ACTIVATE = "ACTIVATE:experiment, user_id, attributes, variation, event";
const TRACK = "TRACK:event_key, user_id, attributes, event_tags, event";

public static function isNotificationTypeValid($notification_type)
{
$oClass = new \ReflectionClass(__CLASS__);
$notificationTypeList = array_values($oClass->getConstants());

return in_array($notification_type, $notificationTypeList);
}

public static function getAll()
{
$oClass = new \ReflectionClass(__CLASS__);
return $oClass->getConstants();
}
}
40 changes: 37 additions & 3 deletions src/Optimizely/Optimizely.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
use Optimizely\Event\Dispatcher\EventDispatcherInterface;
use Optimizely\Logger\LoggerInterface;
use Optimizely\Logger\NoOpLogger;
use Optimizely\Notification\NotificationCenter;
use Optimizely\Notification\NotificationType;
use Optimizely\UserProfile\UserProfileServiceInterface;
use Optimizely\Utils\EventTagUtils;
use Optimizely\Utils\Validator;
Expand Down Expand Up @@ -82,6 +84,11 @@ class Optimizely
*/
private $_logger;

/**
* @var NotificationCenter
*/
private $_notificationCenter;

/**
* Optimizely constructor for managing Full Stack PHP projects.
*
Expand Down Expand Up @@ -129,6 +136,7 @@ public function __construct($datafile,

$this->_eventBuilder = new EventBuilder();
$this->_decisionService = new DecisionService($this->_logger, $this->_config, $userProfileService);
$this->_notificationCenter = new NotificationCenter($this->_logger, $this->_errorHandler);
}

/**
Expand Down Expand Up @@ -169,6 +177,7 @@ private function validateUserInputs($attributes, $eventTags = null) {
$this->_errorHandler->handleError(
new InvalidEventTagException('Provided event tags are in an invalid format.')
);
return false;
}
}

Expand All @@ -180,7 +189,7 @@ private function validateUserInputs($attributes, $eventTags = null) {
* is one that is in "Running" state and into which the user has been bucketed.
*
* @param $event string Event key representing the event which needs to be recorded.
* @param $userId string ID for user.
* @param $user string ID for user.
* @param $attributes array Attributes of the user.
*
* @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.
Expand Down Expand Up @@ -238,6 +247,17 @@ protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $
$exception->getMessage()
));
}

$this->_notificationCenter->sendNotifications(
NotificationType::ACTIVATE,
array(
$this->_config->getExperimentFromKey($experimentKey),
$userId,
$attributes,
$this->_config->getVariationFromKey($experimentKey, $variationKey),
$impressionEvent
)
);
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the sendImpression. Why not add a sendConversion as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendImpression logic got separated from activate when we implemented isFeatureEnabled that had to send an impression event as well. Track is the only method right now that sends a conversion event. Separating out unit tests for sendConversion will require a lot of changes. Shall I do it in this PR or can we do it later?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do it later if necessary.

$this->_notificationCenter->sendNotifications(
NotificationType::TRACK,
array(
$eventKey,
$userId,
$attributes,
$eventTags,
$conversionEvent
)
);

} else {
$this->_logger->log(
Logger::INFO,
Expand Down Expand Up @@ -452,18 +483,21 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null)
return false;
}

$experiment_id = $decision->getExperimentId();
$variation_id = $decision->getVariationId();

if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_EXPERIMENT) {
$experiment_id = $decision->getExperimentId();
$variation_id = $decision->getVariationId();
$experiment = $this->_config->getExperimentFromId($experiment_id);
$variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id);

$this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes);

} else {
$this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'.");
}

$this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'.");

return true;
}

Expand Down
Loading