You are here

class AcquiaLiftAgent in Acquia Lift Connector 7

Hierarchy

  • class \AcquiaLiftAgent extends \PersonalizeAgentBase implements \PersonalizeAgentGoalInterface, \PersonalizeExplicitTargetingInterface, \PersonalizeAutoTargetingInterface, \PersonalizeAgentReportInterface, AcquiaLiftAgentInterface

Expanded class hierarchy of AcquiaLiftAgent

1 string reference to 'AcquiaLiftAgent'
acquia_lift_get_agent_types in ./acquia_lift.module
Returns the agent types this module provides.

File

plugins/agent_types/AcquiaLiftAgent.inc, line 159
Provides an agent type for Acquia Lift

View source
class AcquiaLiftAgent extends PersonalizeAgentBase implements PersonalizeAgentGoalInterface, AcquiaLiftAgentInterface, PersonalizeExplicitTargetingInterface, PersonalizeAutoTargetingInterface, PersonalizeAgentReportInterface {

  /**
   * An object containing the agent data.
   *
   * @var stdClass
   */
  protected $agent;

  /**
   * An instance of AcquiaLiftAPI.
   *
   * @var AcquiaLiftAPI
   */
  protected $liftAPI;

  /**
   * An instance of PersonalizeAgentReportInterface.
   *
   * The agent acts as a facade to the reporting class for reporting requests.
   *
   * @var PersonalizeAgentReportInterface
   */
  protected $reporting;

  /**
   * An instance of DrupalQueueInterface
   *
   * @var DrupalQueueInterface
   */
  protected $queue;
  protected $globalConfig;

  /**
   * Implements PersonalizeAgentInterface::create().
   */
  public static function create($agent_data) {
    try {
      $acquia_lift_api = AcquiaLiftAPI::getInstance(variable_get('acquia_lift_account_info', array()));
      $status = personalize_agent_get_status($agent_data->machine_name);
      $config = array(
        'confidence_measure' => variable_get('acquia_lift_confidence_measure', 95),
        'minimum_runtime' => acquia_lift_config_min_runtime(),
        'minimum_decisions' => variable_get('acquia_lift_min_decisions', 1000),
      );
      return new static($agent_data->machine_name, $agent_data->label, $agent_data->data, $status, !empty($agent_data->started) ? $agent_data->started : NULL, $acquia_lift_api, $config);
    } catch (AcquiaLiftException $e) {
      watchdog('Acquia Lift', 'Unable to instantiate Acquia Lift Agent');
      return NULL;
    }
  }

  /**
   * Constructs an Acquia Lift agent.
   *
   * @param stdClass $agent_data
   *   An object containing the agent data.
   * @param $acquia_lift_api
   *   An instance of the AcquiaLiftAPI class.
   */
  public function __construct($machine_name, $title, $data, $status, $started, AcquiaLiftAPI $acquia_lift_api, $global_config) {
    parent::__construct($machine_name, $title, $data, $status, $started);
    $this->liftAPI = $acquia_lift_api;
    $this->globalConfig = $global_config;
  }

  /**
   * Implements PersonalizeAgentInterface::getType();
   */
  public function getType() {
    return 'acquia_lift';
  }

  /**
   * Implements PersonalizeAgentInterface::getAssets().
   */
  public function getAssets() {
    $path = drupal_get_path('module', 'acquia_lift');
    return array(
      'js' => array(
        array(
          'type' => 'setting',
          'data' => array(
            'acquia_lift' => array(
              'apiKey' => $this->liftAPI
                ->getApiKey(),
              'owner' => $this->liftAPI
                ->getOwnerCode(),
              'baseUrl' => $this->liftAPI
                ->getApiUrl(),
              'featureStringReplacePattern' => AcquiaLiftAPI::FEATURE_STRING_REPLACE_PATTERN,
              'featureStringMaxLength' => AcquiaLiftAPI::FEATURE_STRING_MAX_LENGTH,
              // @todo Add support for mutually exclusive values.
              'featureStringSeparator' => AcquiaLiftAPI::FEATURE_STRING_SEPARATOR_NONMUTEX,
              'batchMode' => variable_get('acquia_lift_batch_decisions', FALSE),
            ),
          ),
        ),
        // Add the Acquia Lift API js wrapper and the Acquia Lift integration js.
        $path . '/js/acquia_lift.js' => array(
          'type' => 'file',
          'scope' => 'footer',
          'defer' => TRUE,
        ),
      ),
      'library' => array(
        array(
          'acquia_lift',
          'acquia_lift.agent_api',
        ),
      ),
    );
  }

  /**
   * Implements PersonalizeAgentGoalInterface::useClientSideGoalDelivery().
   */
  public function useClientSideGoalDelivery() {
    return variable_get('acquia_lift_client_side_goals', TRUE);
  }

  /**
   * Implements PersonalizeAgentGoalInterface::sendGoal().
   */
  public function sendGoal($goal_name, $value = NULL) {

    // @todo Implement server-side goal delivery.
  }

  /**
   * Implements PersonalizeAgentInterface::optionsForm().
   */
  public static function optionsForm($agent_data, $option_parents = array()) {
    return self::buildOptionsForm($agent_data, FALSE, $option_parents);
  }

  /**
   * Builds the options to display for a campaign form.
   *
   * @param $agent_data
   *   The existing campaign data.
   * @param bool $simplified
   *   Indicates if the form should be shown in simplified format or with all
   *   advanced options.
   *
   * @return array
   *   The form render array.
   */
  protected static function buildOptionsForm($agent_data, $simplified = FALSE, $option_parents = array()) {
    $account_info = variable_get('acquia_lift_account_info', array());
    if (empty($account_info)) {
      drupal_set_message(t('Your Acquia Lift account info has not been configured. Any Acquia Lift campaigns you create here will not work until you configure your account info !here', array(
        '!here' => l('here', 'admin/config/content/personalize/acquia_lift'),
      )), 'error');
    }
    $form = array();
    $form['#attached'] = array(
      'css' => array(
        drupal_get_path('module', 'acquia_lift') . '/css/personalize_acquia_lift_admin.css',
        drupal_get_path('module', 'acquia_lift') . '/css/acquia_lift.admin.css',
      ),
      'js' => array(
        drupal_get_path('module', 'acquia_lift') . '/js/acquia_lift.agent.admin.js',
      ),
    );
    if (empty($option_parents)) {
      $option_parents = array(
        'agent_basic_info',
        'options',
        'acquia_lift',
      );
    }
    $control_rate = isset($agent_data->data['control_rate']) ? $agent_data->data['control_rate'] : 10;
    if ($simplified) {
      $form['control_rate'] = array(
        '#type' => 'value',
        '#value' => $control_rate,
      );
    }
    else {
      $form['control'] = array(
        '#type' => 'fieldset',
        '#tree' => FALSE,
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#title' => t('Control (@control_rate%)', array(
          '@control_rate' => $control_rate,
        )),
      );
      $control_rate_parents = $option_parents;
      $control_rate_parents[] = 'control_rate';
      $form['control']['control_rate'] = array(
        '#type' => 'acquia_lift_percentage',
        '#parents' => $control_rate_parents,
        '#title' => t('Control Group'),
        '#field_suffix' => '%',
        '#size' => 3,
        '#description' => t('A fixed baseline variation will be shown, by default the first variation in the set.'),
        '#default_value' => $control_rate,
        '#rest_title' => t('Test Group'),
        '#rest_description' => t('Personalized variations will be shown.'),
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
      );
    }
    $decision_style = isset($agent_data->data['decision_style']) ? $agent_data->data['decision_style'] : 'adaptive';
    if ($simplified) {
      $form['decision_style'] = array(
        '#type' => 'value',
        '#value' => $decision_style,
      );
    }
    else {
      $form['decision_style'] = array(
        '#type' => 'radios',
        '#title' => t('Decision Style'),
        '#options' => array(
          'adaptive' => t('Auto-personalize'),
          'random' => t('Test only'),
        ),
        '#default_value' => $decision_style,
        '#title_display' => 'invisible',
      );
      $form['decision_style']['adaptive'] = array(
        '#description' => t('Adapts to users and chooses the best option over time.'),
      );
      $form['decision_style']['random'] = array(
        '#description' => t('Tests variations and reports results.'),
      );
    }
    $explore_rate = isset($agent_data->data['explore_rate']) ? $agent_data->data['explore_rate'] : 20;
    if ($simplified) {
      $form['distribution'] = array(
        '#type' => 'value',
        '#value' => $explore_rate,
      );
    }
    else {
      $decision_style_parents = $option_parents;
      $decision_style_parents[] = 'decision_style';
      $decision_style_form_element = '';
      foreach ($decision_style_parents as $i => $parent_name) {
        $decision_style_form_element .= $i ? '[' . $parent_name . ']' : $parent_name;
      }
      $form['distribution'] = array(
        '#type' => 'fieldset',
        '#tree' => FALSE,
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#title' => t('Distribution (@explore_rate/@rest)', array(
          '@explore_rate' => $explore_rate,
          '@rest' => 100 - $explore_rate,
        )),
        '#states' => array(
          'visible' => array(
            ':input[name="' . $decision_style_form_element . '"]' => array(
              'value' => 'adaptive',
            ),
          ),
        ),
      );
      $explore_rate_parents = $option_parents;
      $explore_rate_parents[] = 'explore_rate';
      $form['distribution']['explore_rate'] = array(
        '#type' => 'acquia_lift_percentage',
        '#parents' => $explore_rate_parents,
        '#title' => t('Random Group'),
        '#field_suffix' => '%',
        '#description' => t('Variations will be shown randomly and tracked to adjust for false positives.'),
        '#size' => 3,
        '#default_value' => isset($agent_data->data['explore_rate']) ? $agent_data->data['explore_rate'] : 20,
        '#rest_title' => t('Personalized Group'),
        '#rest_description' => t('The "best" variation will be shown for each visitor based on our algorithm.'),
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
      );
    }

    // This will get overridden by the 'campaign_end' dropdown.
    $form['auto_stop'] = array(
      '#type' => 'value',
      '#value' => 0,
    );
    return $form;
  }

  /**
   * Implements PersonalizeAgentInterface::optionsFormValidate().
   */
  public static function optionsFormValidate($form, &$form_state, $option_parents = array()) {
    $values =& $form_state['values'];
    foreach ($option_parents as $parent) {
      $values =& $values[$parent];
    }
    $error_parents = implode('][', $option_parents);
    if (isset($values['control_rate'])) {
      $rate = $values['control_rate'];
      if (!is_numeric($rate) || !($rate >= 0 && $rate <= 100)) {
        $error_element = empty($error_parents) ? 'control_rate' : $error_parents . '][contral_rate';
        form_set_error($error_element, t('Invalid percent to test specified'));
      }
    }
    if (isset($values['explore_rate'])) {
      $rate = $values['explore_rate'];
      if (!is_numeric($rate) || !($rate >= 0 && $rate <= 100)) {
        $error_element = empty($error_parents) ? 'explore_rate' : $error_parents . '][explore_rate';
        form_set_error($error_element, t('Invalid percent to test specified'));
      }
    }

    // If "Run until a winner is found" was selected, we need to add this to the agent's
    // data property.
    if (isset($form_state['values']['campaign_end']) && $form_state['values']['campaign_end'] == 'auto') {
      $values['auto_stop'] = 1;
    }
  }

  /**
   * Implements PersonalizeAgentInterface::postSave().
   */
  public function postSave($old_data) {
    $items = $this
      ->getAgentSyncOperations(isset($old_data->data['visitor_context']['acquia_lift_context']));
    $this
      ->queueItems($items);
  }

  /**
   * Returns the operations needed to sync an agent to Acquia Lift.
   *
   * @param $targeting_rule_exists
   *   Whether the existing agent in Lift has a targeting rule set up that may
   *   need to be deleted.
   * @return array
   *   An array of items representing API calls to be made to Acquia Lift.
   */
  public function getAgentSyncOperations($targeting_rule_exists = FALSE) {
    $items = array();
    $acquia_lift_control_rate = 0.1;
    $acquia_lift_explore_rate = 0.2;
    if (isset($this->data['control_rate'])) {

      // Acquia Lift takes the control rate as a number between 0 and 1.
      $acquia_lift_control_rate = $this->data['control_rate'] / 100;
    }
    if (isset($this->data['explore_rate']) && isset($this->data['decision_style'])) {
      if ($this->data['decision_style'] === 'adaptive') {

        // Acquia Lift takes the explore rate as a number between 0 and 1.
        $acquia_lift_explore_rate = $this->data['explore_rate'] / 100;
      }
      else {

        // If the decision style is to only test, then the explore rate is the
        // full group.
        $acquia_lift_explore_rate = 1;
      }
    }

    // Add an item for saving the agent to Acquia Lift.
    $items[] = array(
      'method' => 'saveAgent',
      'args' => array(
        $this->machineName,
        $this->title,
        $this->data['decision_style'],
        $this->status,
        $acquia_lift_control_rate,
        $acquia_lift_explore_rate,
        isset($this->data['cache_decisions']) && $this->data['cache_decisions'],
      ),
    );
    $acquia_lift_context_needs_deleting = $targeting_rule_exists;
    if (isset($this->data['visitor_context']['acquia_lift_context'])) {

      // Whereas non-Acquia Lift visitor_context plugins operate by adding extra
      // info at the time of getting a decision, Acquia Lift targeting needs to
      // be set up on the Acquia Lift side before any decisions are made.
      $auto_targeting = array_filter($this->data['visitor_context']['acquia_lift_context']);
      if (!empty($auto_targeting)) {
        $acquia_lift_context_needs_deleting = FALSE;

        // Add an item for saving the targeting rule.
        $items[] = array(
          'method' => 'saveAutoTargetingRule',
          'args' => array(
            $this->machineName,
            array_keys($auto_targeting),
          ),
        );
      }
    }
    if ($acquia_lift_context_needs_deleting) {

      // Acquia Lift may have a targeting rule for this agent, so we need
      // to delete it.
      $items[] = array(
        'method' => 'deleteAutoTargetingRule',
        'args' => array(
          $this->machineName,
        ),
      );
    }
    return $items;
  }

  /**
   * Implements PersonalizeExplicitTargetingInterface::explicitTargetingSupportMultiple().
   */
  public static function explicitTargetingSupportMultiple() {
    return PersonalizeExplicitTargetingInterface::EXPLICIT_TARGETING_MULTIPLE_BOTH;
  }

  /**
   * Implements PersonalizeAutoTargetingInterface::constrainExplicitTargetingContexts().
   */
  public static function constrainExplicitTargetingContexts() {
    return TRUE;
  }

  /**
   * Implements PersonalizeAgentInterface::convertContextToFeatureString().
   *
   * This is essentially a PHP version of the js in the convertContextToFeatureString
   * function in acquia_lift.js
   */
  public static function convertContextToFeatureString($name, $value, $is_mutex = FALSE) {
    $separator = $is_mutex ? AcquiaLiftAPI::FEATURE_STRING_SEPARATOR_MUTEX : AcquiaLiftAPI::FEATURE_STRING_SEPARATOR_NONMUTEX;
    $prefix_max_length = floor((AcquiaLiftAPI::FEATURE_STRING_MAX_LENGTH - strlen($separator)) / 2);
    $prefix = AcquiaLiftAPI::cleanFeatureString($name);
    $value = AcquiaLiftAPI::cleanFeatureString($value);

    // Make a string of the visitor context item in the format Acquia Lift can
    // consume.
    $feature_string = $prefix . $separator . $value;
    while (strlen($feature_string) > AcquiaLiftAPI::FEATURE_STRING_MAX_LENGTH) {

      // Acquia Lift has a hard character limit for feature strings.
      if (strlen($prefix) > $prefix_max_length) {

        // Start by truncating the prefix down to half the max length.
        $prefix = substr($prefix, 0, $prefix_max_length);
        $feature_string = $prefix . $separator . $value;
      }
      else {

        // Otherwise just truncate the whole thing down to the max length.
        $feature_string = substr($feature_string, 0, AcquiaLiftAPI::FEATURE_STRING_MAX_LENGTH);
      }
    }
    return $feature_string;
  }

  /**
   * Get a reference to this agent's reporting class.
   *
   * Allows for delayed instantiation of reporting class.
   */
  protected function getReporting() {
    if (empty($this->reporting)) {
      $this->reporting = AcquiaLiftReportFactory::create($this, $this->liftAPI);
    }
    return $this->reporting;
  }

  /**
   * Implements PersonalizeAgentReportInterface::renderStatsForOptionSet().
   *
   * A facade to the reporting class.
   */
  public function renderStatsForOptionSet($option_set, $date_from, $date_to = NULL) {
    return $this
      ->getReporting()
      ->renderStatsForOptionSet($option_set, $date_from, $date_to);
  }

  /**
   * Implements PersonalizeAgentReportInterface::buildCampaignReports().
   *
   * A facade to the reporting class.
   */
  public function buildCampaignReports($options) {
    return $this
      ->getReporting()
      ->buildCampaignReports($options);
  }

  /**
   * Implements AcquiaLiftReportInterface()::buildConversionReport().
   */
  public function buildConversionReport($options) {
    return $this
      ->getReporting()
      ->buildConversionReport($options);
  }

  /**
   * Implements AcquiaLiftAgent::convertOptionSetsToDecisions().
   */
  public static function convertOptionSetsToDecisions($option_sets) {
    $points = array();
    foreach ($option_sets as $option_set) {

      // If for some reason one of our option sets is missing a point name or
      // decision name, throw an exception as we cannot proceed.
      if (!isset($option_set->decision_point) || !isset($option_set->decision_name)) {
        throw new AcquiaLiftException('Cannot convert option sets to a structured decision hierarchy without decision points and decision names');
      }
      $points[$option_set->decision_point] = isset($points[$option_set->decision_point]) ? $points[$option_set->decision_point] : array();
      $points[$option_set->decision_point][$option_set->decision_name] = isset($points[$option_set->decision_point][$option_set->decision_name]) ? $points[$option_set->decision_point][$option_set->decision_name] : array();
      foreach ($option_set->options as $option) {
        $points[$option_set->decision_point][$option_set->decision_name][] = $option['option_id'];
      }
    }
    return $points;
  }

  /**
   * Implements PersonalizeAgentInterface::errors().
   */
  public function errors() {
    $errors = array();
    try {
      $acquia_lift_agent = $this->liftAPI
        ->getAgent($this->machineName);
    } catch (AcquiaLiftException $e) {
      return $this
        ->convertAgentExceptionToErrors($e, $errors);
    }
    if ($acquia_lift_agent['status'] === AcquiaLiftAPI::PROVISIONAL_STATUS) {
      $errors[] = t('The status of the Acquia Lift agent is @status', array(
        '@status' => $acquia_lift_agent['status'],
      ));
    }

    // Make sure Acquia Lift knows about the agent's goals.
    $goals = personalize_goal_load_by_conditions(array(
      'agent' => $this->machineName,
    ));
    $discrepancies = FALSE;
    if (empty($goals)) {

      // Acquia Lift agents need goals.
      $errors[] = t('No goals have been set up for this agent');
    }
    try {
      $acquia_lift_goals = $this->liftAPI
        ->getGoalsForAgent($this->machineName);
    } catch (AcquiaLiftException $e) {
      return $this
        ->convertAgentExceptionToErrors($e, $errors);
    }
    foreach ($goals as $goal) {
      if (!in_array($goal->action, $acquia_lift_goals)) {
        $errors[] = t('Goal @goal has not been sync\'d to the Acquia Lift agent.', array(
          '@goal' => $goal->action,
        ));
        $discrepancies = TRUE;
      }
    }

    // Make sure all decision points are known by Acquia Lift.
    $option_sets = personalize_option_set_load_by_agent($this->machineName);
    if (empty($option_sets)) {

      // Acquia Lift agents need option sets.
      $errors[] = t('No variation sets have been set up for this agent');
    }
    $decision_tree = self::convertOptionSetsToDecisions($option_sets);
    try {
      $acquia_lift_points = $this->liftAPI
        ->getPointsForAgent($this->machineName);
    } catch (AcquiaLiftException $e) {
      return $this
        ->convertAgentExceptionToErrors($e, $errors);
    }
    foreach ($decision_tree as $point => $decisions) {
      if (!in_array($point, $acquia_lift_points)) {
        $errors[] = t('Point @point has not been sync\'d to the Acquia Lift agent.', array(
          '@point' => $point,
        ));
        $discrepancies = TRUE;
        continue;
      }
      try {
        $acquia_lift_decisions = $this->liftAPI
          ->getDecisionsForPoint($this->machineName, $point);
      } catch (AcquiaLiftException $e) {
        return $this
          ->convertAgentExceptionToErrors($e, $errors);
      }
      foreach ($decisions as $decision_name => $options) {
        if (!in_array($decision_name, $acquia_lift_decisions)) {
          $errors[] = t('Decision @decision has not been sync\'d to the Acquia Lift agent.', array(
            '@decision' => $decision_name,
          ));
          $discrepancies = TRUE;
        }
        try {
          $acquia_lift_choices = $this->liftAPI
            ->getChoicesForDecision($this->machineName, $point, $decision_name);
        } catch (AcquiaLiftException $e) {
          return $this
            ->convertAgentExceptionToErrors($e, $errors);
        }
        foreach ($options as $option) {
          if (!in_array($option, $acquia_lift_choices)) {
            $errors[] = t('Option @choice has not been sync\'d to the Acquia Lift agent.', array(
              '@choice' => $option,
            ));
            $discrepancies = TRUE;
          }
        }
      }

      // @todo Check the fixed targeting for each decision point.
    }
    if ($discrepancies) {

      // Add a general message about how to resolve discrepancies.
      $message = t('To resolve the discrepancies between your agent configuration here and what has been sync\'d to the Acquia Lift service, try saving your campaign and its variation sets and goals again.');
      if (user_access('administer site configuration')) {
        $message .= t(' If that still does not resolve it, try <a href="@cron">running cron</a>.', array(
          '@cron' => url('admin/reports/status/run-cron'),
        ));
      }
      $errors[] = $message;
    }
    return $errors;
  }

  /**
   * Implements PersonalizeAgentInterface::stopNow().
   */
  public function stopNow() {

    // Check whether the agent is configured to stop once a winner's been found.
    if (!isset($this->data['auto_stop']) || !$this->data['auto_stop']) {
      return parent::stopNow();
    }

    // If neither minimum runtime nor minimum decisions has been configured,
    // then return FALSE.
    if (!$this->globalConfig['minimum_runtime'] && !$this->globalConfig['minimum_decisions']) {
      return FALSE;
    }

    // If there a minimum runtime, check that it has been reached.
    $time_now = time();
    if ($this->globalConfig['minimum_runtime']) {
      $runtime = $time_now - $this->startTime;
      if ($runtime < $this->globalConfig['minimum_runtime']) {

        // Campaign has not been running long enough.
        return FALSE;
      }
    }

    // Need to go through each option set for this agent and establish whether each
    // of them satisfies the minimum number of decisions made, if this has been set.
    $option_sets = $this->data['decisions'];
    if (empty($option_sets)) {
      return FALSE;
    }
    $decision_points = self::convertOptionSetsToDecisions($option_sets);
    $points = array_keys($decision_points);
    $winners = array();
    foreach ($points as $point) {
      $num_decisions = 0;
      $vMeans = array();

      // Get the confidence report directly from Lift.
      $report_options = array(
        'confidence-measure' => $this->globalConfig['confidence_measure'],
        'aggregated-over-dates' => TRUE,
        'features' => "(none)",
      );
      $confidence_report = $this->liftAPI
        ->getConfidenceReport($this->machineName, $this->startTime, $time_now, $point, $report_options);
      $items = $confidence_report['data']['items'];
      if (empty($items)) {
        return FALSE;
      }
      foreach ($items as $item) {
        $choice = $item['choice'];
        $vMeans[$choice] = $item['vMean'];
        $num_decisions += $item['count'];
      }
      if ($this->globalConfig['minimum_decisions']) {

        // If any of the decision points has fewer than the required minimum number
        // of decisions, don't stop the campaign.
        if ($num_decisions < $this->globalConfig['minimum_decisions']) {
          return FALSE;
        }
      }

      // Find the variation with the highest estimated value.
      arsort($vMeans);
      $winners[$point] = key($vMeans);
    }

    // If we reach here, we have a winner at each decision point and have satisfied all
    // criteria for choosing a winner and pausing the campaign. Get all option sets for the
    // agent and set the winner for each one.
    $option_sets = personalize_option_set_load_by_agent($this->machineName);
    foreach ($winners as $point => $choice) {

      // The "winner" at any decision point might be a single decision or a combination
      // of decisions, in which case they'll be separated by a comma.
      $decisions = explode(',', $choice);
      foreach ($decisions as $decision) {
        list($decision_name, $option_id) = explode(':', $decision);

        // Get all option sets for this decision name.
        foreach ($option_sets as $osid => $option_set) {
          if ($option_set->decision_point == $point && $option_set->decision_name == $decision_name) {
            $option_set->winner = $option_id;
            personalize_option_set_save($option_set);
          }
        }
      }
    }
    return TRUE;
  }

  /**
   * Converts an exception thrown by the API class into errors added to the passed in array.
   *
   * @param AcquiaLiftException $e
   *   The excetion that was thrown.
   * @param $errors
   *   An array of errors to add to.
   * @return array
   *   The new errors array.
   */
  protected function convertAgentExceptionToErrors(AcquiaLiftException $e, &$errors) {
    if ($e instanceof AcquiaLiftNotFoundException) {
      $errors[] = t('This agent has not yet been pushed to Acquia Lift');
    }
    else {
      $errors[] = t('There was a problem communicating with the Acquia Lift server.');
    }
    return $errors;
  }

  /**
   * Returns a queue to use.
   *
   * @return DrupalQueueInterface
   */
  protected function getQueue() {
    if ($this->queue !== NULL) {
      return $this->queue;
    }
    return DrupalQueue::get('acquia_lift_sync');
  }

  /**
   * Sets the queue to use.
   *
   * @param DrupalQueueInterface $queue
   */
  public function setQueue(DrupalQueueInterface $queue) {
    $this->queue = $queue;
  }

  /**
   * Implements AcquiaLiftAgentInterface::syncAgentStatus().
   */
  public function syncAgentStatus() {
    $items = array();
    $items[] = array(
      'method' => 'updateAgentStatus',
      'args' => array(
        $this->machineName,
        $this->status,
      ),
    );
    $this
      ->queueItems($items);
  }

  /**
   * Implements AcquiaLiftAgentInterface::syncDecisions().
   */
  public function syncDecisions($old_decisions, $new_decisions) {
    $items = $this
      ->getDecisionSyncOperations($old_decisions, $new_decisions);
    $this
      ->queueItems($items);
  }

  /**
   * Returns the operations needed to sync decisions to Acquia Lift.
   *
   * @param $old_decisions
   *   An array representing the old decisions Acquia Lift knows about.
   * @param $new_decisions
   *   An array representing the new decisions.
   * @return array
   *   An array of items representing API calls to be made to Acquia Lift.
   */
  public function getDecisionSyncOperations($old_decisions, $new_decisions) {
    $items = array();

    // Save everything in $new_decisions to Acquia Lift.
    foreach ($new_decisions as $point => $decisions) {
      $items[] = array(
        'method' => 'savePoint',
        'args' => array(
          $this->machineName,
          $point,
        ),
      );
      foreach ($decisions as $decision_name => $choices) {
        $items[] = array(
          'method' => 'saveDecision',
          'args' => array(
            $this->machineName,
            $point,
            $decision_name,
          ),
        );
        foreach ($choices as $choice) {
          $items[] = array(
            'method' => 'saveChoice',
            'args' => array(
              $this->machineName,
              $point,
              $decision_name,
              $choice,
            ),
          );
        }
      }
    }

    // Now remove anything that was in $old_decisions but not in
    // $new_decisions.
    foreach ($old_decisions as $point => $decisions) {
      if (!isset($new_decisions[$point])) {
        $items[] = array(
          'method' => 'deletePoint',
          'args' => array(
            $this->machineName,
            $point,
          ),
        );
      }
      else {
        foreach ($decisions as $decision_name => $choices) {
          if (!isset($new_decisions[$point][$decision_name])) {

            // Delete this decision from the decision point.
            $items[] = array(
              'method' => 'deleteDecision',
              'args' => array(
                $this->machineName,
                $point,
                $decision_name,
              ),
            );
          }
          else {
            foreach ($choices as $choice) {
              if (!in_array($choice, $new_decisions[$point][$decision_name])) {

                // Delete this choice from the decision.
                $items[] = array(
                  'method' => 'deleteChoice',
                  'args' => array(
                    $this->machineName,
                    $point,
                    $decision_name,
                    $choice,
                  ),
                );
              }
            }
          }
        }
      }
    }
    return $items;
  }

  /**
   * Implements AcquiaLiftAgentInterface::syncGoals().
   */
  public function syncGoals($old_goals, $new_goals) {
    $items = $this
      ->getGoalSyncOperations($old_goals, $new_goals);
    $this
      ->queueItems($items);
  }

  /**
   * Returns the operations needed to sync goals to Acquia Lift.
   *
   * @param $old_goals
   *   An array representing the old goals Acquia Lift knows about.
   * @param $new_goals
   *   An array representing the new goals.
   * @return array
   *   An array of items representing API calls to be made to Acquia Lift.
   */
  public function getGoalSyncOperations($old_goals, $new_goals) {
    $items = array();

    // Save the new goals to Acquia Lift
    foreach ($new_goals as $goal_name => $goal_value) {
      $items[] = array(
        'method' => 'saveGoal',
        'args' => array(
          $this->machineName,
          $goal_name,
        ),
      );
    }

    // Now delete any old goals that are not in the new goals array.
    foreach ($old_goals as $goal_name => $goal_value) {
      if (!isset($new_goals[$goal_name])) {
        $items[] = array(
          'method' => 'deleteGoal',
          'args' => array(
            $this->machineName,
            $goal_name,
          ),
        );
      }
    }
    return $items;
  }

  /**
   * Implements AcquiaLiftAgentInterface::syncFixedTargeting().
   */
  public function syncFixedTargeting($option_sets) {
    $items = $this
      ->getFixedTargetingSyncOperations($option_sets);
    $this
      ->queueItems($items);
  }

  /**
   * Returns the operations needed to sync goals to Acquia Lift.
   *
   * @param $option_sets
   *   An array representing the option sets whose targeting rules need to be sync'd.
   * @return array
   *    An array of items representing API calls to be made to Acquia Lift.
   */
  public function getFixedTargetingSyncOperations($option_sets) {
    $items = array();

    // If any of this agent's option sets has explicit targeting mappings configured,
    // we need to send these mappings to Acquia Lift.
    $mappings = array();
    foreach ($option_sets as $option_set) {
      if (empty($option_set->targeting)) {
        continue;
      }
      $point_name = $option_set->decision_point;
      $decision_name = $option_set->decision_name;
      $mappings[$point_name] = isset($mappings[$point_name]) ? $mappings[$point_name] : array();
      foreach ($option_set->targeting as $targ) {
        if (!isset($targ['option_id'])) {
          continue;
        }
        if (isset($targ['targeting_features'])) {

          // Check if we're supposed to AND or OR mulitple features together.
          if (isset($targ['targeting_strategy']) && $targ['targeting_strategy'] == 'AND') {

            // Create a single mapping, with a comma-separated list of features.
            $mappings[$point_name][] = array(
              'feature' => implode(',', $targ['targeting_features']),
              'decision' => $decision_name . ':' . $targ['option_id'],
            );
          }
          else {

            // Create a mapping for each feature and they will be OR'd together.
            foreach ($targ['targeting_features'] as $feature) {
              $mappings[$point_name][] = array(
                'feature' => $feature,
                'decision' => $decision_name . ':' . $targ['option_id'],
              );
            }
          }
        }
      }
    }

    // Send mappings per decision point.
    foreach ($mappings as $point_name => $map) {
      $items[] = array(
        'method' => 'saveFixedTargetingMapping',
        'args' => array(
          $this->machineName,
          $point_name,
          $map,
        ),
      );
    }
    return $items;
  }

  /**
   * Adds items to the queue and sets a message to inform the user.
   *
   * @param $items
   *   An array of items to add to the queue.
   */
  protected function queueItems($items) {
    if (!empty($items)) {
      foreach ($items as $item) {

        // Create a hash of the item. This will prevent duplicate items from
        // being added to the queue.
        $hash = md5(serialize($item));
        $item['hash'] = $hash;

        // We add the agent machine name to the queue item so that if it fails
        // the agent in question can be paused.
        $item['agent'] = $this->machineName;
        $this
          ->getQueue()
          ->createItem($item);
      }

      // Make sure the queue gets triggered on the next request.
      $_SESSION['acquia_lift_queue_trigger'] = 1;
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
AcquiaLiftAgent::$agent protected property An object containing the agent data.
AcquiaLiftAgent::$globalConfig protected property
AcquiaLiftAgent::$liftAPI protected property An instance of AcquiaLiftAPI.
AcquiaLiftAgent::$queue protected property An instance of DrupalQueueInterface
AcquiaLiftAgent::$reporting protected property An instance of PersonalizeAgentReportInterface.
AcquiaLiftAgent::buildCampaignReports public function Implements PersonalizeAgentReportInterface::buildCampaignReports().
AcquiaLiftAgent::buildConversionReport public function Implements AcquiaLiftReportInterface()::buildConversionReport(). Overrides AcquiaLiftAgentInterface::buildConversionReport
AcquiaLiftAgent::buildOptionsForm protected static function Builds the options to display for a campaign form.
AcquiaLiftAgent::constrainExplicitTargetingContexts public static function Implements PersonalizeAutoTargetingInterface::constrainExplicitTargetingContexts().
AcquiaLiftAgent::convertAgentExceptionToErrors protected function Converts an exception thrown by the API class into errors added to the passed in array.
AcquiaLiftAgent::convertContextToFeatureString public static function Implements PersonalizeAgentInterface::convertContextToFeatureString().
AcquiaLiftAgent::convertOptionSetsToDecisions public static function Implements AcquiaLiftAgent::convertOptionSetsToDecisions(). Overrides AcquiaLiftAgentInterface::convertOptionSetsToDecisions
AcquiaLiftAgent::create public static function Implements PersonalizeAgentInterface::create().
AcquiaLiftAgent::errors public function Implements PersonalizeAgentInterface::errors().
AcquiaLiftAgent::explicitTargetingSupportMultiple public static function Implements PersonalizeExplicitTargetingInterface::explicitTargetingSupportMultiple().
AcquiaLiftAgent::getAgentSyncOperations public function Returns the operations needed to sync an agent to Acquia Lift.
AcquiaLiftAgent::getAssets public function Implements PersonalizeAgentInterface::getAssets().
AcquiaLiftAgent::getDecisionSyncOperations public function Returns the operations needed to sync decisions to Acquia Lift.
AcquiaLiftAgent::getFixedTargetingSyncOperations public function Returns the operations needed to sync goals to Acquia Lift.
AcquiaLiftAgent::getGoalSyncOperations public function Returns the operations needed to sync goals to Acquia Lift.
AcquiaLiftAgent::getQueue protected function Returns a queue to use.
AcquiaLiftAgent::getReporting protected function Get a reference to this agent's reporting class.
AcquiaLiftAgent::getType public function Implements PersonalizeAgentInterface::getType();
AcquiaLiftAgent::optionsForm public static function Implements PersonalizeAgentInterface::optionsForm().
AcquiaLiftAgent::optionsFormValidate public static function Implements PersonalizeAgentInterface::optionsFormValidate().
AcquiaLiftAgent::postSave public function Implements PersonalizeAgentInterface::postSave().
AcquiaLiftAgent::queueItems protected function Adds items to the queue and sets a message to inform the user.
AcquiaLiftAgent::renderStatsForOptionSet public function Implements PersonalizeAgentReportInterface::renderStatsForOptionSet().
AcquiaLiftAgent::sendGoal public function Implements PersonalizeAgentGoalInterface::sendGoal().
AcquiaLiftAgent::setQueue public function Sets the queue to use.
AcquiaLiftAgent::stopNow public function Implements PersonalizeAgentInterface::stopNow().
AcquiaLiftAgent::syncAgentStatus public function Implements AcquiaLiftAgentInterface::syncAgentStatus(). Overrides AcquiaLiftAgentInterface::syncAgentStatus
AcquiaLiftAgent::syncDecisions public function Implements AcquiaLiftAgentInterface::syncDecisions(). Overrides AcquiaLiftAgentInterface::syncDecisions
AcquiaLiftAgent::syncFixedTargeting public function Implements AcquiaLiftAgentInterface::syncFixedTargeting(). Overrides AcquiaLiftAgentInterface::syncFixedTargeting
AcquiaLiftAgent::syncGoals public function Implements AcquiaLiftAgentInterface::syncGoals(). Overrides AcquiaLiftAgentInterface::syncGoals
AcquiaLiftAgent::useClientSideGoalDelivery public function Implements PersonalizeAgentGoalInterface::useClientSideGoalDelivery().
AcquiaLiftAgent::__construct public function Constructs an Acquia Lift agent.