You are here

entityreference_autocreate.module in Entityreference Autocreate 7

Intercepts entityreference autocomplete submission validation and creates a target node on the fly if it doesn't yet exist.

Config: Edit your entityreference field settings, and choose 'Autocreate if not found'.

File

entityreference_autocreate.module
View source
<?php

/**
 * @file
 * Intercepts entityreference autocomplete submission validation and
 * creates a target node on the fly if it doesn't yet exist.
 *
 * Config:
 * Edit your entityreference field settings, and choose
 * 'Autocreate if not found'.
 */

/**
 * Implements hook_widget_info_alter().
 *
 * Adds settings that we need to declare to widgets we are extending.
 * Need this so that the settings we add to the widget settings form get saved.
 */
function entityreference_autocreate_field_widget_info_alter(array &$info) {
  $info['entityreference_autocomplete']['settings']['entityreference_autocreate'] = 0;
  $info['entityreference_autocomplete_tags']['settings']['entityreference_autocreate'] = 0;
}

/**
 * Adds our extra option to the field widget settings form.
 *
 * eg at
 * /admin/structure/types/manage/{CONTENT_TYPE}/fields/{ENTITYREFERENCE_FIELD}
 *
 * Implements hook_form_FORMID_alter().
 */
function entityreference_autocreate_form_field_ui_field_edit_form_alter(&$form, $form_state) {
  $supported_widgets = array(
    'entityreference_autocomplete',
    'entityreference_autocomplete_tags',
  );
  if (in_array($form['instance']['widget']['type']['#value'], $supported_widgets)) {

    // $field = $form['#field'];
    $instance = field_info_instance($form['instance']['entity_type']['#value'], $form['instance']['field_name']['#value'], $form['instance']['bundle']['#value']);
    $widget = $instance['widget'];
    $defaults = field_info_widget_settings($widget['type']);
    $settings = array_merge($defaults, $widget['settings']);
    $form['instance']['widget']['settings']['entityreference_autocreate'] = array(
      '#type' => 'fieldset',
      '#title' => t('EntityReference Autocreate settings'),
      '#collapsible' => TRUE,
    );
    $form['instance']['widget']['settings']['entityreference_autocreate']['active'] = array(
      '#type' => 'checkbox',
      '#title' => t('Autocreate target if not found'),
      '#description' => t('This will replace the normal validation that checks to see if a target item exists with code that just makes up whatever it was you asked for.'),
      '#default_value' => isset($settings['entityreference_autocreate']['active']) ? $settings['entityreference_autocreate']['active'] : FALSE,
    );

    // Use form API UI field visibility toggles to conditionally show the rest.
    // The following options only apply to 'node' entities AFAIK.
    // So hide them when inappropriate.
    $form['instance']['widget']['settings']['entityreference_autocreate']['author_current_user'] = array(
      '#type' => 'checkbox',
      '#title' => t('Author current user'),
      '#description' => t('This will set the current user as author of the new entities. Uncheck to get more options.'),
      '#default_value' => isset($settings['entityreference_autocreate']['author_current_user']) ? $settings['entityreference_autocreate']['author_current_user'] : TRUE,
      '#states' => array(
        'visible' => array(
          '#edit-instance-widget-settings-entityreference-autocreate-active' => array(
            'checked' => TRUE,
          ),
          '#edit-field-settings-target-type' => array(
            'value' => 'node',
          ),
        ),
      ),
    );
    $form['instance']['widget']['settings']['entityreference_autocreate']['author'] = array(
      '#type' => 'textfield',
      '#title' => t('Authored by'),
      '#maxlength' => 60,
      '#autocomplete_path' => 'user/autocomplete',
      '#default_value' => isset($settings['entityreference_autocreate']['author']) ? $settings['entityreference_autocreate']['author'] : '',
      '#description' => t('This will set the chosen user as author of the new entities. Leave blank for %anonymous.', array(
        '%anonymous' => variable_get('anonymous', t('Anonymous')),
      )),
      '#states' => array(
        'visible' => array(
          '#edit-instance-widget-settings-entityreference-autocreate-author-current-user' => array(
            'checked' => FALSE,
          ),
          '#edit-field-settings-target-type' => array(
            'value' => 'node',
          ),
        ),
      ),
    );
    $form['instance']['widget']['settings']['entityreference_autocreate']['status'] = array(
      '#type' => 'select',
      '#title' => t('Published status'),
      '#options' => array(
        -1 => 'Bundle default',
        1 => 'Published',
        0 => 'Unpublished',
      ),
      '#default_value' => isset($settings['entityreference_autocreate']['status']) ? $settings['entityreference_autocreate']['status'] : -1,
      '#states' => array(
        'visible' => array(
          '#edit-instance-widget-settings-entityreference-autocreate-active' => array(
            'checked' => TRUE,
          ),
          '#edit-field-settings-target-type' => array(
            'value' => 'node',
          ),
        ),
      ),
    );
    $form['instance']['widget']['settings']['entityreference_autocreate']['help'] = array(
      '#markup' => t('Autocreation can only work if there is exactly one <b>Target bundle</b> selected in "<b>Entity Selection</b>" below.'),
    );

    // If using views, it's basically impossible to autodetect the type:bundle.
    // Will have to let the admin do it manually.
    // This means enumerating every possibility, and letting them select.
    $options = array();
    foreach (entity_get_info() as $entity_type => $entity_info) {
      foreach ($entity_info['bundles'] as $bundle_id => $bundle_info) {
        $options[$entity_info['label']][$bundle_id] = $bundle_info['label'];
      }
    }
    $form['instance']['widget']['settings']['entityreference_autocreate']['bundle'] = array(
      '#type' => 'select',
      '#title' => t('Bundle'),
      '#description' => t('If using views lookups, you need to define me what <em>type</em> of thing to create. Warning - results can be unreliable!'),
      '#options' => $options,
      '#default_value' => isset($settings['entityreference_autocreate']['bundle']) ? $settings['entityreference_autocreate']['bundle'] : '',
      '#states' => array(
        'visible' => array(
          ':input[name="field[settings][handler]"]' => array(
            'value' => 'views',
          ),
        ),
      ),
    );
  }
}

/**
 * Adjust the behaviour of entityreference autocomplete widgets.
 *
 * Replaces the normal validation that prevents linking to imaginary entities
 * with our own, which makes it on the fly if needed.
 *
 * hook_field_widget_form_alter()
 */
function entityreference_autocreate_field_widget_form_alter(&$element, &$form_state, $context) {

  // First check if we are relevant or needed on this field widget.
  if ($context['field']['type'] != 'entityreference') {
    return;
  }
  if (empty($context['instance']['widget']['settings']['entityreference_autocreate']['active'])) {
    return;
  }

  // We are an autocomplete. What's the details?
  $target_bundle = entityreference_autocreate_get_target_bundle($context['field']);
  $title = t('Autocreate enabled - any title put here will cause a "!bundle" to be created if no autocomplete match is found.', array(
    '!bundle' => $target_bundle,
  ));

  // So adjust the form field now.
  if ($context['instance']['widget']['type'] == 'entityreference_autocomplete') {

    // If it's autocomplete standard, there is a 'target_id'
    $element['target_id']['#attributes']['title'] = $title;
    $element['target_id']['#entityreference_autocreate_settings'] = $context['instance']['widget']['settings']['entityreference_autocreate'];

    // To bypass the normal validation, need to REPLACE it totally.
    $element['target_id']['#element_validate'] = array(
      'entityreference_autocreate_validate',
    );
  }
  if ($context['instance']['widget']['type'] == 'entityreference_autocomplete_tags') {

    // If it's autocomplete tags style ..
    $element['#attributes']['title'] = $title;
    $element['#entityreference_autocreate_settings'] = $context['instance']['widget']['settings']['entityreference_autocreate'];

    // To bypass the normal validation, need to REPLACE it totally.
    $element['#element_validate'] = array(
      'entityreference_autocreate_validate_tags',
    );
  }
}

/**
 * Make a missing target if asked for by name.
 *
 * An element_validate callback for autocomplete fields.
 * Replaces _entityreference_autocomplete_validate().
 *
 * @see _entityreference_autocomplete_validate()
 */
function entityreference_autocreate_validate($element, &$form_state, $form) {
  if (empty($element['#value'])) {
    return;
  }
  $field = field_info_field($element['#field_name']);
  $field['settings']['entityreference_autocreate'] = $element['#entityreference_autocreate_settings'];

  // Fetch an entity ID, making it on the fly if needed.
  if ($value = entityreference_autocreate_get_entity_by_title($field, $element['#value'])) {
    form_set_value($element, $value, $form_state);
    return;
  }

  // Something has failed.
  // Either could not create the target
  // (permissions or something?)
  // Or did a lookup and found two identically named targets already existing,
  // so bailed.
  $strings = array(
    '!target' => $element['#value'],
  );
  form_error($element, t('Failed to create or find a target called !target (entityreference_autocreate). This may be due to permissions, autocreate settings on the widget, or possibly if there are two targets with identical titles already on the system.', $strings));
}

/**
 * Validate handler that makes things up on the fly if needed.
 *
 * @see _entityreference_autocomplete_tags_validate()
 */
function entityreference_autocreate_validate_tags($element, &$form_state, $form) {
  $value = array();

  // If a value was entered into the autocomplete...
  if (!empty($element['#value'])) {
    $field = field_info_field($element['#field_name']);
    $field['settings']['entityreference_autocreate'] = $element['#entityreference_autocreate_settings'];
    $entities = drupal_explode_tags($element['#value']);
    foreach ($entities as $title) {
      if ($target_id = entityreference_autocreate_get_entity_by_title($field, $title)) {
        $value[] = array(
          'target_id' => $target_id,
        );
      }
    }
  }

  // Update the values.
  form_set_value($element, $value, $form_state);
}

/**
 * Fetch the named entity for the field, create it if not found.
 *
 * @param array $field_info
 *   As loaded from field_info_field()
 * @param string $title
 *   Title to search for.
 *
 * @return object|NULL
 *   Pre-existing or new entity. is_new should be set on it if it is fresh.
 *   Returns NULL on unexpected failure. A failure should probably be caught.
 */
function entityreference_autocreate_get_entity_by_title($field_info, $title) {
  $title = trim($title);
  if (empty($title)) {
    return NULL;
  }

  // Take "label (entity id)', match the id from parenthesis.
  if (preg_match("/.+\\((\\d+)\\)/", $title, $matches)) {
    return $matches[1];
  }

  // Try to get a match from the input string when the user didn't use the
  // autocomplete but filled in a value manually.
  $handler = entityreference_get_selection_handler($field_info);

  // Search for matches (exact), limit to 2 so we can detect if there is a
  // potential conflict.
  $entities = $handler
    ->getReferencableEntities($title, '=', 2);

  // Case where $entities looks like $entities[BUNDLE][ETID] = HTML.
  if (is_array(reset($entities))) {

    // Extract items from results. The return is keyed by bundle.
    $target_bundle = entityreference_autocreate_get_target_bundle($field_info);
    if (!empty($target_bundle)) {
      $entities = $entities[$target_bundle];
    }
  }
  if (count($entities) == 1) {

    // Exact match, no confusion, use that.
    return key($entities);
  }
  if (count($entities) > 1) {

    // More than one match.
    // This is a genuine form validation error I can't automate.
    return NULL;
  }

  // By now we've eliminated the options. There is no match.
  if (count($entities) == 0) {

    // Now make one of the named things.
    return entityreference_autocreate_new_entity($field_info, $title);
  }
  return NULL;
}

/**
 * Create a placeholder item of the type described in the field settings.
 */
function entityreference_autocreate_new_entity($field_info, $title) {

  // Now make one of the named things.
  $entity_type = $field_info['settings']['target_type'];
  $target_bundle = entityreference_autocreate_get_target_bundle($field_info);
  if (empty($target_bundle)) {
    watchdog(__FUNCTION__, 'Cannot create new entity underneath field %field_name as the desired target bundle is undefined. See the field settings. Autocreate has trouble with view-based entityreference lookups.', array(
      '%field_name' => $field_info['field_name'],
    ), WATCHDOG_WARNING);
    return NULL;
  }

  // Select user depending on settings.
  if (!empty($field_info['settings']['entityreference_autocreate']['author_current_user'])) {
    global $user;
  }
  elseif (!empty($field_info['settings']['entityreference_autocreate']['author'])) {
    $user = user_load_by_name($field_info['settings']['entityreference_autocreate']['author']);
  }
  else {
    $user = user_load(0);
  }

  // Make a skeleton/minimal whatever entity. Probably a node.
  // @see entity_create_stub_entity($entity_type, $ids).
  $entity_info = entity_get_info($entity_type);
  $label_key = 'title';
  if (!empty($entity_info['entity keys']['label'])) {
    $label_key = $entity_info['entity keys']['label'];
  }
  $bundle_key = 'type';
  if (!empty($info['entity keys']['bundle'])) {
    $bundle_key = $info['entity keys']['bundle'];
  }
  $new_entity = NULL;

  // These two attributes seem common to each entity I've met so far.
  $new_entity_values = array(
    $bundle_key => $target_bundle,
    $label_key => $title,
  );
  switch ($entity_type) {
    case 'node':

      // Check the expected published status.
      $status = TRUE;
      if (isset($field_info['settings']['entityreference_autocreate']['status'])) {
        $status = $field_info['settings']['entityreference_autocreate']['status'];
        if ($status == -1) {

          // Use the bundle default.
          $node_options = variable_get('node_options_' . $target_bundle, array(
            'status',
            'promote',
          ));
          $status = in_array('status', $node_options);
        }
      }
      $new_entity_values += array(
        'uid' => $user->uid,
        'name' => isset($user->name) ? $user->name : '',
        'language' => LANGUAGE_NONE,
        'status' => $status,
      );
      $new_entity = entity_create($entity_type, $new_entity_values);
      break;
    case 'taxonomy_term':
      if ($vocabulair = taxonomy_vocabulary_machine_name_load($target_bundle)) {
        $new_entity_values += array(
          'vid' => $vocabulair->vid,
        );
        $new_entity = entity_create($entity_type, $new_entity_values);
      }
      break;
    case 'user':

      // Creating users on the fly is a bit risky,
      // so they are not enabled by default.
      //
      // Entity_info did not define the label_key,
      // and users dont really have bundles.
      $label_key = 'name';
      $target_bundle = 'user';
      $new_entity_values = array(
        $bundle_key => $target_bundle,
        $label_key => $title,
      );
      $new_entity = entity_create($entity_type, $new_entity_values);
      break;
    default:

      // It's some unknown/custom entity.
      // We really can't guess what shape it is.
      // It's likely that each field listed in the infos
      // $entity_info['entity keys']
      // will be required though?
      //
      // It's probably a *little* like a node...
      // but it's a crap-shoot really.
      // Hopefully entity API will take care of the rest of the abstraction
      // and validation needed from here.
      // YMMV.
      $new_entity = entity_create($entity_type, $new_entity_values);
      break;
  }

  // Allow other modules to work on this before we save it.
  drupal_alter('entityreference_autocreate_new_entity', $new_entity, $field_info, $title);
  if (empty($new_entity)) {

    // The entity is unknown so don't continue.
    drupal_set_message(t("The entity that needs to be created is unknown (entityreference_autocreate)"), 'error');
    return NULL;
  }
  entity_save($entity_type, $new_entity);

  // The return from this isn't reliable, check for an ID instead.
  $target_id = entity_id($entity_type, $new_entity);
  $uri = entity_uri($entity_type, $new_entity);
  $strings = array(
    '%entity_type' => $entity_type,
    '%target_bundle' => $target_bundle,
    '!target' => l($new_entity->{$label_key}, $uri['path']),
    '%title' => $title,
  );
  if ($target_id) {
    drupal_alter('entityreference_autocreate_new_saved_entity', $target_id, $entity_type);
    drupal_set_message(t('Created a new %entity_type %target_bundle : !target (entityreference_autocreate)', $strings));
    return $target_id;
  }
  else {

    // Can't say why, but it's probably worth complaining about.
    drupal_set_message(t("Failed to created a new %target_bundle called %title, no id returned (entityreference_autocreate)", $strings), 'error');
    return NULL;
  }
}

/**
 * Load feeds support.
 *
 * Implements hook_init().
 */
function entityreference_autocreate_init() {

  // Include feeds.module integration.
  if (module_exists('feeds')) {
    module_load_include('inc', 'entityreference_autocreate', 'entityreference_autocreate.feeds');
  }
}

/**
 * Find the desired target entity type (bundle name).
 *
 * Utility function.
 * Abstracted as we need to know this often, and we also need to handle
 * the basic vs views-based lookup each time.
 *
 * Returns either an array (which usually indicates a config problem)
 * or just the first element in that array.
 *
 * @param $field_info
 *   The field info from the current context.
 * @param bool $multiple
 *   If set, return an array of bundle names.
 *
 * @return string|array|null
 *   The bundle name.
 */
function entityreference_autocreate_get_target_bundle($field_info, $multiple = FALSE) {

  // We look in different places depending on the selected lookup 'handler'.
  $settings = $field_info['settings'];
  if ($settings['handler'] == 'base') {

    // The autocomplete is easy.
    if (!empty($settings['handler_settings']['target_bundles'])) {
      $target_bundles = $settings['handler_settings']['target_bundles'];
    }
  }
  if ($settings['handler'] == 'views') {

    // Figuring the base entity bundle from views is difficult.
    // We have required it to be defined via UI, not introspection.
    // So the needed data is not in field_info (base),
    // it was packaged with the widget settings (instance).
    // Need to retrieve it from there...

    #$field_instance = field_info_instance($entity_type, $field_info['field_name'], $bundle);

    // @Squash warning in case it's undefined due to an upgrade.
    $target_bundles = array(
      @$settings['entityreference_autocreate']['bundle'],
    );
    if (empty($target_bundles)) {
      watchdog('entityreference_autocreate', 'Deducing the bundle from a view config is not yet possible, or it failed. Giving up, sorry.', array(), WATCHDOG_NOTICE);
      return NULL;
    }
  }

  // Fallback.
  if (empty($target_bundles)) {

    // So, not all entities have bundles. 'user' doesn't. Fake it for now.
    // User entities are special - they have no bundle.
    // (or it's 'user' but not explicit about it in the entityreference options)
    // I guess there may be other entities like that also. Try to catch them,
    // by assuming that their entity type and their bundle id are the same.
    $target_bundles = array(
      $settings['target_type'],
    );
    watchdog('entityreference_autocreate', 'Guessing that the "bundle" for things of type %entity_type is %target_bundle. This may be wrong, please report what you are trying to do as an issue.', array(
      '%entity_type' => $settings['target_type'],
      '%target_bundle' => reset($target_bundles),
    ), WATCHDOG_NOTICE);
  }
  if (count($target_bundles) != 1) {
    watchdog('entityreference_autocreate', 'Can only autocreate an entity if there is exactly one target bundle. Check the widget settings for %field_name', array(
      '%field_name' => $field_info['field_name'],
    ), WATCHDOG_NOTICE);
  }
  if (empty($target_bundles)) {
    watchdog('entityreference_autocreate', 'No valid target bundle setting found for field %field_name', array(
      '%field_name' => $field_info['field_name'],
    ), WATCHDOG_NOTICE);
    return NULL;
  }
  return $multiple ? $target_bundles : reset($target_bundles);
}

Functions

Namesort descending Description
entityreference_autocreate_field_widget_form_alter Adjust the behaviour of entityreference autocomplete widgets.
entityreference_autocreate_field_widget_info_alter Implements hook_widget_info_alter().
entityreference_autocreate_form_field_ui_field_edit_form_alter Adds our extra option to the field widget settings form.
entityreference_autocreate_get_entity_by_title Fetch the named entity for the field, create it if not found.
entityreference_autocreate_get_target_bundle Find the desired target entity type (bundle name).
entityreference_autocreate_init Load feeds support.
entityreference_autocreate_new_entity Create a placeholder item of the type described in the field settings.
entityreference_autocreate_validate Make a missing target if asked for by name.
entityreference_autocreate_validate_tags Validate handler that makes things up on the fly if needed.