Commit 15089393 authored by catch's avatar catch

Issue #2271419 by alexpott, larowlan: Fixed Allow field types, widgets,...

Issue #2271419 by alexpott, larowlan: Fixed Allow field types, widgets, formatters to specify config dependencies.
parent eb56c4d3
......@@ -12,7 +12,7 @@
*
* @ingroup plugin_api
*/
interface ConfigurablePluginInterface {
interface ConfigurablePluginInterface extends DependentPluginInterface {
/**
* Returns this plugin's configuration.
......@@ -38,29 +38,4 @@ public function setConfiguration(array $configuration);
*/
public function defaultConfiguration();
/**
* Calculates dependencies for the configured plugin.
*
* Dependencies are saved in the plugin's configuration entity and are used to
* determine configuration synchronization order. For example, if the plugin
* integrates with specific user roles, this method should return an array of
* dependencies listing the specified roles.
*
* @return array
* An array of dependencies grouped by type (config, content, module,
* theme). For example:
* @code
* array(
* 'config' => array('user.role.anonymous', 'user.role.authenticated'),
* 'content' => array('node:article:f0a189e6-55fb-47fb-8005-5bef81c44d6d'),
* 'module' => array('node', 'user'),
* 'theme' => array('seven'),
* );
* @endcode
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
* @see \Drupal\Core\Entity\EntityInterface::getConfigDependencyName()
*/
public function calculateDependencies();
}
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\DependentPluginInterface.
*/
namespace Drupal\Component\Plugin;
/**
* Provides an interface for a plugin that has dependencies.
*
* @ingroup plugin_api
*/
interface DependentPluginInterface {
/**
* Calculates dependencies for the configured plugin.
*
* Dependencies are saved in the plugin's configuration entity and are used to
* determine configuration synchronization order. For example, if the plugin
* integrates with specific user roles, this method should return an array of
* dependencies listing the specified roles.
*
* @return array
* An array of dependencies grouped by type (config, content, module,
* theme). For example:
* @code
* array(
* 'config' => array('user.role.anonymous', 'user.role.authenticated'),
* 'content' => array('node:article:f0a189e6-55fb-47fb-8005-5bef81c44d6d'),
* 'module' => array('node', 'user'),
* 'theme' => array('seven'),
* );
* @endcode
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
* @see \Drupal\Core\Entity\EntityInterface::getConfigDependencyName()
*/
public function calculateDependencies();
}
......@@ -172,8 +172,12 @@ public function getDependentEntities($type, $name) {
$entities_to_check[] = $entity->getConfigDependencyName();
}
}
return array_merge($dependent_entities, $this->createGraphConfigEntityDependencies($entities_to_check));
$dependencies = array_merge($this->createGraphConfigEntityDependencies($entities_to_check), $dependent_entities);
// Sort dependencies in the reverse order of the graph. So the least
// dependent is at the top. For example, this ensures that fields are
// always after field storages. This is because field storages need to be
// created before a field.
return array_reverse(array_intersect_key($this->graph, $dependencies));
}
/**
......
......@@ -9,11 +9,12 @@
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
/**
* Provides a common interface for entity displays.
*/
interface EntityDisplayInterface extends ThirdPartySettingsInterface, ConfigEntityInterface {
interface EntityDisplayInterface extends ConfigEntityInterface, EntityWithPluginCollectionInterface, ThirdPartySettingsInterface {
/**
* Creates a duplicate of the entity display object on a different view mode.
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\EntityDisplayPluginCollection;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\EntityDisplayBase;
......@@ -243,4 +244,23 @@ public function __wakeup() {
$this->__construct($values, $this->entityTypeId);
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
$configurations = array();
foreach ($this->getComponents() as $field_name => $configuration) {
if (!empty($configuration['type']) && ($field_definition = $this->getFieldDefinition($field_name))) {
$configurations[$configuration['type']] = $configuration + array(
'field_definition' => $field_definition,
'form_mode' => $this->mode,
);
}
}
return array(
'widgets' => new EntityDisplayPluginCollection($this->pluginManager, $configurations)
);
}
}
......@@ -9,6 +9,7 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityDisplayPluginCollection;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityDisplayBase;
......@@ -252,4 +253,22 @@ public function buildMultiple(array $entities) {
return $build_list;
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
$configurations = array();
foreach ($this->getComponents() as $field_name => $configuration) {
if (!empty($configuration['type']) && ($field_definition = $this->getFieldDefinition($field_name))) {
$configurations[$configuration['type']] = $configuration + array(
'field_definition' => $field_definition,
'view_mode' => $this->originalMode,
);
}
}
return array(
'formatters' => new EntityDisplayPluginCollection($this->pluginManager, $configurations)
);
}
}
......@@ -175,17 +175,6 @@ public function calculateDependencies() {
if ($field) {
$this->addDependency('config', $field->getConfigDependencyName());
}
// Create a dependency on the module that provides the formatter or
// widget.
if (isset($component['type']) && $definition = $this->pluginManager->getDefinition($component['type'], FALSE)) {
$this->addDependency('module', $definition['provider']);
}
// Create dependencies on any modules providing third party settings.
if (isset($component['third_party_settings'])) {
foreach($component['third_party_settings'] as $module => $settings) {
$this->addDependency('module', $module);
}
}
}
// Depend on configured modes.
if ($this->mode != 'default') {
......
<?php
/**
* @file
* Contains \Drupal\filter\EntityDisplayPluginCollection.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\Plugin\DefaultLazyPluginCollection;
/**
* A collection of formatters or widgets.
*/
class EntityDisplayPluginCollection extends DefaultLazyPluginCollection {
/**
* The key within the plugin configuration that contains the plugin ID.
*
* @var string
*/
protected $pluginKey = 'type';
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Field;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ThirdPartySettingsTrait;
use Drupal\Core\Entity\EntityStorageInterface;
......@@ -228,10 +229,25 @@ public function getTargetBundle() {
*/
public function calculateDependencies() {
parent::calculateDependencies();
// Add dependencies from the field type plugin. We can not use
// self::calculatePluginDependencies() because instantiation of a field item
// plugin requires a parent entity.
/** @var $field_type_manager \Drupal\Core\Field\FieldTypePluginManagerInterface */
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$definition = $field_type_manager->getDefinition($this->getType());
$this->addDependency('module', $definition['provider']);
// Plugins can declare additional dependencies in their definition.
if (isset($definition['config_dependencies'])) {
$this->addDependencies($definition['config_dependencies']);
}
// Let the field type plugin specify its own dependencies.
// @see \Drupal\Core\Field\FieldItemInterface::calculateDependencies()
$this->addDependencies($definition['class']::calculateDependencies($this));
// If the target entity type uses entities to manage its bundles then
// depend on the bundle entity.
$bundle_entity_type_id = $this->entityManager()->getDefinition($this->entity_type)->getBundleEntityType();
if ($bundle_entity_type_id != 'bundle') {
// If the target entity type uses entities to manage its bundles then
// depend on the bundle entity.
$bundle_entity = $this->entityManager()->getStorage($bundle_entity_type_id)->load($this->bundle);
$this->addDependency('config', $bundle_entity->getConfigDependencyName());
}
......
......@@ -289,4 +289,11 @@ public static function fieldSettingsFromConfigData(array $settings) {
return $settings;
}
/**
* {@inheritdoc}
*/
public static function calculateDependencies(FieldDefinitionInterface $field_definition) {
return array();
}
}
......@@ -372,4 +372,32 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state);
/**
* Calculates dependencies for field items.
*
* Dependencies are saved in the field configuration entity and are used to
* determine configuration synchronization order. For example, if the field
* type's default value is a content entity, this method should return an
* array of dependencies listing the content entities.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* An array of dependencies grouped by type (config, content, module,
* theme). For example:
* @code
* array(
* 'config' => array('user.role.anonymous', 'user.role.authenticated'),
* 'content' => array('node:article:f0a189e6-55fb-47fb-8005-5bef81c44d6d'),
* 'module' => array('node', 'user'),
* 'theme' => array('seven'),
* );
* @endcode
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::getConfigDependencyName()
*/
public static function calculateDependencies(FieldDefinitionInterface $field_definition);
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Field\Plugin\Field\FieldType;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
......@@ -15,6 +16,7 @@
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\DataReferenceDefinition;
use Drupal\entity_reference\Exception\MissingDefaultValueException;
/**
* Defines the 'entity_reference' entity field type.
......@@ -242,4 +244,27 @@ public function hasUnsavedEntity() {
return $this->target_id === NULL && ($entity = $this->entity) && $entity->isNew();
}
/**
* {@inheritdoc}
*/
public static function calculateDependencies(FieldDefinitionInterface $field_definition) {
$dependencies = [];
if (is_array($field_definition->default_value) && count($field_definition->default_value)) {
$target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
$key = $target_entity_type instanceof ConfigEntityType ? 'config' : 'content';
foreach ($field_definition->default_value as $default_value) {
if (is_array($default_value) && isset($default_value['target_uuid'])) {
$entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $default_value['target_uuid']);
// If the entity does not exist do not create the dependency.
// @see \Drupal\Core\Field\EntityReferenceFieldItemList::processDefaultValue()
if ($entity) {
$dependencies[$key][] = $entity->getConfigDependencyName();
}
}
}
}
return $dependencies;
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Field;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Plugin\PluginBase;
/**
......@@ -14,7 +15,7 @@
*
* This class handles lazy replacement of default settings values.
*/
abstract class PluginSettingsBase extends PluginBase implements PluginSettingsInterface {
abstract class PluginSettingsBase extends PluginBase implements PluginSettingsInterface, DependentPluginInterface {
/**
* The plugin settings.
......@@ -108,4 +109,17 @@ public function setThirdPartySetting($module, $key, $value) {
return $this;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
if (!empty($this->thirdPartySettings)) {
// Create dependencies on any modules providing third party settings.
return array(
'module' => array_keys($this->thirdPartySettings)
);
}
return array();
}
}
......@@ -7,7 +7,7 @@
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Entity\DependencyTrait;
......@@ -24,7 +24,7 @@ trait PluginDependencyTrait {
* Dependencies are added for the module that provides the plugin, as well
* as any dependencies declared by the instance's calculateDependencies()
* method, if it implements
* \Drupal\Component\Plugin\ConfigurablePluginInterface.
* \Drupal\Component\Plugin\DependentPluginInterface.
*
* @param \Drupal\Component\Plugin\PluginInspectionInterface $instance
* The plugin instance.
......@@ -36,8 +36,8 @@ protected function calculatePluginDependencies(PluginInspectionInterface $instan
if (isset($definition['config_dependencies'])) {
$this->addDependencies($definition['config_dependencies']);
}
// If a plugin is configurable, calculate its dependencies.
if ($instance instanceof ConfigurablePluginInterface && $plugin_dependencies = $instance->calculateDependencies()) {
// If a plugin is dependent, calculate its dependencies.
if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) {
$this->addDependencies($plugin_dependencies);
}
}
......
......@@ -6,5 +6,6 @@ version: VERSION
core: 8.x
configure: aggregator.admin_settings
dependencies:
- entity_reference
- file
- options
<?php
/**
* @file
* Contains \Drupal\config\Tests\AssertConfigEntityImportTrait.
*/
namespace Drupal\config\Tests;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides test assertions for testing config entity synchronisation.
*
* Can be used by test classes that extend \Drupal\simpletest\WebTestBase or
* \Drupal\simpletest\KernelTestBase.
*/
trait AssertConfigEntityImportTrait {
/**
* Asserts that a config entity can be imported without changing it.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
* The config entity to test importing.
*/
public function assertConfigEntityImport(ConfigEntityInterface $entity) {
// Save original config information.
$entity_uuid = $entity->uuid();
$entity_type_id = $entity->getEntityTypeId();
$original_data = $entity->toArray();
// Copy everything to staging.
$this->copyConfig(\Drupal::service('config.storage'), \Drupal::service('config.storage.staging'));
// Delete the configuration from active. Don't worry about side effects of
// deleting config like fields cleaning up field storages. The coming import
// should recreate everything as necessary.
$entity->delete();
$this->configImporter()->reset()->import();
$imported_entity = \Drupal::entityManager()->loadEntityByUuid($entity_type_id, $entity_uuid);
$this->assertIdentical($original_data, $imported_entity->toArray());
}
}
......@@ -31,7 +31,7 @@ class ConfigImportRecreateTest extends DrupalUnitTestBase {
*
* @var array
*/
public static $modules = array('system', 'entity', 'field', 'text', 'user', 'node');
public static $modules = array('system', 'entity', 'field', 'text', 'user', 'node', 'entity_reference');
protected function setUp() {
parent::setUp();
......
......@@ -34,7 +34,7 @@ class ConfigImportRenameValidationTest extends DrupalUnitTestBase {
*
* @var array
*/
public static $modules = array('system', 'user', 'node', 'field', 'text', 'entity', 'config_test');
public static $modules = array('system', 'user', 'node', 'field', 'text', 'entity', 'config_test', 'entity_reference');
/**
* {@inheritdoc}
......
......@@ -77,6 +77,9 @@ function testEntityReferenceDefaultValue() {
$config_entity = $this->container->get('config.factory')->get('field.field.node.reference_content.' . $field_name)->get();
$this->assertTrue(isset($config_entity['default_value'][0]['target_uuid']), 'Default value contains target_uuid property');
$this->assertEqual($config_entity['default_value'][0]['target_uuid'], $referenced_node->uuid(), 'Content uuid and config entity uuid are the same');
// Ensure the configuration has the expected dependency on the entity that
// is being used a default value.
$this->assertEqual(array($referenced_node->getConfigDependencyName()), $config_entity['dependencies']['content']);
// Clear field definitions cache in order to avoid stale cache values.
\Drupal::entityManager()->clearCachedFieldDefinitions();
......
......@@ -7,6 +7,10 @@
namespace Drupal\entity_reference\Tests;
use Drupal\Component\Utility\String;
use Drupal\config\Tests\AssertConfigEntityImportTrait;
use Drupal\entity_reference\Exception\MissingDefaultValueException;
use Drupal\field\Entity\FieldConfig;
use Drupal\simpletest\WebTestBase;
/**
......@@ -15,6 +19,7 @@
* @group entity_reference
*/
class EntityReferenceIntegrationTest extends WebTestBase {
use AssertConfigEntityImportTrait;
/**
* The entity type used in this test.
......@@ -42,7 +47,7 @@ class EntityReferenceIntegrationTest extends WebTestBase {
*
* @var array
*/
public static $modules = array('config_test', 'entity_test', 'entity_reference');
public static $modules = array('config_test', 'entity_test', 'entity_reference', 'field_ui');
/**
* {@inheritdoc}
......@@ -51,7 +56,7 @@ protected function setUp() {
parent::setUp();
// Create a test user.
$web_user = $this->drupalCreateUser(array('administer entity_test content'));
$web_user = $this->drupalCreateUser(array('administer entity_test content', 'administer entity_test fields'));
$this->drupalLogin($web_user);
}
......@@ -59,7 +64,7 @@ protected function setUp() {
* Tests the entity reference field with all its supported field widgets.
*/
public function testSupportedEntityTypesAndWidgets() {
foreach ($this->getTestEntities() as $referenced_entities) {
foreach ($this->getTestEntities() as $key => $referenced_entities) {
$this->fieldName = 'field_test_' . $referenced_entities[0]->getEntityTypeId();
// Create an Entity reference field.
......@@ -122,6 +127,33 @@ public function testSupportedEntityTypesAndWidgets() {
$this->drupalPostForm($this->entityType . '/manage/' . $entity->id(), array(), t('Save'));
$this->assertFieldValues($entity_name, $referenced_entities);
}
// Reset to the default 'entity_reference_autocomplete' widget.
entity_get_form_display($this->entityType, $this->bundle, 'default')->setComponent($this->fieldName)->save();
// Set first entity as the default_value.
$field_edit = array(
'default_value_input[' . $this->fieldName . '][0][target_id]' => $referenced_entities[0]->label() . ' (' . $referenced_entities[0]->id() . ')',
);
if ($key == 'content') {
$field_edit['field[settings][handler_settings][target_bundles][' . $referenced_entities[0]->getEntityTypeId() . ']'] = TRUE;
}
$this->drupalPostForm($this->entityType . '/structure/' . $this->bundle .'/fields/' . $this->entityType . '.' . $this->bundle . '.' . $this->fieldName, $field_edit, t('Save settings'));
// Ensure the configuration has the expected dependency on the entity that
// is being used a default value.
$field = FieldConfig::loadByName($this->entityType, $this->bundle, $this->fieldName);
$this->assertTrue(in_array($referenced_entities[0]->getConfigDependencyName(), $field->getDependencies()[$key]), String::format('Expected @type dependency @name found', ['@type' => $key, '@name' => $referenced_entities[0]->getConfigDependencyName()]));
// Ensure that the field can be imported without change even after the
// default value deleted.
$referenced_entities[0]->delete();
$this->assertConfigEntityImport($field);
// Once the default value has been removed after saving the dependency
// should be removed.
$field = FieldConfig::loadByName($this->entityType, $this->bundle, $this->fieldName);
$field->save();
$dependencies = $field->getDependencies();
$this->assertFalse(isset($dependencies[$key]) && in_array($referenced_entities[0]->getConfigDependencyName(), $dependencies[$key]), String::format('@type dependency @name does not exist.', ['@type' => $key, '@name' => $referenced_entities[0]->getConfigDependencyName()]));
}
}
......
......@@ -39,7 +39,7 @@ protected function setUp() {
// Create a form display for the default form mode.
entity_get_form_display('entity_test', 'entity_test', 'default')
->setComponent('field_boolean', array(
'type' => 'boolean',
'type' => 'boolean_checkbox',
))
->save();
}
......
......@@ -20,6 +20,8 @@ class FieldAttachOtherTest extends FieldUnitTestBase {
protected function setUp()</