diff --git a/migrate_tools.module b/migrate_tools.module index 98b5a51d1e18a2f83262aca41d5e697d1bb98b35..976891fa65d81459e1befb72e37a1468a2abc302 100644 --- a/migrate_tools.module +++ b/migrate_tools.module @@ -96,6 +96,6 @@ function migrate_tools_migrate_prepare_row(Row $row, MigrateSourceInterface $sou // Keep track of all source rows here, as SourcePluginBase::next() might // skip some rows, and we need them all to detect missing items in source to // delete in destination. - $migrateTools->addToSyncSourceIds($migration->getPluginId(), $row->getSourceIdValues()); + $migrateTools->addToSyncSourceIds($migration->getPluginId(), $row->getSourceIdValues(), $source); } } diff --git a/src/MigrateTools.php b/src/MigrateTools.php index 5de7c6f4955f8137ba3bb08f8f26d85d69ea743a..529db5b8c7a7119d39616dd90431be67f0bc57d6 100644 --- a/src/MigrateTools.php +++ b/src/MigrateTools.php @@ -6,6 +6,7 @@ namespace Drupal\migrate_tools; use Drupal\Core\Database\Connection; use Drupal\migrate\Plugin\MigrateIdMapInterface; +use Drupal\migrate\Plugin\MigrateSourceInterface; use Drupal\migrate\Plugin\MigrationInterface; /** @@ -94,11 +95,13 @@ class MigrateTools { * Migration ID. * @param array $sourceIds * A set of SyncSourceIds. Gets serialized to retain its structure. + * @param \Drupal\migrate\Plugin\MigrateSourceInterface $source + * The migrate source. * * @throws \Exception */ - public function addToSyncSourceIds(string $migrationId, array $sourceIds): void - { + public function addToSyncSourceIds(string $migrationId, array $sourceIds, MigrateSourceInterface $source): void { + $sourceIds = $this->prepareSourceIdValues($sourceIds, $source); $this->bufferedSyncIdsEntries[] = [ 'migration_id' => $migrationId, // Serialize source IDs before saving them to retain their structure. @@ -109,6 +112,37 @@ class MigrateTools { } } + /** + * Ensure all source IDs match the expected type. + * + * @param array $rowSourceIds + * The source IDs from the current row. + * @param \Drupal\migrate\Plugin\MigrateSourceInterface $source + * The migrate source. + * + * @return array + * The updated source values. + * + * @see migrate_tools_migrate_prepare_row() + * @see https://www.drupal.org/node/3104268 + */ + protected function prepareSourceIdValues(array $rowSourceIds, MigrateSourceInterface $source): array { + $sourceIds = $source->getIds(); + foreach ($rowSourceIds as $key => $value) { + if (!array_key_exists($key, $sourceIds)) { + continue; + } + + // Cast the source ID as the configured type to avoid rollback + // and recreations when an exact match is not found. + if (in_array($sourceIds[$key]['type'], ['string', 'integer'], TRUE)) { + $rowSourceIds[$key] = $sourceIds[$key]['type'] === 'string' ? (string) $rowSourceIds[$key] : (int) $rowSourceIds[$key]; + } + } + + return $rowSourceIds; + } + /** * Flushes any pending SyncSourceIds to the database. * diff --git a/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.mixed_terms.yml b/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.mixed_terms.yml new file mode 100644 index 0000000000000000000000000000000000000000..549a707e48b9d04de996c1b0c155f5736c8bc93c --- /dev/null +++ b/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.mixed_terms.yml @@ -0,0 +1,37 @@ +langcode: en +status: true +id: mixed_terms +label: Mixed Terms +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: [] +migration_group: default +source: + plugin: embedded_data + data_rows: + - + name: 1 + - + name: '2' + - + name: Orange + - + name: 4.1 + ids: + name: + type: string + constants: + vocabulary: fruit +process: + name: name + vid: constants/vocabulary +destination: + plugin: entity:taxonomy_term +migration_dependencies: + required: [] + optional: [] +dependencies: + enforced: + module: + - migrate_tools_test diff --git a/tests/src/Functional/DrushCommandsTest.php b/tests/src/Functional/DrushCommandsTest.php index db429de37e7d3dc77eb590600165cb1c3dfdf1d3..1ca8a3bedf746860a829f259d923ca638e87bfa9 100644 --- a/tests/src/Functional/DrushCommandsTest.php +++ b/tests/src/Functional/DrushCommandsTest.php @@ -104,6 +104,16 @@ final class DrushCommandsTest extends BrowserTestBase { 'message_count' => 0, 'last_imported' => '', ], + [ + 'group' => 'Default (default)', + 'id' => 'mixed_terms', + 'imported' => 0, + 'status' => 'Idle', + 'total' => 4, + 'unprocessed' => 4, + 'message_count' => 0, + 'last_imported' => '', + ], [ 'group' => 'Default (default)', 'id' => 'source_exception', @@ -222,4 +232,32 @@ EOT; $this->assertCount(3, $id_map); } + /** + * Tests synced import with and without update enforced. + */ + public function testSyncMixedKeysImport(): void { + $this->drush('mim', ['mixed_terms']); + $this->assertStringContainsString('1/4', $this->getErrorOutput()); + $this->assertStringContainsString('4/4', $this->getErrorOutput()); + $this->assertMatchesRegularExpression('/4\/4[^\n]+\[notice\][^\n]+Processed 4 items \(4 created, 0 updated, 0 failed, 0 ignored\) - done with \'mixed_terms\'/', $this->getErrorOutput()); + $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load(1); + $this->assertEquals('1', $term->label()); + $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load(2); + $this->assertEquals('2', $term->label()); + $source = $this->container->get('config.factory')->getEditable('migrate_plus.migration.mixed_terms')->get('source'); + $source['data_rows'][] = ['name' => 'Grape']; + $this->container->get('config.factory')->getEditable('migrate_plus.migration.mixed_terms')->set('source', $source)->save(); + // Flush cache so the recently changed migration can be refreshed. + drupal_flush_all_caches(); + $this->drush('mim', ['mixed_terms'], ['sync' => NULL, 'update' => NULL]); + $this->assertStringContainsString('1/5', $this->getErrorOutput()); + $this->assertMatchesRegularExpression('/5\/5[^\n]+\[notice\][^\n]+Processed 5 items \(1 created, 4 updated, 0 failed, 0 ignored\) - done with \'mixed_terms\'/', $this->getErrorOutput()); + $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadMultiple(); + $this->assertEquals(5, count($terms)); + + // If keys are converted correctly terms should not get deleted and + // recreated, so we should still have IDs 1 to 5. + $this->assertEquals(range(1, 5), array_keys($terms)); + } + }