Commit c1933e73 authored by catch's avatar catch
Browse files

Issue #3239298 by alexpott, andypost, quietone, huzooka, daffie, longwave, Wim...

Issue #3239298 by alexpott, andypost, quietone, huzooka, daffie, longwave, Wim Leers, larowlan: Fix \Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::updateEntity() so that config translation migrations can be rolled back
parent 84862bae
......@@ -65,6 +65,7 @@ process:
- admin_theme
destination:
plugin: entity:block
translations: true
migration_dependencies:
required:
- d6_block
......
......@@ -25,6 +25,7 @@ process:
destination:
plugin: entity:taxonomy_vocabulary
destination_module: config_translation
translations: true
migration_dependencies:
required:
- d6_taxonomy_vocabulary
......
......@@ -72,6 +72,7 @@ process:
- admin_theme
destination:
plugin: entity:block
translations: true
migration_dependencies:
required:
......
......@@ -53,6 +53,7 @@ process:
field_name: type
destination:
plugin: entity:field_config
translations: true
migration_dependencies:
required:
- language
......
......@@ -26,6 +26,7 @@ process:
destination:
plugin: entity:menu
destination_module: config_translation
translations: true
migration_dependencies:
required:
- language
......
......@@ -26,6 +26,7 @@ process:
destination:
plugin: entity:taxonomy_vocabulary
destination_module: config_translation
translations: true
migration_dependencies:
required:
- language
......
......@@ -313,7 +313,7 @@ public function rollback() {
$destination_key = $id_map->currentDestination();
if ($destination_key) {
$map_row = $id_map->getRowByDestination($destination_key);
if ($map_row['rollback_action'] == MigrateIdMapInterface::ROLLBACK_DELETE) {
if (!isset($map_row['rollback_action']) || $map_row['rollback_action'] == MigrateIdMapInterface::ROLLBACK_DELETE) {
$this->getEventDispatcher()
->dispatch(new MigrateRowDeleteEvent($this->migration, $destination_key), MigrateEvents::PRE_ROW_DELETE);
$destination->rollback($destination_key);
......
......@@ -211,7 +211,7 @@ public function getRowBySource(array $source_id_values);
* The destination identifier keyed values of the record, e.g. ['nid' => 5].
*
* @return array
* The row(s) of data.
* The row(s) of data or an empty array when there is no matching map row.
*/
public function getRowByDestination(array $destination_id_values);
......
......@@ -50,6 +50,7 @@
* bundle: 'constants/bundle'
* field_name: name
* ...
* property: property
* translation: translation
* destination:
* plugin: entity:field_config
......@@ -58,7 +59,8 @@
*
* Because the translations configuration is set to "true", this will save the
* migrated, processed row to a "field_config" entity associated with the
* designated langcode.
* designated langcode. Note that the this makes the "translation" and
* "property" properties required.
*/
class EntityConfigBase extends Entity {
......@@ -190,12 +192,16 @@ public function getIds() {
* The entity to update.
* @param \Drupal\migrate\Row $row
* The row object to update from.
*
* @throws \LogicException
* Thrown if the destination is for translations and either the "property"
* or "translation" property does not exist.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
// 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) {
if ($this->isTranslationDestination() && $row->hasDestinationProperty('langcode') && $this->languageManager instanceof ConfigurableLanguageManager) {
$config = $entity->getConfigDependencyName();
$langcode = $this->configFactory->get('langcode');
if ($langcode != $row->getDestinationProperty('langcode')) {
......@@ -204,6 +210,12 @@ protected function updateEntity(EntityInterface $entity, Row $row) {
}
if ($translation) {
if (!$row->hasDestinationProperty('property')) {
throw new \LogicException('The "property" property is required');
}
if (!$row->hasDestinationProperty('translation')) {
throw new \LogicException('The "translation" property is required');
}
$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();
......
......@@ -3,11 +3,13 @@
namespace Drupal\Tests\migrate\Unit;
use Drupal\Component\Utility\Html;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\migrate\MigrateExecutable
......@@ -15,6 +17,13 @@
*/
class MigrateExecutableTest extends MigrateTestCase {
/**
* Stores ID map records of the ID map plugin from ::getTestRollbackIdMap.
*
* @var string[][]
*/
protected static $idMapRecords;
/**
* The mocked migration entity.
*
......@@ -36,6 +45,13 @@ class MigrateExecutableTest extends MigrateTestCase {
*/
protected $executable;
/**
* A mocked event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $eventDispatcher;
/**
* The migration's configuration values.
*
......@@ -50,10 +66,11 @@ class MigrateExecutableTest extends MigrateTestCase {
*/
protected function setUp(): void {
parent::setUp();
static::$idMapRecords = [];
$this->migration = $this->getMigration();
$this->message = $this->createMock('Drupal\migrate\MigrateMessageInterface');
$event_dispatcher = $this->createMock('Symfony\Contracts\EventDispatcher\EventDispatcherInterface');
$this->executable = new TestMigrateExecutable($this->migration, $this->message, $event_dispatcher);
$this->eventDispatcher = $this->createMock('Symfony\Contracts\EventDispatcher\EventDispatcherInterface');
$this->executable = new TestMigrateExecutable($this->migration, $this->message, $this->eventDispatcher);
$this->executable->setStringTranslation($this->getStringTranslationStub());
}
......@@ -498,4 +515,209 @@ protected function getMockSource() {
return $source;
}
/**
* Tests rollback.
*
* @param array[] $id_map_records
* The ID map records to test with.
* @param bool $rollback_called
* Sets an expectation that the destination's rollback() will or will not be
* called.
* @param string[] $source_id_keys
* The keys of the source IDs. The provided source ID keys must be defined
* in the $id_map_records parameter. Optional, defaults to ['source'].
* @param string[] $destination_id_keys
* The keys of the destination IDs. The provided keys must be defined in the
* $id_map_records parameter. Optional, defaults to ['destination'].
* @param int $expected_result
* The expected result of the rollback action. Optional, defaults to
* MigrationInterface::RESULT_COMPLETED.
*
* @dataProvider providerTestRollback
*
* @covers ::rollback
*/
public function testRollback(array $id_map_records, bool $rollback_called = TRUE, array $source_id_keys = ['source'], array $destination_id_keys = ['destination'], int $expected_result = MigrationInterface::RESULT_COMPLETED) {
$id_map = $this
->getTestRollbackIdMap($id_map_records, $source_id_keys, $destination_id_keys)
->reveal();
$migration = $this->getMigration($id_map);
$destination = $this->prophesize(MigrateDestinationInterface::class);
if ($rollback_called) {
$destination->rollback($id_map->currentDestination())->shouldBeCalled();
}
else {
$destination->rollback()->shouldNotBeCalled();
}
$migration
->method('getDestinationPlugin')
->willReturn($destination->reveal());
$executable = new TestMigrateExecutable($migration, $this->message, $this->eventDispatcher);
$this->assertEquals($expected_result, $executable->rollback());
}
/**
* Data provider for ::testRollback.
*
* @return array
* The test cases.
*/
public function providerTestRollback() {
return [
'Rollback delete' => [
'ID map records' => [
[
'source' => '1',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
],
],
'Rollback preserve' => [
'ID map records' => [
[
'source' => '1',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_PRESERVE,
],
],
'Rollback called' => FALSE,
],
'Rolling back a failed row' => [
'ID map records' => [
[
'source' => '1',
'destination' => NULL,
'source_row_status' => '2',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
],
'Rollback called' => FALSE,
],
'Rolling back with ID map having records with duplicated destination ID' => [
'ID map records' => [
[
'source_1' => '1',
'source_2' => '1',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
[
'source_1' => '2',
'source_2' => '2',
'destination' => '2',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_PRESERVE,
],
[
'source_1' => '3',
'source_2' => '3',
'destination' => '1',
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
],
],
'Rollback called' => TRUE,
'Source ID keys' => ['source_1', 'source_2'],
],
'Rollback NULL' => [
'ID map records' => [
[
'source' => '1',
'destination' => '1',
'rollback_action' => NULL,
],
],
],
'Rollback missing' => [
'ID map records' => [
[
'source' => '1',
'destination' => '1',
],
],
],
];
}
/**
* Returns an ID map object prophecy used in ::testRollback.
*
* @return \Prophecy\Prophecy\ObjectProphecy
* An ID map object prophecy.
*/
public function getTestRollbackIdMap(array $items, array $source_id_keys, array $destination_id_keys) {
static::$idMapRecords = array_map(function (array $item) {
return $item + [
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
];
}, $items);
$array_iterator = new \ArrayIterator(static::$idMapRecords);
$id_map = $this->prophesize(MigrateIdMapInterface::class);
$id_map->setMessage(Argument::cetera())->willReturn(NULL);
$id_map->rewind()->will(function () use ($array_iterator) {
$array_iterator->rewind();
});
$id_map->valid()->will(function () use ($array_iterator) {
return $array_iterator->valid();
});
$id_map->next()->will(function () use ($array_iterator) {
$array_iterator->next();
});
$id_map->currentDestination()->will(function () use ($array_iterator, $destination_id_keys) {
$current = $array_iterator->current();
$destination_values = array_filter($current, function ($key) use ($destination_id_keys) {
return in_array($key, $destination_id_keys, TRUE);
}, ARRAY_FILTER_USE_KEY);
return empty(array_filter($destination_values, 'is_null'))
? array_combine($destination_id_keys, array_values($destination_values))
: NULL;
});
$id_map->currentSource()->will(function () use ($array_iterator, $source_id_keys) {
$current = $array_iterator->current();
$source_values = array_filter($current, function ($key) use ($source_id_keys) {
return in_array($key, $source_id_keys, TRUE);
}, ARRAY_FILTER_USE_KEY);
return empty(array_filter($source_values, 'is_null'))
? array_combine($source_id_keys, array_values($source_values))
: NULL;
});
$id_map->getRowByDestination(Argument::type('array'))->will(function () {
$destination_ids = func_get_args()[0][0];
$return = array_reduce(self::$idMapRecords, function (array $carry, array $record) use ($destination_ids) {
if (array_merge($record, $destination_ids) === $record) {
$carry = $record;
}
return $carry;
}, []);
return $return;
});
$id_map->deleteDestination(Argument::type('array'))->will(function () {
$destination_ids = func_get_args()[0][0];
$matching_records = array_filter(self::$idMapRecords, function (array $record) use ($destination_ids) {
return array_merge($record, $destination_ids) === $record;
});
foreach (array_keys($matching_records) as $record_key) {
unset(self::$idMapRecords[$record_key]);
}
});
$id_map->delete(Argument::type('array'))->will(function () {
$source_ids = func_get_args()[0][0];
$matching_records = array_filter(self::$idMapRecords, function (array $record) use ($source_ids) {
return array_merge($record, $source_ids) === $record;
});
foreach (array_keys($matching_records) as $record_key) {
unset(self::$idMapRecords[$record_key]);
}
});
return $id_map;
}
}
......@@ -4,6 +4,7 @@
use Drupal\Core\Database\Driver\sqlite\Connection;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\UnitTestCase;
......@@ -36,16 +37,22 @@ abstract class MigrateTestCase extends UnitTestCase {
/**
* Retrieves a mocked migration.
*
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface|\PHPUnit\Framework\MockObject\MockObject|null $id_map
* An ID map plugin to use, or NULL for using a mocked one. Optional,
* defaults to NULL.
*
* @return \Drupal\migrate\Plugin\MigrationInterface|\PHPUnit\Framework\MockObject\MockObject
* The mocked migration.
*/
protected function getMigration() {
protected function getMigration($id_map = NULL) {
$this->migrationConfiguration += ['migrationClass' => 'Drupal\migrate\Plugin\Migration'];
$this->idMap = $this->createMock('Drupal\migrate\Plugin\MigrateIdMapInterface');
$this->idMap
->method('getQualifiedMapTableName')
->willReturn('test_map');
$this->idMap = $id_map;
if (is_null($id_map)) {
$this->idMap = $this->createMock(MigrateIdMapInterface::class);
$this->idMap
->method('getQualifiedMapTableName')
->willReturn('test_map');
}
$migration = $this->getMockBuilder($this->migrationConfiguration['migrationClass'])
->disableOriginalConstructor()
......
......@@ -37,7 +37,7 @@ class SubProcessTest extends MigrateTestCase {
* @dataProvider providerTestSubProcess
*/
public function testSubProcess($process_configuration, $source_values = []) {
$migration = $this->getMigration($process_configuration);
$migration = $this->getMigration();
// Set up the properties for the sub_process.
$plugin = new SubProcess($process_configuration, 'sub_process', []);
// Manually create the plugins. Migration::getProcessPlugins does this
......
......@@ -2,6 +2,7 @@
namespace Drupal\Tests\system\Kernel\Migrate\d7;
use Drupal\migrate\MigrateExecutable;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
......@@ -62,6 +63,22 @@ public function testMenuTranslation() {
$config_translation = $language_manager->getLanguageConfigOverride('is', 'menu-fixedlang');
$this->assertNull($config_translation->get('description'));
$this->assertNull($config_translation->get('label'));
// Test rollback.
$this->migration = $this->getMigration("d7_menu_translation");
(new MigrateExecutable($this->migration, $this))->rollback();
$config_translation = $language_manager->getLanguageConfigOverride('is', 'system.menu.main');
$this->assertNull($config_translation->get('description'));
$this->assertNull($config_translation->get('label'));
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'system.menu.main');
$this->assertNull($config_translation->get('description'));
$this->assertNull($config_translation->get('label'));
// Translate and localize menu.
$config_translation = $language_manager->getLanguageConfigOverride('fr', 'system.menu.menu-test-menu');
$this->assertNull($config_translation->get('description'));
}
}
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