PageRenderTime 60ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php

https://gitlab.com/guillaumev/alkarama
PHP | 361 lines | 195 code | 40 blank | 126 comment | 36 complexity | 73c87d3120555af19106553cc5518cda MD5 | raw file
  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'] = array();
  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. // it's value is properly checked for access.
  38. $info['#process_default_value'] = TRUE;
  39. $info['#element_validate'] = array(array($class, 'validateEntityAutocomplete'));
  40. array_unshift($info['#process'], array($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'] = array($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'] = array(
  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 = array(
  135. 'target_type' => $element['#target_type'],
  136. 'handler' => $element['#selection_handler'],
  137. 'handler_settings' => $element['#selection_settings'],
  138. );
  139. /** @var /Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */
  140. $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options);
  141. $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface;
  142. // GET forms might pass the validated data around on the next request, in
  143. // which case it will already be in the expected format.
  144. if (is_array($element['#value'])) {
  145. $value = $element['#value'];
  146. }
  147. else {
  148. $input_values = $element['#tags'] ? Tags::explode($element['#value']) : array($element['#value']);
  149. foreach ($input_values as $input) {
  150. $match = static::extractEntityIdFromAutocompleteInput($input);
  151. if ($match === NULL) {
  152. // Try to get a match from the input string when the user didn't use
  153. // the autocomplete but filled in a value manually.
  154. $match = static::matchEntityByTitle($handler, $input, $element, $form_state, !$autocreate);
  155. }
  156. if ($match !== NULL) {
  157. $value[] = array(
  158. 'target_id' => $match,
  159. );
  160. }
  161. elseif ($autocreate) {
  162. /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface $handler */
  163. // Auto-create item. See an example of how this is handled in
  164. // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
  165. $value[] = array(
  166. 'entity' => $handler->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']),
  167. );
  168. }
  169. }
  170. }
  171. // Check that the referenced entities are valid, if needed.
  172. if ($element['#validate_reference'] && !empty($value)) {
  173. // Validate existing entities.
  174. $ids = array_reduce($value, function ($return, $item) {
  175. if (isset($item['target_id'])) {
  176. $return[] = $item['target_id'];
  177. }
  178. return $return;
  179. });
  180. if ($ids) {
  181. $valid_ids = $handler->validateReferenceableEntities($ids);
  182. if ($invalid_ids = array_diff($ids, $valid_ids)) {
  183. foreach ($invalid_ids as $invalid_id) {
  184. $form_state->setError($element, t('The referenced entity (%type: %id) does not exist.', array('%type' => $element['#target_type'], '%id' => $invalid_id)));
  185. }
  186. }
  187. }
  188. // Validate newly created entities.
  189. $new_entities = array_reduce($value, function ($return, $item) {
  190. if (isset($item['entity'])) {
  191. $return[] = $item['entity'];
  192. }
  193. return $return;
  194. });
  195. if ($new_entities) {
  196. if ($autocreate) {
  197. $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities);
  198. $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
  199. }
  200. else {
  201. // If the selection handler does not support referencing newly
  202. // created entities, all of them should be invalidated.
  203. $invalid_new_entities = $new_entities;
  204. }
  205. foreach ($invalid_new_entities as $entity) {
  206. /** @var \Drupal\Core\Entity\EntityInterface $entity */
  207. $form_state->setError($element, t('This entity (%type: %label) cannot be referenced.', array('%type' => $element['#target_type'], '%label' => $entity->label())));
  208. }
  209. }
  210. }
  211. // Use only the last value if the form element does not support multiple
  212. // matches (tags).
  213. if (!$element['#tags'] && !empty($value)) {
  214. $last_value = $value[count($value) - 1];
  215. $value = isset($last_value['target_id']) ? $last_value['target_id'] : $last_value;
  216. }
  217. }
  218. $form_state->setValueForElement($element, $value);
  219. }
  220. /**
  221. * Finds an entity from an autocomplete input without an explicit ID.
  222. *
  223. * The method will return an entity ID if one single entity unambuguously
  224. * matches the incoming input, and sill assign form errors otherwise.
  225. *
  226. * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler
  227. * Entity reference selection plugin.
  228. * @param string $input
  229. * Single string from autocomplete element.
  230. * @param array $element
  231. * The form element to set a form error.
  232. * @param \Drupal\Core\Form\FormStateInterface $form_state
  233. * The current form state.
  234. * @param bool $strict
  235. * Whether to trigger a form error if an element from $input (eg. an entity)
  236. * is not found.
  237. *
  238. * @return int|null
  239. * Value of a matching entity ID, or NULL if none.
  240. */
  241. protected static function matchEntityByTitle(SelectionInterface $handler, $input, array &$element, FormStateInterface $form_state, $strict) {
  242. $entities_by_bundle = $handler->getReferenceableEntities($input, '=', 6);
  243. $entities = array_reduce($entities_by_bundle, function ($flattened, $bundle_entities) {
  244. return $flattened + $bundle_entities;
  245. }, []);
  246. $params = array(
  247. '%value' => $input,
  248. '@value' => $input,
  249. );
  250. if (empty($entities)) {
  251. if ($strict) {
  252. // Error if there are no entities available for a required field.
  253. $form_state->setError($element, t('There are no entities matching "%value".', $params));
  254. }
  255. }
  256. elseif (count($entities) > 5) {
  257. $params['@id'] = key($entities);
  258. // Error if there are more than 5 matching entities.
  259. $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));
  260. }
  261. elseif (count($entities) > 1) {
  262. // More helpful error if there are only a few matching entities.
  263. $multiples = array();
  264. foreach ($entities as $id => $name) {
  265. $multiples[] = $name . ' (' . $id . ')';
  266. }
  267. $params['@id'] = $id;
  268. $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)".', array('%multiple' => implode('", "', $multiples)) + $params));
  269. }
  270. else {
  271. // Take the one and only matching entity.
  272. return key($entities);
  273. }
  274. }
  275. /**
  276. * Converts an array of entity objects into a string of entity labels.
  277. *
  278. * This method is also responsible for checking the 'view label' access on the
  279. * passed-in entities.
  280. *
  281. * @param \Drupal\Core\Entity\EntityInterface[] $entities
  282. * An array of entity objects.
  283. *
  284. * @return string
  285. * A string of entity labels separated by commas.
  286. */
  287. public static function getEntityLabels(array $entities) {
  288. $entity_labels = array();
  289. foreach ($entities as $entity) {
  290. // Use the special view label, since some entities allow the label to be
  291. // viewed, even if the entity is not allowed to be viewed.
  292. $label = ($entity->access('view label')) ? $entity->label() : t('- Restricted access -');
  293. // Take into account "autocreated" entities.
  294. if (!$entity->isNew()) {
  295. $label .= ' (' . $entity->id() . ')';
  296. }
  297. // Labels containing commas or quotes must be wrapped in quotes.
  298. $entity_labels[] = Tags::encode($label);
  299. }
  300. return implode(', ', $entity_labels);
  301. }
  302. /**
  303. * Extracts the entity ID from the autocompletion result.
  304. *
  305. * @param string $input
  306. * The input coming from the autocompletion result.
  307. *
  308. * @return mixed|null
  309. * An entity ID or NULL if the input does not contain one.
  310. */
  311. public static function extractEntityIdFromAutocompleteInput($input) {
  312. $match = NULL;
  313. // Take "label (entity id)', match the ID from inside the parentheses.
  314. // @todo Add support for entities containing parentheses in their ID.
  315. // @see https://www.drupal.org/node/2520416
  316. if (preg_match("/.+\s\(([^\)]+)\)/", $input, $matches)) {
  317. $match = $matches[1];
  318. }
  319. return $match;
  320. }
  321. }