You are here

entity_reference_multiple.module in Entity Reference Multiple 7.2

Same filename and directory in other branches
  1. 7 entity_reference_multiple.module

Primarily Drupal hooks.

This is the main module file for Entity Reference Multiple.

File

entity_reference_multiple.module
View source
<?php

/**
 * @file
 * Primarily Drupal hooks.
 *
 * This is the main module file for Entity Reference Multiple.
 */

/**
 * Defines the name for the selects widget.
 *
 * @var string
 */
define('ENTITY_REFERENCE_MULTIPLE_SELECTS_WIDGET_NAME', 'entity_reference_multiple_selects');

/**
 * Defines the name for the autocomplete widget.
 *
 * @var string
 */
define('ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_NAME', 'entity_reference_multiple_autocomplete');

/**
 * Defines the limit for autocomple of the autocomplete widget.
 *
 * This is just the default setting. It will be overwritten by widget settings.
 *
 * @var string
 */
define('ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_AUTOCOMPLETE_LIMIT', 15);

/**
 * Defines the query like type for autocomple of the autocomplete widget.
 *
 * It can either be 'CONTAINS' or 'STARTS_WITH'.
 * This is just the default setting. It will be overwritten by widget settings.
 *
 * @var string
 */
define('ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_AUTOCOMPLETE_QUERY_LIKE_TYPE', 'STARTS_WITH');

/**
 * Implements hook_menu().
 */
function entity_reference_multiple_menu() {
  return array(
    'admin/config/system/entity-reference-multiple' => array(
      'title' => 'Entity short codes',
      'description' => 'Manage short codes for entities.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'entity_reference_multiple_settings_form',
      ),
      'access arguments' => array(
        'administer site configuration',
      ),
      'file' => 'entity_reference_multiple.admin.inc',
    ),
    'entity_reference_multiple/autocomplete/%/%/%/%' => array(
      'page callback' => 'entity_reference_multiple_autocomplete_page_callback',
      'page arguments' => array(
        2,
        3,
        4,
        5,
      ),
      'access callback' => 'entity_reference_multiple_autocomplete_access_callback',
      'access arguments' => array(
        2,
        3,
        4,
      ),
      'file' => 'entity_reference_multiple.pages.inc',
      'type' => MENU_CALLBACK,
    ),
  );
}

/**
 * Implements hook_theme().
 */
function entity_reference_multiple_theme() {
  return array(
    'entity_reference_multiple_none' => array(
      'variables' => array(
        'instance' => NULL,
        'option' => NULL,
      ),
      'file' => 'theme/theme.inc',
    ),
    'entity_reference_multiple_short_codes_settings_table' => array(
      'render element' => 'element',
      'file' => 'theme/theme.inc',
    ),
  );
}

/**
 * Implements hook_flush_caches().
 */
function entity_reference_multiple_flush_caches() {

  // Because of the intricacies of the info hooks, we are forced to keep a
  // separate list of the base tables of each entities, so that we can use
  // it in entity_reference_multiple_field_schema() without calling
  // entity_get_info(). See http://drupal.org/node/1416558 for details.
  $base_tables = array();
  foreach (entity_get_info() as $entity_type => $entity_info) {
    if (!empty($entity_info['base table']) && !empty($entity_info['entity keys']['id'])) {
      $base_tables[$entity_type] = array(
        $entity_info['base table'],
        $entity_info['entity keys']['id'],
      );
    }
  }

  // We are using a variable because cache is going to be cleared right after
  // hook_flush_caches() is finished.
  variable_set('entity_reference_multiple:base-tables', $base_tables);
}

/**
 * Implements hook_field_info().
 */
function entity_reference_multiple_field_info() {
  return array(
    'entity_reference_multiple' => array(
      'label' => t('Entity Reference Multiple'),
      'description' => t('This field reference another entity.'),
      'settings' => array(
        'target_types' => array(
          'node' => 'node',
        ),
        'target_entities_bundles' => array(),
      ),
      'instance_settings' => array(),
      'default_widget' => ENTITY_REFERENCE_MULTIPLE_SELECTS_WIDGET_NAME,
      'default_formatter' => 'entity_reference_multiple_label',
      'property_type' => 'entity_reference_multiple',
      'property_callbacks' => array(
        'entity_reference_multiple_field_property_callback',
      ),
    ),
  );
}

/**
 * Implements hook_field_settings_form().
 */
function entity_reference_multiple_field_settings_form($field, $instance, $has_data) {

  // The field settings infrastructure is not AJAX enabled by default,
  // because it doesn't pass over the $form_state.
  // Build the whole form into a #process in which we actually have access
  // to the form state.
  $form = array(
    '#type' => 'container',
    '#process' => array(
      '_entity_reference_multiple_field_settings_process',
      '_entity_reference_multiple_field_settings_ajax_process',
    ),
    '#element_validate' => array(
      '_entity_reference_multiple_field_settings_validate',
    ),
    '#field' => $field,
    '#instance' => $instance,
    '#has_data' => $has_data,
  );
  return $form;
}

/**
 * Implements hook_field_validate().
 */
function entity_reference_multiple_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  $ids = array();
  foreach ($items as $delta => $item) {
    if (!entity_reference_multiple_field_is_empty($item, $field)) {
      $ids[$item['target_type']][$item['target_id']] = $delta;
    }
  }
  foreach ($ids as $target_type => $target_ids) {
    $valid_ids = _entity_reference_multiple_validate_referencable_entities($field, $target_type, array_keys($target_ids));
    if ($invalid_entities = array_diff_key($ids[$target_type], array_flip($valid_ids))) {
      foreach ($invalid_entities as $id => $delta) {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => 'entity_reference_multiple_invalid_entity',
          'message' => t('The referenced entity (@type: @id) is invalid.', array(
            '@type' => $target_type,
            '@id' => $id,
          )),
        );
      }
    }
  }
}

/**
 * Implements hook_field_is_empty().
 */
function entity_reference_multiple_field_is_empty($item, $field) {
  $empty_target_type = !isset($item['target_type']) || !entity_get_info($item['target_type']);
  $empty_target_id = !isset($item['target_id']) || !is_numeric($item['target_id']);
  return $empty_target_type || $empty_target_id;
}

/**
 * Performs validation on form elements.
 *
 * @param array $form
 *   Nested array of form elements that comprise the form.
 * @param array $form_state
 *   A keyed array containing the current state of the form.
 */
function _entity_reference_multiple_field_settings_validate($form, &$form_state) {

  // Store the new values in the form state.
  $field = $form['#field'];
  if (isset($form_state['values']['field'])) {
    $field['settings'] = $form_state['values']['field']['settings'];
  }
  $form_state['entity_reference_multiple']['field'] = $field;
}

/**
 * Add settings to a field settings form.
 *
 * @param array $form
 *   Nested array of form elements that comprise the form.
 * @param array $form_state
 *   A keyed array containing the current state of the form.
 *
 * @return array
 *   Nested array of form elements that comprise the form.
 */
function _entity_reference_multiple_field_settings_process($form, $form_state) {
  $field = isset($form_state['entity_reference_multiple']['field']) ? $form_state['entity_reference_multiple']['field'] : $form['#field'];
  $settings = $field['settings'];
  $has_data = $form['#has_data'];

  // Select the target entity type.
  $entity_type_options = array();
  foreach (entity_get_info() as $entity_type => $entity_info) {
    $entity_type_options[$entity_type] = $entity_info['label'];
  }
  $form['target_types'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Target types'),
    '#options' => $entity_type_options,
    '#default_value' => $settings['target_types'],
    '#required' => TRUE,
    '#description' => t('The entity types that can be referenced through this field.'),
    '#disabled' => $has_data,
    '#size' => 1,
    '#ajax' => TRUE,
    '#limit_validation_errors' => array(),
  );
  foreach ($field['settings']['target_types'] as $entity_type) {
    if ($entity_type && ($entity_info = entity_get_info($entity_type))) {
      $form['target_entities_bundles'][$entity_type] = array(
        '#type' => 'fieldset',
        '#title' => $entity_info['label'],
        '#tree' => TRUE,
        '#collapsible' => TRUE,
        '#collapsed' => FALSE,
        '#access' => FALSE,
      );
      $form['target_entities_bundles'][$entity_type]['target_bundles'] = array(
        '#type' => 'value',
        '#value' => array(),
      );
      if (!empty($entity_info['entity keys']['bundle'])) {
        $bundles = array();
        foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
          $bundles[$bundle_name] = $bundle_info['label'];
        }
        $form['target_entities_bundles'][$entity_type]['#access'] = TRUE;
        $form['target_entities_bundles'][$entity_type]['target_bundles'] = array(
          '#type' => 'checkboxes',
          '#title' => t('Target bundles'),
          '#options' => $bundles,
          '#default_value' => isset($settings['target_entities_bundles'][$entity_type]['target_bundles']) ? $settings['target_entities_bundles'][$entity_type]['target_bundles'] : array(),
          '#size' => 6,
          '#multiple' => TRUE,
          '#description' => t('The bundles of the entity type that can be referenced. Optional, leave empty for all bundles.'),
          '#element_validate' => array(
            '_entity_reference_multiple_element_validate_filter',
          ),
        );
      }
    }
  }
  return $form;
}

/**
 * Form processing handler for the #ajax form property.
 *
 * @param array $form
 *   Nested array of form elements that comprise the form.
 * @param array $form_state
 *   A keyed array containing the current state of the form.
 *
 * @return array
 *   Nested array of form elements that comprise the form.
 */
function _entity_reference_multiple_field_settings_ajax_process($form, $form_state) {
  _entity_reference_multiple_field_settings_ajax_process_element($form, $form);
  return $form;
}

/**
 * Form element processing handler for the #ajax form property.
 *
 * @param array $element
 *   An associative array containing the properties of the element.
 * @param array $main_form
 *   Nested array of form elements that comprise the form.
 */
function _entity_reference_multiple_field_settings_ajax_process_element(&$element, $main_form) {
  if (isset($element['#ajax']) && $element['#ajax'] === TRUE) {
    $element['#ajax'] = array(
      'callback' => 'entity_reference_multiple_settings_ajax',
      'wrapper' => $main_form['#id'],
      'element' => $main_form['#array_parents'],
    );
  }
  foreach (element_children($element) as $key) {
    _entity_reference_multiple_field_settings_ajax_process_element($element[$key], $main_form);
  }
}

/**
 * Ajax callback for the handler settings form.
 */
function entity_reference_multiple_settings_ajax($form, $form_state) {
  $trigger = $form_state['triggering_element'];
  return drupal_array_get_nested_value($form, $trigger['#ajax']['element']);
}

/**
 * Submit handler for the non-JS case.
 */
function entity_reference_multiple_settings_ajax_submit($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
}

/**
 * Validate that entities can be referenced by this field.
 *
 * @param array $field
 *   The field definition.
 * @param string $target_type
 *   The type of the entity.
 * @param array $target_ids
 *   The ids of the entities.
 *
 * @return array
 *   An array of entity ids that are valid.
 */
function _entity_reference_multiple_validate_referencable_entities($field, $target_type, $target_ids) {
  $query = _entity_reference_multiple_build_query($field, $target_type);
  $query
    ->entityCondition('entity_id', $target_ids, 'IN');
  $result = $query
    ->execute();
  if (!empty($result[$target_type])) {
    return array_keys($result[$target_type]);
  }
  return array();
}

/**
 * Build an EntityFieldQuery to get referencable entities.
 *
 * @param array $field
 *   The field definition.
 * @param string $target_type
 *   The type of the entity.
 *
 * @return EntityFieldQuery
 *   The built query.
 */
function _entity_reference_multiple_build_query($field, $target_type) {
  $query = new EntityFieldQuery();
  $query
    ->entityCondition('entity_type', $target_type);
  if (!empty($field['settings']['target_entities_bundles'][$target_type]) && ($bundles = _entity_reference_multiple_bundles_prepare($field['settings']['target_entities_bundles'][$target_type]['target_bundles']))) {
    $query
      ->entityCondition('bundle', $bundles, 'IN');
  }

  // Add a generic entity access tag to the query.
  $query
    ->addTag($target_type . '_access');
  $query
    ->addMetaData('field', $field);

  // Add the sort option.
  $entity_info = entity_get_info($target_type);
  if (!empty($entity_info) && !empty($entity_info['entity keys']['label'])) {
    $query
      ->propertyOrderBy($entity_info['entity keys']['label']);
  }
  return $query;
}

/**
 * Property callback for the Entity Metadata framework.
 */
function entity_reference_multiple_field_property_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $property =& $info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';
  unset($property['query callback']);
  $property['property info']['entity'] = array(
    'type' => 'entity',
    'label' => t('The entity.'),
    'getter callback' => 'entity_reference_multiple_metadata_field_property_entity_get',
    'translatable' => !empty($field['translatable']),
    // Specify that this property stems from a field.
    'field' => TRUE,
    'required' => !empty($instance['required']),
  );
}

/**
 * Callback for getting referencable entity.
 */
function entity_reference_multiple_metadata_field_property_entity_get($item) {
  if ($entity = entity_load($item['target_type'], array(
    $item['target_id'],
  ))) {
    return entity_metadata_wrapper($item['target_type'], reset($entity));
  }
  return NULL;
}

/**
 * Callback for validate entity's bandles.
 */
function _entity_reference_multiple_element_validate_filter(&$element, &$form_state) {
  $element['#value'] = array_filter($element['#value']);
  form_set_value($element, $element['#value'], $form_state);
}

/**
 * Implements hook_field_widget_info().
 */
function entity_reference_multiple_field_widget_info() {
  return array(
    ENTITY_REFERENCE_MULTIPLE_SELECTS_WIDGET_NAME => array(
      'label' => t('Entity Reference Multiple Selects'),
      'field types' => array(
        'entity_reference_multiple',
      ),
    ),
    ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_NAME => array(
      'label' => t('Entity Reference Multiple Autocomplete'),
      'field types' => array(
        'entity_reference_multiple',
      ),
      'settings' => array(
        'autocomplete_path' => '',
        'autocomplete_limit' => ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_AUTOCOMPLETE_LIMIT,
        'autocomplete_query_like_type' => ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_AUTOCOMPLETE_QUERY_LIKE_TYPE,
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_settings_form().
 */
function entity_reference_multiple_field_widget_settings_form($field, $instance) {
  $form = array();
  if ($instance['widget']['type'] === ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_NAME) {
    $form['autocomplete_limit'] = array(
      '#type' => 'textfield',
      '#title' => t('Limit of results to show in autocomplete.'),
      '#default_value' => $instance['widget']['settings']['autocomplete_limit'],
      '#element_validate' => array(
        'element_validate_integer_positive',
      ),
    );
    $form['autocomplete_query_like_type'] = array(
      '#type' => 'select',
      '#title' => t('Autocomplete matching'),
      '#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of nodes.'),
      '#options' => array(
        'STARTS_WITH' => t('Starts with'),
        'CONTAINS' => t('Contains'),
      ),
      '#default_value' => $instance['widget']['settings']['autocomplete_query_like_type'],
    );
  }
  return $form;
}

/**
 * Implements hook_field_widget_form().
 */
function entity_reference_multiple_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  if ($instance['widget']['type'] == ENTITY_REFERENCE_MULTIPLE_SELECTS_WIDGET_NAME || $instance['widget']['type'] == ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_NAME) {
    $element['#attached']['css'][] = drupal_get_path('module', 'entity_reference_multiple') . '/css/entity-reference-multiple.css';
    $wrapper_id_array = array_merge($element['#field_parents'], array(
      $element['#field_name'],
      $element['#language'],
      $delta,
    ));
    $wrapper_id = str_replace('_', '-', implode('-', $wrapper_id_array));
    $element[$element['#field_name']] = array(
      '#type' => 'container',
      '#attributes' => array(
        'id' => $wrapper_id,
        'class' => array(
          'entity-reference-multiple-inline',
        ),
      ),
    );

    // Prepare the list of options.
    $column = 'target_type';
    $options = _entity_reference_multiple_get_options($field, $instance, $items, $element, $column);
    $target_type_value = isset($items[$delta][$column]) ? $items[$delta][$column] : NULL;
    $parents = array_merge($element['#field_parents'], array(
      $element['#field_name'],
      $element['#language'],
      $delta,
      $column,
    ));
    if (!empty($form_state['input'])) {
      $target_type_value = drupal_array_get_nested_value($form_state['input'], $parents);
    }
    $element[$element['#field_name']][$column] = array(
      '#type' => 'select',
      '#parents' => $parents,
      '#title' => t('Entity type'),
      '#options' => $options,
      '#default_value' => $target_type_value,
      '#required' => $element['#required'],
      '#value_key' => $column,
      '#ajax' => array(
        'wrapper' => $wrapper_id,
        'callback' => '_entity_reference_multiple_field_widget_ajax_process',
      ),
    );

    // Build target id field.
    $column = 'target_id';
    $target_id_value = isset($items[$delta][$column]) ? $items[$delta][$column] : NULL;
    $parents = array_merge($element['#field_parents'], array(
      $element['#field_name'],
      $element['#language'],
      $delta,
      $column,
    ));
    if (isset($form_state['triggering_element']['#value_key']) && $form_state['triggering_element']['#value_key'] == 'target_type') {
      $target_id_value = NULL;
      drupal_array_set_nested_value($form_state['input'], $parents, $target_id_value);
    }
    switch ($instance['widget']['type']) {
      case ENTITY_REFERENCE_MULTIPLE_SELECTS_WIDGET_NAME:

        // Prepare the list of options.
        $options = _entity_reference_multiple_get_options($field, $instance, $items, $element, $column, $target_type_value);
        $element[$element['#field_name']][$column] = array(
          '#type' => 'select',
          '#parents' => $parents,
          '#title' => t('Entity'),
          '#options' => $options,
          '#default_value' => $target_id_value,
          '#required' => !empty($target_type_value) && $target_type_value != '_none' || $element['#required'],
          '#value_key' => $column,
        );
        break;
      case ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_NAME:
        $entity_info = entity_get_info();

        // Build an array of entities ID.
        $entities = array();
        foreach ($items as $item) {
          if (isset($entity_info[$item['target_type']])) {
            $entities[$item['target_type']][$item['target_id']] = $item['target_id'];
          }
        }

        // Load those entities and loop through them to extract their labels.
        foreach ($entities as $entity_type => &$entity_ids) {
          $entity_ids = entity_load($entity_type, $entity_ids);
          foreach ($entity_ids as &$entity) {
            $entity = _entity_reference_multiple_prepare_label_for_autocomplete($entity_type, $entity);
          }
        }
        if (!empty($target_type_value) && isset($items[$delta][$column]) && !empty($entities[$target_type_value]) && !empty($entities[$target_type_value][$items[$delta][$column]])) {
          $target_id_value = $entities[$target_type_value][$items[$delta][$column]];
        }
        $element[$element['#field_name']][$column] = array(
          '#type' => 'textfield',
          '#parents' => $parents,
          '#title' => t('Entity'),
          '#default_value' => $target_id_value,
          '#description' => _entity_reference_multiple_autocomplete_widget_description($field, $target_type_value),
          '#required' => !empty($target_type_value) && $target_type_value != '_none' || $element['#required'],
          '#value_key' => $column,
        );
        if (!empty($target_type_value) && $target_type_value != '_none') {

          // Build autocomplete path as array for better understanding.
          $autocomplete_path = array(
            'entity_reference_multiple/autocomplete',
            $field['field_name'],
            $instance['entity_type'],
            $instance['bundle'],
            $target_type_value,
          );
          $element[$element['#field_name']][$column]['#autocomplete_path'] = implode('/', $autocomplete_path);
        }
        $element['#element_validate'][] = '_entity_reference_multiple_autocomplete_validate';
        break;
    }
  }
  return $element;
}

/**
 * Creates a description for the widget form for the short code usage.
 *
 * @param array $field
 *   The field definition.
 * @param string $entity_type
 *   The entity type to explain short codes for.
 *
 * @return string
 *   The description for a form field.
 */
function _entity_reference_multiple_autocomplete_widget_description($field, $entity_type) {
  $description = '';
  if ($entity_type && ($entity_info = entity_get_info($entity_type))) {
    $short_codes = variable_get('entity_reference_multiple:short-codes', array());

    // Prepare short codes array.
    $bundles = array_keys($entity_info['bundles']);
    $short_codes = !empty($short_codes[$entity_type]) ? $short_codes[$entity_type] : array();
    if (!empty($bundles)) {
      if (empty($short_codes)) {
        $short_codes = drupal_map_assoc($bundles);
      }
      else {
        $short_codes = array_intersect_key($short_codes, array_flip($bundles));
        foreach ($bundles as $bundle) {
          if (empty($short_codes[$bundle])) {
            $short_codes[$bundle] = $bundle;
          }
        }
      }

      // Remove short codes for unavailable bundles.
      if (!empty($field['settings']['target_entities_bundles'][$entity_type]) && ($bundles = _entity_reference_multiple_bundles_prepare($field['settings']['target_entities_bundles'][$entity_type]['target_bundles']))) {
        $short_codes = array_intersect_key($short_codes, $bundles);
      }
    }
    $short_code_items = array();
    foreach ($short_codes as $bundle => $code) {
      $short_code_items[] = $entity_info['bundles'][$bundle]['label'] . ': ' . $code;
    }
    $description = t('To make it easier to use this autocomplete with a lot of entities and bundles, there are certain short codes available for this entity type. <br />
      Use them like this: <em>i:search term</em>
      "i" is here the short code from the list below and "search term" is the part of the title of an entity or the beginning of an entity id. <br />
      The following short codes are available: <br /> !items', array(
      '!items' => theme('item_list', array(
        'items' => $short_code_items,
      )),
    ));
  }
  return $description;
}

/**
 * Validation callback for Entity Reference Multiple widget settings form.
 */
function _entity_reference_multiple_autocomplete_validate($element, &$form_state, $form) {
  $value = '';
  if (!empty($element[$element['#field_name']]['target_id']['#value'])) {
    $target_value = $element[$element['#field_name']]['target_id']['#value'];
    if (is_numeric($target_value)) {
      $value = $target_value;
    }
    elseif (preg_match("/.+\\((\\d+)\\)/", $target_value, $matches)) {
      $value = $matches[1];
    }
    else {
      if (!empty($element[$element['#field_name']]['target_type']['#value'])) {
        $target_type = $element[$element['#field_name']]['target_type']['#value'];
        if ($entity_info = entity_get_info($target_type)) {
          $entity_keys = $entity_info['entity keys'];
          $field = field_info_field($element['#field_name']);
          $query = _entity_reference_multiple_build_query($field, $target_type);
          $query
            ->propertyCondition($entity_keys['label'], $target_value);
          $query
            ->range(0, 6);
          $results = $query
            ->execute();
          $entities = array();
          if (!empty($results[$target_type])) {
            $entities = entity_load($target_type, array_keys($results[$target_type]));
          }
          if (empty($entities)) {

            // Error if there are no entities available for a required field.
            form_error($element[$element['#field_name']]['target_id'], t('There are no entities matching "%value"', array(
              '%value' => $target_value,
            )));
          }
          elseif (count($entities) > 5) {

            // Error if there are more than 5 matching entities.
            form_error($element[$element['#field_name']]['target_id'], t('Many entities are called %value. Specify the one you want by appending the id in parentheses, like "@value"', array(
              '%value' => $target_value,
              '@value' => _entity_reference_multiple_prepare_label_for_autocomplete($target_type, current($entities)),
            )));
          }
          elseif (count($entities) > 1) {

            // More helpful error if there are only a few matching entities.
            $multiples = array();
            foreach ($entities as $entity) {
              $multiples[] = _entity_reference_multiple_prepare_label_for_autocomplete($target_type, $entity);
            }
            form_error($element[$element['#field_name']]['target_id'], t('Multiple entities match this reference; "%multiple"', array(
              '%multiple' => implode('", "', $multiples),
            )));
          }
          else {

            // Take the one and only matching entity.
            $value = _entity_reference_multiple_prepare_label_for_autocomplete($target_type, current($entities));
          }
        }
      }
    }
  }

  // Update the value of this element so the field can validate
  // the IDs.
  form_set_value($element[$element['#field_name']]['target_id'], $value, $form_state);
}

/**
 * Implements hook_field_widget_error().
 */
function entity_reference_multiple_field_widget_error($element, $error, $form, &$form_state) {
  form_error($element, $error['message']);
}

/**
 * Menu Access callback for the autocomplete widget.
 *
 * @param string $field_name
 *   The name of the field.
 * @param string $entity_type
 *   The entity type.
 * @param string $bundle_name
 *   The bundle name.
 *
 * @return bool
 *   TRUE if user can access this menu item.
 */
function entity_reference_multiple_autocomplete_access_callback($field_name, $entity_type, $bundle_name) {
  $field = field_info_field($field_name);
  $instance = field_info_instance($entity_type, $field_name, $bundle_name);
  if (!$field || !$instance || $field['type'] != 'entity_reference_multiple' || !field_access('edit', $field, $entity_type)) {
    return FALSE;
  }
  return TRUE;
}

/**
 * Form processing handler for the #ajax form property.
 *
 * @param array $form
 *   Nested array of form elements that comprise the form.
 * @param array $form_state
 *   A keyed array containing the current state of the form.
 *
 * @return array
 *   The new form element.
 */
function _entity_reference_multiple_field_widget_ajax_process($form, $form_state) {
  $ref =& $form;
  $parents = $form_state['triggering_element']['#array_parents'];
  array_pop($parents);
  foreach ($parents as $parent) {
    if (is_array($ref) && array_key_exists($parent, $ref)) {
      $ref =& $ref[$parent];
    }
  }
  return $ref;
}

/**
 * Collects the options for a field.
 */
function _entity_reference_multiple_get_options($field, $instance, $items, $element, $column, $target_type = NULL) {
  $has_value = isset($items[0][$column]);
  $properties = _options_properties('select', TRUE, $element['#required'], $has_value);
  $options = array();
  if ($column == 'target_type') {

    // Get the list of options for target_type column.
    $options = _entity_reference_multiple_target_type_allowed_values($field);
  }
  elseif ($column == 'target_id') {

    // Get the list of options for target_id column.
    $options = _entity_reference_multiple_target_id_allowed_values($field, $target_type);
  }

  // Sanitize the options.
  _options_prepare_options($options, $properties);
  if (!$properties['optgroups']) {
    $options = options_array_flatten($options);
  }
  if ($properties['empty_option'] && $column == 'target_type' || $properties['empty_option'] && (empty($target_type) || $target_type == '_none')) {
    $label = theme('entity_reference_multiple_none', array(
      'instance' => $instance,
      'option' => $properties['empty_option'],
    ));
    $options = array(
      '_none' => $label,
    ) + $options;
  }
  return $options;
}

/**
 * Returns the set of valid entity types for a entity_reference_multiple field.
 *
 * @param array $field
 *   The field definition.
 *
 * @return array
 *   The array of valid entity types for this field, keyed by entity type.
 */
function _entity_reference_multiple_target_type_allowed_values($field) {
  $entity_info = entity_get_info();
  $options = array();
  foreach (array_keys($field['settings']['target_entities_bundles']) as $target_type) {
    if (!empty($entity_info[$target_type])) {
      $options[$target_type] = $entity_info[$target_type]['label'];
    }
  }
  return $options;
}

/**
 * Returns the set of valid entities for a entity_reference_multiple field.
 *
 * @param array $field
 *   The field definition.
 * @param string $target_type
 *   The type of the entity.
 *
 * @return array
 *   The array of valid entities for this field, keyed by entity id and
 *   grouped by bundles.
 */
function _entity_reference_multiple_target_id_allowed_values($field, $target_type) {
  $entity_info = entity_get_info();
  $options = array();
  if (in_array($target_type, $field['settings']['target_types'], TRUE)) {
    $query = _entity_reference_multiple_build_query($field, $target_type);
    $results = $query
      ->execute();
    if (!empty($results[$target_type])) {
      $entities = entity_load($target_type, array_keys($results[$target_type]));
      foreach ($entities as $entity_id => $entity) {
        list(, , $bundle) = entity_extract_ids($target_type, $entity);
        $group = $entity_info[$target_type]['bundles'][$bundle]['label'];
        $options[$group][$entity_id] = check_plain(entity_label($target_type, $entity));
      }
    }
  }
  return $options;
}

/**
 * Collects entity ids for entities matching the given search string.
 *
 * @param array $field
 *   A field configuration.
 * @param array $instance
 *   A field instance configuration.
 * @param string $target_type
 *   The entity type to search for.
 * @param string $search_string
 *   The string to search for.
 *
 * @return array
 *   Entity ids of entities matching the search string including additional
 *   data as returned by EntityFieldQuery::execute().
 *
 * @see EntityFieldQuery::execute()
 */
function _entity_reference_multiple_entities_matching_search($field, $instance, $target_type, $search_string) {
  $result = array();
  $entity_query = _entity_reference_multiple_build_query($field, $target_type);

  // Get entity info.
  $entity_info = entity_get_info($target_type);
  $entity_keys = $entity_info['entity keys'];

  // Get settings.
  $limit = $instance['widget']['settings']['autocomplete_limit'];
  $query_like_type = $instance['widget']['settings']['autocomplete_query_like_type'];

  // Check if the search string is completely an integer, because then we would
  // rather search for an entity id than for a title of an entity.
  $filter_var_options = array(
    'options' => array(
      'min_range' => 1,
    ),
  );
  if (strpos($search_string, ':') !== FALSE) {
    $search_string_array = explode(':', $search_string, 2);
    $search_string = $search_string_array[1];
    $bundle = $search_string_array[0];
    $short_codes = variable_get('entity_reference_multiple:short-codes', FALSE);
    if ($short_codes && !empty($short_codes[$target_type]) && ($value = array_search($bundle, $short_codes[$target_type]))) {
      $bundle = $value;
    }
    if (!empty($field['settings']['target_entities_bundles'][$target_type])) {
      $bundles = $field['settings']['target_entities_bundles'][$target_type]['target_bundles'];
      if (empty($bundles) || array_search($bundle, $bundles)) {
        $entity_query
          ->entityCondition('bundle', $bundle);
      }
    }
  }
  if (!empty($entity_keys['id']) && ($int_search_string = filter_var($search_string, FILTER_VALIDATE_INT, $filter_var_options))) {
    $entity_id_query = clone $entity_query;

    // Make sure we only get entities with ids starting with the search string.
    $entity_id_query
      ->propertyCondition($entity_keys['id'], $int_search_string, 'STARTS_WITH');

    // Set limit.
    $entity_id_query
      ->range(0, $limit);

    // Execute query.
    $id_results = $entity_id_query
      ->execute();

    // Add results to $result.
    if (!empty($id_results) && !empty($id_results[$target_type])) {
      $result = $id_results[$target_type];
    }
  }

  // If we have not enough results already (id query), we are filling up the
  // result array with additional results matching the title of the entity.
  if (count($result) < $limit) {

    // Add title condition.
    $entity_query
      ->propertyCondition($entity_keys['label'], $search_string, $query_like_type);

    // Make sure we do not get the same results as in the id query.
    if (!empty($result)) {

      // Get excluded ids.
      $excluded_ids = array_keys($result);

      // Add id exclude condition.
      $entity_query
        ->propertyCondition($entity_keys['id'], $excluded_ids, 'NOT IN');
    }

    // Calculate remaining results.
    $left_over_limit = $limit - count($result);

    // Set limit.
    $entity_query
      ->range(0, $left_over_limit);

    // Execute query.
    $title_results = $entity_query
      ->execute();

    // Merge with result.
    if (!empty($title_results) && !empty($title_results[$target_type])) {
      $result += $title_results[$target_type];
    }
  }
  return $result;
}

/**
 * Prepares the given entity ids for autocomplete output.
 *
 * We only need this extra function to properly get a bundle for taxonomy terms.
 * All other entities should properly return a "bundle" after being queried via
 * EntityFieldQuery. If not those are VERY special and either not core or
 * ECK related.
 * If we would not need the bundle to be display in the autocomplete, it would
 * be perfectly fine, to just alter the select query triggered by
 * EntityFieldQuery according to a specific tag or meta data like for example
 * 'entity_reference_multiple_autocomplete_configuration' and add the title for
 * the entity there via entity_get_info() and the 'label' in 'entity keys'.
 *
 * @param array $entities
 *   The entity ids to prepare.
 * @param string $target_type
 *   The entity type to search for.
 *
 * @return array
 *   Search result.
 */
function _entity_reference_multiple_prepare_entities_for_autocomplete($entities, $target_type) {
  $matches = array();
  if (count($entities) > 0) {

    // Get entity info.
    $entity_info = entity_get_info($target_type);

    // Get label key.
    $label_key = NULL;
    if (!empty($entity_info['entity keys']) && !empty($entity_info['entity keys']['label'])) {
      $label_key = $entity_info['entity keys']['label'];
    }

    // If the first entity has a bundle, we do not need a bundle key, because
    // we assume all others have a bundle, too.
    $first_entity = current($entities);
    $bundle_key_needed = empty($first_entity->bundle);

    // Get bundle key, if needed.
    if ($bundle_key_needed) {
      $bundle_key = NULL;
      if (!empty($entity_info['entity keys']) && !empty($entity_info['entity keys']['bundle'])) {
        $bundle_key = $entity_info['entity keys']['bundle'];
      }
    }

    // We need a label key and maybe a bundle key to form the entries for the
    // autocomplete. So, do not proceed without it.
    if ($label_key && (!$bundle_key_needed || $bundle_key_needed && !empty($bundle_key))) {

      // Load entities.
      $loaded_entities = entity_load($target_type, array_keys($entities));
      foreach ($loaded_entities as $entity) {
        $label = _entity_reference_multiple_prepare_label_for_autocomplete($target_type, $entity, $entity_info);

        // Strip things like starting/trailing white spaces, line breaks
        // and tags.
        $key = preg_replace('/\\s\\s+/', ' ', str_replace("\n", '', trim(decode_entities(strip_tags($label)))));

        // Names containing commas or quotes must be wrapped in quotes.
        if (strpos($key, ',') !== FALSE || strpos($key, '"') !== FALSE) {
          $key = '"' . str_replace('"', '""', $key) . '"';
        }

        // Add to result.
        $matches[$key] = $label;
      }
    }
  }
  return $matches;
}

/**
 * Prepares the label for entity for autocomplete output.
 *
 * @param string $entity_type
 *   The searchable entity type.
 * @param object $entity
 *   The searchable entity.
 * @param null $entity_info
 *   The searchable entity info.
 *
 * @return string
 *   Entity label in format $label ($bundle) ($id).
 */
function _entity_reference_multiple_prepare_label_for_autocomplete($entity_type, $entity, $entity_info = NULL) {
  if (!$entity_info) {
    $entity_info = entity_get_info($entity_type);
  }
  list($entity_id, , $bundle) = entity_extract_ids($entity_type, $entity);

  // Form array for the entry label. We are going to implode it later.
  $entry_label = array(
    check_plain(entity_label($entity_type, $entity)),
    '(' . $entity_info['bundles'][$bundle]['label'] . ')',
    '(' . $entity_id . ')',
  );
  return implode(' ', $entry_label);
}

/**
 * Get selected bundles.
 *
 * @param array $bundles
 *   An array contains all bundles.
 *
 * @return array
 *   An array contains selected bundles.
 */
function _entity_reference_multiple_bundles_prepare($bundles) {
  foreach ($bundles as $key => $bundle) {
    if (!$bundle) {
      unset($bundles[$key]);
    }
  }
  return $bundles;
}

/**
 * Implements hook_field_formatter_info().
 */
function entity_reference_multiple_field_formatter_info() {
  return array(
    'entity_reference_multiple_label' => array(
      'label' => t('Label'),
      'description' => t('Display the label of the referenced entities.'),
      'field types' => array(
        'entity_reference_multiple',
      ),
      'settings' => array(
        'link' => FALSE,
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function entity_reference_multiple_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();
  if ($display['type'] == 'entity_reference_multiple_label') {
    $element['link'] = array(
      '#title' => t('Link label to the referenced entity'),
      '#type' => 'checkbox',
      '#default_value' => $settings['link'],
    );
  }
  return $element;
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function entity_reference_multiple_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $summary = array();
  if ($display['type'] == 'entity_reference_multiple_label') {
    $summary[] = $settings['link'] ? t('Link to the referenced entity') : t('No link');
  }
  return implode('<br />', $summary);
}

/**
 * Implements hook_field_formatter_prepare_view().
 */
function entity_reference_multiple_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  $targets = array();

  // Collect every possible entity attached to any of the entities.
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => $item) {
      if (isset($item['target_id']) && isset($item['target_type'])) {
        $targets[$item['target_type']][] = $item['target_id'];
      }
    }
  }
  $target_entities = array();
  foreach ($targets as $target_type => $target_ids) {
    $target_entities[$target_type] = entity_load($target_type, $target_ids);
  }

  // Iterate through the fieldable entities again to attach the loaded data.
  foreach ($entities as $id => $entity) {
    $rekey = FALSE;
    foreach ($items[$id] as $delta => $item) {

      // Check whether the referenced entity could be loaded.
      if (isset($target_entities[$item['target_type']][$item['target_id']])) {

        // Replace the instance value with the term data.
        $items[$id][$delta]['entity'] = $target_entities[$item['target_type']][$item['target_id']];

        // Check whether the user has access to the referenced entity.
        $has_view_access = entity_access('view', $item['target_type'], $target_entities[$item['target_type']][$item['target_id']]);
        $has_update_access = entity_access('update', $item['target_type'], $target_entities[$item['target_type']][$item['target_id']]);
        $items[$id][$delta]['access'] = $has_view_access || $has_update_access;
      }
      else {
        unset($items[$id][$delta]);
        $rekey = TRUE;
      }
    }
    if ($rekey) {

      // Rekey the items array.
      $items[$id] = array_values($items[$id]);
    }
  }
}

/**
 * Implements hook_field_formatter_view().
 */
function entity_reference_multiple_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $result = array();

  // Rebuild the items list to contain only those with access.
  foreach ($items as $key => $item) {
    if (empty($item['access'])) {
      unset($items[$key]);
    }
  }
  switch ($display['type']) {
    case 'entity_reference_multiple_label':
      foreach ($items as $delta => $item) {
        $label = entity_label($item['target_type'], $item['entity']);

        // If the link is to be displayed and the entity has a uri,
        // display a link. Note the assignment ($url = ) here is intended to
        // be an assignment.
        if ($display['settings']['link'] && ($uri = entity_uri($item['target_type'], $item['entity']))) {
          $result[$delta] = array(
            '#markup' => l($label, $uri['path'], $uri['options']),
          );
        }
        else {
          $result[$delta] = array(
            '#markup' => check_plain($label),
          );
        }
      }
      break;
  }
  return $result;
}

Functions

Namesort descending Description
entity_reference_multiple_autocomplete_access_callback Menu Access callback for the autocomplete widget.
entity_reference_multiple_field_formatter_info Implements hook_field_formatter_info().
entity_reference_multiple_field_formatter_prepare_view Implements hook_field_formatter_prepare_view().
entity_reference_multiple_field_formatter_settings_form Implements hook_field_formatter_settings_form().
entity_reference_multiple_field_formatter_settings_summary Implements hook_field_formatter_settings_summary().
entity_reference_multiple_field_formatter_view Implements hook_field_formatter_view().
entity_reference_multiple_field_info Implements hook_field_info().
entity_reference_multiple_field_is_empty Implements hook_field_is_empty().
entity_reference_multiple_field_property_callback Property callback for the Entity Metadata framework.
entity_reference_multiple_field_settings_form Implements hook_field_settings_form().
entity_reference_multiple_field_validate Implements hook_field_validate().
entity_reference_multiple_field_widget_error Implements hook_field_widget_error().
entity_reference_multiple_field_widget_form Implements hook_field_widget_form().
entity_reference_multiple_field_widget_info Implements hook_field_widget_info().
entity_reference_multiple_field_widget_settings_form Implements hook_field_widget_settings_form().
entity_reference_multiple_flush_caches Implements hook_flush_caches().
entity_reference_multiple_menu Implements hook_menu().
entity_reference_multiple_metadata_field_property_entity_get Callback for getting referencable entity.
entity_reference_multiple_settings_ajax Ajax callback for the handler settings form.
entity_reference_multiple_settings_ajax_submit Submit handler for the non-JS case.
entity_reference_multiple_theme Implements hook_theme().
_entity_reference_multiple_autocomplete_validate Validation callback for Entity Reference Multiple widget settings form.
_entity_reference_multiple_autocomplete_widget_description Creates a description for the widget form for the short code usage.
_entity_reference_multiple_build_query Build an EntityFieldQuery to get referencable entities.
_entity_reference_multiple_bundles_prepare Get selected bundles.
_entity_reference_multiple_element_validate_filter Callback for validate entity's bandles.
_entity_reference_multiple_entities_matching_search Collects entity ids for entities matching the given search string.
_entity_reference_multiple_field_settings_ajax_process Form processing handler for the #ajax form property.
_entity_reference_multiple_field_settings_ajax_process_element Form element processing handler for the #ajax form property.
_entity_reference_multiple_field_settings_process Add settings to a field settings form.
_entity_reference_multiple_field_settings_validate Performs validation on form elements.
_entity_reference_multiple_field_widget_ajax_process Form processing handler for the #ajax form property.
_entity_reference_multiple_get_options Collects the options for a field.
_entity_reference_multiple_prepare_entities_for_autocomplete Prepares the given entity ids for autocomplete output.
_entity_reference_multiple_prepare_label_for_autocomplete Prepares the label for entity for autocomplete output.
_entity_reference_multiple_target_id_allowed_values Returns the set of valid entities for a entity_reference_multiple field.
_entity_reference_multiple_target_type_allowed_values Returns the set of valid entity types for a entity_reference_multiple field.
_entity_reference_multiple_validate_referencable_entities Validate that entities can be referenced by this field.

Constants

Namesort descending Description
ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_AUTOCOMPLETE_LIMIT Defines the limit for autocomple of the autocomplete widget.
ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_AUTOCOMPLETE_QUERY_LIKE_TYPE Defines the query like type for autocomple of the autocomplete widget.
ENTITY_REFERENCE_MULTIPLE_AUTOCOMPLETE_WIDGET_NAME Defines the name for the autocomplete widget.
ENTITY_REFERENCE_MULTIPLE_SELECTS_WIDGET_NAME Defines the name for the selects widget.