diff --git a/core/lib/Drupal/Core/Entity/DependencyTrait.php b/core/lib/Drupal/Core/Entity/DependencyTrait.php index e4c8cef..638b010 100644 --- a/core/lib/Drupal/Core/Entity/DependencyTrait.php +++ b/core/lib/Drupal/Core/Entity/DependencyTrait.php @@ -74,4 +74,46 @@ protected function addDependencies(array $dependencies) { } } + /** + * Returns the plugin dependencies being removed. + * + * The function recursively computes the intersection between all plugin + * dependencies and all removed dependencies. + * + * Note: The two arguments do not have the same structure. + * + * @param array[] $plugin_dependencies + * A list of dependencies having the same structure as the return value of + * ConfigEntityInterface::calculateDependencies(). + * @param array[] $removed_dependencies + * A list of dependencies having the same structure as the input argument of + * ConfigEntityInterface::onDependencyRemoval(). + * + * @return array + * A recursively computed intersection. + * + * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies() + * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval() + */ + protected function getPluginRemovedDependencies(array $plugin_dependencies, array $removed_dependencies) { + $intersect = []; + foreach ($plugin_dependencies as $type => $dependencies) { + if (isset($removed_dependencies[$type]) && $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/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index b835327..0b1ff1d 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -468,48 +468,6 @@ public function onDependencyRemoval(array $dependencies) { } /** - * Returns the plugin dependencies being removed. - * - * The function recursively computes the intersection between all plugin - * dependencies and all removed dependencies. - * - * Note: The two arguments do not have the same structure. - * - * @param array[] $plugin_dependencies - * A list of dependencies having the same structure as the return value of - * ConfigEntityInterface::calculateDependencies(). - * @param array[] $removed_dependencies - * A list of dependencies having the same structure as the input argument of - * ConfigEntityInterface::onDependencyRemoval(). - * - * @return array - * A recursively computed intersection. - * - * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies() - * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval() - */ - protected function getPluginRemovedDependencies(array $plugin_dependencies, array $removed_dependencies) { - $intersect = []; - foreach ($plugin_dependencies as $type => $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; - } - - /** * {@inheritdoc} */ public function __sleep() { diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index e2a3436..1ae933d 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -292,6 +292,29 @@ public function calculateDependencies() { /** * {@inheritdoc} */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + + $executable = $this->getExecutable(); + $executable->initDisplay(); + $executable->initStyle(); + + /** @var \Drupal\Component\Plugin\DependentPluginInterface $display */ + foreach ($executable->displayHandlers as $display_id => &$display) { + $display_removed_dependencies = $this->getPluginRemovedDependencies($display->calculateDependencies(), $dependencies); + /** @var \Drupal\views\Plugin\views\ViewsHandlerDependencyInterface $display */ + if ($display_removed_dependencies && $display->onDependencyRemoval($display_removed_dependencies)) { + // The display was already updated. + $changed = TRUE; + } + } + + return $changed; + } + + /** + * {@inheritdoc} + */ public function preSave(EntityStorageInterface $storage) { parent::preSave($storage); diff --git a/core/modules/views/src/Plugin/views/HandlerBase.php b/core/modules/views/src/Plugin/views/HandlerBase.php index b5bb16d..7546fd4 100644 --- a/core/modules/views/src/Plugin/views/HandlerBase.php +++ b/core/modules/views/src/Plugin/views/HandlerBase.php @@ -26,7 +26,7 @@ * * @ingroup views_plugins */ -abstract class HandlerBase extends PluginBase implements ViewsHandlerInterface { +abstract class HandlerBase extends PluginBase implements ViewsHandlerInterface, ViewsHandlerDependencyInterface { /** * Where the $query object will reside: @@ -843,4 +843,12 @@ public function submitTemporaryForm($form, FormStateInterface $form_state) { // Write to cache $view->cacheSet(); } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + return FALSE; + } + } diff --git a/core/modules/views/src/Plugin/views/ViewsHandlerDependencyInterface.php b/core/modules/views/src/Plugin/views/ViewsHandlerDependencyInterface.php new file mode 100644 index 0000000..7a3a48b --- /dev/null +++ b/core/modules/views/src/Plugin/views/ViewsHandlerDependencyInterface.php @@ -0,0 +1,36 @@ +addDependencies(parent::calculateDependencies()); + $this->dependencies = parent::calculateDependencies(); // Collect all the dependencies of handlers and plugins. Only calculate // their dependencies if they are configured by this display. $plugins = array_merge($this->getAllHandlers(TRUE), $this->getAllPlugins(TRUE)); @@ -960,6 +962,55 @@ public function calculateDependencies() { return $this->dependencies; } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = FALSE; + + $types = Views::getHandlerTypes(); + foreach (array_keys($types) as $type) { + $option = $this->getOption($types[$type]['plural']); + /** @var \Drupal\Component\Plugin\DependentPluginInterface $handler */ + foreach ($this->getHandlers($type) as $handler_id => &$handler) { + $plugin_removed_dependencies = $this->getPluginRemovedDependencies($handler->calculateDependencies(), $dependencies); + if ($plugin_removed_dependencies) { + if ($handler->onDependencyRemoval($plugin_removed_dependencies)) { + // Update the handler. + $option[$handler_id] = $handler->options; + $changed = TRUE; + } + + // If there are still unresolved deleted dependencies left, remove + // this handler to avoid the removal of the entire view. + if ($this->getPluginRemovedDependencies($handler->calculateDependencies(), $dependencies)) { + unset($option[$handler_id], $this->handlers[$type][$handler_id]); + $arguments = [ + '@view' => $this->view->id(), + '@display' => $this->getPluginId(), + '@type' => $types[$type]['stitle'], + '@name' => $handler_id, + ]; + $this->getLogger()->warning("View '@view', display '@display': @type '@name' was removed because its settings depend on removed dependencies.", $arguments); + $changed = TRUE; + } + } + } + $this->setOption($types[$type]['plural'], $option); + } + + return $changed; + } + + /** + * Provides the 'views' channel logger service. + * + * @return \Psr\Log\LoggerInterface + * The 'views' channel logger. + */ + protected function getLogger() { + return \Drupal::logger('views'); + } /** * {@inheritdoc} diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php index 7077ed8..2bb335d 100644 --- a/core/modules/views/src/Plugin/views/field/Field.php +++ b/core/modules/views/src/Plugin/views/field/Field.php @@ -8,7 +8,6 @@ namespace Drupal\views\Plugin\views\field; use Drupal\Component\Plugin\DependentPluginInterface; -use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\Xss; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableDependencyInterface; @@ -476,14 +475,9 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $form['field_api_classes']['#description'] .= ' ' . $this->t('Checking this option will cause the group Display Type and Separator values to be ignored.'); } - // Get the currently selected formatter. - $format = $this->options['type']; - - $settings = $this->options['settings'] + $this->formatterPluginManager->getDefaultSettings($format); - // Get the settings form. $settings_form = array('#value' => array()); - if ($formatter = $this->getFormatterInstance($field, $format, $settings)) { + if ($formatter = $this->getFormatterInstance()) { $settings_form = $formatter->settingsForm($form, $form_state); // Convert field UI selector states to work in the Views field form. FormHelper::rewriteStatesSelector($settings_form, "fields[{$field->getName()}][settings_edit_form]", 'options'); @@ -945,26 +939,22 @@ protected function addSelfTokens(&$tokens, $item) { /** * Returns the field formatter instance. * - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The field definition - * @param string $formatter - * The formatter type. - * @param array $formatter_settings - * The formatter settings. - * * @return \Drupal\Core\Field\FormatterInterface|null + * The field formatter instance. */ - protected function getFormatterInstance(FieldDefinitionInterface $field_definition, $formatter, array $formatter_settings) { - $options = array( - 'field_definition' => $field_definition, + protected function getFormatterInstance() { + $settings = $this->options['settings'] + $this->formatterPluginManager->getDefaultSettings($this->options['type']); + + $options = [ + 'field_definition' => $this->getFieldDefinition(), 'configuration' => [ - 'type' => $formatter, - 'settings' => $formatter_settings, + 'type' => $this->options['type'], + 'settings' => $settings, 'label' => '', 'weight' => 0, ], 'view_mode' => '_custom', - ); + ]; return $this->formatterPluginManager->getInstance($options); } @@ -983,7 +973,7 @@ public function calculateDependencies() { if (!empty($this->options['type'])) { $this->dependencies['module'][] = $this->formatterPluginManager->getDefinition($this->options['type'])['provider']; - if (($formatter = $this->getFormatterInstance($this->getFieldDefinition(), $this->options['type'], $this->options['settings'])) && $formatter instanceof DependentPluginInterface) { + if (($formatter = $this->getFormatterInstance()) && $formatter instanceof DependentPluginInterface) { $this->calculatePluginDependencies($formatter); } } @@ -994,6 +984,20 @@ public function calculateDependencies() { /** * {@inheritdoc} */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + if (($formatter = $this->getFormatterInstance()) && $formatter instanceof DependentPluginInterface) { + if ($formatter->onDependencyRemoval($dependencies)) { + $this->options['settings'] = $formatter->getSettings(); + $changed = TRUE; + } + } + return $changed; + } + + /** + * {@inheritdoc} + */ public function getCacheMaxAge() { return Cache::PERMANENT; } diff --git a/core/modules/views/src/Tests/DependenciesTest.php b/core/modules/views/src/Tests/DependenciesTest.php deleted file mode 100644 index 4820086..0000000 --- a/core/modules/views/src/Tests/DependenciesTest.php +++ /dev/null @@ -1,137 +0,0 @@ -enableViewsTestModule(); - - // Create Basic page and Article node types. - if ($this->profile != 'standard') { - $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); - $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); - } - - $this->adminUser = $this->drupalCreateUser(['access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer node fields', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles', 'administer node display']); - $this->drupalLogin($this->adminUser); - - // Create an image field that uses the new style. - $field_name = 'field_image'; - $this->createImageField($field_name, 'article'); - - ViewTestData::createTestViews(get_class($this), ['views_test_config']); - } - - /** - * Tests Image Style Dependency. - */ - public function testImageStyleDependency() { - $this->drupalGet('admin/config/media/image-styles/manage/thumbnail/delete'); - - // A warning shows that the view will be deleted. - $this->assertRaw('Show Image Fields'); - - /** @var \Drupal\views\Entity\View $view */ - $view = View::load('entity_test_fields_dependencies'); - $expected = [ - 'module' => ['image', 'node', 'user'], - 'config' => [ - 'field.storage.node.field_image', - 'image.style.thumbnail', - ], - ]; - $view->calculateDependencies(); - $this->assertEqual($expected, $view->getDependencies()); - } - - /** - * Create a new image field. - * - * @param string $name - * The name of the new field (all lowercase), exclude the "field_" prefix. - * @param string $type_name - * The node type that this field will be added to. - * @param array $storage_settings - * (optional) A list of field storage settings that will be added to the defaults. - * @param array $field_settings - * (optional) A list of instance settings that will be added to the instance defaults. - * @param array $widget_settings - * (optional) A list of widget settings that will be added to the widget defaults. - * - * @return \Drupal\field\FieldConfigInterface - * The created field config. - */ - function createImageField($name, $type_name, $storage_settings = [], $field_settings = [], $widget_settings = []) { - FieldStorageConfig::create([ - 'field_name' => $name, - 'entity_type' => 'node', - 'type' => 'image', - 'settings' => $storage_settings, - 'cardinality' => !empty($storage_settings['cardinality']) ? $storage_settings['cardinality'] : 1, - ])->save(); - - $field_config = FieldConfig::create([ - 'field_name' => $name, - 'label' => $name, - 'entity_type' => 'node', - 'bundle' => $type_name, - 'required' => !empty($field_settings['required']), - 'settings' => $field_settings, - ]); - $field_config->save(); - - EntityFormDisplay::load("node.$type_name.default") - ->setComponent($name, [ - 'type' => 'image_image', - 'settings' => $widget_settings, - ]) - ->save(); - - EntityViewDisplay::load("node.$type_name.default") - ->setComponent($name) - ->save(); - - return $field_config; - } - -} diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields_dependencies.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields_dependencies.yml deleted file mode 100644 index 3598a5a..0000000 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.entity_test_fields_dependencies.yml +++ /dev/null @@ -1,179 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.node.field_image - module: - - image - - node - - user -id: entity_test_fields_dependencies -label: 'Show Image Fields' -module: views -description: '' -tag: '' -base_table: node_field_data -base_field: nid -core: 8.x -display: - default: - display_plugin: default - id: default - display_title: Master - position: 0 - display_options: - access: - type: perm - options: - perm: 'access content' - cache: - type: tag - options: { } - query: - type: views_query - options: - disable_sql_rewrite: false - distinct: false - replica: false - query_comment: '' - query_tags: { } - exposed_form: - type: basic - options: - submit_button: Apply - reset_button: false - reset_button_label: Reset - exposed_sorts_label: 'Sort by' - expose_sort_order: true - sort_asc_label: Asc - sort_desc_label: Desc - pager: - type: full - options: - items_per_page: 10 - offset: 0 - id: 0 - total_pages: null - expose: - items_per_page: false - items_per_page_label: 'Items per page' - items_per_page_options: '5, 10, 25, 50' - items_per_page_options_all: false - items_per_page_options_all_label: '- All -' - offset: false - offset_label: Offset - tags: - previous: '‹ Previous' - next: 'Next ›' - first: '« First' - last: 'Last »' - quantity: 9 - style: - type: default - row: - type: fields - fields: - field_image: - id: field_image - table: node__field_image - field: field_image - relationship: none - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: target_id - type: image - settings: - image_style: thumbnail - image_link: '' - group_column: '' - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - plugin_id: field - filters: { } - sorts: { } - title: 'Show Image Fields' - header: { } - footer: { } - empty: { } - relationships: { } - arguments: { } - display_extenders: { } - filter_groups: - operator: AND - groups: { } - cache_metadata: - max-age: -1 - contexts: - - 'languages:language_content' - - 'languages:language_interface' - - url.query_args - - 'user.node_grants:view' - - user.permissions - tags: - - 'config:field.storage.node.field_image' - page_1: - display_plugin: page - id: page_1 - display_title: Page - position: 1 - display_options: - display_extenders: { } - path: show-image-fields - cache_metadata: - max-age: -1 - contexts: - - 'languages:language_content' - - 'languages:language_interface' - - url.query_args - - 'user.node_grants:view' - - user.permissions - tags: - - 'config:field.storage.node.field_image' diff --git a/core/modules/views/tests/src/Kernel/ViewsIntegrationTest.php b/core/modules/views/tests/src/Kernel/ViewsIntegrationTest.php index 0e341f6..95098f0 100644 --- a/core/modules/views/tests/src/Kernel/ViewsIntegrationTest.php +++ b/core/modules/views/tests/src/Kernel/ViewsIntegrationTest.php @@ -22,7 +22,7 @@ class ViewsIntegrationTest extends ViewsKernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['field', 'file', 'image', 'entity_test']; + public static $modules = ['field', 'file', 'image', 'entity_test', 'dblog']; /** * {@inheritdoc} @@ -33,8 +33,12 @@ class ViewsIntegrationTest extends ViewsKernelTestBase { * Tests integration with image module. */ public function testImage() { + $this->installSchema('dblog', ['watchdog']); + + /** @var \Drupal\image\ImageStyleInterface $style */ $style = ImageStyle::create(['name' => 'foo']); $style->save(); + ImageStyle::create(['name' => 'baz'])->save(); // Create a new image field 'bar' to be used in 'entity_test_fields' view. FieldStorageConfig::create([ @@ -58,6 +62,8 @@ public function testImage() { 'field' => 'bar', 'plugin_id' => 'field', 'table' => 'entity_test__bar', + 'entity_type' => 'entity_test', + 'entity_field' => 'bar', 'type' => 'image', 'settings' => ['image_style' => 'foo', 'image_link' => ''], ]; @@ -68,12 +74,53 @@ public function testImage() { // Checks that style 'foo' is a dependency of view 'entity_test_fields'. $this->assertTrue(in_array('image.style.foo', $dependencies['config'])); - // Delete the image style. + // Delete the 'foo' image style. Before that, emulate the UI process of + // selecting a replacement style by setting the replacement image style ID + // in the image style storage. + /** @var \Drupal\image\ImageStyleStorageInterface $storage */ + $storage = $this->container->get('entity_type.manager')->getStorage($style->getEntityTypeId()); + $storage->setReplacementId('foo', 'baz'); $style->delete(); // Checks that the view was not deleted too. $view = View::load('entity_test_fields'); $this->assertNotNull($view); + + // Checks that field 'bar' exists. + $display = $view->getDisplay('default'); + $this->assertFalse(empty($display['display_options']['fields']['bar'])); + + // Checks that the field 'bar' formatter settings were updated. + $this->assertSame('baz', $display['display_options']['fields']['bar']['settings']['image_style']); + + // Delete 'baz' image style without providing a replacement. + ImageStyle::load('baz')->delete(); + + // Checks that the view was not deleted too. + $view = View::load('entity_test_fields'); + $this->assertNotNull($view); + + // Checks that field 'bar' has been removed from the view. + $display = $view->getDisplay('default'); + $this->assertTrue(empty($display['display_options']['fields']['bar'])); + + $arguments = [ + '@view' => 'entity_test_fields', + '@display' => 'default', + '@type' => t('Field'), + '@name' => 'bar', + ]; + $logged = (bool) $this->container->get('database') + ->select('watchdog') + ->fields('watchdog', ['wid']) + ->condition('type', 'views') + ->condition('message', "View '@view', display '@display': @type '@name' was removed because its settings depend on removed dependencies.") + ->condition('variables', serialize($arguments)) + ->execute() + ->fetchField(); + + // Checks that the correct log entry has been logged. + $this->assertTrue($logged); } } diff --git a/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php b/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php index 1f6b419..9d6e47a 100644 --- a/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php +++ b/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php @@ -9,7 +9,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Database\Query\SelectInterface; -use Drupal\Tests\token\Kernel\KernelTestBase; +use Drupal\KernelTests\KernelTestBase; use Drupal\views\Tests\ViewResultAssertionTrait; use Drupal\views\Tests\ViewTestData; @@ -46,7 +46,7 @@ class ViewsKernelTestBase extends KernelTestBase { protected function setUp($import_test_views = TRUE) { parent::setUp(); - $this->installSchema('system', ['sequences', 'key_value_expire']); + $this->installSchema('system', ['router', 'sequences', 'key_value_expire']); $this->setUpFixtures(); if ($import_test_views) {