diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index ab6b944..2690ffa 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -769,6 +769,7 @@ entity_reference_selection: target_bundles: type: sequence label: 'types' + nullable: true sequence: type: string label: 'Type' diff --git a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php index 88914c0..fb89063 100644 --- a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php +++ b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php @@ -186,4 +186,14 @@ public function setTypedConfig(TypedConfigManagerInterface $typed_config) { $this->typedConfig = $typed_config; } + /** + * Determines if this element allows NULL as a value. + * + * @return bool + * TRUE if NULL is a valid value, FALSE otherwise. + */ + public function isNullable() { + return isset($this->definition['nullable']) && $this->definition['nullable'] == TRUE; + } + } diff --git a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php index e1ce8dc..2193a55 100644 --- a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php +++ b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php @@ -106,9 +106,13 @@ protected function checkValue($key, $value) { (($type == 'double' || $type == 'integer') && $element instanceof FloatInterface) || ($type == 'boolean' && $element instanceof BooleanInterface) || ($type == 'string' && $element instanceof StringInterface) || - // Null values are allowed for all types. + // Null values are allowed for all primitive types. ($value === NULL); } + // Array elements can also opt-in for allowing a NULL value. + elseif ($element instanceof ArrayElement && $element->isNullable() && $value === NULL) { + $success = TRUE; + } $class = get_class($element); if (!$success) { return array($error_key => "variable type is $type but applied schema class is $class"); diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/SelectionBase.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/SelectionBase.php index c67dc68..3f8bccd 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/SelectionBase.php +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/SelectionBase.php @@ -111,7 +111,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta // Merge-in default values. $selection_handler_settings += array( - 'target_bundles' => array(), + // For the 'target_bundles' setting, a NULL value is equivalent to "allow + // entities from any bundle to be referenced" and an empty array value is + // equivalent to "no entities from any bundle can be referenced". + 'target_bundles' => NULL, 'sort' => array( 'field' => '_none', ), @@ -128,7 +131,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#type' => 'checkboxes', '#title' => $this->t('Bundles'), '#options' => $bundle_options, - '#default_value' => (!empty($selection_handler_settings['target_bundles'])) ? $selection_handler_settings['target_bundles'] : array(), + '#default_value' => (array) $selection_handler_settings['target_bundles'], '#required' => TRUE, '#size' => 6, '#multiple' => TRUE, @@ -207,7 +210,14 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta /** * {@inheritdoc} */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { } + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + // Normalize the value of the 'target_bundles' setting. In case it is an + // empty array we need to change it to NULL in order to allow the field to + // reference all bundles of the target entity type. + if ($form_state->getValue(['settings', 'handler_settings', 'target_bundles']) === []) { + $form_state->setValue(['settings', 'handler_settings', 'target_bundles'], NULL); + } + } /** * {@inheritdoc} @@ -326,6 +336,15 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') $entity_type = $this->entityManager->getDefinition($target_type); $query = $this->entityManager->getStorage($target_type)->getQuery(); + + // If the 'target_bundles' setting is an empty array, force the query to + // never return anything and bail out early. + if (isset($handler_settings['target_bundles']) && $handler_settings['target_bundles'] === []) { + $query->condition($entity_type->getKey('id'), NULL, '='); + + return $query; + } + if (!empty($handler_settings['target_bundles'])) { $query->condition($entity_type->getKey('bundle'), $handler_settings['target_bundles'], 'IN'); } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php index bb5e762..7aaf80d 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -284,9 +284,12 @@ public function hasNewEntity() { * {@inheritdoc} */ public static function calculateDependencies(FieldDefinitionInterface $field_definition) { - $dependencies = []; + $dependencies = parent::calculateDependencies($field_definition); + $manager = \Drupal::entityManager(); + $target_entity_type = $manager->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type')); + + // Depend on default values entity types configurations. if ($default_value = $field_definition->getDefaultValueLiteral()) { - $target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type')); foreach ($default_value as $value) { if (is_array($value) && isset($value['target_uuid'])) { $entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']); @@ -298,6 +301,19 @@ public static function calculateDependencies(FieldDefinitionInterface $field_def } } } + + // Depend on target bundle configurations. + $handler = $field_definition->getSetting('handler_settings'); + if (!empty($handler['target_bundles'])) { + if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { + if ($storage = $manager->getStorage($bundle_entity_type_id)) { + foreach ($storage->loadMultiple($handler['target_bundles']) as $bundle) { + $dependencies[$bundle->getConfigDependencyKey()][] = $bundle->getConfigDependencyName(); + } + } + } + } + return $dependencies; } @@ -305,12 +321,14 @@ public static function calculateDependencies(FieldDefinitionInterface $field_def * {@inheritdoc} */ public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) { - $changed = FALSE; + $changed = parent::calculateDependencies($field_definition, $dependencies); + $entity_manager = \Drupal::entityManager(); + $target_entity_type = $entity_manager->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type')); + if ($default_value = $field_definition->getDefaultValueLiteral()) { - $target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type')); foreach ($default_value as $key => $value) { if (is_array($value) && isset($value['target_uuid'])) { - $entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']); + $entity = $entity_manager->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']); // @see \Drupal\Core\Field\EntityReferenceFieldItemList::processDefaultValue() if ($entity && isset($dependencies[$entity->getConfigDependencyKey()][$entity->getConfigDependencyName()])) { unset($default_value[$key]); @@ -322,6 +340,39 @@ public static function onDependencyRemoval(FieldDefinitionInterface $field_defin $field_definition->setDefaultValue($default_value); } } + + $bundles_changed = FALSE; + $handler_settings = $field_definition->getSetting('handler_settings'); + if (!empty($handler_settings['target_bundles'])) { + if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { + if ($storage = $entity_manager->getStorage($bundle_entity_type_id)) { + foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) { + if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) { + unset($handler_settings['target_bundles'][$bundle->id()]); + $bundles_changed = TRUE; + + // In case we deleted the only target bundle allowed by the field we + // have to log a warning message because the field will not function + // correctly anymore. + if ($handler_settings['target_bundles'] === []) { + \Drupal::logger('entity_reference')->critical('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference field (entity_type: %entity_type, bundle: %bundle) no longer has any valid bundle it can reference. The field is not working correctly anymore and has to be adjusted.', [ + '%target_bundle' => $bundle->label(), + '%target_entity_type' => $bundle->getEntityType()->getBundleOf(), + '%field_name' => $field_definition->getName(), + '%entity_type' => $field_definition->getTargetEntityTypeId(), + '%bundle' => $field_definition->getTargetBundle() + ]); + } + } + } + } + } + } + if ($bundles_changed) { + $field_definition->setSetting('handler_settings', $handler_settings); + } + $changed |= $bundles_changed; + return $changed; } diff --git a/core/modules/entity_reference/entity_reference.module b/core/modules/entity_reference/entity_reference.module index 6ddade6..cd9e1d7 100644 --- a/core/modules/entity_reference/entity_reference.module +++ b/core/modules/entity_reference/entity_reference.module @@ -10,7 +10,6 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldConfig; use Drupal\field\FieldStorageConfigInterface; use Drupal\field\FieldConfigInterface; @@ -211,3 +210,78 @@ function entity_reference_query_entity_reference_alter(AlterableInterface $query $handler = $query->getMetadata('entity_reference_selection_handler'); $handler->entityQueryAlter($query); } + +/** + * Implements hook_entity_bundle_rename(). + */ +function entity_reference_entity_bundle_rename($entity_type_id, $bundle_old, $bundle_new) { + // Gather a list of all entity reference fields. + $map = \Drupal::entityManager()->getFieldMapByFieldType('entity_reference'); + $ids = []; + foreach ($map as $type => $info) { + foreach ($info as $name => $data) { + foreach ($data['bundles'] as $bundle) { + $ids[] = "$type.$bundle.$name"; + } + } + } + + // Update the 'target_bundles' handler setting if needed. + foreach (FieldConfig::loadMultiple($ids) as $field_config) { + if ($field_config->getSetting('target_type') == $entity_type_id) { + $handler_settings = $field_config->getSetting('handler_settings'); + if (isset($handler_settings['target_bundles'][$bundle_old])) { + unset($handler_settings['target_bundles'][$bundle_old]); + $handler_settings['target_bundles'][$bundle_new] = $bundle_new; + $field_config->setSetting('handler_settings', $handler_settings); + $field_config->save(); + } + } + } +} + +/** + * Implements hook_entity_bundle_delete(). + * + * We are duplicating the work done by + * \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onDependencyRemoval() + * because we need to take into account bundles that are not provided by a + * config entity type so they are not part of the config dependencies. + */ +function entity_reference_entity_bundle_delete($entity_type_id, $bundle) { + // Gather a list of all entity reference fields. + $map = \Drupal::entityManager()->getFieldMapByFieldType('entity_reference'); + $ids = []; + foreach ($map as $type => $info) { + foreach ($info as $name => $data) { + foreach ($data['bundles'] as $bundle) { + $ids[] = "$type.$bundle.$name"; + } + } + } + + // Update the 'target_bundles' handler setting if needed. + foreach (FieldConfig::loadMultiple($ids) as $field_config) { + if ($field_config->getSetting('target_type') == $entity_type_id) { + $handler_settings = $field_config->getSetting('handler_settings'); + if (isset($handler_settings['target_bundles'][$bundle])) { + unset($handler_settings['target_bundles'][$bundle]); + $field_config->setSetting('handler_settings', $handler_settings); + $field_config->save(); + + // In case we deleted the only target bundle allowed by the field we + // have to log a warning message because the field will not function + // correctly anymore. + if ($handler_settings['target_bundles'] === []) { + \Drupal::logger('entity_reference')->critical('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference field (entity_type: %entity_type, bundle: %bundle) no longer has any valid bundle it can reference. The field is not working correctly anymore and has to be adjusted.', [ + '%target_bundle' => $bundle, + '%target_entity_type' => $entity_type_id, + '%field_name' => $field_config->getName(), + '%entity_type' => $field_config->getTargetEntityTypeId(), + '%bundle' => $field_config->getTargetBundle() + ]); + } + } + } + } +} diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceSettingsTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceSettingsTest.php new file mode 100644 index 0000000..a7e6177 --- /dev/null +++ b/core/modules/entity_reference/src/Tests/EntityReferenceSettingsTest.php @@ -0,0 +1,96 @@ +installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + + $this->nodeType = NodeType::create([ + 'type' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + ]); + $this->nodeType->save(); + + $this->vocabulary = Vocabulary::create([ + 'vid' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + ]); + $this->vocabulary->save(); + } + + /** + * Tests that bundle changes are mirrored in field definitions. + */ + public function testTargetBundleChanges() { + // Attach an entity reference field to $this->nodeType. + $name = Unicode::strtolower($this->randomMachineName()); + $label = $this->randomString(); + $vid = $this->vocabulary->id(); + $handler_settings = ['target_bundles' => [$vid => $vid]]; + $this->createEntityReferenceField('node', $this->nodeType->id(), $name, $label, 'taxonomy_term', 'default', $handler_settings); + + // Change the vocabulary vid. + $new_vid = Unicode::strtolower($this->randomMachineName()); + $this->vocabulary->set('vid', $new_vid); + $this->vocabulary->save(); + + // Reload the field config and test the target vocabulary. + $field_config = FieldConfig::loadByName('node', $this->nodeType->id(), $name); + $handler_settings = $field_config->getSetting('handler_settings'); + $vid = key($handler_settings['target_bundles']); + $this->assertIdentical($vid, $new_vid); + + // Delete the vocabulary. + $this->vocabulary->delete(); + // Reload and test again. + $field_config = FieldConfig::loadByName('node', $this->nodeType->id(), $name); + $handler_settings = $field_config->getSetting('handler_settings'); + $this->assertTrue(empty($handler_settings['target_bundles'])); + } + +} diff --git a/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php b/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php index 274df65..46cc952 100644 --- a/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php +++ b/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php @@ -77,7 +77,7 @@ public function testNodeHandler() { 'target_type' => 'node', 'handler' => 'default', 'handler_settings' => array( - 'target_bundles' => array(), + 'target_bundles' => NULL, ), ); @@ -203,7 +203,7 @@ public function testUserHandler() { 'target_type' => 'user', 'handler' => 'default', 'handler_settings' => array( - 'target_bundles' => array(), + 'target_bundles' => NULL, 'include_anonymous' => TRUE, ), ); @@ -345,7 +345,7 @@ public function testCommentHandler() { 'target_type' => 'comment', 'handler' => 'default', 'handler_settings' => array( - 'target_bundles' => array(), + 'target_bundles' => NULL, ), ); diff --git a/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php b/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php index 98be261..3d0d4d1 100644 --- a/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php +++ b/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php @@ -100,7 +100,7 @@ public function testSort() { 'target_type' => 'node', 'handler' => 'default', 'handler_settings' => array( - 'target_bundles' => array(), + 'target_bundles' => NULL, // Add sorting. 'sort' => array( 'field' => 'field_text.value', diff --git a/core/modules/taxonomy/src/Entity/Vocabulary.php b/core/modules/taxonomy/src/Entity/Vocabulary.php index 2855c65..31eb26b 100644 --- a/core/modules/taxonomy/src/Entity/Vocabulary.php +++ b/core/modules/taxonomy/src/Entity/Vocabulary.php @@ -127,40 +127,6 @@ public function getDescription() { */ public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); - - if ($update && $this->getOriginalId() != $this->id() && !$this->isSyncing()) { - // Reflect machine name changes in the definitions of existing 'taxonomy' - // fields. - $field_ids = array(); - $field_map = \Drupal::entityManager()->getFieldMapByFieldType('entity_reference'); - foreach ($field_map as $entity_type => $field_storages) { - foreach ($field_storages as $field_storage => $info) { - $field_ids[] = $entity_type . '.' . $field_storage; - } - } - - $field_storages = \Drupal::entityManager()->getStorage('field_storage_config')->loadMultiple($field_ids); - $taxonomy_fields = array_filter($field_storages, function ($field_storage) { - return $field_storage->getType() == 'entity_reference' && $field_storage->getSetting('target_type') == 'taxonomy_term'; - }); - - foreach ($taxonomy_fields as $field_storage) { - $update_storage = FALSE; - - $allowed_values = $field_storage->getSetting('allowed_values'); - foreach ($allowed_values as &$value) { - if ($value['vocabulary'] == $this->getOriginalId()) { - $value['vocabulary'] = $this->id(); - $update_storage = TRUE; - } - } - $field_storage->setSetting('allowed_values', $allowed_values); - - if ($update_storage) { - $field_storage->save(); - } - } - } $storage->resetCache($update ? array($this->getOriginalId()) : array()); } diff --git a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php index 25e7393..f965149 100644 --- a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php +++ b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php @@ -96,6 +96,14 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta } /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + // Don't call the parent validation handler because we don't have any + // 'target_bundles' setting. + } + + /** * Initializes a view. * * @param string|null $match