Issue #2468045 by Lendude, vaplas, geertvd, tim.plunkett, dawehner, xjm, cilefen, catch, Berdir, alexpott: When deleting a content type field, users do not realize the related View also is deleted
							parent
							
								
									e4e32ed659
								
							
						
					
					
						commit
						f7520a2969
					
				| 
						 | 
				
			
			@ -5,6 +5,7 @@ namespace Drupal\field_ui\Tests;
 | 
			
		|||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\simpletest\WebTestBase;
 | 
			
		||||
use Drupal\views\Entity\View;
 | 
			
		||||
use Drupal\views\Tests\ViewTestData;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +75,13 @@ class FieldUIDeleteTest extends WebTestBase {
 | 
			
		|||
    \Drupal::service('module_installer')->install(['views']);
 | 
			
		||||
    ViewTestData::createTestViews(get_class($this), ['field_test_views']);
 | 
			
		||||
 | 
			
		||||
    $view = View::load('test_view_field_delete');
 | 
			
		||||
    $this->assertNotNull($view);
 | 
			
		||||
    $this->assertTrue($view->status());
 | 
			
		||||
    // Test that the View depends on the field.
 | 
			
		||||
    $dependencies = $view->getDependencies() + ['config' => []];
 | 
			
		||||
    $this->assertTrue(in_array("field.storage.node.$field_name", $dependencies['config']));
 | 
			
		||||
 | 
			
		||||
    // Check the config dependencies of the first field, the field storage must
 | 
			
		||||
    // not be shown as being deleted yet.
 | 
			
		||||
    $this->drupalGet("$bundle_path1/fields/node.$type_name1.$field_name/delete");
 | 
			
		||||
| 
						 | 
				
			
			@ -91,13 +99,13 @@ class FieldUIDeleteTest extends WebTestBase {
 | 
			
		|||
 | 
			
		||||
    // Check the config dependencies of the first field.
 | 
			
		||||
    $this->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 +114,14 @@ class FieldUIDeleteTest extends WebTestBase {
 | 
			
		|||
    $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());
 | 
			
		||||
    // Test that the View no longer depends on the deleted field.
 | 
			
		||||
    $dependencies = $view->getDependencies() + ['config' => []];
 | 
			
		||||
    $this->assertFalse(in_array("field.storage.node.$field_name", $dependencies['config']));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ use Drupal\Core\Entity\ContentEntityTypeInterface;
 | 
			
		|||
use Drupal\Core\Entity\EntityStorageInterface;
 | 
			
		||||
use Drupal\Core\Entity\FieldableEntityInterface;
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
 | 
			
		||||
use Drupal\views\Views;
 | 
			
		||||
use Drupal\views\ViewEntityInterface;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -512,4 +513,61 @@ class View extends ConfigEntityBase implements ViewEntityInterface {
 | 
			
		|||
    \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();
 | 
			
		||||
 | 
			
		||||
    // Find all the handlers and check whether they want to do something on
 | 
			
		||||
    // dependency removal.
 | 
			
		||||
    foreach ($this->display as $display_id => $display_plugin_base) {
 | 
			
		||||
      $this->getExecutable()->setDisplay($display_id);
 | 
			
		||||
      $display = $this->getExecutable()->getDisplay();
 | 
			
		||||
 | 
			
		||||
      foreach (array_keys($handler_types) as $handler_type) {
 | 
			
		||||
        $handlers = $display->getHandlers($handler_type);
 | 
			
		||||
        foreach ($handlers as $handler_id => $handler) {
 | 
			
		||||
          if ($handler instanceof DependentWithRemovalPluginInterface) {
 | 
			
		||||
            if ($handler->onDependencyRemoval($dependencies)) {
 | 
			
		||||
              // Remove the handler and indicate we made changes.
 | 
			
		||||
              unset($this->display[$display_id]['display_options'][$handler_types[$handler_type]['plural']][$handler_id]);
 | 
			
		||||
              $changed = TRUE;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Disable the View if we made changes.
 | 
			
		||||
    // @todo https://www.drupal.org/node/2832558 Give better feedback for
 | 
			
		||||
    // disabled config.
 | 
			
		||||
    if ($changed) {
 | 
			
		||||
      // Force a recalculation of the dependencies if we made changes.
 | 
			
		||||
      $this->getExecutable()->current_display = NULL;
 | 
			
		||||
      $this->calculateDependencies();
 | 
			
		||||
      $this->disable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $this->getExecutable()->setDisplay($current_display);
 | 
			
		||||
    return $changed;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\views\Plugin;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides an interface for a plugin that has dependencies that can be removed.
 | 
			
		||||
 *
 | 
			
		||||
 * @ingroup views_plugins
 | 
			
		||||
 */
 | 
			
		||||
interface DependentWithRemovalPluginInterface {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Allows a plugin to define whether it should be removed.
 | 
			
		||||
   *
 | 
			
		||||
   * If this method returns TRUE then the plugin should be removed.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $dependencies
 | 
			
		||||
   *   An array of dependencies that will be deleted keyed by dependency type.
 | 
			
		||||
   *   Dependency types are, for example, entity, module and theme.
 | 
			
		||||
   *
 | 
			
		||||
   * @return bool
 | 
			
		||||
   *   TRUE if the plugin instance should be removed.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Core\Config\Entity\ConfigDependencyManager
 | 
			
		||||
   * @see \Drupal\Core\Config\ConfigEntityBase::preDelete()
 | 
			
		||||
   * @see \Drupal\Core\Config\ConfigManager::uninstall()
 | 
			
		||||
   * @see \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval()
 | 
			
		||||
   */
 | 
			
		||||
  public function onDependencyRemoval(array $dependencies);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ use Drupal\Core\TypedData\TypedDataInterface;
 | 
			
		|||
use Drupal\views\FieldAPIHandlerTrait;
 | 
			
		||||
use Drupal\views\Entity\Render\EntityFieldRenderer;
 | 
			
		||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
 | 
			
		||||
use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
 | 
			
		||||
use Drupal\views\ResultRow;
 | 
			
		||||
use Drupal\views\ViewExecutable;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerInterface;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +35,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 | 
			
		|||
 *
 | 
			
		||||
 * @ViewsField("field")
 | 
			
		||||
 */
 | 
			
		||||
class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface {
 | 
			
		||||
class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface, DependentWithRemovalPluginInterface {
 | 
			
		||||
 | 
			
		||||
  use FieldAPIHandlerTrait;
 | 
			
		||||
  use PluginDependencyTrait;
 | 
			
		||||
| 
						 | 
				
			
			@ -1077,4 +1078,29 @@ class EntityField extends FieldPluginBase implements CacheableDependencyInterfac
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ namespace Drupal\Tests\views\Kernel;
 | 
			
		|||
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,13 +18,23 @@ class ViewsConfigDependenciesIntegrationTest extends ViewsKernelTestBase {
 | 
			
		|||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static $modules = ['field', 'file', 'image', 'entity_test'];
 | 
			
		||||
  public static $modules = ['field', 'file', 'image', 'entity_test', 'user', 'text'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  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.
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +80,81 @@ class ViewsConfigDependenciesIntegrationTest extends ViewsKernelTestBase {
 | 
			
		|||
    // 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());
 | 
			
		||||
 | 
			
		||||
    $dependencies = $view->getDependencies() + ['config' => []];
 | 
			
		||||
    // Checks that the dependency on style 'foo' has been removed.
 | 
			
		||||
    $this->assertFalse(in_array('image.style.foo', $dependencies['config']));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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([
 | 
			
		||||
      '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'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue