diff --git a/core/lib/Drupal/Component/Plugin/DependencyRemovalPluginInterface.php b/core/lib/Drupal/Component/Plugin/DependencyRemovalPluginInterface.php new file mode 100644 index 0000000..4bdf9b9 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/DependencyRemovalPluginInterface.php @@ -0,0 +1,30 @@ + $dependencies) { - if ($removed_dependencies[$type]) { - // Config and content entities have the dependency names as keys while - // module and theme dependencies are indexed arrays of dependency names. - // @see \Drupal\Core\Config\ConfigManager::callOnDependencyRemoval() - if (in_array($type, ['config', 'content'])) { - $removed = array_intersect_key($removed_dependencies[$type], array_flip($dependencies)); - } - else { - $removed = array_values(array_intersect($removed_dependencies[$type], $dependencies)); - } - if ($removed) { - $intersect[$type] = $removed; - } - } - } - return $intersect; - } - - /** * Gets the default region. * * @return string diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php index cf9bb7e..202b7bc 100644 --- a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php @@ -76,9 +76,6 @@ public function getSelectionGroups($entity_type_id) { $plugins = []; $definitions = $this->getDefinitions(); - // Do not display the 'broken' plugin in the UI. - unset($definitions['broken']); - foreach ($definitions as $plugin_id => $plugin) { if (empty($plugin['entity_types']) || in_array($entity_type_id, $plugin['entity_types'])) { $plugins[$plugin['group']][$plugin_id] = $plugin; diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php index e8251e6..3a20dee 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php @@ -10,7 +10,8 @@ * * @EntityReferenceSelection( * id = "broken", - * label = @Translation("Broken/Missing") + * label = @Translation("Broken/Missing"), + * group = "broken" * ) */ class Broken extends SelectionPluginBase { diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php index 2764866..97c71e4 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php @@ -388,4 +388,82 @@ protected function reAlterQuery(AlterableInterface $query, $tag, $base_table) { $query->alterMetaData = $old_metadata; } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + + $target_entity_type = $this->entityManager->getDefinition($this->configuration['target_type']); + // Depend on target bundle configurations. Dependencies for 'target_bundles' + // also covers the 'auto_create_bundle' setting, if any, because its value + // is included in the 'target_bundles' list. + $handler_settings = $this->configuration['handler_settings']; + if (!empty($handler_settings['target_bundles'])) { + if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { + if ($storage = $this->entityManager->getStorage($bundle_entity_type_id)) { + foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) { + $dependencies[$bundle->getConfigDependencyKey()][] = $bundle->getConfigDependencyName(); + } + } + } + } + + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies, FieldDefinitionInterface $field_definition = NULL) { + $changed = parent::onDependencyRemoval($dependencies); + + // Update the 'target_bundles' handler setting if a bundle config dependency + // has been removed. + $configuration = $this->getConfiguration(); + $handler_settings = $configuration['handler_settings']; + if (!empty($handler_settings['target_bundles'])) { + $target_entity_type = $this->entityManager->getDefinition($this->configuration['target_type']); + if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { + if ($storage = $this->entityManager->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()]); + + // If this bundle is also used in the 'auto_create_bundle' + // setting, disable the auto-creation feature completely. + $auto_create_bundle = !empty($handler_settings['auto_create_bundle']) ? $handler_settings['auto_create_bundle'] : FALSE; + if ($auto_create_bundle && $auto_create_bundle == $bundle->id()) { + $handler_settings['auto_create'] = FALSE; + $handler_settings['auto_create_bundle'] = NULL; + } + } + } + + if ($handler_settings !== $configuration['handler_settings']) { + // Update the configuration only if handler settings were changed. + $configuration['handler_settings'] = $handler_settings; + $this->setConfiguration($configuration); + $changed = TRUE; + + // In case we deleted the only target bundle allowed by the field we + // have to log a critical message because the field will not + // function correctly anymore. + if (!empty($bundle) && ($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() + ]); + } + } + } + } + } + + return $changed; + } + } diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/SelectionPluginBase.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/SelectionPluginBase.php new file mode 100644 index 0000000..2617f91 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/SelectionPluginBase.php @@ -0,0 +1,134 @@ +setConfiguration($configuration); + $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager'), + $container->get('module_handler'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'target_type' => NULL, + 'handler' => NULL, + 'handler_settings' => $this->defaultHandlerSettings(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = NestedArray::mergeDeep( + $this->defaultConfiguration(), + $configuration + ); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + return FALSE; + } + + /** + * Returns the default handler settings. + * + * @return array + * + * @todo Add this to SelectionInterface in Drupal 9.0.x. + */ + public function defaultHandlerSettings() { + return []; + } + +} 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 70f9dc9..189d237 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -2,6 +2,8 @@ namespace Drupal\Core\Field\Plugin\Field\FieldType; +use Drupal\Component\Plugin\DependencyRemovalPluginInterface; +use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityInterface; @@ -14,6 +16,7 @@ use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\OptGroup; +use Drupal\Core\Plugin\PluginRemovedDependencyTrait; use Drupal\Core\Render\Element; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -40,6 +43,8 @@ */ class EntityReferenceItem extends FieldItemBase implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface { + use PluginRemovedDependencyTrait; + /** * {@inheritdoc} */ @@ -323,6 +328,10 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) { // Get all selection plugins for this entity type. $selection_plugins = \Drupal::service('plugin.manager.entity_reference_selection')->getSelectionGroups($this->getSetting('target_type')); + + // Do not display the 'broken' plugin in the UI. + unset($selection_plugins['broken']); + $handlers_options = []; foreach (array_keys($selection_plugins) as $selection_group_id) { // We only display base plugins (e.g. 'default', 'views', ...) and not @@ -431,18 +440,11 @@ public static function calculateDependencies(FieldDefinitionInterface $field_def } } - // Depend on target bundle configurations. Dependencies for 'target_bundles' - // also covers the 'auto_create_bundle' setting, if any, because its value - // is included in the 'target_bundles' list. - $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(); - } - } - } + $selection_handler = static::getSelectionManager()->getSelectionHandler($field_definition); + + // Let the selection handler plugin add its dependencies. + if ($selection_handler instanceof DependentPluginInterface) { + $dependencies += $selection_handler->calculateDependencies(); } return $dependencies; @@ -483,35 +485,28 @@ public static function onDependencyRemoval(FieldDefinitionInterface $field_defin } } - // Update the 'target_bundles' handler setting if a bundle config dependency - // has been removed. - $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()]); - - // If this bundle is also used in the 'auto_create_bundle' - // setting, disable the auto-creation feature completely. - $auto_create_bundle = !empty($handler_settings['auto_create_bundle']) ? $handler_settings['auto_create_bundle'] : FALSE; - if ($auto_create_bundle && $auto_create_bundle == $bundle->id()) { - $handler_settings['auto_create'] = NULL; - $handler_settings['auto_create_bundle'] = NULL; - } - - $bundles_changed = TRUE; - } - } + // Let the selection handler plugin react on dependency removal. + $handler = static::getSelectionManager()->getSelectionHandler($field_definition); + if ($handler instanceof DependencyRemovalPluginInterface) { + $handler_removed_dependencies = static::getPluginRemovedDependencies($handler->calculateDependencies(), $dependencies); + if ($handler_removed_dependencies) { + if ($handler->onDependencyRemoval($handler_removed_dependencies, $field_definition)) { + $field_definition->setSetting('handler_settings', $handler->getConfiguration()['handler_settings']); + $changed = TRUE; + } + if (static::getPluginRemovedDependencies($handler->calculateDependencies(), $dependencies)) { + /** @var \Drupal\field\Entity\FieldConfig $field_definition */ + $field_definition + ->setSetting('handler', 'broken') + ->setSetting('handler_settings', []); + $changed = TRUE; + $arguments = [ + '@field' => "{$field_definition->getTargetEntityTypeId()}.{$field_definition->getTargetBundle()}.{$field_definition->getName()}", + ]; + \Drupal::logger('entity_reference')->critical("Field @field selection handler settings have dependencies that were removed. The field is not working correctly anymore and has to be adjusted.", $arguments); } } } - if ($bundles_changed) { - $field_definition->setSetting('handler_settings', $handler_settings); - } - $changed |= $bundles_changed; return $changed; } @@ -655,4 +650,15 @@ public static function getPreconfiguredOptions() { return $options; } + /** + * Returns the selection plugin manager service. + * + * @return \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface + * + * @todo Properly inject this in Drupal 9.0.x. + */ + protected static function getSelectionManager() { + return \Drupal::service('plugin.manager.entity_reference_selection'); + } + } diff --git a/core/lib/Drupal/Core/Plugin/PluginRemovedDependencyTrait.php b/core/lib/Drupal/Core/Plugin/PluginRemovedDependencyTrait.php new file mode 100644 index 0000000..77c95c9 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/PluginRemovedDependencyTrait.php @@ -0,0 +1,52 @@ + $dependencies) { + if ($removed_dependencies[$type]) { + // Config and content entities have the dependency names as keys while + // module and theme dependencies are indexed arrays of dependency names. + // @see \Drupal\Core\Config\ConfigManager::callOnDependencyRemoval() + if (in_array($type, ['config', 'content'])) { + $removed = array_intersect_key($removed_dependencies[$type], array_flip($dependencies)); + } + else { + $removed = array_values(array_intersect($removed_dependencies[$type], $dependencies)); + } + if ($removed) { + $intersect[$type] = $removed; + } + } + } + return $intersect; + } + +} diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceDependencyTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceDependencyTest.php new file mode 100644 index 0000000..6337bb9 --- /dev/null +++ b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceDependencyTest.php @@ -0,0 +1,62 @@ +drupalLogin($this->drupalCreateUser(['access content'])); + + // Create an entity reference field using ViewsSelection handler. + $handler_settings = [ + 'view' => [ + 'view_name' => 'test_entity_reference', + 'display_name' => 'entity_reference_1', + 'arguments' => [], + ] + ]; + $this->createEntityReferenceField('entity_test', 'entity_test', 'reference', 'Referenced content', 'node', 'views', $handler_settings); + + /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */ + $selection_manager = $this->container->get('plugin.manager.entity_reference_selection'); + $field_config = FieldConfig::loadByName('entity_test', 'entity_test', 'reference'); + $entities = $selection_manager->getSelectionHandler($field_config)->getReferenceableEntities(); + + // Check that the selection handler returns an empty list of entities. + $this->assertSame([], $entities); + + // Delete the view that provides options for the entity reference field. + View::load('test_entity_reference')->delete(); + + // Re-load the field config. + $field_config = FieldConfig::loadByName('entity_test', 'entity_test', 'reference'); + + // Check that the entity reference field was not deleted. + $this->assertNotNull($field_config); + + // Check that the entity reference has 'broken' as handler. + $this->assertSame('broken', $field_config->getSetting('handler')); + $this->assertSame([], $field_config->getSetting('handler_settings')); + } + +} diff --git a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php index 9911f61..1cb3043 100644 --- a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php +++ b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php @@ -4,6 +4,7 @@ use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginBase; use Drupal\Core\Entity\EntityReferenceSelection\SelectionTrait; +use Drupal\Core\Field\FieldException; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Url; @@ -124,16 +125,21 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta * * @return bool * Return TRUE if the view was initialized, FALSE otherwise. + * + * @throws \Drupal\Core\Field\FieldException + * When the view or view display stored in entity reference field handler + * settings are missed or inaccessible. */ protected function initializeView($match = NULL, $match_operator = 'CONTAINS', $limit = 0, $ids = NULL) { $view_name = $this->getConfiguration()['view']['view_name']; $display_name = $this->getConfiguration()['view']['display_name']; // Check that the view is valid and the display still exists. - $this->view = Views::getView($view_name); - if (!$this->view || !$this->view->access($display_name)) { - drupal_set_message(t('The reference view %view_name cannot be found.', ['%view_name' => $view_name]), 'warning'); - return FALSE; + if (!$view_name || !($this->view = Views::getView($view_name))) { + throw new FieldException("The reference view $view_name cannot be found."); + } + elseif (!$display_name || !$this->view->access($display_name)) { + throw new FieldException("The reference display $view_name:$display_name doesn't exist or is inaccessible."); } $this->view->setDisplay($display_name); @@ -152,13 +158,13 @@ protected function initializeView($match = NULL, $match_operator = 'CONTAINS', $ * {@inheritdoc} */ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { + $this->initializeView($match, $match_operator, $limit); + $display_name = $this->getConfiguration()['view']['display_name']; $arguments = $this->getConfiguration()['view']['arguments']; - $result = []; - if ($this->initializeView($match, $match_operator, $limit)) { - // Get the results. - $result = $this->view->executeDisplay($display_name, $arguments); - } + + // Get the results. + $result = $this->view->executeDisplay($display_name, $arguments); $return = []; if ($result) { @@ -182,15 +188,14 @@ public function countReferenceableEntities($match = NULL, $match_operator = 'CON * {@inheritdoc} */ public function validateReferenceableEntities(array $ids) { + $this->initializeView(NULL, 'CONTAINS', 0, $ids); + $display_name = $this->getConfiguration()['view']['display_name']; $arguments = $this->getConfiguration()['view']['arguments']; - $result = []; - if ($this->initializeView(NULL, 'CONTAINS', 0, $ids)) { - // Get the results. - $entities = $this->view->executeDisplay($display_name, $arguments); - $result = array_keys($entities); - } - return $result; + + // Get the results. + $entities = $this->view->executeDisplay($display_name, $arguments); + return array_keys($entities); } /** @@ -222,4 +227,23 @@ public static function settingsFormValidate($element, FormStateInterface $form_s $form_state->setValueForElement($element, $value); } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + + try { + $this->initializeView(NULL, 'CONTAINS', 0); + $view = $this->view->storage; + // Add the view as dependency. + $dependencies[$view->getConfigDependencyKey()][] = $view->getConfigDependencyName(); + } + catch (\Exception $e) { + // Nothing to do. + } + + return $dependencies; + } + }