You are here

mailchimp_lists.module in Mailchimp 2.x

Mailchimp lists/audiences module.

File

modules/mailchimp_lists/mailchimp_lists.module
View source
<?php

/**
 * @file
 * Mailchimp lists/audiences module.
 */
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\TypedData\DataReferenceInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\mailchimp_lists\Plugin\Field\FieldType\MailchimpListsSubscription;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\field\Entity\FieldConfig;

/**
 * Implements hook_entity_delete().
 */
function mailchimp_lists_entity_delete($entity) {

  // Only act on content entities.
  if (!$entity instanceof ContentEntityInterface) {
    return;
  }
  $field_definitions = $entity
    ->getFieldDefinitions();
  if (empty($field_definitions)) {
    return;
  }

  // Filter fields to only subscription fields marked to unsubscribe on delete.
  $list_fields = array_keys(array_filter($field_definitions, function (FieldDefinitionInterface $field) {
    return $field
      ->getType() == 'mailchimp_lists_subscription' && $field
      ->getSetting('unsubscribe_on_delete');
  }));

  /* @var $item \Drupal\mailchimp_lists\Plugin\Field\FieldType\MailchimpListsSubscription */
  foreach ($list_fields as $field) {

    // Additional foreach to support multiple values.
    foreach ($entity
      ->get($field) as $item) {
      mailchimp_lists_process_subscribe_form_choices([
        'subscribe' => FALSE,
      ], $item, $entity);
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function mailchimp_lists_form_field_storage_config_edit_form_alter(&$form, FormStateInterface &$form_state, $form_id) {
  $storage = $form_state
    ->getStorage();

  /* @var $field_config \Drupal\field\Entity\FieldConfig */
  $field_config = $storage['field_config'];
  $field_type = $field_config
    ->get('field_type');
  if ($field_type == 'mailchimp_lists_subscription') {

    // Hide the cardinality setting:
    $form['cardinality_container']['cardinality_number']['#default_value'] = 1;
    $form['cardinality_container']['#access'] = FALSE;
    $form['#validate'][] = 'mailchimp_lists_form_field_ui_field_edit_form_validate';
  }
}

/**
 * Validation handler for mailchimp_lists_form_field_ui_field_edit_form.
 *
 * Ensure cardinality is set to 1 on mailchimp_lists_subscription fields.
 */
function mailchimp_lists_form_field_ui_field_edit_form_validate(&$form, FormStateInterface &$form_state) {
  $storage = $form_state
    ->getStorage();

  /* @var $field_config \Drupal\field\Entity\FieldConfig */
  $field_config = $storage['field_config'];
  if ($field_config
    ->get('field_type') == 'mailchimp_lists_subscription') {
    if ($form_state
      ->getValue('cardinality_number') != 1) {
      $form_state
        ->setErrorByName('cardinality_number', t('Cardinality on mailchimp audiences fields must be set to 1.'));
    }
  }
}

/**
 * Helper function to check if a valid email is configured for an entity field.
 *
 * Returns an email address or FALSE.
 */
function mailchimp_lists_load_email(MailchimpListsSubscription $instance, EntityInterface $entity, $log_errors = TRUE) {
  $merge_fields = $instance
    ->getFieldDefinition()
    ->getSetting('merge_fields');
  if (empty($merge_fields) || !isset($merge_fields['EMAIL'])) {
    if ($log_errors) {
      \Drupal::logger('mailchimp_lists')
        ->notice('Mailchimp Audiences field "{field}" on {entity} -> {bundle} has no EMAIL field configured, subscription actions cannot take place.', [
        'field' => $instance
          ->getFieldDefinition()
          ->getName(),
        'entity' => $entity
          ->getEntityType()
          ->getLabel(),
        'bundle' => $entity
          ->bundle(),
      ]);
    }
    return FALSE;
  }

  // Get the entity's email property value from the user-defined email field.
  $mail_property = $instance
    ->getFieldDefinition()
    ->getSetting('merge_fields')['EMAIL'];

  /* @var $mail_field \Drupal\Core\Field\FieldItemList */
  $mail_field = $entity
    ->get($mail_property);
  if ($mail_field != NULL && \Drupal::service('email.validator')
    ->isValid($mail_field
    ->getString())) {
    return $mail_field
      ->getString();
  }
  else {
    return FALSE;
  }
}

/**
 * Processor for various list form submissions.
 *
 * Subscription blocks, user settings, and new user creation.
 *
 * @param array $choices
 *   An array representing the form values selected.
 * @param \Drupal\mailchimp_lists\Plugin\Field\FieldType\MailchimpListsSubscription $instance
 *   A mailchimp_lists_subscription field instance configuration.
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   An Entity that has the $instance field.
 */
function mailchimp_lists_process_subscribe_form_choices(array $choices, MailchimpListsSubscription $instance, EntityInterface $entity) {

  // Never process a unsubscribe when the entity is new.
  // This is important to prevent unwanted subscription overwrites.
  if ($entity
    ->isNew() && $choices['subscribe'] == 0) {
    return;
  }
  $email = mailchimp_lists_load_email($instance, $entity);
  if (!$email) {

    // We can't do much subscribing without an email address.
    return;
  }
  $settings = $instance
    ->getFieldDefinition()
    ->getSettings();
  $function = FALSE;
  $subscribed = mailchimp_is_subscribed($settings['mc_list_id'], $email);
  if ($choices['subscribe'] != $subscribed) {

    // Subscription selection has changed.
    if ($choices['subscribe']) {
      $function = 'add';
    }
    else {
      $function = 'remove';
    }
  }
  elseif ($choices['subscribe']) {
    $function = 'update';
  }
  if ($function) {
    if ($function == 'remove') {
      $mergevars = [];
    }
    else {
      $mergevars = _mailchimp_lists_mergevars_populate($settings['merge_fields'], $entity);
    }
    $interests = isset($choices['interest_groups']) ? $choices['interest_groups'] : [];
    switch ($function) {
      case 'add':
        $ret = mailchimp_subscribe($settings['mc_list_id'], $email, $mergevars, $interests, $settings['double_opt_in']);
        break;
      case 'remove':
        $ret = mailchimp_unsubscribe($settings['mc_list_id'], $email);
        break;
      case 'update':
        if (_mailchimp_lists_subscription_has_changed($instance, $entity, $email, $choices)) {
          $ret = mailchimp_update_member($settings['mc_list_id'], $email, $mergevars, $interests);
        }
        else {
          $ret = TRUE;
        }
        break;
    }
    if (empty($ret)) {
      \Drupal::messenger()
        ->addWarning(t('There was a problem with your newsletter signup.'));
    }
  }
}

/**
 * Helper function to avoid sending superfluous updates to Mailchimp.
 *
 * This is necessary due to the nature of the field implementation of
 * subscriptions. If we don't do this, we send an update to mailchimp every time
 * an entity is updated.
 *
 * returns bool
 */
function _mailchimp_lists_subscription_has_changed(MailchimpListsSubscription $instance, EntityInterface $entity, $email, $choices) {
  $settings = $instance
    ->getFieldDefinition()
    ->getSettings();
  if (isset($entity->original)) {

    /* @var $original \Drupal\Core\Entity\EntityInterface */
    $original = $entity->original;

    // First compare Interest Group settings.
    if ($settings['show_interest_groups']) {
      $field_name = $instance
        ->getFieldDefinition()
        ->getName();

      /* @var $old_field_settings \Drupal\mailchimp_lists\Plugin\Field\FieldType\MailchimpListsSubscription */
      $old_field_settings = $original->{$field_name}
        ->get(0);

      /* @var $new_field_settings \Drupal\mailchimp_lists\Plugin\Field\FieldType\MailchimpListsSubscription */
      $new_field_settings = $entity->{$field_name}
        ->get(0);
      $old_interest_groups = is_null($old_field_settings) ? [] : $old_field_settings
        ->getInterestGroups();
      $new_interest_groups = $new_field_settings
        ->getInterestGroups();
      foreach ($new_interest_groups as $id => $new_interests) {

        // Check for change in entire interest group.
        if (!isset($old_interest_groups[$id])) {
          return TRUE;
        }
        $old_interests = $old_interest_groups[$id];

        // Check for changes in individual interests.
        foreach ($new_interests as $interest_id => $interest_status) {
          if (!isset($old_interests[$interest_id]) || $old_interests[$interest_id] !== $interest_status) {
            return TRUE;
          }
        }
      }
    }

    // Compare merge field settings.
    $mergevars = _mailchimp_lists_mergevars_populate($settings['merge_fields'], $entity);
    $original_mergevars = _mailchimp_lists_mergevars_populate($settings['merge_fields'], $original);
    return $mergevars != $original_mergevars;
  }
  else {
    $member_info = mailchimp_get_memberinfo($settings['mc_list_id'], $email);
    if (isset($member_info->merge_fields->GROUPINGS)) {
      foreach ($member_info->merge_fields->GROUPINGS as $grouping) {
        if (!isset($choices['interest_groups'][$grouping['id']])) {
          return TRUE;
        }
        else {
          if (!is_array($choices['interest_groups'][$grouping['id']])) {

            // Standardize formatting of choices:
            $choices['interest_groups'][$grouping['id']] = [
              $choices['interest_groups'][$grouping['id']] => $choices['interest_groups'][$grouping['id']],
            ];
          }
          foreach ($grouping['groups'] as $group) {
            $selected = isset($choices['interest_groups'][$grouping['id']][$group['name']]) && $choices['interest_groups'][$grouping['id']][$group['name']];
            if ($group['interested'] && !$selected || !$group['interested'] && $selected) {
              return TRUE;
            }
          }
        }
      }
    }
  }

  // No changes detected.
  return FALSE;
}

/**
 * Helper function to complete a mailchimp-api-ready mergevars array.
 *
 * @see \Drupal\ctools\TypedDataResolver::getContextFromProperty()
 */
function _mailchimp_lists_mergevars_populate($merge_fields, EntityInterface $entity) {
  $mergevars = [];
  foreach (array_filter($merge_fields) as $label => $property_path) {
    $data = $entity
      ->getTypedData();
    $value = NULL;
    foreach (explode(':', $property_path) as $name) {
      if ($data instanceof ListInterface) {
        if (!is_numeric($name)) {

          // Implicitly default to delta 0 for audiences when not specified.
          $data = $data
            ->first();
        }
        else {

          // If we have a delta, fetch it and continue with the next part.
          $data = $data
            ->get($name);
          continue;
        }
      }

      // Forward to the target value if this is a data reference.
      if ($data instanceof DataReferenceInterface) {
        $data = $data
          ->getTarget();
      }

      // If there no data then the field is empty, ignore this.
      if (!$data) {
        break;
      }
      if (!$data
        ->getDataDefinition()
        ->getPropertyDefinition($name)) {

        // @todo What should we do here, ignore silently?
        throw new \Exception("Unknown property {$name} in property path {$property_path}");
      }
      $data = $data
        ->get($name);
    }

    // It would be easier if the structure would always include the property
    // as well. For backwards compatibility, that is not done, in that case
    // default to the main property.
    if ($data instanceof FieldItemListInterface || $data instanceof FieldItemInterface) {
      $main_property = $data
        ->getFieldDefinition()
        ->getFieldStorageDefinition()
        ->getMainPropertyName();
      $value = $data->{$main_property};
    }
    elseif ($data instanceof TypedDataInterface) {
      $value = $data
        ->getValue();
    }

    // Cast to string to avoid problems with NULL values that the API does not
    // accept.
    // @todo: Check if this causes problems with integers or other non-string
    // merge fields, assuming the exist.
    $mergevars[$label] = (string) $value;
  }

  // Allow other modules to alter the merge vars.
  // @todo: Remove entity type argument.
  $entity_type_id = $entity
    ->getEntityTypeId();
  \Drupal::moduleHandler()
    ->alter('mailchimp_lists_mergevars', $mergevars, $entity, $entity_type_id);
  return $mergevars;
}

/**
 * Triggers an update of all merge field values for appropriate entities.
 */
function mailchimp_lists_update_member_merge_values($entity_type, $bundle_name, $field_name) {
  $field = FieldConfig::loadByName($entity_type, $bundle_name, $field_name);
  $mc_list_id = $field
    ->getFieldStorageDefinition()
    ->getSetting('mc_list_id');
  $merge_fields = $field
    ->getSetting('merge_fields');

  // Assemble a list of current subscription statuses so we don't alter them.
  // Because of cacheing we don't want to use the standard checks. Expiring the
  // cache would kill the point of doing this as a batch API operation.
  $batch = [
    'operations' => [
      [
        'mailchimp_lists_get_subscribers',
        [
          $field,
        ],
      ],
      [
        'mailchimp_lists_populate_member_batch',
        [
          $entity_type,
          $bundle_name,
          $field,
          $merge_fields,
        ],
      ],
      [
        'mailchimp_lists_execute_mergevar_batch_update',
        [
          $mc_list_id,
        ],
      ],
    ],
    'finished' => 'mailchimp_lists_populate_member_batch_complete',
    'title' => t('Processing Merge Variable Updates'),
    'init_message' => t('Starting Mailchimp Merge Variable Update.'),
    'progress_message' => t('Processed @current out of @total.'),
    'error_message' => t('Mailchimp Merge Variable Update Failed.'),
  ];
  batch_set($batch);
}

/**
 * Batch processor for pulling in subscriber information for a list/audience.
 */
function mailchimp_lists_get_subscribers(FieldConfig $field, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $context['results']['subscribers'] = [];
    $context['sandbox']['progress'] = 0;
  }
  $limit = 100;
  $options = [
    'offset' => $context['sandbox']['progress'] / $limit,
    'count' => $limit,
  ];
  $mc_list_id = $field
    ->getFieldStorageDefinition()
    ->getSetting('mc_list_id');
  $matches = mailchimp_get_members($mc_list_id, 'subscribed', $options);
  if ($matches) {
    if (!isset($context['sandbox']['max'])) {
      $context['sandbox']['max'] = $matches->total_items;
    }
    foreach ($matches->members as $result) {
      $context['results']['subscribers'][strtolower($result->email_address)] = $result;
      $context['sandbox']['progress']++;
    }
    $context['message'] = t('Check subscription status for contact %count of %total.', [
      '%count' => $context['sandbox']['progress'],
      '%total' => $context['sandbox']['max'],
    ]);
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch processor for member mergevar updates to built the mergevar arrays.
 */
function mailchimp_lists_populate_member_batch($entity_type, $bundle_name, $field, $mergefields, &$context) {
  if (!isset($context['sandbox']['progress'])) {

    // Load up all our eligible entities.
    $query = \Drupal::entityQuery($entity_type);
    $definition = \Drupal::entityTypeManager()
      ->getDefinition($entity_type);
    if ($definition
      ->hasKey('bundle')) {
      $query
        ->condition($definition
        ->getKey('bundle'), $bundle_name);
    }
    $query_results = $query
      ->execute();
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = isset($query_results) ? count($query_results) : 0;
    if ($context['sandbox']['max']) {
      $context['sandbox']['entity_ids'] = array_keys($query_results);
      $context['results']['update_queue'] = [];
    }
  }
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $batch_size = 50;
    $item_ids = array_slice($context['sandbox']['entity_ids'], $context['sandbox']['progress'], $batch_size);
    $entities = \Drupal::entityTypeManager()
      ->getStorage($entity_type)
      ->loadMultiple($item_ids);
    foreach ($entities as $entity) {
      $merge_vars = _mailchimp_lists_mergevars_populate($mergefields, $entity);
      if ($merge_vars['EMAIL'] && isset($context['results']['subscribers'][strtolower($merge_vars['EMAIL'])])) {
        $context['results']['update_queue'][] = [
          'email' => $merge_vars['EMAIL'],
          // Preserve subscribers's email type selection:
          'email_type' => $context['results']['subscribers'][strtolower($merge_vars['EMAIL'])]->email_type,
          'merge_vars' => $merge_vars,
        ];
      }
      $context['sandbox']['progress']++;
    }
    $context['message'] = t('Checking for changes on items %count - %next.', [
      '%count' => $context['sandbox']['progress'],
      '%next' => $context['sandbox']['progress'] + $batch_size,
    ]);
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch processor for member mergevar updates to submit batches to Mailchimp.
 */
function mailchimp_lists_execute_mergevar_batch_update($list_id, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $config = \Drupal::config('mailchimp.settings');
    $batch_limit = $config
      ->get('mailchimp_batch_limit');
    if (empty($batch_limit)) {
      $batch_limit = 1000;
    }
    $context['sandbox']['mc_batch_limit'] = $batch_limit;
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['total'] = count($context['results']['update_queue']);
    $context['results']['updates'] = 0;
    $context['results']['errors'] = 0;
  }
  if ($context['sandbox']['progress'] < $context['sandbox']['total']) {
    $batch = array_slice($context['results']['update_queue'], $context['sandbox']['progress'], $context['sandbox']['mc_batch_limit']);
    $result = mailchimp_batch_update_members($list_id, $batch, FALSE, TRUE, FALSE);
    if ($result) {

      /* @var \Mailchimp\Mailchimp $mc */
      $mc = mailchimp_get_api_object();
      $batch_result = $mc
        ->getBatchOperation($result->id);
      $context['results']['updates'] += count($context['results']['update_queue']);
      $context['results']['errors'] += $batch_result->errored_operations;
    }
    $batch_size = count($batch);
    $context['sandbox']['progress'] += $batch_size;
    $context['message'] = t('Updating Mailchimp mergevars for items %count - %next.', [
      '%count' => $context['sandbox']['progress'],
      '%next' => $context['sandbox']['progress'] + $batch_size,
    ]);
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['total'];
  }
}

/**
 * Batch completion processor for member mergevar updates.
 */
function mailchimp_lists_populate_member_batch_complete($success, $results, $operations) {
  if ($success) {
    if ($results['errors']) {
      \Drupal::messenger()
        ->addWarning(t('Update errors occurred: merge variables updated on %count records, errors occurred on %errors records.', [
        '%count' => $results['updates'],
        '%errors' => $results['errors'],
      ]));
    }
    else {
      \Drupal::messenger()
        ->addStatus(t('Merge variables updated on %count records.', [
        '%count' => $results['updates'],
      ]));
    }
  }
  else {
    \Drupal::messenger()
      ->addError(t('Merge variable update failed.'));
  }
}

/**
 * Gets an array of default Mailchimp webhook event names.
 *
 * @return array
 *   Default webhook event names, indexed by the IDs used by the Mailchimp API.
 */
function mailchimp_lists_default_webhook_events() {
  return [
    'subscribe' => 'Subscribes',
    'unsubscribe' => 'Unsubscribes',
    'profile' => 'Profile Updates',
    'cleaned' => 'Cleaned Emails',
    'upemail' => 'Email Address Changes',
    'campaign' => 'Campaign Sending Status',
  ];
}

/**
 * Returns an array of enabled webhook events.
 *
 * @param string $list_id
 *   The Mailchimp list/audience ID to return webhook actions for.
 *
 * @return array
 *   An array of enabled webhook event names.
 */
function mailchimp_lists_enabled_webhook_events($list_id) {
  $enabled_events = [];
  $webhook_url = mailchimp_webhook_url();
  $webhooks = mailchimp_webhook_get($list_id);
  if ($webhooks) {
    foreach ($webhooks as $webhook) {
      if ($webhook_url == $webhook->url) {
        foreach ($webhook->events as $event => $enabled) {
          if ($enabled) {
            $enabled_events[] = $event;
          }
        }
      }
    }
  }
  return $enabled_events;
}

Functions

Namesort descending Description
mailchimp_lists_default_webhook_events Gets an array of default Mailchimp webhook event names.
mailchimp_lists_enabled_webhook_events Returns an array of enabled webhook events.
mailchimp_lists_entity_delete Implements hook_entity_delete().
mailchimp_lists_execute_mergevar_batch_update Batch processor for member mergevar updates to submit batches to Mailchimp.
mailchimp_lists_form_field_storage_config_edit_form_alter Implements hook_form_FORM_ID_alter().
mailchimp_lists_form_field_ui_field_edit_form_validate Validation handler for mailchimp_lists_form_field_ui_field_edit_form.
mailchimp_lists_get_subscribers Batch processor for pulling in subscriber information for a list/audience.
mailchimp_lists_load_email Helper function to check if a valid email is configured for an entity field.
mailchimp_lists_populate_member_batch Batch processor for member mergevar updates to built the mergevar arrays.
mailchimp_lists_populate_member_batch_complete Batch completion processor for member mergevar updates.
mailchimp_lists_process_subscribe_form_choices Processor for various list form submissions.
mailchimp_lists_update_member_merge_values Triggers an update of all merge field values for appropriate entities.
_mailchimp_lists_mergevars_populate Helper function to complete a mailchimp-api-ready mergevars array.
_mailchimp_lists_subscription_has_changed Helper function to avoid sending superfluous updates to Mailchimp.