diff --git a/core/lib/Drupal/Core/Plugin/DependentWithRemovalPluginInterface.php b/core/lib/Drupal/Core/Plugin/DependentWithRemovalPluginInterface.php new file mode 100644 index 0000000..a123f8a --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/DependentWithRemovalPluginInterface.php @@ -0,0 +1,31 @@ +drupalGet("$bundle_path2/fields/node.$type_name2.$field_name/delete"); - $this->assertText(t('The listed configuration will be deleted.')); + $this->assertText(t('The listed configuration will be updated.')); $this->assertText(t('View')); $this->assertText('test_view_field_delete'); $xml = $this->cssSelect('#edit-entity-deletes'); - // Remove the wrapping HTML. - $this->assertIdentical(FALSE, strpos($xml[0]->asXml(), $field_label), 'The currently being deleted field is not shown in the entity deletions.'); + // Test that nothing is scheduled for deletion. + $this->assertFalse(isset($xml[0]), 'The field currently being deleted is not shown in the entity deletions.'); // Delete the second field. $this->fieldUIDeleteField($bundle_path2, "node.$type_name2.$field_name", $field_label, $type_name2); @@ -106,6 +107,11 @@ function testDeleteField() { $this->assertNull(FieldConfig::loadByName('node', $type_name2, $field_name), 'Field was deleted.'); // Check that the field storage was deleted too. $this->assertNull(FieldStorageConfig::loadByName('node', $field_name), 'Field storage was deleted.'); + + // Test that the View isn't deleted and has been disabled. + $view = View::load('test_view_field_delete'); + $this->assertNotNull($view); + $this->assertFalse($view->status()); } } diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index 1e9fac1..6bda25a 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -7,6 +7,7 @@ use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Plugin\DependentWithRemovalPluginInterface; use Drupal\views\Views; use Drupal\views\ViewEntityInterface; @@ -468,4 +469,88 @@ public function invalidateCaches() { \Drupal::service('cache_tags.invalidator')->invalidateTags($tags); } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = FALSE; + + // Don't intervene if the views module is removed. + if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) { + return FALSE; + } + + // If the base table for the View is provided by a module being removed, we + // delete the View because this is not something that can be fixed manually. + $views_data = Views::viewsData(); + $base_table = $this->get('base_table'); + $base_table_data = $views_data->get($base_table); + if (!empty($base_table_data['table']['provider']) && in_array($base_table_data['table']['provider'], $dependencies['module'])) { + return FALSE; + } + + $current_display = $this->getExecutable()->current_display; + $handler_types = Views::getHandlerTypes(); + + // Check if the handlers have resolved all removed dependencies. + if ($changed) { + // Force the displays to be reinitialised to use the changed settings. + $this->getExecutable()->current_display = NULL; + $this->calculateDependencies(); + $new_dependencies = $this->getDependencies(); + foreach ($dependencies as $group => $dependency_list) { + foreach ($dependency_list as $config_key) { + if (isset($new_dependencies[$group]) && array_key_exists($config_key, $new_dependencies[$group])) { + // If any of the dependencies still exist in the new dependencies we + // will disable the view. + $disable = TRUE; + break 2; + } + } + } + } + + // Disable and log a message that the View was disabled if made changes and + // if we have resolved all dependencies. + // @todo https://www.drupal.org/node/2832558 Give better feedback for + // disabled config. + if ($changed) { + $disable = TRUE; + // If any of the dependencies still exist, the View will be deleted so + // there is no need to disable the View or log a message. + $this->getExecutable()->current_display = NULL; + $this->calculateDependencies(); + $new_dependencies = $this->getDependencies(); + foreach ($dependencies as $group => $dependency_list) { + foreach ($dependency_list as $config_key) { + if (isset($new_dependencies[$group]) && array_key_exists($config_key, $new_dependencies[$group])) { + $disable = FALSE; + break 2; + } + } + } + if ($disable) { + $this->disable(); + $arguments = [ + '@id' => $this->id(), + ]; + $this->getLogger() + ->warning("View '@id': View was disabled because its settings depend on removed dependencies.", $arguments); + } + } + + $this->getExecutable()->setDisplay($current_display); + return $changed; + } + + /** + * Provides the 'system' channel logger service. + * + * @return \Psr\Log\LoggerInterface + * The 'system' channel logger. + */ + protected function getLogger() { + return \Drupal::logger('system'); + } + } diff --git a/core/modules/views/src/Plugin/views/field/EntityField.php b/core/modules/views/src/Plugin/views/field/EntityField.php index 4c8032d..0d48653 100644 --- a/core/modules/views/src/Plugin/views/field/EntityField.php +++ b/core/modules/views/src/Plugin/views/field/EntityField.php @@ -14,6 +14,7 @@ use Drupal\Core\Form\FormHelper; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Plugin\DependentWithRemovalPluginInterface; use Drupal\Core\Plugin\PluginDependencyTrait; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; @@ -34,7 +35,7 @@ * * @ViewsField("field") */ -class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface { +class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface, DependentWithRemovalPluginInterface { use FieldAPIHandlerTrait; use PluginDependencyTrait; @@ -1070,4 +1071,29 @@ public function getValue(ResultRow $values, $field = NULL) { } } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + // See if this handler is responsible for any of the dependencies being + // removed. If this is the case, indicate that this handler needs to be + // removed from the View. + $remove = FALSE; + // Get all the current dependencies for this handler. + $current_dependencies = $this->calculateDependencies(); + foreach ($current_dependencies as $group => $dependency_list) { + // Check if any of the handler dependencies match the dependencies being + // removed. + foreach ($dependency_list as $config_key) { + if (isset($dependencies[$group]) && array_key_exists($config_key, $dependencies[$group])) { + // This handlers dependency matches a dependency being removed, + // indicate that this handler needs to be removed. + $remove = TRUE; + break 2; + } + } + } + return $remove; + } + } diff --git a/core/modules/views/tests/src/Kernel/ViewsConfigDependenciesIntegrationTest.php b/core/modules/views/tests/src/Kernel/ViewsConfigDependenciesIntegrationTest.php index f5a2e39..dc51afc 100644 --- a/core/modules/views/tests/src/Kernel/ViewsConfigDependenciesIntegrationTest.php +++ b/core/modules/views/tests/src/Kernel/ViewsConfigDependenciesIntegrationTest.php @@ -5,6 +5,7 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\image\Entity\ImageStyle; +use Drupal\user\Entity\Role; use Drupal\views\Entity\View; /** @@ -17,7 +18,7 @@ class ViewsConfigDependenciesIntegrationTest extends ViewsKernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['field', 'file', 'image', 'entity_test']; + public static $modules = ['field', 'file', 'image', 'entity_test', 'user', 'text']; /** * {@inheritdoc} @@ -25,6 +26,16 @@ class ViewsConfigDependenciesIntegrationTest extends ViewsKernelTestBase { public static $testViews = ['entity_test_fields']; /** + * {@inheritdoc} + */ + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); + + $this->installEntitySchema('user'); + $this->installSchema('user', ['users_data']); + } + + /** * Tests integration with image module. */ public function testImage() { @@ -69,7 +80,77 @@ public function testImage() { // Delete the 'foo' image style. $style->delete(); + $view = View::load('entity_test_fields'); + + // Checks that the view has not been deleted too. + $this->assertNotNull(View::load('entity_test_fields')); + + // Checks that the image field was removed from the View. + $display = $view->getDisplay('default'); + $this->assertFalse(isset($display['display_options']['fields']['bar'])); + + // Checks that the view has been disabled. + $this->assertFalse($view->status()); + } + + /** + * Tests removing a config dependency that deletes the View. + */ + public function testConfigRemovalRole() { + // Create a role we can add to the View and delete. + $role = Role::create(array( + 'id' => 'dummy', + 'label' => 'dummy', + )); + + $role->save(); + + /** @var \Drupal\views\ViewEntityInterface $view */ + $view = View::load('entity_test_fields'); + $display = &$view->getDisplay('default'); + + // Set the access to be restricted by the dummy role. + $display['display_options']['access'] = [ + 'type' => 'role', + 'options' => [ + 'role' => [ + $role->id() => $role->id(), + ], + ], + ]; + $view->save(); + + // Check that the View now has a dependency on the Role. + $dependencies = $view->getDependencies() + ['config' => []]; + $this->assertTrue(in_array('user.role.dummy', $dependencies['config'])); + + // Delete the role. + $role->delete(); + + $view = View::load('entity_test_fields'); + // Checks that the view has been deleted too. + $this->assertNull($view); + } + + /** + * Tests uninstalling a module that provides a base table for a View. + */ + public function testConfigRemovalBaseTable() { + // Find all the entity types provided by the entity_test module and install + // the schema for them so we can uninstall them. + $entities = \Drupal::entityTypeManager()->getDefinitions(); + foreach ($entities as $entity_type_id => $definition) { + if ($definition->getProvider() == 'entity_test') { + $this->installEntitySchema($entity_type_id); + }; + } + + // Check that removing the module that provides the base table for a View, + // deletes the View. + $this->assertNotNull(View::load('entity_test_fields')); + $this->container->get('module_installer')->uninstall(['entity_test']); + // Check that the View has been deleted. $this->assertNull(View::load('entity_test_fields')); }