diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 33369bd..4356141 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -374,6 +374,11 @@ field_config_base: label: 'Default value function' settings: type: field.[%parent.field_type].instance_settings + third_party_settings: + type: sequence + label: 'Third party settings' + sequence: + - type: field_config.third_party.[%key] field_type: type: string label: 'Field type' diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 210e259..9916615 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -326,6 +326,13 @@ public function calculateDependencies() { } } } + if ($this instanceof ThirdPartySettingsInterface) { + // Configuration entities need to depend on the providers of any third + // parties that they store the configuration for. + foreach ($this->getThirdPartyProviders() as $provider) { + $this->addDependency('module', $provider); + } + } return $this->dependencies; } diff --git a/core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsException.php b/core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsException.php new file mode 100644 index 0000000..7320d81 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/ThirdPartySettingsException.php @@ -0,0 +1,15 @@ +checkThirdPartyArray(); + $this->{$this->thirdPartySettingKey}[$module][$key] = $value; + return $this; + } + + /** + * Gets the value of a third-party setting. + * + * @param string $module + * The module providing the third-party setting. + * @param string $key + * The setting name. + * @param mixed $default + * The default value + * + * @return mixed + * The value. + * + * @throws \Drupal\Core\Config\Entity\ThirdPartySettingsException + * If the third party providers property is not an array. + */ + public function getThirdPartySetting($module, $key, $default = NULL) { + $this->checkThirdPartyArray(); + if (isset($this->{$this->thirdPartySettingKey}[$module][$key])) { + return $this->{$this->thirdPartySettingKey}[$module][$key]; + } + else { + return $default; + } + } + + /** + * Unsets a third-party setting. + * + * @param string $module + * The module providing the third-party setting. + * @param string $key + * The setting name. + * + * @return mixed + * The value. + * + * @throws \Drupal\Core\Config\Entity\ThirdPartySettingsException + * If the third party providers property is not an array. + */ + public function unsetThirdPartySetting($module, $key) { + $this->checkThirdPartyArray(); + unset($this->{$this->thirdPartySettingKey}[$module][$key]); + // If the third party is no longer storing any information completely remove + // the setting. + if (empty($this->{$this->thirdPartySettingKey}[$module])) { + unset($this->{$this->thirdPartySettingKey}[$module]); + } + return $this; + } + + /** + * Gets the list of third parties that store information. + * + * @return array + * The list of third parties. + * + * @throws \Drupal\Core\Config\Entity\ThirdPartySettingsException + * If the third party providers property is not an array. + */ + public function getThirdPartyProviders() { + $this->checkThirdPartyArray(); + return array_keys($this->{$this->thirdPartySettingKey}); + } + + /** + * Checks that the third party settings property is an array. + * + * @throws \Drupal\Core\Config\Entity\ThirdPartySettingsException + * If the third party providers property is not an array. + */ + protected function checkThirdPartyArray() { + if (!is_array($this->{$this->thirdPartySettingKey})) { + throw new ThirdPartySettingsException('Third party settings storage should be an array.'); + } + } + +} diff --git a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php index 536a4cb..0836eaf 100644 --- a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php +++ b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php @@ -80,11 +80,6 @@ protected function checkValue($key, $value) { $error_key = $this->configName . ':' . $key; $element = $this->schema->get($key); if ($element instanceof Undefined) { - // @todo Temporary workaround for https://www.drupal.org/node/2224761. - $key_parts = explode('.', $key); - if (array_pop($key_parts) == 'translation_sync' && strpos($this->configName, 'field.') === 0) { - return array(); - } return array($error_key => 'Missing schema.'); } diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php index 1d12480..189b417 100644 --- a/core/lib/Drupal/Core/Field/FieldConfigBase.php +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Field; use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\Core\Config\Entity\ThirdPartySettingsTrait; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\TypedData\FieldItemDataDefinition; @@ -16,6 +17,7 @@ * Base class for configurable field definitions. */ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigInterface { + use ThirdPartySettingsTrait; /** * The instance ID. @@ -97,6 +99,15 @@ public $settings = array(); /** + * Third party field-type specific settings. + * + * An arrays of key/value pairs keyed by provider. + * + * @var array + */ + protected $third_party_settings = array(); + + /** * Flag indicating whether the field is required. * * TRUE if a value for this field is required when used with this bundle, diff --git a/core/lib/Drupal/Core/Field/FieldConfigInterface.php b/core/lib/Drupal/Core/Field/FieldConfigInterface.php index 1207356..93c71b5 100644 --- a/core/lib/Drupal/Core/Field/FieldConfigInterface.php +++ b/core/lib/Drupal/Core/Field/FieldConfigInterface.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Field; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; /** * Defines an interface for configurable field definitions. @@ -19,7 +20,7 @@ * @see \Drupal\Core\Field\Entity\BaseFieldOverride * @see \Drupal\field\Entity\FieldInstanceConfig */ -interface FieldConfigInterface extends FieldDefinitionInterface, ConfigEntityInterface { +interface FieldConfigInterface extends FieldDefinitionInterface, ConfigEntityInterface, ThirdPartySettingsInterface { /** * Sets the field definition label. diff --git a/core/modules/content_translation/config/schema/content_translation.schema.yml b/core/modules/content_translation/config/schema/content_translation.schema.yml new file mode 100644 index 0000000..6e4167e --- /dev/null +++ b/core/modules/content_translation/config/schema/content_translation.schema.yml @@ -0,0 +1,12 @@ +# Schema for the Content Translation module. + +field_config.third_party.content_translation: + type: mapping + label: 'Content translation field settings' + mapping: + translation_sync: + type: sequence + label: 'Field properties for which to synchronize translations' + sequence: + - type: string + label: 'Field column for which to synchronize translations' diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc index 36ac92e..86d355d 100644 --- a/core/modules/content_translation/content_translation.admin.inc +++ b/core/modules/content_translation/content_translation.admin.inc @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\String; +use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Form\FormStateInterface; @@ -36,14 +37,16 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field) $default[$group] = !empty($info['translatable']) ? $group : FALSE; } - $settings = array('dependent_selectors' => array('instance[settings][translation_sync]' => array('file'))); + $settings = array('dependent_selectors' => array('instance[third_party_settings][content_translation][translation_sync]' => array('file'))); - $translation_sync = $field->getSetting('translation_sync'); + if ($field instanceof ThirdPartySettingsInterface ) { + $default = $field->getThirdPartySetting('content_translation', 'translation_sync', $default); + } $element = array( '#type' => 'checkboxes', '#title' => t('Translatable elements'), '#options' => $options, - '#default_value' => !empty($translation_sync) ? $translation_sync : $default, + '#default_value' => $default, '#attached' => array( 'library' => array( 'content_translation/drupal.content_translation.admin', @@ -313,9 +316,32 @@ function content_translation_form_language_content_settings_submit(array $form, } } } + if (isset($bundle_settings['translatable'])) { + // Store whether a bundle has translation enabled or not. + content_translation_set_config($entity_type_id, $bundle, 'enabled', $bundle_settings['translatable']); + + // Save translation_sync settings. + if (!empty($bundle_settings['columns'])) { + foreach ($bundle_settings['columns'] as $field_name => $column_settings) { + $field_config = $fields[$field_name]->getConfig($bundle); + if ($field_config->isTranslatable()) { + $field_config->setThirdPartySetting('content_translation', 'translation_sync', $column_settings); + } + // If the field does not have translatable enabled we need to reset + // the sync settings to their defaults. + else { + $field_config->unsetThirdPartySetting('content_translation', 'translation_sync'); + } + $field_config->save(); + } + } + } } } - content_translation_save_settings($settings); + // Ensure entity and menu router information are correctly rebuilt. + \Drupal::entityManager()->clearCachedDefinitions(); + \Drupal::service('router.builder')->setRebuildNeeded(); + drupal_set_message(t('Settings successfully updated.')); } diff --git a/core/modules/content_translation/content_translation.install b/core/modules/content_translation/content_translation.install index 06a355b..4aa7e73 100644 --- a/core/modules/content_translation/content_translation.install +++ b/core/modules/content_translation/content_translation.install @@ -88,19 +88,6 @@ function content_translation_install() { // hook_module_implements_alter() is run among the last ones. module_set_weight('content_translation', 10); \Drupal::service('language_negotiator')->saveConfiguration(LanguageInterface::TYPE_CONTENT, array(LanguageNegotiationUrl::METHOD_ID => 0)); - - $config_names = \Drupal::configFactory()->listAll(\Drupal::entityManager()->getDefinition('field_storage_config')->getConfigPrefix() . '.'); - foreach ($config_names as $name) { - \Drupal::config($name) - ->set('settings.translation_sync', FALSE) - ->save(); - } - $config_names = \Drupal::configFactory()->listAll('field.instance.'); - foreach ($config_names as $name) { - \Drupal::config($name) - ->set('settings.translation_sync', FALSE) - ->save(); - } } /** @@ -118,21 +105,3 @@ function content_translation_enable() { $message = t('Enable translation for content types, taxonomy vocabularies, accounts, or any other element you wish to translate.', $t_args); drupal_set_message($message, 'warning'); } - -/** - * Implements hook_uninstall(). - */ -function content_translation_uninstall() { - $config_names = \Drupal::configFactory()->listAll(\Drupal::entityManager()->getDefinition('field_storage_config')->getConfigPrefix() . '.'); - foreach ($config_names as $name) { - \Drupal::config($name) - ->clear('settings.translation_sync') - ->save(); - } - $config_names = \Drupal::configFactory()->listAll('field.instance.'); - foreach ($config_names as $name) { - \Drupal::config($name) - ->clear('settings.translation_sync') - ->save(); - } -} diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index d218a8d..72d5bb9 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -642,8 +642,8 @@ function content_translation_form_field_ui_field_instance_edit_form_alter(array module_load_include('inc', 'content_translation', 'content_translation.admin'); $element = content_translation_field_sync_widget($instance); if ($element) { - $form['instance']['settings']['translation_sync'] = $element; - $form['instance']['settings']['translation_sync']['#weight'] = -10; + $form['instance']['third_party_settings']['content_translation']['translation_sync'] = $element; + $form['instance']['third_party_settings']['content_translation']['translation_sync']['#weight'] = -10; } } } @@ -652,16 +652,6 @@ function content_translation_form_field_ui_field_instance_edit_form_alter(array * Implements hook_entity_presave(). */ function content_translation_entity_presave(EntityInterface $entity) { - // By default no column has to be synchronized. - // @todo Replace with own storage in https://drupal.org/node/2224761 - if ($entity->getEntityTypeId() === 'field_storage_config') { - $entity->settings += array('translation_sync' => FALSE); - } - // Synchronization can be enabled per instance. - // @todo Replace with own storage in https://drupal.org/node/2224761 - if ($entity->getEntityTypeId() === 'field_instance_config') { - $entity->settings += array('translation_sync' => FALSE); - } if ($entity instanceof ContentEntityInterface && $entity->isTranslatable()) { // @todo Avoid using request attributes once translation metadata become // regular fields. @@ -791,45 +781,3 @@ function content_translation_preprocess_language_content_settings_table(&$variab module_load_include('inc', 'content_translation', 'content_translation.admin'); _content_translation_preprocess_language_content_settings_table($variables); } - -/** - * Stores content translation settings. - * - * @param array $settings - * An associative array of settings keyed by entity type and bundle. At bundle - * level the following keys are available: - * - translatable: The bundle translatability status, which is a bool. - * - columns: An associative array of translation synchronization settings - * keyed by field names. - */ -function content_translation_save_settings($settings) { - foreach ($settings as $entity_type => $entity_settings) { - foreach ($entity_settings as $bundle => $bundle_settings) { - // The 'translatable' value is set only if it is possible to enable. - if (isset($bundle_settings['translatable'])) { - // Store whether a bundle has translation enabled or not. - content_translation_set_config($entity_type, $bundle, 'enabled', $bundle_settings['translatable']); - - // Save translation_sync settings. - if (!empty($bundle_settings['columns'])) { - foreach ($bundle_settings['columns'] as $field_name => $column_settings) { - $instance = FieldInstanceConfig::loadByName($entity_type, $bundle, $field_name); - if ($instance->isTranslatable()) { - $instance->settings['translation_sync'] = $column_settings; - } - // If the field does not have translatable enabled we need to reset - // the sync settings to their defaults. - else { - unset($instance->settings['translation_sync']); - } - $instance->save(); - } - } - } - } - } - - // Ensure entity and menu router information are correctly rebuilt. - \Drupal::entityManager()->clearCachedDefinitions(); - \Drupal::service('router.builder')->setRebuildNeeded(); -} diff --git a/core/modules/content_translation/src/FieldTranslationSynchronizer.php b/core/modules/content_translation/src/FieldTranslationSynchronizer.php index 196512b..6715577 100644 --- a/core/modules/content_translation/src/FieldTranslationSynchronizer.php +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php @@ -7,6 +7,7 @@ namespace Drupal\content_translation; +use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; @@ -54,6 +55,7 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode return; } + /** @var \Drupal\Core\Field\FieldItemListInterface $items */ foreach ($entity as $field_name => $items) { $field_definition = $items->getFieldDefinition(); $field_type_definition = $field_type_manager->getDefinition($field_definition->getType()); @@ -61,7 +63,7 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode // Sync if the field is translatable, not empty, and the synchronization // setting is enabled. - if ($field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getSetting('translation_sync')) { + if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) { // Retrieve all the untranslatable column groups and merge them into // single list. $groups = array_keys(array_diff($translation_sync, array_filter($translation_sync))); diff --git a/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php b/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php index 0c60b18..0a5f38a 100644 --- a/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php +++ b/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php @@ -20,7 +20,8 @@ * Field column synchronization takes care of propagating any change in the * field items order and in the column values themselves to all the available * translations. This functionality is provided by defining a - * 'translation_sync' key in the field instance settings, holding an array of + * 'translation_sync' key for the 'content_translation' module's portion of + * the field definition's 'third_party_settings', holding an array of * column names to be synchronized. The synchronized column values are shared * across translations, while the rest varies per-language. This is useful for * instance to translate the "alt" and "title" textual elements of an image diff --git a/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php b/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php index 98b9540..5783ed8 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationSyncImageTest.php @@ -62,11 +62,13 @@ protected function setupTestFields() { 'field_name' => $this->fieldName, 'bundle' => $this->entityTypeId, 'label' => 'Test translatable image field', - 'settings' => array( - 'translation_sync' => array( - 'file' => FALSE, - 'alt' => 'alt', - 'title' => 'title', + 'third_party_settings' => array( + 'content_translation' => array( + 'translation_sync' => array( + 'file' => FALSE, + 'alt' => 'alt', + 'title' => 'title', + ), ), ), ))->save(); @@ -87,11 +89,11 @@ function testImageFieldSync() { // Check that the alt and title fields are enabled for the image field. $this->drupalLogin($this->editor); $this->drupalGet('entity_test_mul/structure/' . $this->entityTypeId . '/fields/' . $this->entityTypeId . '.' . $this->entityTypeId . '.' . $this->fieldName); - $this->assertFieldChecked('edit-instance-settings-translation-sync-alt'); - $this->assertFieldChecked('edit-instance-settings-translation-sync-title'); + $this->assertFieldChecked('edit-instance-third-party-settings-content-translation-translation-sync-alt'); + $this->assertFieldChecked('edit-instance-third-party-settings-content-translation-translation-sync-title'); $edit = array( - 'instance[settings][translation_sync][alt]' => FALSE, - 'instance[settings][translation_sync][title]' => FALSE, + 'instance[third_party_settings][content_translation][translation_sync][alt]' => FALSE, + 'instance[third_party_settings][content_translation][translation_sync][title]' => FALSE, ); $this->drupalPostForm(NULL, $edit, t('Save settings')); diff --git a/core/modules/field_ui/src/Form/FieldInstanceEditForm.php b/core/modules/field_ui/src/Form/FieldInstanceEditForm.php index bc72248..dacd4f2 100644 --- a/core/modules/field_ui/src/Form/FieldInstanceEditForm.php +++ b/core/modules/field_ui/src/Form/FieldInstanceEditForm.php @@ -186,7 +186,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Merge incoming values into the instance. foreach ($form_state->getValue('instance') as $key => $value) { - $this->instance->$key = $value; + $this->instance->set($key, $value); } $this->instance->save(); diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php index 8a58798..aef8d23 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php @@ -280,6 +280,18 @@ public function providerCalculateDependenciesWithPluginBags() { } /** + * @covers ::calculateDependencies + */ + public function testCalculateDependenciesWithThirdPartySettings() { + $this->entity = $this->getMockForAbstractClass('\Drupal\Tests\Core\Config\Entity\Fixtures\ConfigEntityBaseWithThirdPartySettings', array(array(), $this->entityTypeId)); + $this->entity->setThirdPartySetting('test_provider', 'test', 'test'); + $this->entity->setThirdPartySetting('test_provider2', 'test', 'test'); + $this->entity->setThirdPartySetting($this->provider, 'test', 'test'); + + $this->assertEquals(array('test_provider', 'test_provider2'), $this->entity->calculateDependencies()['module']); + } + + /** * @covers ::setOriginalId * @covers ::getOriginalId */ diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/Fixtures/ConfigEntityBaseWithThirdPartySettings.php b/core/tests/Drupal/Tests/Core/Config/Entity/Fixtures/ConfigEntityBaseWithThirdPartySettings.php new file mode 100644 index 0000000..9d5db7c --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Config/Entity/Fixtures/ConfigEntityBaseWithThirdPartySettings.php @@ -0,0 +1,24 @@ +getRandomGenerator()->string(); + + $trait_object = new TestThirdPartySettingsTrait($third_party_settings_key); + + // Test getThirdPartySetting() with no settings. + $this->assertEquals($value, $trait_object->getThirdPartySetting($third_party, $key, $value)); + $this->assertNull($trait_object->getThirdPartySetting($third_party, $key)); + + // Test setThirdPartySetting(). + $trait_object->setThirdPartySetting($third_party, $key, $value); + $this->assertEquals($value, $trait_object->getThirdPartySetting($third_party, $key)); + $this->assertEquals($value, $trait_object->getThirdPartySetting($third_party, $key, $this->randomGenerator->string())); + + // Test getThirdPartyProviders(). + $trait_object->setThirdPartySetting('test_provider2', $key, $value); + $this->assertEquals(array($third_party, 'test_provider2'), $trait_object->getThirdPartyProviders()); + + // Test unsetThirdPartyProviders(). + $trait_object->unsetThirdPartySetting('test_provider2', $key); + $this->assertEquals(array($third_party), $trait_object->getThirdPartyProviders()); + } + + public function providerTestThirdPartySettings() { + return array(array(NULL), array('settings')); + } + + /** + * @covers ::getThirdPartySetting + * @covers ::setThirdPartySetting + * @covers ::unsetThirdPartySetting + * @covers ::getThirdPartyProviders + */ + public function testThirdPartySettingsException() { + $key = 'test'; + $third_party = 'test_provider'; + $value = $this->getRandomGenerator()->string(); + + $trait_object = new TestThirdPartySettingsTrait('exception'); + $this->assertEquals('integer', gettype($trait_object->exception)); + + // Test getThirdPartySetting() with no settings. + try { + $trait_object->getThirdPartySetting($third_party, $key, $value); + $this->fail('getThirdPartySettings throws no exception when third party storage is not an array.'); + } + catch (\Exception $e) { } + + // Test setThirdPartySetting(). + try { + $trait_object->setThirdPartySetting($third_party, $key, $value); + $this->fail('setThirdPartySettings throws no exception when third party storage is not an array.'); + } + catch (\Exception $e) { } + + // Test getThirdPartyProviders(). + try { + $trait_object->getThirdPartyProviders(); + $this->fail('getThirdPartyProviders throws no exception when third party storage is not an array.'); + } + catch (\Exception $e) { } + + // Test unsetThirdPartyProviders(). + try { + $trait_object->unsetThirdPartySetting('test_provider', $key); + $this->fail('unsetThirdPartySetting throws no exception when third party storage is not an array.'); + } + catch (\Exception $e) { } + } + +} + +class TestThirdPartySettingsTrait { + use ThirdPartySettingsTrait; + + public function __construct($third_party_settings_key = NULL) { + if ($third_party_settings_key) { + $this->thirdPartySettingKey = $third_party_settings_key; + } + if ($third_party_settings_key != 'exception') { + $this->{$this->thirdPartySettingKey} = array(); + } + else { + $this->{$this->thirdPartySettingKey} = 12; + } + } + +}