Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 162 |
| EntityAutocomplete | |
0.00% |
0 / 1 |
|
0.00% |
0 / 8 |
2756 | |
0.00% |
0 / 162 |
| getInfo | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 13 |
|||
| valueCallback | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 13 |
|||
| processEntityAutocomplete | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 24 |
|||
| validateEntityAutocomplete | |
0.00% |
0 / 1 |
462 | |
0.00% |
0 / 65 |
|||
| anonymous function | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
| matchEntityByTitle | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 1 |
|||
| getEntityLabels | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 10 |
|||
| extractEntityIdFromAutocompleteInput | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 9 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\Core\Entity\Element\EntityAutocomplete. | |
| */ | |
| namespace Drupal\Core\Entity\Element; | |
| use Drupal\Component\Utility\Crypt; | |
| use Drupal\Component\Utility\Tags; | |
| use Drupal\Core\Entity\EntityInterface; | |
| use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface; | |
| use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface; | |
| use Drupal\Core\Form\FormStateInterface; | |
| use Drupal\Core\Render\Element\Textfield; | |
| use Drupal\Core\Site\Settings; | |
| /** | |
| * Provides an entity autocomplete form element. | |
| * | |
| * The #default_value accepted by this element is either an entity object or an | |
| * array of entity objects. | |
| * | |
| * @FormElement("entity_autocomplete") | |
| */ | |
| class EntityAutocomplete extends Textfield { | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getInfo() { | |
| $info = parent::getInfo(); | |
| $class = get_class($this); | |
| // Apply default form element properties. | |
| $info['#target_type'] = NULL; | |
| $info['#selection_handler'] = 'default'; | |
| $info['#selection_settings'] = array(); | |
| $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 | |
| // it's value is properly checked for access. | |
| $info['#process_default_value'] = TRUE; | |
| $info['#element_validate'] = array(array($class, 'validateEntityAutocomplete')); | |
| array_unshift($info['#process'], array($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 (!is_array($element['#default_value'])) { | |
| // Convert the default value into an array for easier processing in | |
| // static::getEntityLabels(). | |
| $element['#default_value'] = array($element['#default_value']); | |
| } | |
| if ($element['#default_value'] && !(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']); | |
| } | |
| } | |
| /** | |
| * 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'] = array( | |
| '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 = array( | |
| 'target_type' => $element['#target_type'], | |
| 'handler' => $element['#selection_handler'], | |
| 'handler_settings' => $element['#selection_settings'], | |
| ); | |
| /** @var /Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */ | |
| $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options); | |
| $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface; | |
| $input_values = $element['#tags'] ? Tags::explode($element['#value']) : array($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[] = array( | |
| '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[] = array( | |
| '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.', array('%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.', array('%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 = array( | |
| '%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 = array(); | |
| 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)".', array('%multiple' => implode('", "', $multiples)))); | |
| } | |
| 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' 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) { | |
| $entity_labels = array(); | |
| foreach ($entities as $entity) { | |
| $label = ($entity->access('view')) ? $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 parenthesis when it's a | |
| // number. | |
| if (preg_match("/.+\s\((\d+)\)/", $input, $matches)) { | |
| $match = $matches[1]; | |
| } | |
| // Match the ID when it's a string (e.g. for config entity types). | |
| elseif (preg_match("/.+\s\(([\w.]+)\)/", $input, $matches)) { | |
| $match = $matches[1]; | |
| } | |
| return $match; | |
| } | |
| } |