Commit ec2b048e authored by Dries's avatar Dries

Issue #2224761 by Gábor Hojtsy, alexpott, pfrenssen, effulgentsia, xjm, mlncn:...

Issue #2224761 by Gábor Hojtsy, alexpott, pfrenssen, effulgentsia, xjm, mlncn: Add a generic way to add third party configuration on configuration entities and implement for field configuration.
parent 68755a3b
......@@ -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'
......
......@@ -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;
}
......
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ThirdPartySettingsInterface.
*/
namespace Drupal\Core\Config\Entity;
/**
* Interface for configuration entities to store third party information.
*
* A third party is a module that needs to store tightly coupled information to
* the configuration entity. For example, a module alters the node type form
* can use this to store its configuration so that it will be deployed with the
* node type.
*/
interface ThirdPartySettingsInterface {
/**
* Sets 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 $value
* The setting value.
*
* @return $this
*/
public function setThirdPartySetting($module, $key, $value);
/**
* 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.
*/
public function getThirdPartySetting($module, $key, $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.
*/
public function unsetThirdPartySetting($module, $key);
/**
* Gets the list of third parties that store information.
*
* @return array
* The list of third parties.
*/
public function getThirdPartyProviders();
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ThirdPartySettingsTrait.
*/
namespace Drupal\Core\Config\Entity;
/**
* Provides generic implementation of ThirdPartySettingsInterface.
*
* The name of the property used to store third party settings is
* 'third_party_settings'. You need to provide configuration schema for that
* setting to ensure it is persisted. See 'third_party_settings' defined on
* field_config_base and other 'field_config.third_party.*' types.
*
* @see \Drupal\Core\Config\Entity\ThirdPartySettingsInterface
*/
trait ThirdPartySettingsTrait {
/**
* Third party entity settings.
*
* An array of key/value pairs keyed by provider.
*
* @var array
*/
protected $third_party_settings = array();
/**
* Sets 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 $value
* The setting value.
*
* @return $this
*/
public function setThirdPartySetting($module, $key, $value) {
$this->third_party_settings[$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.
*/
public function getThirdPartySetting($module, $key, $default = NULL) {
if (isset($this->third_party_settings[$module][$key])) {
return $this->third_party_settings[$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.
*/
public function unsetThirdPartySetting($module, $key) {
unset($this->third_party_settings[$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->third_party_settings[$module])) {
unset($this->third_party_settings[$module]);
}
return $this;
}
/**
* Gets the list of third parties that store information.
*
* @return array
* The list of third parties.
*/
public function getThirdPartyProviders() {
return array_keys($this->third_party_settings);
}
}
......@@ -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.');
}
......
......@@ -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;
......@@ -17,6 +18,8 @@
*/
abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigInterface {
use ThirdPartySettingsTrait;
/**
* The instance ID.
*
......
......@@ -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.
......
# 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'
......@@ -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;
......@@ -23,8 +24,12 @@
* A form element to configure field synchronization.
*/
function content_translation_field_sync_widget(FieldDefinitionInterface $field) {
$element = array();
// No way to store field sync information on this field.
if (!($field instanceof ThirdPartySettingsInterface)) {
return array();
}
$element = array();
$definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->getType());
$column_groups = $definition['column_groups'];
if (!empty($column_groups) && count($column_groups) > 1) {
......@@ -36,14 +41,14 @@ 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')));
$default = $field->getThirdPartySetting('content_translation', 'translation_sync', $default);
$translation_sync = $field->getSetting('translation_sync');
$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',
......@@ -108,16 +113,12 @@ function _content_translation_form_language_content_settings_form_alter(array &$
'#default_value' => $definition->isTranslatable(),
);
// Display the column translatability configuration widget.
// @todo Remove this special casing when arbitrary settings can be
// stored for any field. See https://drupal.org/node/2224761.
if ($definition instanceof FieldInstanceConfigInterface) {
$column_element = content_translation_field_sync_widget($definition);
if ($column_element) {
$form['settings'][$entity_type_id][$bundle]['columns'][$field_name] = $column_element;
// @todo This should not concern only files.
if (isset($column_element['#options']['file'])) {
$dependent_options_settings["settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]"] = array('file');
}
$column_element = content_translation_field_sync_widget($definition);
if ($column_element) {
$form['settings'][$entity_type_id][$bundle]['columns'][$field_name] = $column_element;
// @todo This should not concern only files.
if (isset($column_element['#options']['file'])) {
$dependent_options_settings["settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]"] = array('file');
}
}
}
......@@ -313,9 +314,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.'));
}
......@@ -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('<a href="!settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, 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();
}
}
......@@ -629,8 +629,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;
}
}
}
......@@ -639,16 +639,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.
......@@ -778,45 +768,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();
}
......@@ -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)));
......
......@@ -20,7 +20,8 @@ interface FieldTranslationSynchronizerInterface {
* 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
......
......@@ -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'));
......
......@@ -134,9 +134,12 @@ public function buildForm(array $form, FormStateInterface $form_state, FieldInst
'#weight' => -5,
);
// Add instance settings for the field type.
// Add instance settings for the field type and a container for third party
// settings that modules can add to via hook_form_FORM_ID_alter().
$form['instance']['settings'] = $items[0]->instanceSettingsForm($form, $form_state);
$form['instance']['settings']['#weight'] = 10;
$form['instance']['third_party_settings'] = array();
$form['instance']['third_party_settings']['#weight'] = 11;
// Add handling for default value.
if ($element = $items->defaultValuesForm($form, $form_state)) {
......@@ -186,7 +189,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();
......
......@@ -279,6 +279,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
......
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Config\Entity\Fixtures\ConfigEntityBaseWithThirdPartySettings.
*/
namespace Drupal\Tests\Core\Config\Entity\Fixtures;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Config\Entity\ThirdPartySettingsTrait;
/**
* Enables testing of dependency calculation.
*
* @see \Drupal\Tests\Core\Config\Entity\ConfigEntityBaseUnitTest::testCalculateDependenciesWithThirdPartySettings()
* @see \Drupal\Core\Config\Entity\ConfigEntityBase::calculateDependencies()
*/
abstract class ConfigEntityBaseWithThirdPartySettings extends ConfigEntityBase implements ThirdPartySettingsInterface {
use ThirdPartySettingsTrait;
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Config\Entity\ThirdPartySettingsTraitTest.
*/
namespace Drupal\Tests\Core\Config\Entity;
use Drupal\Core\Config\Entity\ThirdPartySettingsTrait;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Config\Entity\ThirdPartySettingsTrait
* @group Config
*/
class ThirdPartySettingsTraitTest extends UnitTestCase {
/**
* @covers ::getThirdPartySetting
* @covers ::setThirdPartySetting
* @covers ::unsetThirdPartySetting
* @covers ::getThirdPartyProviders
*/
public function testThirdPartySettings() {
$key = 'test';
$third_party = 'test_provider';
$value = $this->getRandomGenerator()->string();
$trait_object = new TestThirdPartySettingsTrait();
// 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());
}
}
class TestThirdPartySettingsTrait {
use ThirdPartySettingsTrait;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment