Issue #2562107 by claudiu.cristea, jibran, yched, Berdir: EntityDisplayBase should react on removal of its components dependencies

8.0.x
Alex Pott 2015-10-03 21:50:03 +01:00
parent 804d87927e
commit d86856a02e
7 changed files with 400 additions and 33 deletions

View File

@ -428,18 +428,87 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
}
}
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.
$component_removed_dependencies = $this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies);
if ($component_removed_dependencies) {
if ($renderer->onDependencyRemoval($component_removed_dependencies)) {
// Update component settings to reflect changes.
$component['settings'] = $renderer->getSettings();
$component['third_party_settings'] = [];
foreach ($renderer->getThirdPartyProviders() as $module) {
$component['third_party_settings'][$module] = $renderer->getThirdPartySettings($module);
}
$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 ($this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies)) {
$this->removeComponent($name);
$arguments = [
'@display' => (string) $this->getEntityType()->getLabel(),
'@id' => $this->id(),
'@name' => $name,
];
$this->getLogger()->warning("@display '@id': Component '@name' was disabled because its settings depend on removed dependencies.", $arguments);
$changed = TRUE;
}
}
}
}
return $changed;
}
/**
* 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}
*/
@ -471,4 +540,14 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
$this->__construct($values, $this->entityTypeId);
}
/**
* Provides the 'system' channel logger service.
*
* @return \Psr\Log\LoggerInterface
* The 'system' channel logger.
*/
protected function getLogger() {
return \Drupal::logger('system');
}
}

View File

@ -94,6 +94,16 @@ abstract class PluginSettingsBase extends PluginBase implements PluginSettingsIn
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartySettings($module = NULL) {
if ($module) {
return isset($this->thirdPartySettings[$module]) ? $this->thirdPartySettings[$module] : NULL;
}
return $this->thirdPartySettings;
}
/**
* {@inheritdoc}
*/
@ -109,6 +119,26 @@ abstract class PluginSettingsBase extends PluginBase implements PluginSettingsIn
return $this;
}
/**
* {@inheritdoc}
*/
public function unsetThirdPartySetting($module, $key) {
unset($this->thirdPartySettings[$module][$key]);
// If the third party is no longer storing any information, completely
// remove the array holding the settings for this module.
if (empty($this->thirdPartySettings[$module])) {
unset($this->thirdPartySettings[$module]);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartyProviders() {
return array_keys($this->thirdPartySettings);
}
/**
* {@inheritdoc}
*/
@ -122,4 +152,17 @@ abstract class PluginSettingsBase extends PluginBase implements PluginSettingsIn
return array();
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = FALSE;
if (!empty($this->thirdPartySettings) && !empty($dependencies['module'])) {
$old_count = count($this->thirdPartySettings);
$this->thirdPartySettings = array_diff_key($this->thirdPartySettings, array_flip($dependencies['module']));
$changed = $old_count != count($this->thirdPartySettings);
}
return $changed;
}
}

View File

@ -8,11 +8,12 @@
namespace Drupal\Core\Field;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
/**
* Interface definition for plugin with settings.
*/
interface PluginSettingsInterface extends PluginInspectionInterface {
interface PluginSettingsInterface extends PluginInspectionInterface, ThirdPartySettingsInterface {
/**
* Defines the default settings for this plugin.
@ -65,34 +66,27 @@ interface PluginSettingsInterface extends PluginInspectionInterface {
public function setSetting($key, $value);
/**
* Returns the value of a third-party setting, or $default if not set.
* Informs the plugin that some configuration it depends on will be deleted.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
* @param mixed $default
* (optional) The default value if the third party setting is not set.
* Defaults to NULL.
* 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 to the dependency
* removal by updating its configuration.
*
* @return mixed|NULL
* The setting value. Returns NULL if the setting does not exist and
* $default is not provided.
* 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 getThirdPartySetting($module, $key, $default = NULL);
/**
* Sets the value of a third-party setting for the plugin.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
* @param mixed $value
* The setting value.
*
* @return $this
*/
public function setThirdPartySetting($module, $key, $value);
public function onDependencyRemoval(array $dependencies);
}

View File

@ -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
@ -49,6 +55,14 @@ field.widget.settings.test_field_widget_multiple:
type: string
label: 'Test setting'
field.widget.third_party.color:
type: mapping
label: 'Field test entity display color module third party settings'
mapping:
foo:
type: string
label: 'Foo setting'
field.storage_settings.test_field:
type: mapping
label: 'Test field storage settings'

View File

@ -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 @@ class TestFieldWidget extends WidgetBase {
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 from being updated. When the dependency
// corresponding to this setting is removed, is expected that the widget
// component will be disabled in 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;
}
}

View File

@ -7,7 +7,12 @@
namespace Drupal\field_ui\Tests;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
@ -15,7 +20,7 @@ use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\simpletest\KernelTestBase;
use Drupal\Tests\Core\Entity\EntityManagerTest;
use Drupal\user\Entity\Role;
/**
* Tests the entity display configuration entities.
@ -470,4 +475,181 @@ class EntityDisplayTest extends KernelTestBase {
$this->assertEqual($form_modes, array('default' => 'Default', 'register' => 'Register'));
}
/**
* Tests components dependencies additions.
*/
public function testComponentDependencies() {
$this->enableModules(['dblog', 'color']);
$this->installSchema('dblog', ['watchdog']);
$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->assertNoDependency('config', $dependencies[0], $form_display);
$this->assertNoDependency('config', $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(),
],
'third_party_settings' => [
'color' => ['foo' => 'bar'],
],
];
$form_display->setComponent($field_name, $component);
$form_display->save();
// Now, the form display should depend on both user roles $roles.
$this->assertDependency('config', $dependencies[0], $form_display);
$this->assertDependency('config', $dependencies[1], $form_display);
// The form display should depend on 'color' module.
$this->assertDependency('module', 'color', $form_display);
// Delete the first user role entity.
$roles[0]->delete();
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertFalse(empty($form_display));
// The form display should not depend on $role[0] anymore.
$this->assertNoDependency('config', $dependencies[0], $form_display);
// The form display should depend on 'anonymous' user role.
$this->assertDependency('config', 'user.role.anonymous', $form_display);
// The form display should depend on 'color' module.
$this->assertDependency('module', 'color', $form_display);
// Manually trigger the removal of configuration belonging to the module
// because KernelTestBase::disableModules() is not aware of this.
$this->container->get('config.manager')->uninstall('module', 'color');
// Uninstall 'color' module.
$this->disableModules(['color']);
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertFalse(empty($form_display));
// The component is still enabled.
$this->assertNotNull($form_display->getComponent($field_name));
// The form display should not depend on 'color' module anymore.
$this->assertNoDependency('module', 'color', $form_display);
// Delete the 2nd user role entity.
$roles[1]->delete();
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertFalse(empty($form_display));
// The component has been disabled.
$this->assertNull($form_display->getComponent($field_name));
$this->assertTrue($form_display->get('hidden')[$field_name]);
// The correct warning message has been logged.
$arguments = ['@display' => (string) t('Entity form display'), '@id' => $form_display->id(), '@name' => $field_name];
$logged = (bool) Database::getConnection()->select('watchdog', 'w')
->fields('w', ['wid'])
->condition('type', 'system')
->condition('message', "@display '@id': Component '@name' was disabled because its settings depend on removed dependencies.")
->condition('variables', serialize($arguments))
->execute()
->fetchAll();
$this->assertTrue($logged);
}
/**
* Asserts that $key is a $type type dependency of $display config entity.
*
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertDependency($type, $key, EntityDisplayInterface $display) {
return $this->assertDependencyHelper(TRUE, $type, $key, $display);
}
/**
* Asserts that $key is not a $type type dependency of $display config entity.
*
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertNoDependency($type, $key, EntityDisplayInterface $display) {
return $this->assertDependencyHelper(FALSE, $type, $key, $display);
}
/**
* Provides a helper for dependency assertions.
*
* @param bool $assertion
* Assertion: positive or negative.
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertDependencyHelper($assertion, $type, $key, EntityDisplayInterface $display) {
$all_dependencies = $display->getDependencies();
$dependencies = !empty($all_dependencies[$type]) ? $all_dependencies[$type] : [];
$context = $display instanceof EntityViewDisplayInterface ? 'View' : 'Form';
$value = $assertion ? in_array($key, $dependencies) : !in_array($key, $dependencies);
$args = ['@context' => $context, '@id' => $display->id(), '@type' => $type, '@key' => $key];
$message = $assertion ? new FormattableMarkup("@context display '@id' depends on @type '@key'.", $args) : new FormattableMarkup("@context display '@id' do not depend on @type '@key'.", $args);
return $this->assert($value, $message);
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\field_ui\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\NodeType;
@ -168,12 +169,26 @@ class ManageDisplayTest extends WebTestBase {
$this->drupalPostAjaxForm(NULL, array(), "field_test_settings_edit");
$this->drupalPostAjaxForm(NULL, $edit, "field_test_plugin_settings_update");
// Uninstall the module providing third party settings and ensure the button
// is no longer there.
// When a module providing third-party settings to a formatter (or widget)
// is uninstalled, the formatter remains enabled but the provided settings,
// together with the corresponding form elements, are removed from the
// display component.
\Drupal::service('module_installer')->uninstall(array('field_third_party_test'));
// Ensure the button is still there after the module has been disabled.
$this->drupalGet($manage_display);
$this->assertResponse(200);
$this->assertNoFieldByName('field_test_settings_edit');
$this->assertFieldByName('field_test_settings_edit');
// Ensure that third-party form elements are not present anymore.
$this->drupalPostAjaxForm(NULL, array(), 'field_test_settings_edit');
$fieldname = 'fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]';
$this->assertNoField($fieldname);
// Ensure that third-party settings were removed from the formatter.
$display = EntityViewDisplay::load("node.{$this->type}.default");
$component = $display->getComponent('field_test');
$this->assertFalse(array_key_exists('field_third_party_test', $component['third_party_settings']));
}
/**