You are here

class EntityAutocomplete in Drupal 10

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php \Drupal\Core\Entity\Element\EntityAutocomplete
  2. 9 core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php \Drupal\Core\Entity\Element\EntityAutocomplete

Provides an entity autocomplete form element.

The autocomplete form element allows users to select one or multiple entities, which can come from all or specific bundles of an entity type.

Properties:

  • #target_type: (required) The ID of the target entity type.
  • #tags: (optional) TRUE if the element allows multiple selection. Defaults to FALSE.
  • #default_value: (optional) The default entity or an array of default entities, depending on the value of #tags.
  • #selection_handler: (optional) The plugin ID of the entity reference selection handler (a plugin of type EntityReferenceSelection). The default value is the lowest-weighted plugin that is compatible with #target_type.
  • #selection_settings: (optional) An array of settings for the selection handler. Settings for the default selection handler \Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection are:

    • target_bundles: Array of bundles to allow (omit to allow all bundles).
    • sort: Array with 'field' and 'direction' keys, determining how results will be sorted. Defaults to unsorted.
  • #autocreate: (optional) Array of settings used to auto-create entities that do not exist (omit to not auto-create entities). Elements:

    • bundle: (required) Bundle to use for auto-created entities.
    • uid: User ID to use as the author of auto-created entities. Defaults to the current user.
  • #process_default_value: (optional) Set to FALSE if the #default_value property is processed and access checked elsewhere (such as by a Field API widget). Defaults to TRUE.
  • #validate_reference: (optional) Set to FALSE if validation of the selected entities is performed elsewhere. Defaults to TRUE.

Usage example:


$form['my_element'] = [
 '#type' => 'entity_autocomplete',
 '#target_type' => 'node',
 '#tags' => TRUE,
 '#default_value' => $node,
 '#selection_handler' => 'default',
 '#selection_settings' => [
   'target_bundles' => ['article', 'page'],
  ],
 '#autocreate' => [
   'bundle' => 'article',
   'uid' => <a valid user ID>,
  ],
];

Plugin annotation

@FormElement("entity_autocomplete");

Hierarchy

Expanded class hierarchy of EntityAutocomplete

See also

\Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection

4 files declare their use of EntityAutocomplete
EntityAutocompleteElementFormTest.php in core/tests/Drupal/KernelTests/Core/Entity/Element/EntityAutocompleteElementFormTest.php
LinkWidget.php in core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
Name.php in core/modules/user/src/Plugin/views/filter/Name.php
TaxonomyIndexTid.php in core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
7 #type uses of EntityAutocomplete
AssignOwnerNode::buildConfigurationForm in core/modules/node/src/Plugin/Action/AssignOwnerNode.php
CommentForm::form in core/modules/comment/src/CommentForm.php
Gets the actual form array to be built.
ContentTranslationHandler::entityFormAlter in core/modules/content_translation/src/ContentTranslationHandler.php
EntityAutocompleteElementFormTest::buildForm in core/tests/Drupal/KernelTests/Core/Entity/Element/EntityAutocompleteElementFormTest.php
EntityReferenceAutocompleteWidget::formElement in core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php
Returns the form for a single field widget.

... See full list

File

core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php, line 68

Namespace

Drupal\Core\Entity\Element
View source
class EntityAutocomplete extends Textfield {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    $info = parent::getInfo();
    $class = static::class;

    // Apply default form element properties.
    $info['#target_type'] = NULL;
    $info['#selection_handler'] = 'default';
    $info['#selection_settings'] = [];
    $info['#tags'] = FALSE;
    $info['#autocreate'] = NULL;

    // This should only be set to FALSE if proper validation by the selection
    // handler is performed at another level on the extracted form values.
    $info['#validate_reference'] = TRUE;

    // IMPORTANT! This should only be set to FALSE if the #default_value
    // property is processed at another level (e.g. by a Field API widget) and
    // its value is properly checked for access.
    $info['#process_default_value'] = TRUE;
    $info['#element_validate'] = [
      [
        $class,
        'validateEntityAutocomplete',
      ],
    ];
    array_unshift($info['#process'], [
      $class,
      'processEntityAutocomplete',
    ]);
    return $info;
  }

  /**
   * {@inheritdoc}
   */
  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {

    // Process the #default_value property.
    if ($input === FALSE && isset($element['#default_value']) && $element['#process_default_value']) {
      if (is_array($element['#default_value']) && $element['#tags'] !== TRUE) {
        throw new \InvalidArgumentException('The #default_value property is an array but the form element does not allow multiple values.');
      }
      elseif (!empty($element['#default_value']) && !is_array($element['#default_value'])) {

        // Convert the default value into an array for easier processing in
        // static::getEntityLabels().
        $element['#default_value'] = [
          $element['#default_value'],
        ];
      }
      if ($element['#default_value']) {
        if (!reset($element['#default_value']) instanceof EntityInterface) {
          throw new \InvalidArgumentException('The #default_value property has to be an entity object or an array of entity objects.');
        }

        // Extract the labels from the passed-in entity objects, taking access
        // checks into account.
        return static::getEntityLabels($element['#default_value']);
      }
    }

    // Potentially the #value is set directly, so it contains the 'target_id'
    // array structure instead of a string.
    if ($input !== FALSE && is_array($input)) {
      $entity_ids = array_map(function (array $item) {
        return $item['target_id'];
      }, $input);
      $entities = \Drupal::entityTypeManager()
        ->getStorage($element['#target_type'])
        ->loadMultiple($entity_ids);
      return static::getEntityLabels($entities);
    }
  }

  /**
   * Adds entity autocomplete functionality to a form element.
   *
   * @param array $element
   *   The form element to process. Properties used:
   *   - #target_type: The ID of the target entity type.
   *   - #selection_handler: The plugin ID of the entity reference selection
   *     handler.
   *   - #selection_settings: An array of settings that will be passed to the
   *     selection handler.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param array $complete_form
   *   The complete form structure.
   *
   * @return array
   *   The form element.
   *
   * @throws \InvalidArgumentException
   *   Exception thrown when the #target_type or #autocreate['bundle'] are
   *   missing.
   */
  public static function processEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {

    // Nothing to do if there is no target entity type.
    if (empty($element['#target_type'])) {
      throw new \InvalidArgumentException('Missing required #target_type parameter.');
    }

    // Provide default values and sanity checks for the #autocreate parameter.
    if ($element['#autocreate']) {
      if (!isset($element['#autocreate']['bundle'])) {
        throw new \InvalidArgumentException("Missing required #autocreate['bundle'] parameter.");
      }

      // Default the autocreate user ID to the current user.
      $element['#autocreate']['uid'] = $element['#autocreate']['uid'] ?? \Drupal::currentUser()
        ->id();
    }

    // Store the selection settings in the key/value store and pass a hashed key
    // in the route parameters.
    $selection_settings = $element['#selection_settings'] ?? [];
    $data = serialize($selection_settings) . $element['#target_type'] . $element['#selection_handler'];
    $selection_settings_key = Crypt::hmacBase64($data, Settings::getHashSalt());
    $key_value_storage = \Drupal::keyValue('entity_autocomplete');
    if (!$key_value_storage
      ->has($selection_settings_key)) {
      $key_value_storage
        ->set($selection_settings_key, $selection_settings);
    }
    $element['#autocomplete_route_name'] = 'system.entity_autocomplete';
    $element['#autocomplete_route_parameters'] = [
      'target_type' => $element['#target_type'],
      'selection_handler' => $element['#selection_handler'],
      'selection_settings_key' => $selection_settings_key,
    ];
    return $element;
  }

  /**
   * Form element validation handler for entity_autocomplete elements.
   */
  public static function validateEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
    $value = NULL;
    if (!empty($element['#value'])) {
      $options = $element['#selection_settings'] + [
        'target_type' => $element['#target_type'],
        'handler' => $element['#selection_handler'],
      ];

      /** @var /Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */
      $handler = \Drupal::service('plugin.manager.entity_reference_selection')
        ->getInstance($options);
      $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface;

      // GET forms might pass the validated data around on the next request, in
      // which case it will already be in the expected format.
      if (is_array($element['#value'])) {
        $value = $element['#value'];
      }
      else {
        $input_values = $element['#tags'] ? Tags::explode($element['#value']) : [
          $element['#value'],
        ];
        foreach ($input_values as $input) {
          $match = static::extractEntityIdFromAutocompleteInput($input);
          if ($match === NULL) {

            // Try to get a match from the input string when the user didn't use
            // the autocomplete but filled in a value manually.
            $match = static::matchEntityByTitle($handler, $input, $element, $form_state, !$autocreate);
          }
          if ($match !== NULL) {
            $value[] = [
              'target_id' => $match,
            ];
          }
          elseif ($autocreate) {

            /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface $handler */

            // Auto-create item. See an example of how this is handled in
            // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
            $value[] = [
              'entity' => $handler
                ->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']),
            ];
          }
        }
      }

      // Check that the referenced entities are valid, if needed.
      if ($element['#validate_reference'] && !empty($value)) {

        // Validate existing entities.
        $ids = array_reduce($value, function ($return, $item) {
          if (isset($item['target_id'])) {
            $return[] = $item['target_id'];
          }
          return $return;
        });
        if ($ids) {
          $valid_ids = $handler
            ->validateReferenceableEntities($ids);
          if ($invalid_ids = array_diff($ids, $valid_ids)) {
            foreach ($invalid_ids as $invalid_id) {
              $form_state
                ->setError($element, t('The referenced entity (%type: %id) does not exist.', [
                '%type' => $element['#target_type'],
                '%id' => $invalid_id,
              ]));
            }
          }
        }

        // Validate newly created entities.
        $new_entities = array_reduce($value, function ($return, $item) {
          if (isset($item['entity'])) {
            $return[] = $item['entity'];
          }
          return $return;
        });
        if ($new_entities) {
          if ($autocreate) {
            $valid_new_entities = $handler
              ->validateReferenceableNewEntities($new_entities);
            $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
          }
          else {

            // If the selection handler does not support referencing newly
            // created entities, all of them should be invalidated.
            $invalid_new_entities = $new_entities;
          }
          foreach ($invalid_new_entities as $entity) {

            /** @var \Drupal\Core\Entity\EntityInterface $entity */
            $form_state
              ->setError($element, t('This entity (%type: %label) cannot be referenced.', [
              '%type' => $element['#target_type'],
              '%label' => $entity
                ->label(),
            ]));
          }
        }
      }

      // Use only the last value if the form element does not support multiple
      // matches (tags).
      if (!$element['#tags'] && !empty($value)) {
        $last_value = $value[count($value) - 1];
        $value = $last_value['target_id'] ?? $last_value;
      }
    }
    $form_state
      ->setValueForElement($element, $value);
  }

  /**
   * Finds an entity from an autocomplete input without an explicit ID.
   *
   * The method will return an entity ID if one single entity unambiguously
   * matches the incoming input, and assign form errors otherwise.
   *
   * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler
   *   Entity reference selection plugin.
   * @param string $input
   *   Single string from autocomplete element.
   * @param array $element
   *   The form element to set a form error.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current form state.
   * @param bool $strict
   *   Whether to trigger a form error if an element from $input (eg. an entity)
   *   is not found.
   *
   * @return int|null
   *   Value of a matching entity ID, or NULL if none.
   */
  protected static function matchEntityByTitle(SelectionInterface $handler, $input, array &$element, FormStateInterface $form_state, $strict) {
    $entities_by_bundle = $handler
      ->getReferenceableEntities($input, '=', 6);
    $entities = array_reduce($entities_by_bundle, function ($flattened, $bundle_entities) {
      return $flattened + $bundle_entities;
    }, []);
    $params = [
      '%value' => $input,
      '@value' => $input,
      '@entity_type_plural' => \Drupal::entityTypeManager()
        ->getDefinition($element['#target_type'])
        ->getPluralLabel(),
    ];
    if (empty($entities)) {
      if ($strict) {

        // Error if there are no entities available for a required field.
        $form_state
          ->setError($element, t('There are no @entity_type_plural matching "%value".', $params));
      }
    }
    elseif (count($entities) > 5) {
      $params['@id'] = key($entities);

      // Error if there are more than 5 matching entities.
      $form_state
        ->setError($element, t('Many @entity_type_plural are called %value. Specify the one you want by appending the id in parentheses, like "@value (@id)".', $params));
    }
    elseif (count($entities) > 1) {

      // More helpful error if there are only a few matching entities.
      $multiples = [];
      foreach ($entities as $id => $name) {
        $multiples[] = $name . ' (' . $id . ')';
      }
      $params['@id'] = $id;
      $form_state
        ->setError($element, t('Multiple @entity_type_plural match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', [
        '%multiple' => strip_tags(implode('", "', $multiples)),
      ] + $params));
    }
    else {

      // Take the one and only matching entity.
      return key($entities);
    }
  }

  /**
   * Converts an array of entity objects into a string of entity labels.
   *
   * This method is also responsible for checking the 'view label' access on the
   * passed-in entities.
   *
   * @param \Drupal\Core\Entity\EntityInterface[] $entities
   *   An array of entity objects.
   *
   * @return string
   *   A string of entity labels separated by commas.
   */
  public static function getEntityLabels(array $entities) {

    /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
    $entity_repository = \Drupal::service('entity.repository');
    $entity_labels = [];
    foreach ($entities as $entity) {

      // Set the entity in the correct language for display.
      $entity = $entity_repository
        ->getTranslationFromContext($entity);

      // Use the special view label, since some entities allow the label to be
      // viewed, even if the entity is not allowed to be viewed.
      $label = $entity
        ->access('view label') ? $entity
        ->label() : t('- Restricted access -');

      // Take into account "autocreated" entities.
      if (!$entity
        ->isNew()) {
        $label .= ' (' . $entity
          ->id() . ')';
      }

      // Labels containing commas or quotes must be wrapped in quotes.
      $entity_labels[] = Tags::encode($label);
    }
    return implode(', ', $entity_labels);
  }

  /**
   * Extracts the entity ID from the autocompletion result.
   *
   * @param string $input
   *   The input coming from the autocompletion result.
   *
   * @return mixed|null
   *   An entity ID or NULL if the input does not contain one.
   */
  public static function extractEntityIdFromAutocompleteInput($input) {
    $match = NULL;

    // Take "label (entity id)', match the ID from inside the parentheses.
    // @todo Add support for entities containing parentheses in their ID.
    // @see https://www.drupal.org/node/2520416
    if (preg_match("/.+\\s\\(([^\\)]+)\\)/", $input, $matches)) {
      $match = $matches[1];
    }
    return $match;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
EntityAutocomplete::extractEntityIdFromAutocompleteInput public static function Extracts the entity ID from the autocompletion result.
EntityAutocomplete::getEntityLabels public static function Converts an array of entity objects into a string of entity labels.
EntityAutocomplete::getInfo public function Returns the element properties for this element. Overrides Textfield::getInfo
EntityAutocomplete::matchEntityByTitle protected static function Finds an entity from an autocomplete input without an explicit ID.
EntityAutocomplete::processEntityAutocomplete public static function Adds entity autocomplete functionality to a form element.
EntityAutocomplete::validateEntityAutocomplete public static function Form element validation handler for entity_autocomplete elements.
EntityAutocomplete::valueCallback public static function Determines how user input is mapped to an element's #value property. Overrides Textfield::valueCallback
FormElement::processAutocomplete public static function Adds autocomplete functionality to elements.
FormElement::processPattern public static function #process callback for #pattern form element property.
FormElement::validatePattern public static function #element_validate callback for #pattern form element property.
MessengerTrait::$messenger protected property The messenger. 18
MessengerTrait::messenger public function Gets the messenger. 18
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition.
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function
PluginBase::getDerivativeId public function
PluginBase::getPluginDefinition public function 2
PluginBase::getPluginId public function
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginBase::__construct public function Constructs a \Drupal\Component\Plugin\PluginBase object. 53
RenderElement::preRenderAjaxForm public static function Adds Ajax information about an element to communicate with JavaScript.
RenderElement::preRenderGroup public static function Adds members of this group as actual elements for rendering.
RenderElement::processAjaxForm public static function Form element processing handler for the #ajax form property. 1
RenderElement::processGroup public static function Arranges elements into groups.
RenderElement::setAttributes public static function Sets a form element's class attribute. Overrides ElementInterface::setAttributes
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 1
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
Textfield::preRenderTextfield public static function Prepares a #type 'textfield' render element for input.html.twig.