diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index 79b9292..1b169de 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -428,19 +428,65 @@ public function onDependencyRemoval(array $dependencies) { } } foreach ($this->getComponents() as $name => $component) { - if (isset($component['type']) && $definition = $this->pluginManager->getDefinition($component['type'], FALSE)) { - if (in_array($definition['provider'], $dependencies['module'])) { + if ($renderer = $this->getRenderer($name)) { + if (in_array($renderer->getPluginDefinition()['provider'], $dependencies['module'])) { // Revert to the defaults if the plugin that supplies the widget or // formatter depends on a module that is being uninstalled. $this->setComponent($name); $changed = TRUE; } + + // Give this component the opportunity to react on dependency removal. + if ($component_changed = $renderer->onDependencyRemoval($dependencies)) { + // Update component settings to reflect changes. + $component['settings'] = $renderer->getSettings(); + $this->setComponent($name, $component); + $changed = TRUE; + } + // If there are unresolved deleted dependencies left, disable this + // component to avoid the removal of the entire display entity. + if (static::isUnresolved($renderer->calculateDependencies(), $dependencies)) { + $this->removeComponent($name); + unset($this->hidden[$name]); + $changed = TRUE; + } } } return $changed; } /** + * Checks if the plugin has unresolved dependencies against the display entity + * removed dependencies. + * + * Note: + * 1. The two arguments have not the same structure. + * 2. $removed_dependencies has already sane defaults. All the types of events + * are filled in, even with empty arrays. + * + * @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 bool + * TRUE if there are unresolved dependencies in $dependencies1. + * + * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies() + * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval() + */ + protected static function isUnresolved(array $plugin_dependencies, array $removed_dependencies) { + foreach ($plugin_dependencies as $type => $dependencies) { + if (array_diff($dependencies, array_keys($removed_dependencies[$type]))) { + return TRUE; + } + } + return FALSE; + } + + /** * {@inheritdoc} */ public function __sleep() { diff --git a/core/lib/Drupal/Core/Field/PluginSettingsBase.php b/core/lib/Drupal/Core/Field/PluginSettingsBase.php index 1baf31d..8e12df3 100644 --- a/core/lib/Drupal/Core/Field/PluginSettingsBase.php +++ b/core/lib/Drupal/Core/Field/PluginSettingsBase.php @@ -122,4 +122,11 @@ public function calculateDependencies() { return array(); } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + return FALSE; + } + } diff --git a/core/lib/Drupal/Core/Field/PluginSettingsInterface.php b/core/lib/Drupal/Core/Field/PluginSettingsInterface.php index 969061f..cc30eed 100644 --- a/core/lib/Drupal/Core/Field/PluginSettingsInterface.php +++ b/core/lib/Drupal/Core/Field/PluginSettingsInterface.php @@ -95,4 +95,29 @@ public function getThirdPartySetting($module, $key, $default = NULL); */ public function setThirdPartySetting($module, $key, $value); + /** + * Runs update actions when this plugin is asked to react on removal of its + * dependencies. + * + * This method allows plugins to keep their configuration up-to-date when a + * dependency calculated with ::calculateDependencies() is removed. For + * example, an entity view display contains a formatter having a setting + * pointing to an arbitrary config entity. When that config entity is deleted, + * this method is called by the view display to react on the dependency + * removal by updating its configuration. + * + * This method must return TRUE if the removal event updated the plugin + * configuration or FALSE otherwise. + * + * @param array $dependencies + * An array of dependencies that will be deleted keyed by dependency type. + * Dependency types are 'config', 'content', 'module' and 'theme'. + * + * @return bool + * TRUE if the plugin configuration has changed, FALSE if not. + * + * @see \Drupal\Core\Entity\EntityDisplayBase + */ + public function onDependencyRemoval(array $dependencies); + } diff --git a/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml b/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml index 8e06db1..04dc5fe 100644 --- a/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml +++ b/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml @@ -40,6 +40,12 @@ field.widget.settings.test_field_widget: test_widget_setting: type: string label: 'Test setting' + role: + type: string + label: 'A referenced role' + role2: + type: string + label: 'A 2nd referenced role' field.widget.settings.test_field_widget_multiple: type: mapping diff --git a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php index b90008a..f29b097 100644 --- a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php +++ b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php @@ -34,6 +34,8 @@ class TestFieldWidget extends WidgetBase { public static function defaultSettings() { return array( 'test_widget_setting' => 'dummy test string', + 'role' => 'anonymous', + 'role2' => 'anonymous', ) + parent::defaultSettings(); } @@ -78,4 +80,42 @@ public function errorElement(array $element, ConstraintViolationInterface $viola return $element['value']; } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + + foreach (['role', 'role2'] as $setting) { + if (!empty($role_id = $this->getSetting($setting))) { + // Create a dependency on the role config entity referenced in settings. + $dependencies['config'][] = "user.role.$role_id"; + } + } + + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + + // Only the setting 'role' is resolved here. When the dependency related to + // this setting is removed, is expected that the widget component will be + // update accordingly in the display entity. The 'role2' setting is + // deliberately left out. When the dependency corresponding to this setting + // is removed, is expected that the widget component will be disabled from + // the display entity. + if (!empty($role_id = $this->getSetting('role'))) { + if (!empty($dependencies['config']["user.role.$role_id"])) { + $this->setSetting('role', 'anonymous'); + $changed = TRUE; + } + } + + return $changed; + } + } diff --git a/core/modules/field_ui/src/Tests/EntityDisplayTest.php b/core/modules/field_ui/src/Tests/EntityDisplayTest.php index 82dc8ab..61d02aa 100644 --- a/core/modules/field_ui/src/Tests/EntityDisplayTest.php +++ b/core/modules/field_ui/src/Tests/EntityDisplayTest.php @@ -7,12 +7,17 @@ namespace Drupal\field_ui\Tests; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Entity\Display\EntityDisplayInterface; +use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\node\Entity\NodeType; use Drupal\simpletest\KernelTestBase; +use Drupal\user\Entity\Role; +use Drupal\user\RoleInterface; /** * Tests the entity display configuration entities. @@ -435,4 +440,103 @@ public function testOnDependencyRemoval() { $display = entity_get_display('entity_test', 'entity_test', 'default'); $this->assertFalse($display->getComponent($field_name)); } + + /** + * Tests components dependencies additions. + */ + public function testComponentDependencies() { + $this->installEntitySchema('user'); + /** @var \Drupal\user\RoleInterface[] $roles */ + $roles = []; + // Create two arbitrary user roles. + for ($i = 0; $i < 2; $i++) { + $roles[$i] = Role::create([ + 'id' => Unicode::strtolower($this->randomMachineName()), + 'label' => $this->randomString(), + ]); + $roles[$i]->save(); + } + + // Create a field of type 'test_field' attached to 'entity_test'. + $field_name = Unicode::strtolower($this->randomMachineName()); + FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'test_field', + ])->save(); + FieldConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + ])->save(); + + // Create a new form display without components. + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = EntityFormDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + ]); + $form_display->save(); + + $dependencies = ['user.role.' . $roles[0]->id(), 'user.role.' . $roles[1]->id()]; + + // The config object should not depend on none of the two $roles. + $this->assertFalse($this->isConfigDependency($dependencies[0], $form_display)); + $this->assertFalse($this->isConfigDependency($dependencies[1], $form_display)); + + // Add a widget of type 'test_field_widget'. + $component = [ + 'type' => 'test_field_widget', + 'settings' => [ + 'test_widget_setting' => $this->randomString(), + 'role' => $roles[0]->id(), + 'role2' => $roles[1]->id(), + ], + ]; + $form_display->setComponent($field_name, $component); + $form_display->save(); + + // Now, the form display should depend on both user roles $roles. + $this->assertTrue($this->isConfigDependency($dependencies[0], $form_display)); + $this->assertTrue($this->isConfigDependency($dependencies[1], $form_display)); + + // Delete the first user role entity. + $roles[0]->delete(); + + // Reload the form display. + $form_display = EntityFormDisplay::load($form_display->id()); + // The form display should not depend on $role[0] anymore. + $this->assertFalse($this->isConfigDependency($dependencies[0], $form_display)); + // The form display should depend on 'anonymous' user role. + $this->assertTrue($this->isConfigDependency('user.role.anonymous', $form_display)); + + // Delete the 2nd user role entity. + $roles[1]->delete(); + + // The display exists. + $this->assertNotNull($form_display = EntityFormDisplay::load($form_display->id())); + // The component has been removed. + $this->assertNull($form_display->getComponent($field_name)); + } + + /** + * Returns TRUE if $key is a config dependency of $entity_display. + * + * @param string $key + * The string to be checked. + * @param \Drupal\Core\Entity\Display\EntityDisplayInterface $entity_display + * The entity display object to get dependencies from. + * + * @return bool + * If the supplied $key is a config dependency of the $entity_display. + * + * @see testComponentDependencies() + */ + protected function isConfigDependency($key, EntityDisplayInterface $entity_display) { + $dependencies = $entity_display->getDependencies(); + $config_dependencies = !empty($dependencies['config']) ? $dependencies['config'] : []; + return in_array($key, $config_dependencies); + } + }