EntityAutocomplete.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <?php
  2. namespace Drupal\Core\Entity\Element;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Component\Utility\Tags;
  5. use Drupal\Core\Entity\EntityInterface;
  6. use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface;
  7. use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface;
  8. use Drupal\Core\Form\FormStateInterface;
  9. use Drupal\Core\Render\Element\Textfield;
  10. use Drupal\Core\Site\Settings;
  11. /**
  12. * Provides an entity autocomplete form element.
  13. *
  14. * The #default_value accepted by this element is either an entity object or an
  15. * array of entity objects.
  16. *
  17. * @FormElement("entity_autocomplete")
  18. */
  19. class EntityAutocomplete extends Textfield {
  20. /**
  21. * {@inheritdoc}
  22. */
  23. public function getInfo() {
  24. $info = parent::getInfo();
  25. $class = get_class($this);
  26. // Apply default form element properties.
  27. $info['#target_type'] = NULL;
  28. $info['#selection_handler'] = 'default';
  29. $info['#selection_settings'] = [];
  30. $info['#tags'] = FALSE;
  31. $info['#autocreate'] = NULL;
  32. // This should only be set to FALSE if proper validation by the selection
  33. // handler is performed at another level on the extracted form values.
  34. $info['#validate_reference'] = TRUE;
  35. // IMPORTANT! This should only be set to FALSE if the #default_value
  36. // property is processed at another level (e.g. by a Field API widget) and
  37. // its value is properly checked for access.
  38. $info['#process_default_value'] = TRUE;
  39. $info['#element_validate'] = [[$class, 'validateEntityAutocomplete']];
  40. array_unshift($info['#process'], [$class, 'processEntityAutocomplete']);
  41. return $info;
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
  47. // Process the #default_value property.
  48. if ($input === FALSE && isset($element['#default_value']) && $element['#process_default_value']) {
  49. if (is_array($element['#default_value']) && $element['#tags'] !== TRUE) {
  50. throw new \InvalidArgumentException('The #default_value property is an array but the form element does not allow multiple values.');
  51. }
  52. elseif (!empty($element['#default_value']) && !is_array($element['#default_value'])) {
  53. // Convert the default value into an array for easier processing in
  54. // static::getEntityLabels().
  55. $element['#default_value'] = [$element['#default_value']];
  56. }
  57. if ($element['#default_value']) {
  58. if (!(reset($element['#default_value']) instanceof EntityInterface)) {
  59. throw new \InvalidArgumentException('The #default_value property has to be an entity object or an array of entity objects.');
  60. }
  61. // Extract the labels from the passed-in entity objects, taking access
  62. // checks into account.
  63. return static::getEntityLabels($element['#default_value']);
  64. }
  65. }
  66. // Potentially the #value is set directly, so it contains the 'target_id'
  67. // array structure instead of a string.
  68. if ($input !== FALSE && is_array($input)) {
  69. $entity_ids = array_map(function (array $item) {
  70. return $item['target_id'];
  71. }, $input);
  72. $entities = \Drupal::entityTypeManager()->getStorage($element['#target_type'])->loadMultiple($entity_ids);
  73. return static::getEntityLabels($entities);
  74. }
  75. }
  76. /**
  77. * Adds entity autocomplete functionality to a form element.
  78. *
  79. * @param array $element
  80. * The form element to process. Properties used:
  81. * - #target_type: The ID of the target entity type.
  82. * - #selection_handler: The plugin ID of the entity reference selection
  83. * handler.
  84. * - #selection_settings: An array of settings that will be passed to the
  85. * selection handler.
  86. * @param \Drupal\Core\Form\FormStateInterface $form_state
  87. * The current state of the form.
  88. * @param array $complete_form
  89. * The complete form structure.
  90. *
  91. * @return array
  92. * The form element.
  93. *
  94. * @throws \InvalidArgumentException
  95. * Exception thrown when the #target_type or #autocreate['bundle'] are
  96. * missing.
  97. */
  98. public static function processEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
  99. // Nothing to do if there is no target entity type.
  100. if (empty($element['#target_type'])) {
  101. throw new \InvalidArgumentException('Missing required #target_type parameter.');
  102. }
  103. // Provide default values and sanity checks for the #autocreate parameter.
  104. if ($element['#autocreate']) {
  105. if (!isset($element['#autocreate']['bundle'])) {
  106. throw new \InvalidArgumentException("Missing required #autocreate['bundle'] parameter.");
  107. }
  108. // Default the autocreate user ID to the current user.
  109. $element['#autocreate']['uid'] = isset($element['#autocreate']['uid']) ? $element['#autocreate']['uid'] : \Drupal::currentUser()->id();
  110. }
  111. // Store the selection settings in the key/value store and pass a hashed key
  112. // in the route parameters.
  113. $selection_settings = isset($element['#selection_settings']) ? $element['#selection_settings'] : [];
  114. $data = serialize($selection_settings) . $element['#target_type'] . $element['#selection_handler'];
  115. $selection_settings_key = Crypt::hmacBase64($data, Settings::getHashSalt());
  116. $key_value_storage = \Drupal::keyValue('entity_autocomplete');
  117. if (!$key_value_storage->has($selection_settings_key)) {
  118. $key_value_storage->set($selection_settings_key, $selection_settings);
  119. }
  120. $element['#autocomplete_route_name'] = 'system.entity_autocomplete';
  121. $element['#autocomplete_route_parameters'] = [
  122. 'target_type' => $element['#target_type'],
  123. 'selection_handler' => $element['#selection_handler'],
  124. 'selection_settings_key' => $selection_settings_key,
  125. ];
  126. return $element;
  127. }
  128. /**
  129. * Form element validation handler for entity_autocomplete elements.
  130. */
  131. public static function validateEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
  132. $value = NULL;
  133. if (!empty($element['#value'])) {
  134. $options = $element['#selection_settings'] + [
  135. 'target_type' => $element['#target_type'],
  136. 'handler' => $element['#selection_handler'],
  137. ];
  138. /** @var /Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */
  139. $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options);
  140. $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface;
  141. // GET forms might pass the validated data around on the next request, in
  142. // which case it will already be in the expected format.
  143. if (is_array($element['#value'])) {
  144. $value = $element['#value'];
  145. }
  146. else {
  147. $input_values = $element['#tags'] ? Tags::explode($element['#value']) : [$element['#value']];
  148. foreach ($input_values as $input) {
  149. $match = static::extractEntityIdFromAutocompleteInput($input);
  150. if ($match === NULL) {
  151. // Try to get a match from the input string when the user didn't use
  152. // the autocomplete but filled in a value manually.
  153. $match = static::matchEntityByTitle($handler, $input, $element, $form_state, !$autocreate);
  154. }
  155. if ($match !== NULL) {
  156. $value[] = [
  157. 'target_id' => $match,
  158. ];
  159. }
  160. elseif ($autocreate) {
  161. /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface $handler */
  162. // Auto-create item. See an example of how this is handled in
  163. // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
  164. $value[] = [
  165. 'entity' => $handler->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']),
  166. ];
  167. }
  168. }
  169. }
  170. // Check that the referenced entities are valid, if needed.
  171. if ($element['#validate_reference'] && !empty($value)) {
  172. // Validate existing entities.
  173. $ids = array_reduce($value, function ($return, $item) {
  174. if (isset($item['target_id'])) {
  175. $return[] = $item['target_id'];
  176. }
  177. return $return;
  178. });
  179. if ($ids) {
  180. $valid_ids = $handler->validateReferenceableEntities($ids);
  181. if ($invalid_ids = array_diff($ids, $valid_ids)) {
  182. foreach ($invalid_ids as $invalid_id) {
  183. $form_state->setError($element, t('The referenced entity (%type: %id) does not exist.', ['%type' => $element['#target_type'], '%id' => $invalid_id]));
  184. }
  185. }
  186. }
  187. // Validate newly created entities.
  188. $new_entities = array_reduce($value, function ($return, $item) {
  189. if (isset($item['entity'])) {
  190. $return[] = $item['entity'];
  191. }
  192. return $return;
  193. });
  194. if ($new_entities) {
  195. if ($autocreate) {
  196. $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities);
  197. $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
  198. }
  199. else {
  200. // If the selection handler does not support referencing newly
  201. // created entities, all of them should be invalidated.
  202. $invalid_new_entities = $new_entities;
  203. }
  204. foreach ($invalid_new_entities as $entity) {
  205. /** @var \Drupal\Core\Entity\EntityInterface $entity */
  206. $form_state->setError($element, t('This entity (%type: %label) cannot be referenced.', ['%type' => $element['#target_type'], '%label' => $entity->label()]));
  207. }
  208. }
  209. }
  210. // Use only the last value if the form element does not support multiple
  211. // matches (tags).
  212. if (!$element['#tags'] && !empty($value)) {
  213. $last_value = $value[count($value) - 1];
  214. $value = isset($last_value['target_id']) ? $last_value['target_id'] : $last_value;
  215. }
  216. }
  217. $form_state->setValueForElement($element, $value);
  218. }
  219. /**
  220. * Finds an entity from an autocomplete input without an explicit ID.
  221. *
  222. * The method will return an entity ID if one single entity unambiguously
  223. * matches the incoming input, and assign form errors otherwise.
  224. *
  225. * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler
  226. * Entity reference selection plugin.
  227. * @param string $input
  228. * Single string from autocomplete element.
  229. * @param array $element
  230. * The form element to set a form error.
  231. * @param \Drupal\Core\Form\FormStateInterface $form_state
  232. * The current form state.
  233. * @param bool $strict
  234. * Whether to trigger a form error if an element from $input (eg. an entity)
  235. * is not found.
  236. *
  237. * @return int|null
  238. * Value of a matching entity ID, or NULL if none.
  239. */
  240. protected static function matchEntityByTitle(SelectionInterface $handler, $input, array &$element, FormStateInterface $form_state, $strict) {
  241. $entities_by_bundle = $handler->getReferenceableEntities($input, '=', 6);
  242. $entities = array_reduce($entities_by_bundle, function ($flattened, $bundle_entities) {
  243. return $flattened + $bundle_entities;
  244. }, []);
  245. $params = [
  246. '%value' => $input,
  247. '@value' => $input,
  248. ];
  249. if (empty($entities)) {
  250. if ($strict) {
  251. // Error if there are no entities available for a required field.
  252. $form_state->setError($element, t('There are no entities matching "%value".', $params));
  253. }
  254. }
  255. elseif (count($entities) > 5) {
  256. $params['@id'] = key($entities);
  257. // Error if there are more than 5 matching entities.
  258. $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));
  259. }
  260. elseif (count($entities) > 1) {
  261. // More helpful error if there are only a few matching entities.
  262. $multiples = [];
  263. foreach ($entities as $id => $name) {
  264. $multiples[] = $name . ' (' . $id . ')';
  265. }
  266. $params['@id'] = $id;
  267. $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));
  268. }
  269. else {
  270. // Take the one and only matching entity.
  271. return key($entities);
  272. }
  273. }
  274. /**
  275. * Converts an array of entity objects into a string of entity labels.
  276. *
  277. * This method is also responsible for checking the 'view label' access on the
  278. * passed-in entities.
  279. *
  280. * @param \Drupal\Core\Entity\EntityInterface[] $entities
  281. * An array of entity objects.
  282. *
  283. * @return string
  284. * A string of entity labels separated by commas.
  285. */
  286. public static function getEntityLabels(array $entities) {
  287. /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
  288. $entity_repository = \Drupal::service('entity.repository');
  289. $entity_labels = [];
  290. foreach ($entities as $entity) {
  291. // Set the entity in the correct language for display.
  292. $entity = $entity_repository->getTranslationFromContext($entity);
  293. // Use the special view label, since some entities allow the label to be
  294. // viewed, even if the entity is not allowed to be viewed.
  295. $label = ($entity->access('view label')) ? $entity->label() : t('- Restricted access -');
  296. // Take into account "autocreated" entities.
  297. if (!$entity->isNew()) {
  298. $label .= ' (' . $entity->id() . ')';
  299. }
  300. // Labels containing commas or quotes must be wrapped in quotes.
  301. $entity_labels[] = Tags::encode($label);
  302. }
  303. return implode(', ', $entity_labels);
  304. }
  305. /**
  306. * Extracts the entity ID from the autocompletion result.
  307. *
  308. * @param string $input
  309. * The input coming from the autocompletion result.
  310. *
  311. * @return mixed|null
  312. * An entity ID or NULL if the input does not contain one.
  313. */
  314. public static function extractEntityIdFromAutocompleteInput($input) {
  315. $match = NULL;
  316. // Take "label (entity id)', match the ID from inside the parentheses.
  317. // @todo Add support for entities containing parentheses in their ID.
  318. // @see https://www.drupal.org/node/2520416
  319. if (preg_match("/.+\s\(([^\)]+)\)/", $input, $matches)) {
  320. $match = $matches[1];
  321. }
  322. return $match;
  323. }
  324. }