Commit 6fc8b983 authored by alexpott's avatar alexpott

Issue #2225717 by quietone, Jo Fitzgerald, Gábor Hojtsy, mikeryan,...

Issue #2225717 by quietone, Jo Fitzgerald, Gábor Hojtsy, mikeryan, phenaproxima, Kristen Pol: Add config translation support to migrations and implement for Drupal 6 user profile fields
parent 30eb9415
id: d6_i18n_user_profile_field_instance
label: User profile field instance configuration
migration_tags:
- Drupal 6
source:
plugin: d6_i18n_profile_field
constants:
entity_type: user
bundle: user
process:
langcode: language
entity_type: 'constants/entity_type'
bundle: 'constants/bundle'
field_name: name
property:
plugin: static_map
source: property
map:
title: label
options: options
explanation: description
translation: translation
destination:
plugin: entity:field_config
translations: true
migration_dependencies:
required:
- user_profile_field
- user_profile_field_instance
<?php
namespace Drupal\config_translation\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* i18n strings profile field source from database.
*
* @MigrateSource(
* id = "d6_i18n_profile_field",
* source_provider = "i18n"
* )
*/
class I18nProfileField extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('profile_fields', 'pf')
->fields('pf', ['fid', 'name'])
->fields('i18n', ['property'])
->fields('lt', ['lid', 'translation', 'language']);
$query->leftJoin('i18n_strings', 'i18n', 'i18n.objectid = pf.name');
$query->leftJoin('locales_target', 'lt', 'lt.lid = i18n.lid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return array(
'fid' => $this->t('Profile field ID.'),
'lid' => $this->t('Locales target language ID.'),
'language' => $this->t('Language for this field.'),
'translation' => $this->t('Translation of either the title or explanation.'),
);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['fid']['type'] = 'integer';
$ids['language']['type'] = 'string';
$ids['lid']['type'] = 'integer';
$ids['lid']['alias'] = 'lt';
return $ids;
}
}
<?php
namespace Drupal\Tests\config_translation\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests the user profile field instance migration.
*
* @group migrate_drupal_6
*/
class MigrateI18nUserProfileFieldInstanceTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['config_translation', 'locale', 'language', 'field'];
/**
* Tests migration of translated user profile fields.
*/
public function testUserProfileFields() {
$this->executeMigrations([
'user_profile_field',
'user_profile_field_instance',
'd6_i18n_user_profile_field_instance',
]);
$language_manager = $this->container->get('language_manager');
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_love_migrations');
$this->assertSame("J'aime les migrations", $config_translation->get('label'));
$this->assertSame("Si vous cochez cette case, vous aimez les migrations.", $config_translation->get('description'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_color');
$this->assertSame('fr - Favorite color', $config_translation->get('label'));
$this->assertSame('Inscrivez votre couleur préférée', $config_translation->get('description'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_biography');
$this->assertSame('fr - Biography', $config_translation->get('label'));
$this->assertSame('fr - Tell people a little bit about yourself', $config_translation->get('description'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_sell_address');
$this->assertSame('fr - Sell your email address?', $config_translation->get('label'));
$this->assertSame("fr - If you check this box, we'll sell your address to spammers to help line the pockets of our shareholders. Thanks!", $config_translation->get('description'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_sold_to');
$this->assertSame('fr - Sales Category', $config_translation->get('label'));
$this->assertSame("fr - Select the sales categories to which this user's address was sold.", $config_translation->get('description'));
$this->assertSame('fr - Pill spammers Fitness spammers Back\slash Forward/slash Dot.in.the.middle', $config_translation->get('options'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_bands');
$this->assertSame('Mes groupes préférés', $config_translation->get('label'));
$this->assertSame("fr - Enter your favorite bands. When you've saved your profile, you'll be able to find other people with the same favorites.", $config_translation->get('description'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_birthdate');
$this->assertSame('fr - Birthdate', $config_translation->get('label'));
$this->assertSame('fr - Enter your birth date and we\'ll send you a coupon.', $config_translation->get('description'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_blog');
$this->assertSame('fr - Blog', $config_translation->get('label'));
$this->assertSame('fr - Paste the full URL, including http://, of your personal blog.', $config_translation->get('description'));
}
}
<?php
namespace Drupal\Tests\config_translation\Kernel\Plugin\migrate\source\d6;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests the i18nProfileField source plugin.
*
* @covers \Drupal\config_translation\Plugin\migrate\source\d6\I18nProfileField
* @group migrate_drupal
*/
class I18nProfileFieldTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['config_translation', 'migrate_drupal', 'user'];
/**
* {@inheritdoc}
*/
public function providerSource() {
$test = [];
$test[0]['source_data'] = [
'profile_fields' => [
[
'fid' => 42,
'title' => 'I love migrations',
'name' => 'profile_love_migrations',
],
],
'i18n_strings' => [
[
'lid' => 10,
'objectid' => 'profile_love_migrations',
'type' => 'field',
'property' => 'title',
],
[
'lid' => 11,
'objectid' => 'profile_love_migrations',
'type' => 'field',
'property' => 'explanation'
]
],
'locales_target' => [
[
'lid' => 10,
'translation' => "J'aime les migrations.",
'language' => 'fr',
],
[
'lid' => 11,
'translation' => 'Si vous cochez cette case, vous aimez les migrations.',
'language' => 'fr',
],
],
];
$test[0]['expected_results'] = [
[
'property' => 'title',
'translation' => "J'aime les migrations.",
'language' => 'fr',
'fid' => '42',
'name' => 'profile_love_migrations',
],
[
'property' => 'explanation',
'translation' => 'Si vous cochez cette case, vous aimez les migrations.',
'language' => 'fr',
'fid' => '42',
'name' => 'profile_love_migrations',
],
];
return $test;
}
}
......@@ -3,10 +3,16 @@
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\language\ConfigurableLanguageManager;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class for importing configuration entities.
......@@ -20,6 +26,63 @@
*/
class EntityConfigBase extends Entity {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface;
*/
protected $configFactory;
/**
* Construct a new entity.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles);
$this->languageManager = $language_manager;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$entity_type_id = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity.manager')->getStorage($entity_type_id),
array_keys($container->get('entity.manager')->getBundleInfo($entity_type_id)),
$container->get('language_manager'),
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
......@@ -39,17 +102,37 @@ public function import(Row $row, array $old_destination_id_values = array()) {
}
}
$entity = $this->getEntity($row, $old_destination_id_values);
$entity->save();
// Translations are already saved in updateEntity by configuration override.
if (!$this->isTranslationDestination()) {
$entity->save();
}
if (count($ids) > 1) {
// This can only be a config entity, content entities have their ID key
// and that's it.
$return = array();
$return = [];
foreach ($id_keys as $id_key) {
$return[] = $entity->get($id_key);
if (($this->isTranslationDestination()) && ($id_key == 'langcode')) {
// Config entities do not have a language property, get the language
// code from the destination.
$return[] = $row->getDestinationProperty($id_key);
}
else {
$return[] = $entity->get($id_key);
}
}
return $return;
}
return array($entity->id());
return [$entity->id()];
}
/**
* Get whether this destination is for translations.
*
* @return bool
* Whether this destination is for translations.
*/
protected function isTranslationDestination() {
return !empty($this->configuration['translations']);
}
/**
......@@ -58,6 +141,9 @@ public function import(Row $row, array $old_destination_id_values = array()) {
public function getIds() {
$id_key = $this->getKey('id');
$ids[$id_key]['type'] = 'string';
if ($this->isTranslationDestination()) {
$ids['langcode']['type'] = 'string';
}
return $ids;
}
......@@ -70,11 +156,28 @@ public function getIds() {
* The row object to update from.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
foreach ($row->getRawDestination() as $property => $value) {
$this->updateEntityProperty($entity, explode(Row::PROPERTY_SEPARATOR, $property), $value);
// This is a translation if the language in the active config does not
// match the language of this row.
$translation = FALSE;
if ($row->hasDestinationProperty('langcode') && $this->languageManager instanceof ConfigurableLanguageManager) {
$config = $entity->getConfigDependencyName();
$langcode = $this->configFactory->get('langcode');
if ($langcode != $row->getDestinationProperty('langcode')) {
$translation = TRUE;
}
}
$this->setRollbackAction($row->getIdMap());
if ($translation) {
$config_override = $this->languageManager->getLanguageConfigOverride($row->getDestinationProperty('langcode'), $config);
$config_override->set(str_replace(Row::PROPERTY_SEPARATOR, '.', $row->getDestinationProperty('property')), $row->getDestinationProperty('translation'));
$config_override->save();
}
else {
foreach ($row->getRawDestination() as $property => $value) {
$this->updateEntityProperty($entity, explode(Row::PROPERTY_SEPARATOR, $property), $value);
}
$this->setRollbackAction($row->getIdMap());
}
}
/**
......@@ -113,9 +216,39 @@ protected function updateEntityProperty(EntityInterface $entity, array $parents,
protected function generateId(Row $row, array $ids) {
$id_values = array();
foreach ($ids as $id) {
if ($this->isTranslationDestination() && $id == 'langcode') {
continue;
}
$id_values[] = $row->getDestinationProperty($id);
}
return implode('.', $id_values);
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
// The entity id does not include the langcode.
$id_values = array();
foreach ($destination_identifier as $key => $value) {
if ($this->isTranslationDestination() && $key == 'langcode') {
continue;
}
$id_values[] = $value;
}
$entity_id = implode('.', $id_values);
$language = $destination_identifier['langcode'];
$config = $this->storage->load($entity_id)->getConfigDependencyName();
$config_override = $this->languageManager->getLanguageConfigOverride($language, $config);
// Rollback the translation.
$config_override->delete();
}
else {
$destination_identifier = implode('.', $destination_identifier);
parent::rollback(array($destination_identifier));
}
}
}
......@@ -18,15 +18,10 @@ public function getIds() {
$ids['entity_type']['type'] = 'string';
$ids['bundle']['type'] = 'string';
$ids['field_name']['type'] = 'string';
if ($this->isTranslationDestination()) {
$ids['langcode']['type'] = 'string';
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
$destination_identifier = implode('.', $destination_identifier);
parent::rollback(array($destination_identifier));
}
}
<?php
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests rolling back of imports.
*
* @group migrate
*/
class MigrateRollbackEntityConfigTest extends MigrateTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['field', 'taxonomy', 'text', 'language', 'config_translation'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('taxonomy_vocabulary');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
}
/**
* Tests rolling back configuration entity translations.
*/
public function testConfigEntityRollback() {
// We use vocabularies to demonstrate importing and rolling back
// configuration entities with translations. First, import vocabularies.
$vocabulary_data_rows = [
['id' => '1', 'name' => 'categories', 'weight' => '2'],
['id' => '2', 'name' => 'tags', 'weight' => '1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_data_rows,
'ids' => $ids,
],
'process' => [
'vid' => 'id',
'name' => 'name',
'weight' => 'weight',
],
'destination' => ['plugin' => 'entity:taxonomy_vocabulary'],
];
/** @var \Drupal\migrate\Plugin\Migration $vocabulary_migration */
$vocabulary_migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$vocabulary_id_map = $vocabulary_migration->getIdMap();
$this->assertTrue($vocabulary_migration->getDestinationPlugin()
->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this);
$vocabulary_executable->import();
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertTrue($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
// Second, import translations of the vocabulary name property.
$vocabulary_i18n_data_rows = [
[
'id' => '1',
'name' => '1',
'language' => 'fr',
'property' => 'name',
'translation' => 'fr - categories'
],
[
'id' => '2',
'name' => '2',
'language' => 'fr',
'property' => 'name',
'translation' => 'fr - tags'
],
];
$ids = [
'id' => ['type' => 'integer'],
'language' => ['type' => 'string'],
];
$definition = [
'id' => 'i18n_vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_i18n_data_rows,
'ids' => $ids,
'constants' => [
'name' => 'name',
]
],
'process' => [
'vid' => 'id',
'langcode' => 'language',
'property' => 'constants/name',
'translation' => 'translation',
],
'destination' => [
'plugin' => 'entity:taxonomy_vocabulary',
'translations' => 'true',
],
];
$vocabulary_i18n__migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$vocabulary_i18n_id_map = $vocabulary_i18n__migration->getIdMap();
$this->assertTrue($vocabulary_i18n__migration->getDestinationPlugin()
->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_i18n_executable = new MigrateExecutable($vocabulary_i18n__migration, $this);
$vocabulary_i18n_executable->import();
$language_manager = \Drupal::service('language_manager');
foreach ($vocabulary_i18n_data_rows as $row) {
$langcode = $row['language'];
$id = 'taxonomy.vocabulary.' . $row['id'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, $id);
$this->assertSame($row['translation'], $config_translation->get('name'));
$map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertNotNull($map_row['destid1']);
}
// Perform the rollback and confirm the translation was deleted and the map
// table row removed.
$vocabulary_i18n_executable->rollback();
foreach ($vocabulary_i18n_data_rows as $row) {
$langcode = $row['language'];
$id = 'taxonomy.vocabulary.' . $row['id'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, $id);
$this->assertNull($config_translation->get('name'));
$map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertFalse($map_row);
}
// Confirm the original vocabulary still exists.
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertTrue($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
}
}
......@@ -2,8 +2,10 @@
namespace Drupal\migrate_drupal\Plugin\migrate\destination;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfig as BaseEntityFieldStorageConfig;
......@@ -45,11 +47,17 @@ class EntityFieldStorageConfig extends BaseEntityFieldStorageConfig {
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager
* The field type plugin manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory