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'] = isset($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 = isset($element['#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 = isset($last_value['target_id']) ? $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 unambuguously * matches the incoming input, and sill 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, ]; if (empty($entities)) { if ($strict) { // Error if there are no entities available for a required field. $form_state->setError($element, t('There are no entities 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 entities 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 entities match this reference; "%multiple". Specify the one you want by appending the id in parentheses, like "@value (@id)".', ['%multiple' => 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; } }