Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Optimizely/Config/DatafileProjectConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,18 @@ public function __construct($datafile, $logger, $errorHandler)
$rollouts = isset($config['rollouts']) ? $config['rollouts'] : [];
$featureFlags = isset($config['featureFlags']) ? $config['featureFlags']: [];

// JSON type is represented in datafile as a subtype of string for the sake of backwards compatibility.
// Converting it to a first-class json type while creating Project Config
foreach ($featureFlags as $featureFlagKey => $featureFlag) {
foreach ($featureFlag['variables'] as $variableKey => $variable) {
if (isset($variable['subType']) && $variable['type'] === FeatureVariable::STRING_TYPE && $variable['subType'] === FeatureVariable::JSON_TYPE) {
$variable['type'] = FeatureVariable::JSON_TYPE;
unset($variable['subType']);
$featureFlags[$featureFlagKey]['variables'][$variableKey] = $variable;
}
}
}

$this->_groupIdMap = ConfigParser::generateMap($groups, 'id', Group::class);
$this->_experimentKeyMap = ConfigParser::generateMap($experiments, 'key', Experiment::class);
$this->_eventKeyMap = ConfigParser::generateMap($events, 'key', Event::class);
Expand Down
14 changes: 9 additions & 5 deletions src/Optimizely/Entity/FeatureVariable.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Copyright 2017, 2019, Optimizely
* Copyright 2017, 2019-2020 Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,9 +22,10 @@ class FeatureVariable

// Feature variable primitive types
const BOOLEAN_TYPE = 'boolean';
const STRING_TYPE = 'string';
const INTEGER_TYPE = 'integer';
const DOUBLE_TYPE = 'double';
const INTEGER_TYPE = 'integer';
const JSON_TYPE = 'json';
const STRING_TYPE = 'string';

/**
* variable to hold the feature variable ID
Expand All @@ -46,6 +47,7 @@ class FeatureVariable
* b. string
* c. integer
* d. double
* e. json
*
* @var String
*/
Expand Down Expand Up @@ -144,10 +146,12 @@ public static function getFeatureVariableMethodName($type)
switch ($type) {
case FeatureVariable::BOOLEAN_TYPE:
return "getFeatureVariableBoolean";
case FeatureVariable::INTEGER_TYPE:
return "getFeatureVariableInteger";
case FeatureVariable::DOUBLE_TYPE:
return "getFeatureVariableDouble";
case FeatureVariable::INTEGER_TYPE:
return "getFeatureVariableInteger";
case FeatureVariable::JSON_TYPE:
return "getFeatureVariableJson";
case FeatureVariable::STRING_TYPE:
return "getFeatureVariableString";
default:
Expand Down
3 changes: 2 additions & 1 deletion src/Optimizely/Enums/DecisionNotificationTypes.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Copyright 2019, Optimizely
* Copyright 2019-2020 Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -23,4 +23,5 @@ class DecisionNotificationTypes
const FEATURE = "feature";
const FEATURE_TEST = "feature-test";
const FEATURE_VARIABLE = "feature-variable";
const ALL_FEATURE_VARIABLES = "all-feature-variables";
}
248 changes: 185 additions & 63 deletions src/Optimizely/Optimizely.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use Exception;
use Optimizely\Config\DatafileProjectConfig;
use Optimizely\Entity\Variation;
use Optimizely\Exceptions\InvalidAttributeException;
use Optimizely\Exceptions\InvalidEventTagException;
use Throwable;
Expand Down Expand Up @@ -292,7 +293,7 @@ public function getOptimizelyConfig()
}

$optConfigService = new OptimizelyConfigService($config);

return $optConfigService->getConfig();
}

Expand Down Expand Up @@ -633,15 +634,18 @@ public function getEnabledFeatures($userId, $attributes = null)
}

/**
* Get the string value of the specified variable in the feature flag.
* Get value of the specified variable in the feature flag.
*
* @param string Feature flag key
* @param string Variable key
* @param string User ID
* @param array Associative array of user attributes
* @param string Variable type
*
* @return string Feature variable value / null
* @return string|boolean|number|array|null Value of the variable cast to the appropriate
* type, or null if the feature key is invalid, the
* variable key is invalid, or there is a mismatch
* with the type of the variable
*/
public function getFeatureVariableValueForType(
$featureFlagKey,
Expand Down Expand Up @@ -673,69 +677,20 @@ public function getFeatureVariableValueForType(
return null;
}

$variable = $config->getFeatureVariableFromKey($featureFlagKey, $variableKey);
if (!$variable) {
// Error message logged in ProjectConfigInterface- getFeatureVariableFromKey
return null;
}

if ($variableType != $variable->getType()) {
$this->_logger->log(
Logger::ERROR,
"Variable is of type '{$variable->getType()}', but you requested it as type '{$variableType}'."
);
$decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes);
$variation = $decision->getVariation();
$experiment = $decision->getExperiment();
$featureEnabled = $variation !== null ? $variation->getFeatureEnabled() : false;
$variableValue = $this->getFeatureVariableValueFromVariation($featureFlagKey, $variableKey, $variableType, $featureEnabled, $variation, $userId);
// returning as variable not found or type is invalid
if ($variableValue === null) {
return null;
}

$featureEnabled = false;
$decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes);
$variableValue = $variable->getDefaultValue();

if ($decision->getVariation() === null) {
$this->_logger->log(
Logger::INFO,
"User '{$userId}'is not in any variation, ".
"returning default value '{$variableValue}'."
if ($variation && $decision->getSource() == FeatureDecision::DECISION_SOURCE_FEATURE_TEST) {
$sourceInfo = (object) array(
'experimentKey'=> $experiment->getKey(),
'variationKey'=> $variation->getKey()
);
} else {
$experiment = $decision->getExperiment();
$variation = $decision->getVariation();
$featureEnabled = $variation->getFeatureEnabled();

if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_FEATURE_TEST) {
$sourceInfo = (object) array(
'experimentKey'=> $experiment->getKey(),
'variationKey'=> $variation->getKey()
);
}

if ($featureEnabled) {
$variableUsage = $variation->getVariableUsageById($variable->getId());
if ($variableUsage) {
$variableValue = $variableUsage->getValue();
$this->_logger->log(
Logger::INFO,
"Returning variable value '{$variableValue}' for variation '{$variation->getKey()}' ".
"of feature flag '{$featureFlagKey}'"
);
} else {
$this->_logger->log(
Logger::INFO,
"Variable '{$variableKey}' is not used in variation '{$variation->getKey()}', ".
"returning default value '{$variableValue}'."
);
}
} else {
$this->_logger->log(
Logger::INFO,
"Feature '{$featureFlagKey}' for variation '{$variation->getKey()}' is not enabled, ".
"returning default value '{$variableValue}'."
);
}
}

if (!is_null($variableValue)) {
$variableValue = VariableTypeUtils::castStringToType($variableValue, $variableType, $this->_logger);
}

$attributes = $attributes ?: [];
Expand Down Expand Up @@ -844,6 +799,173 @@ public function getFeatureVariableString($featureFlagKey, $variableKey, $userId,
);
}

/**
* Get the JSON value of the specified variable in the feature flag.
*
* @param string Feature flag key
* @param string Variable key
* @param string User ID
* @param array Associative array of user attributes
*
* @return array Associative array of json variable including key and value
*/
public function getFeatureVariableJson($featureFlagKey, $variableKey, $userId, $attributes = null)
{
return $this->getFeatureVariableValueForType(
$featureFlagKey,
$variableKey,
$userId,
$attributes,
FeatureVariable::JSON_TYPE
);
}

/**
* Returns values for all the variables attached to the given feature
*
* @param string Feature flag key
* @param string User ID
* @param array Associative array of user attributes
*
* @return array|null array of all the variables, or null if the feature key is invalid
*/
public function getAllFeatureVariables($featureFlagKey, $userId, $attributes = null)
{
$config = $this->getConfig();
if ($config === null) {
$this->_logger->log(Logger::ERROR, sprintf(Errors::INVALID_DATAFILE, __FUNCTION__));
return null;
}

if (!$this->validateInputs(
[
self::FEATURE_FLAG_KEY => $featureFlagKey,
self::USER_ID => $userId
]
)
) {
return null;
}

$featureFlag = $config->getFeatureFlagFromKey($featureFlagKey);
if ($featureFlag && (!$featureFlag->getId())) {
return null;
}

$decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes);
$variation = $decision->getVariation();
$experiment = $decision->getExperiment();
$featureEnabled = $variation !== null ? $variation->getFeatureEnabled() : false;

$allVariables = [];
foreach ($featureFlag->getVariables() as $variable) {
$allVariables[$variable->getKey()] = $this->getFeatureVariableValueFromVariation(
$featureFlagKey,
$variable->getKey(),
null,
$featureEnabled,
$variation,
$userId
);
}

$sourceInfo = (object) array();
if ($variation && $decision->getSource() == FeatureDecision::DECISION_SOURCE_FEATURE_TEST) {
$sourceInfo = (object) array(
'experimentKey'=> $experiment->getKey(),
'variationKey'=> $variation->getKey()
);
}

$attributes = $attributes ?: [];

$this->notificationCenter->sendNotifications(
NotificationType::DECISION,
array(
DecisionNotificationTypes::ALL_FEATURE_VARIABLES,
$userId,
$attributes,
(object)array(
'featureKey' => $featureFlagKey,
'featureEnabled' => $featureEnabled,
'variableValues' => $allVariables,
'source' => $decision->getSource(),
'sourceInfo' => $sourceInfo
)
)
);

return $allVariables;
}

/**
* Get the value of the specified variable on the basis of its status and usage
*
* @param string Feature flag key
* @param string Variable key
* @param boolean Feature Status
* @param Variation for feature
* @param string User Id
*
* @return string|boolean|number|array|null Value of the variable cast to the appropriate
* type, or null if the feature key is invalid, the
* variable key is invalid, or there is a mismatch
* with the type of the variable
*/
private function getFeatureVariableValueFromVariation($featureFlagKey, $variableKey, $variableType, $featureEnabled, $variation, $userId)
{
$config = $this->getConfig();
$variable = $config->getFeatureVariableFromKey($featureFlagKey, $variableKey);
if (!$variable) {
// Error message logged in ProjectConfigInterface- getFeatureVariableFromKey
return null;
}
if ($variableType && $variableType != $variable->getType()) {
$this->_logger->log(
Logger::ERROR,
"Variable is of type '{$variable->getType()}', but you requested it as type '{$variableType}'."
);
return null;
}
$variableValue = $variable->getDefaultValue();
if ($variation === null) {
$this->_logger->log(
Logger::INFO,
"User '{$userId}'is not in any variation, ".
"returning default value '{$variableValue}'."
);
} else {
if ($featureEnabled) {
$variableUsage = $variation->getVariableUsageById($variable->getId());
if ($variableUsage) {
$variableValue = $variableUsage->getValue();
$this->_logger->log(
Logger::INFO,
"Returning variable value '{$variableValue}' for variation '{$variation->getKey()}' ".
"of feature flag '{$featureFlagKey}'"
);
} else {
$this->_logger->log(
Logger::INFO,
"Variable '{$variableKey}' is not used in variation '{$variation->getKey()}', ".
"returning default value '{$variableValue}'."
);
}
} else {
$this->_logger->log(
Logger::INFO,
"Feature '{$featureFlagKey}' for variation '{$variation->getKey()}' is not enabled, ".
"returning default value '{$variableValue}'."
);
}
}

if (!is_null($variableValue)) {
$variableValue = VariableTypeUtils::castStringToType($variableValue, $variable->getType(), $this->_logger);
}
return $variableValue;
}

/**
* Determine if the instance of the Optimizely client is valid.
* An instance can be deemed invalid if it was not initialized
Expand Down
6 changes: 5 additions & 1 deletion src/Optimizely/Utils/VariableTypeUtils.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Copyright 2017, 2019, Optimizely
* Copyright 2017, 2019-2020 Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -48,6 +48,10 @@ public static function castStringToType($value, $variableType, LoggerInterface $
$return_value = (float) $value;
}
break;

case FeatureVariable::JSON_TYPE:
$return_value = json_decode($value, true);
break;
}

if (is_null($return_value) && $logger) {
Expand Down
Loading