diff --git a/config/schema/dynamic_entity_reference.schema.yml b/config/schema/dynamic_entity_reference.schema.yml index 1c6d275..9aabf7c 100644 --- a/config/schema/dynamic_entity_reference.schema.yml +++ b/config/schema/dynamic_entity_reference.schema.yml @@ -18,15 +18,18 @@ field.storage_settings.dynamic_entity_reference: label: 'Entity Type ID' field.field_settings.dynamic_entity_reference: - type: mapping + type: sequence label: 'Dynamic entity reference settings' - mapping: - handler: - type: string - label: 'Reference method' - handler_settings: - type: entity_reference.[%parent.handler].handler_settings - label: 'Reference method settings' + sequence: + - type: mapping + label: 'Entity Type ID' + mapping: + handler: + type: string + label: 'Reference method' + handler_settings: + type: entity_reference.[%parent.handler].handler_settings + label: 'Reference method settings' field.value.dynamic_entity_reference: type: mapping diff --git a/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php b/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php index e48bf98..0f64f86 100644 --- a/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php +++ b/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php @@ -1,4 +1,5 @@ TRUE, 'entity_type_ids' => array(), - ) + parent::defaultStorageSettings(); + ); } /** * {@inheritdoc} */ public static function defaultFieldSettings() { - return array( - 'handler' => 'default', - ) + parent::defaultFieldSettings(); + $default_settings = array(); + $labels = \Drupal::entityManager()->getEntityTypeLabels(TRUE); + $options = $labels['Content']; + // Field storage settings are not accessible here so we are assuming that + // all the entity types are referenceable by default. + // see https://www.drupal.org/node/2346273#comment-9385179 for more details. + foreach (array_keys($options) as $entity_type_id) { + $default_settings[$entity_type_id] = array( + 'handler' => 'default', + 'handler_settings' => array(), + ); + } + return $default_settings; } /** @@ -126,7 +139,6 @@ class DynamicEntityReferenceItem extends ConfigurableEntityReferenceItem { /** * {@inheritdoc} - * @todo update */ public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { // @todo inject this. @@ -155,7 +167,20 @@ class DynamicEntityReferenceItem extends ConfigurableEntityReferenceItem { * {@inheritdoc} */ public function fieldSettingsForm(array $form, FormStateInterface $form_state) { - return array(); + + $settings_form = array(); + $field = $form_state->get('field'); + $settings = $this->getSettings(); + $entity_type_ids = static::getAllEntityTypeIds($settings); + foreach (array_keys($entity_type_ids) as $entity_type_id) { + // We put the dummy value here so selection plugins can work. + $field->settings['target_type'] = $entity_type_id; + $field->settings['handler'] = $settings[$entity_type_id]['handler']; + $field->settings['handler_settings'] = $settings[$entity_type_id]['handler_settings']; + $settings_form[$entity_type_id] = parent::fieldSettingsForm($form, $form_state); + $settings_form[$entity_type_id]['handler']['#title'] = t('Reference type for @entity_type_id', array('@entity_type_id' => $entity_type_ids[$entity_type_id])); + } + return $settings_form; } /** @@ -165,11 +190,18 @@ class DynamicEntityReferenceItem extends ConfigurableEntityReferenceItem { * The form where the settings form is being included in. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state of the (entire) configuration form. - * @todo update */ public static function fieldSettingsFormValidate(array $form, FormStateInterface $form_state) { if ($form_state->hasValue('field')) { - $form_state->unsetValue(array('field', 'settings', 'handler_submit')); + $settings = $form_state->getValue(array('field', 'settings')); + foreach (array_keys($settings) as $entity_typ_id) { + $form_state->unsetValue(array( + 'field', + 'settings', + $entity_typ_id, + 'handler_submit', + )); + } $form_state->get('field')->settings = $form_state->getValue(array('field', 'settings')); } } @@ -212,9 +244,56 @@ class DynamicEntityReferenceItem extends ConfigurableEntityReferenceItem { // ::hasNewEntity() will return FALSE but $this->target_id will still be // empty. if ((empty($this->target_id) || empty($this->target_type)) && $this->entity) { - $this->target_id = $this->entity->id(); - $this->target_type = $this->entity->getEntityTypeId(); + $this->set('target_id', $this->entity->id(), FALSE); + $this->set('target_type', $this->entity->getEntityTypeId(), FALSE); + } + } + + /** + * Helper function to get all the entity type ids that can be referenced. + * + * @param array $settings + * The settings of the field storage. + * + * @return string[] + * All the entity type ids that can be referenced. + */ + protected static function getAllEntityTypeIds($settings) { + $labels = \Drupal::entityManager()->getEntityTypeLabels(TRUE); + $options = $labels['Content']; + + if ($settings['exclude_entity_types']) { + $entity_type_ids = array_diff_key($options, $settings['entity_type_ids'] ?: array()); + } + else { + $entity_type_ids = array_intersect_key($options, $settings['entity_type_ids'] ?: array()); + } + return $entity_type_ids; + } + + /** + * {@inheritdoc} + */ + public static function calculateDependencies(FieldDefinitionInterface $field_definition) { + $dependencies = []; + + if (is_array($field_definition->default_value) && count($field_definition->default_value)) { + $target_entity_types = static::getAllEntityTypeIds($field_definition->getFieldStorageDefinition()->getSettings()); + foreach ($target_entity_types as $target_entity_type) { + $key = $target_entity_type instanceof ConfigEntityType ? 'config' : 'content'; + foreach ($field_definition->default_value as $default_value) { + if (is_array($default_value) && isset($default_value['target_uuid'])) { + $entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $default_value['target_uuid']); + // If the entity does not exist do not create the dependency. + // @see \Drupal\Core\Field\EntityReferenceFieldItemList::processDefaultValue() + if ($entity) { + $dependencies[$key][] = $entity->getConfigDependencyName(); + } + } + } + } } + return $dependencies; } } diff --git a/src/Plugin/Field/FieldWidget/DynamicEntityReferenceWidget.php b/src/Plugin/Field/FieldWidget/DynamicEntityReferenceWidget.php index c197546..962ed3d 100644 --- a/src/Plugin/Field/FieldWidget/DynamicEntityReferenceWidget.php +++ b/src/Plugin/Field/FieldWidget/DynamicEntityReferenceWidget.php @@ -108,8 +108,10 @@ class DynamicEntityReferenceWidget extends AutocompleteWidget { * Checks whether a content entity is referenced. * * @param string $target_type - * The value target entity type + * The value target entity type. + * * @return bool + * TRUE if a content entity is referenced. */ protected function isContentReferenced($target_type = NULL) { $target_type_info = \Drupal::entityManager()->getDefinition($target_type); @@ -125,10 +127,17 @@ class DynamicEntityReferenceWidget extends AutocompleteWidget { if (!empty($element['#value'])) { // If this is the default value of the field. if ($form_state->hasValue('default_value_input')) { - $values = $form_state->getValue(array('default_value_input', $element['#field_name'], $element['#delta'])); + $values = $form_state->getValue(array( + 'default_value_input', + $element['#field_name'], + $element['#delta'], + )); } else { - $values = $form_state->getValue(array($element['#field_name'], $element['#delta'])); + $values = $form_state->getValue(array( + $element['#field_name'], + $element['#delta'], + )); } // Take "label (entity id)', match the id from parenthesis. if ($this->isContentReferenced($values['target_type']) && preg_match("/.+\((\d+)\)/", $element['#value'], $matches)) { @@ -137,10 +146,29 @@ class DynamicEntityReferenceWidget extends AutocompleteWidget { elseif (preg_match("/.+\(([\w.]+)\)/", $element['#value'], $matches)) { $value = $matches[1]; } - if (!$value) { - // Try to get a match from the input string when the user didn't use the - // autocomplete but filled in a value manually. - $value = $this->validateAutocompleteInput($values['target_type'], $element['#value'], $element, $form_state, $form); + $auto_create = $this->getHandlerSetting('auto_create', $values['target_type']); + // Try to get a match from the input string when the user didn't use the + // autocomplete but filled in a value manually. + if ($value === NULL) { + // To use entity_reference selection_handler for this target_type we + // have to change these settings to entity_reference field settings. + $this->fakeFieldSettings($values['target_type']); + /** @var \Drupal\entity_reference\Plugin\Type\Selection\SelectionInterface $handler */ + $handler = \Drupal::service('plugin.manager.entity_reference.selection')->getSelectionHandler($this->fieldDefinition); + $value = $handler->validateAutocompleteInput($element['#value'], $element, $form_state, $form, !$auto_create); + } + // Auto-create item. See + // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave(). + if (!$value && $auto_create && (count($this->getHandlerSetting('target_bundles', $values['target_type'])) == 1)) { + $value = array( + 'target_id' => NULL, + 'entity' => $this->createNewEntity($element['#value'], $element['#autocreate_uid']), + // Keep the weight property. + '_weight' => $element['#weight'], + ); + // Change the element['#parents'], so in form_set_value() we populate + // the correct key. + array_pop($element['#parents']); } } @@ -148,55 +176,37 @@ class DynamicEntityReferenceWidget extends AutocompleteWidget { } /** - * {@inheritdoc} + * Sets the fake field settings values. + * + * These settings are required by + * \Drupal\entity_reference\Plugin\Type\SelectionPluginManager to select the + * proper selection plugin and these settings are also used by + * \Drupal\entity_reference\Plugin\entity_reference\selection\SelectionBase + * + * @param string $entity_type_id + * The id of the entity type. */ - public function validateAutocompleteInput($target_type, $input, &$element, FormStateInterface $form_state, $form) { - // @todo Make this a service. - $controller = new DynamicEntityReferenceController(\Drupal::service('entity.query')); - $bundled_entities = $controller->getReferenceableEntities($target_type, $input, '=', 6); - $params = array( - '%value' => $input, - '@value' => $input, - ); - $entities = array(); - foreach ($bundled_entities as $entities_list) { - $entities += $entities_list; - } - if (empty($entities)) { - // 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; - $params['%multiple'] = implode('", "', $multiples); - $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)".', $params)); - } - else { - // Take the one and only matching entity. - return key($entities); - } + protected function fakeFieldSettings($entity_type_id) { + $settings = $this->getFieldSettings(); + $this->fieldDefinition->settings['target_type'] = $entity_type_id; + $this->fieldDefinition->settings['handler'] = $settings[$entity_type_id]['handler']; + $this->fieldDefinition->settings['handler_settings'] = $settings[$entity_type_id]['handler_settings']; } /** - * {@inheritdoc} + * Returns the value of a setting for the entity reference selection handler. + * + * @param string $setting_name + * The setting name. + * @param string $entity_type_id + * The id of the entity type. + * + * @return mixed + * The setting value. */ - protected function getEntityType(FieldItemListInterface $items, $delta) { - // The autocomplete widget outputs one entity label per form element. - if (isset($items[$delta])) { - return $items[$delta]->target_type; - } - - return FALSE; + protected function getHandlerSetting($setting_name, $entity_type_id) { + $settings = $this->getFieldSettings(); + return isset($settings[$entity_type_id]['handler_settings'][$setting_name]) ? $settings[$entity_type_id]['handler_settings'][$setting_name] : NULL; } } diff --git a/src/Tests/DynamicEntityReferenceSchemaTest.php b/src/Tests/DynamicEntityReferenceSchemaTest.php index 3fa73b1..221b72f 100644 --- a/src/Tests/DynamicEntityReferenceSchemaTest.php +++ b/src/Tests/DynamicEntityReferenceSchemaTest.php @@ -60,7 +60,20 @@ class DynamicEntityReferenceSchemaTest extends EntityUnitTestBase { 'entity_type' => 'entity_test', 'field_name' => 'field_test', 'bundle' => 'entity_test', - 'settings' => array(), + 'settings' => array( + 'entity_test' => array( + 'handler' => 'default', + 'handler_settings' => array( + 'target_bundles' => array( + 'entity_test' => 'entity_test', + ), + 'sort' => array( + 'field' => '_none', + ), + 'auto_create' => FALSE, + ), + ), + ), )); $entity_storage= \Drupal::entityManager()->getStorage('entity_test'); $referenced_entity = $entity_storage->create(array()); diff --git a/src/Tests/DynamicEntityReferenceTest.php b/src/Tests/DynamicEntityReferenceTest.php index 37a96a6..dbb8818 100644 --- a/src/Tests/DynamicEntityReferenceTest.php +++ b/src/Tests/DynamicEntityReferenceTest.php @@ -7,9 +7,14 @@ namespace Drupal\dynamic_entity_reference\Tests; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Language\LanguageInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\simpletest\WebTestBase; +use Drupal\taxonomy\Entity\Term; +use Drupal\taxonomy\Entity\Vocabulary; use Symfony\Component\CssSelector\CssSelector; /** @@ -72,13 +77,31 @@ class DynamicEntityReferenceTest extends WebTestBase { ); $this->drupalPostForm(NULL, $edit, t('Save')); $this->drupalPostForm(NULL, array( - 'field_storage[cardinality]' => '-1', + 'field_storage[cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 'field_storage[settings][entity_type_ids][]' => 'user', ), t('Save field settings')); $this->assertFieldByName('default_value_input[field_foobar][0][target_type]'); $this->assertFieldByXPath(CssSelector::toXPath('select[name="default_value_input[field_foobar][0][target_type]"] > option[value=entity_test]'), 'entity_test'); $this->assertNoFieldByXPath(CssSelector::toXPath('select[name="default_value_input[field_foobar][0][target_type]"] > option[value=user]'), 'user'); - $this->drupalPostForm(NULL, array(), t('Save settings')); + $edit = array( + 'field[settings][entity_test_label][handler_settings][target_bundles][entity_test_label]' => TRUE, + 'field[settings][entity_test_no_id][handler_settings][target_bundles][entity_test_no_id]' => TRUE, + 'field[settings][entity_test_no_label][handler_settings][target_bundles][entity_test_no_label]' => TRUE, + 'field[settings][entity_test_label_callback][handler_settings][target_bundles][entity_test_label_callback]' => TRUE, + 'field[settings][entity_test][handler_settings][target_bundles][entity_test]' => TRUE, + 'field[settings][entity_test_base_field_display][handler_settings][target_bundles][entity_test_base_field_display]' => TRUE, + 'field[settings][entity_test_mul][handler_settings][target_bundles][entity_test_mul]' => TRUE, + 'field[settings][entity_test_rev][handler_settings][target_bundles][entity_test_rev]' => TRUE, + 'field[settings][entity_test_mulrev][handler_settings][target_bundles][entity_test_mulrev]' => TRUE, + 'field[settings][entity_test_constraint_violation][handler_settings][target_bundles][entity_test_constraint_violation]' => TRUE, + 'field[settings][entity_test_field_override][handler_settings][target_bundles][entity_test_field_override]' => TRUE, + 'field[settings][entity_test_default_value][handler_settings][target_bundles][entity_test_default_value]' => TRUE, + 'field[settings][entity_test_update][handler_settings][target_bundles][entity_test_update]' => TRUE, + 'field[settings][entity_test_default_access][handler_settings][target_bundles][entity_test_default_access]' => TRUE, + 'field[settings][entity_test_cache][handler_settings][target_bundles][entity_test_cache]' => TRUE, + 'field[settings][entity_test_string_id][handler_settings][target_bundles][entity_test_string_id]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, t('Save settings')); $this->assertRaw(t('Saved %name configuration', array('%name' => 'Foobar'))); $excluded_entity_type_ids = FieldStorageConfig::loadByName('entity_test', 'field_foobar') ->getSetting('entity_type_ids'); @@ -87,7 +110,7 @@ class DynamicEntityReferenceTest extends WebTestBase { // Check the include entity settings. $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.field_foobar/storage'); $this->drupalPostForm(NULL, array( - 'field_storage[cardinality]' => '-1', + 'field_storage[cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 'field_storage[settings][exclude_entity_types]' => FALSE, 'field_storage[settings][entity_type_ids][]' => 'user', ), t('Save field settings')); @@ -101,7 +124,7 @@ class DynamicEntityReferenceTest extends WebTestBase { ->getSetting('entity_type_ids'); $this->assertNotNull($excluded_entity_type_ids); $this->assertIdentical(array_keys($excluded_entity_type_ids), array('user')); - + // Check the default settings. $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.field_foobar'); $this->drupalPostForm(NULL, array( 'default_value_input[field_foobar][0][target_type]' => 'user', @@ -127,10 +150,28 @@ class DynamicEntityReferenceTest extends WebTestBase { ); $this->drupalPostForm(NULL, $edit, t('Save')); $this->drupalPostForm(NULL, array( - 'field_storage[cardinality]' => '-1', + 'field_storage[cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, ), t('Save field settings')); - $this->drupalPostForm(NULL, array(), t('Save settings')); + $edit = array( + 'field[settings][entity_test_label][handler_settings][target_bundles][entity_test_label]' => TRUE, + 'field[settings][entity_test_no_id][handler_settings][target_bundles][entity_test_no_id]' => TRUE, + 'field[settings][entity_test_no_label][handler_settings][target_bundles][entity_test_no_label]' => TRUE, + 'field[settings][entity_test_label_callback][handler_settings][target_bundles][entity_test_label_callback]' => TRUE, + 'field[settings][entity_test][handler_settings][target_bundles][entity_test]' => TRUE, + 'field[settings][entity_test_base_field_display][handler_settings][target_bundles][entity_test_base_field_display]' => TRUE, + 'field[settings][entity_test_mul][handler_settings][target_bundles][entity_test_mul]' => TRUE, + 'field[settings][entity_test_rev][handler_settings][target_bundles][entity_test_rev]' => TRUE, + 'field[settings][entity_test_mulrev][handler_settings][target_bundles][entity_test_mulrev]' => TRUE, + 'field[settings][entity_test_constraint_violation][handler_settings][target_bundles][entity_test_constraint_violation]' => TRUE, + 'field[settings][entity_test_field_override][handler_settings][target_bundles][entity_test_field_override]' => TRUE, + 'field[settings][entity_test_default_value][handler_settings][target_bundles][entity_test_default_value]' => TRUE, + 'field[settings][entity_test_update][handler_settings][target_bundles][entity_test_update]' => TRUE, + 'field[settings][entity_test_default_access][handler_settings][target_bundles][entity_test_default_access]' => TRUE, + 'field[settings][entity_test_cache][handler_settings][target_bundles][entity_test_cache]' => TRUE, + 'field[settings][entity_test_string_id][handler_settings][target_bundles][entity_test_string_id]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, t('Save settings')); $this->assertRaw(t('Saved %name configuration', array('%name' => 'Foobar'))); // Create some items to reference. @@ -278,4 +319,77 @@ class DynamicEntityReferenceTest extends WebTestBase { $this->assertEqual($entity->field_foobar[1]->entity->label(), 'Bazbar'); } + /** + * Tests entity auto creation using dynamic entity reference. + */ + public function testDynamicEntityReferenceAutoCreate() { + \Drupal::service('module_installer')->install(array('taxonomy')); + $vocabulary = Vocabulary::create(array( + 'name' => $this->randomMachineName(), + 'vid' => Unicode::strtolower($this->randomMachineName()), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + )); + $vocabulary->save(); + $term = Term::create(array( + 'name' => $this->randomMachineName(), + 'vid' => $vocabulary->id(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + )); + $term->save(); + $this->drupalLogin($this->adminUser); + // Add a new dynamic entity reference field. + $this->drupalGet('entity_test/structure/entity_test/fields'); + $edit = array( + 'fields[_add_new_field][label]' => 'Foobar', + 'fields[_add_new_field][field_name]' => 'foobar', + 'fields[_add_new_field][type]' => 'dynamic_entity_reference', + ); + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->drupalPostForm(NULL, array( + 'field_storage[cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'field_storage[settings][exclude_entity_types]' => FALSE, + 'field_storage[settings][entity_type_ids][]' => array('taxonomy_term', 'user'), + ), t('Save field settings')); + $edit = array( + 'field[settings][taxonomy_term][handler_settings][target_bundles]['. $vocabulary->id() .']' => $vocabulary->id(), + 'field[settings][taxonomy_term][handler_settings][auto_create]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, t('Save settings')); + $this->drupalGet('entity_test/add'); + + // Add some extra dynamic entity reference fields. + $this->drupalPostAjaxForm(NULL, array(), array('field_foobar_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'entity-test-entity-test-form'); + $this->drupalPostAjaxForm(NULL, array(), array('field_foobar_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'entity-test-entity-test-form'); + $edit = array( + 'field_foobar[0][target_id]' => $this->adminUser->label() . ' (' . $this->adminUser->id() . ')', + 'field_foobar[0][target_type]' => 'user', + // Add a non-existing term. + 'field_foobar[1][target_id]' => 'tag', + 'field_foobar[1][target_type]' => 'taxonomy_term', + 'field_foobar[2][target_id]' => $term->label() . ' (' . $term->id() . ')', + 'field_foobar[2][target_type]' => 'taxonomy_term', + 'name[0][value]' => 'Barfoo', + 'user_id[0][target_id]' => $this->adminUser->label() . ' (' . $this->adminUser->id() . ')', + ); + + $this->drupalPostForm(NULL, $edit, t('Save')); + $entities = entity_load_multiple_by_properties('entity_test', array( + 'name' => 'Barfoo', + )); + $this->assertEqual(1, count($entities), 'Entity was saved'); + $entity = reset($entities); + + $this->assertEqual(count($entity->field_foobar), 3, 'Three items in field'); + $this->assertEqual($entity->field_foobar[0]->entity->label(), $this->adminUser->label()); + $this->assertEqual($entity->field_foobar[1]->entity->label(), 'tag'); + $this->assertEqual($entity->field_foobar[2]->entity->label(), $term->label()); + + $this->drupalGet('entity_test/' . $entity->id()); + $this->assertText('Barfoo'); + $this->assertText($this->adminUser->label()); + $this->assertText('tag'); + $this->assertText($term->label()); + + } + }