From 6c078bf1cb4a6c51d6ce67f4a49f557cf0603c22 Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Fri, 27 Feb 2026 11:51:04 +0100 Subject: [PATCH 1/9] perf: improve mapping lookup performance --- mapping-experiment.php | 168 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 mapping-experiment.php diff --git a/mapping-experiment.php b/mapping-experiment.php new file mode 100644 index 000000000..ee06561cb --- /dev/null +++ b/mapping-experiment.php @@ -0,0 +1,168 @@ + + */ + private array $pendingTasks = []; + + /** + * Important: this method returns a reference to a string which is also internally stored. + * It must be called like this: + * + * $data['someId'] = &$mappingService->getMapping('sourceId'); + * + * The use of '&' is very important, otherwise it will only copy the placeholder string. + * More info on this can be found in the PHP docs: + * https://www.php.net/manual/en/language.references.return.php + */ + public function &getMapping(string $sourceId): string + { + // previously we would fetch the mapping from the DB here, + // but reaching out to the DB every time is expensive + + // so instead we just return a placeholder string + // and update it later, when we know all mappings that are needed + + // and then use the string reference to update the placeholders + + $pointer = 'placeholder-mapping-uuid'; + + $task = new MappingTask($pointer, $sourceId); + $this->pendingTasks[] = $task; + + return $pointer; + } + + public function resolvePendingMappings(): void + { + // in here we can fetch all requested mappings from the DB + // in a single query and update all corresponding strings + foreach ($this->pendingTasks as $task) { + $task->reference = 'changed-' . $task->sourceId; + unset($task->reference); + } + } +} + +// a stripped-down example of a converter +function convertData(MappingService $mappingService): array +{ + $data['name'] = 'John Doe'; + $data['manufacturerId'] = &$mappingService->getMapping('manufacturer01'); + $data['price'] = []; + for ($i = 0; $i < 3; ++$i) { + $data['price'][] = convertCurrency($i, $mappingService); + } + + return $data; +} + +// a stripped-down example of internal logic in a converter +function convertCurrency(int $i, MappingService $mappingService): array +{ + $price = [ + 'id' => $i, + // unfortunately, this does not work, but there is an easy workaround below + // 'currencyId' => &$mappingService->getMapping('currency0' . $i), + ]; + + // this works fine + $price['currencyId'] = &$mappingService->getMapping('currency0' . $i); + + return $price; +} + +// === Main logic below === + +$mappingService = new MappingService(); +$convertedData = convertData($mappingService); +echo '============== Initial data after converter run ==============' . \PHP_EOL; +var_dump($convertedData); + +echo '============== data after mapping tasks got resolved ==============' . \PHP_EOL; +$mappingService->resolvePendingMappings(); +var_dump($convertedData); + +// === Output looks like this === +/* +============== Initial data after converter run ============== +array(3) { + ["name"]=> + string(8) "John Doe" + ["manufacturerId"]=> + &string(24) "placeholder-mapping-uuid" + ["price"]=> + array(3) { + [0]=> + array(2) { + ["id"]=> + int(0) + ["currencyId"]=> + &string(24) "placeholder-mapping-uuid" + } + [1]=> + array(2) { + ["id"]=> + int(1) + ["currencyId"]=> + &string(24) "placeholder-mapping-uuid" + } + [2]=> + array(2) { + ["id"]=> + int(2) + ["currencyId"]=> + &string(24) "placeholder-mapping-uuid" + } + } +} +============== data after mapping tasks got resolved ============== +array(3) { + ["name"]=> + string(8) "John Doe" + ["manufacturerId"]=> + string(22) "changed-manufacturer01" + ["price"]=> + array(3) { + [0]=> + array(2) { + ["id"]=> + int(0) + ["currencyId"]=> + string(18) "changed-currency00" + } + [1]=> + array(2) { + ["id"]=> + int(1) + ["currencyId"]=> + string(18) "changed-currency01" + } + [2]=> + array(2) { + ["id"]=> + int(2) + ["currencyId"]=> + string(18) "changed-currency02" + } + } +} + */ From e9728c763385c17f1a104389de052bac05074470 Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Fri, 27 Feb 2026 16:20:14 +0100 Subject: [PATCH 2/9] feat: wip MappingServiceV2 implementation --- mapping-experiment.php | 4 +- src/Migration/Mapping/MappingPromise.php | 39 ++++++++++ src/Migration/Mapping/MappingService.php | 4 + src/Migration/Mapping/MappingServiceV2.php | 77 +++++++++++++++++++ .../Handler/Processor/FetchingProcessor.php | 21 +++++ 5 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/Migration/Mapping/MappingPromise.php create mode 100644 src/Migration/Mapping/MappingServiceV2.php diff --git a/mapping-experiment.php b/mapping-experiment.php index ee06561cb..c3ca56eb6 100644 --- a/mapping-experiment.php +++ b/mapping-experiment.php @@ -18,6 +18,8 @@ public function __construct( class MappingService { + public const PLACEHOLDER = 'placeholder-mapping-uuid'; + /** * @var list */ @@ -43,7 +45,7 @@ public function &getMapping(string $sourceId): string // and then use the string reference to update the placeholders - $pointer = 'placeholder-mapping-uuid'; + $pointer = self::PLACEHOLDER; $task = new MappingTask($pointer, $sourceId); $this->pendingTasks[] = $task; diff --git a/src/Migration/Mapping/MappingPromise.php b/src/Migration/Mapping/MappingPromise.php new file mode 100644 index 000000000..80f7635a6 --- /dev/null +++ b/src/Migration/Mapping/MappingPromise.php @@ -0,0 +1,39 @@ +mappings[$cacheKey]; } + // simulate network latency + // todo: remove me + // \usleep(1000); // 1ms + $sql = 'SELECT id, connection_id AS connectionId, entity, diff --git a/src/Migration/Mapping/MappingServiceV2.php b/src/Migration/Mapping/MappingServiceV2.php new file mode 100644 index 000000000..f09ef5255 --- /dev/null +++ b/src/Migration/Mapping/MappingServiceV2.php @@ -0,0 +1,77 @@ + + */ + private array $unfulfilledPromises = []; + + public function __construct( + protected readonly Connection $connection, + ) { + } + + public function &getMapping( + string $entityName, + string $oldIdentifier, + bool $shouldCreate = false, + ): string { + $ref = self::PLACEHOLDER; + + $promise = new MappingPromise($ref, $entityName, $oldIdentifier, $shouldCreate); + $this->unfulfilledPromises[] = $promise; + + return $ref; + } + + public function resolvePromises( + string $connectionId, + ): void { + $sql = ' + SELECT id, + connection_id AS connectionId, + entity, + old_identifier AS oldIdentifier, + entity_id AS entityId, + entity_value AS entityValue, + checksum, + additional_data AS additionalData + FROM swag_migration_mapping + WHERE connection_id = :connectionId + AND (entity, old_identifier) IN :mappingLookups; + '; + + $mappingLookups = array_map( + fn ($mappingPromise) => [$mappingPromise->entity, $mappingPromise->sourceId], + $this->unfulfilledPromises, + ); + + $mappings = $this->connection->fetchAssociative( + $sql, + [ + 'connectionId' => Uuid::fromHexToBytes($connectionId), + 'mappingLookups' => $mappingLookups, + ], + [ + 'connectionId' => 'binary', + 'mappingLookups' => 'array', + ] + ); + + // todo: iterate DB mappings and apply them + + // todo: also create new mappings if necessary based on the promises + } +} diff --git a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php index 2dfaa742b..d255bf9d9 100644 --- a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php +++ b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php @@ -61,6 +61,20 @@ public function process( ): void { $runId = $migrationContext->getRunUuid(); $totalCountOfCurrentEntity = $progress->getDataSets()->getTotalByEntityName($progress->getCurrentEntity()); + + // todo: remove benchmarking code + if ($progress->getCurrentEntity() === 'product' && $progress->getCurrentEntityProgress() === 0) { + $blackfire = new \Blackfire\Client(); + $config = new \Blackfire\Profile\Configuration(); + $config->setTitle('Fetching ' . $progress->getCurrentEntity() . ' ' . $progress->getCurrentEntityProgress() . ' / ' . $totalCountOfCurrentEntity . ' limit ' . $migrationContext->getLimit()); + $config->setMetadata('entity', $progress->getCurrentEntity()); + $config->setMetadata('progress', (string) $progress->getCurrentEntityProgress()); + $config->setMetadata('total', (string) $totalCountOfCurrentEntity); + $config->setMetadata('limit', (string) $migrationContext->getLimit()); + $probe = $blackfire->createProbe($config); + // code being profiled... + } + $data = $this->migrationDataFetcher->fetchData($migrationContext, $context); if ($progress->getCurrentEntityProgress() >= $totalCountOfCurrentEntity) { @@ -81,6 +95,13 @@ public function process( $progress->setProgress($progress->getProgress() + \count($data)); $this->updateProgress($runId, $progress, $context); + + // todo: remove benchmarking code + if ($progress->getCurrentEntity() === 'product' && $progress->getCurrentEntityProgress() === 0) { + // end profiling + $profile = $blackfire->endProbe($probe); + } + $this->bus->dispatch(new MigrationProcessMessage($context, $migrationContext->getRunUuid())); } } From 3089f3d6c7e354fa761dcdcf118a264beb27ee82 Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Thu, 5 Mar 2026 17:20:02 +0100 Subject: [PATCH 3/9] feat: wip intermediate commit of non working state --- src/DependencyInjection/migration.xml | 8 ++ src/DependencyInjection/shopware.xml | 1 + src/Migration/Mapping/MappingPromise.php | 10 +- src/Migration/Mapping/MappingServiceV2.php | 116 +++++++++++++++--- .../Handler/Processor/FetchingProcessor.php | 2 +- .../Service/MigrationDataConverter.php | 4 + .../Shopware/Converter/ProductConverter.php | 82 ++++--------- .../Mapping/MappingServiceV2Test.php | 22 ++++ 8 files changed, 169 insertions(+), 76 deletions(-) create mode 100644 tests/Migration/Mapping/MappingServiceV2Test.php diff --git a/src/DependencyInjection/migration.xml b/src/DependencyInjection/migration.xml index a4235fa4f..9d4310b56 100644 --- a/src/DependencyInjection/migration.xml +++ b/src/DependencyInjection/migration.xml @@ -48,6 +48,13 @@ + + + + + + + @@ -226,6 +233,7 @@ + diff --git a/src/DependencyInjection/shopware.xml b/src/DependencyInjection/shopware.xml index bc5201298..aea5462de 100644 --- a/src/DependencyInjection/shopware.xml +++ b/src/DependencyInjection/shopware.xml @@ -115,6 +115,7 @@ + reference = $uuid; + unset($this->reference); + } } diff --git a/src/Migration/Mapping/MappingServiceV2.php b/src/Migration/Mapping/MappingServiceV2.php index f09ef5255..bcac32b32 100644 --- a/src/Migration/Mapping/MappingServiceV2.php +++ b/src/Migration/Mapping/MappingServiceV2.php @@ -3,23 +3,30 @@ namespace SwagMigrationAssistant\Migration\Mapping; use Doctrine\DBAL\Connection; +use Psr\Log\LoggerInterface; +use Shopware\Core\Defaults; use Shopware\Core\Framework\Uuid\Uuid; +use Symfony\Contracts\Service\ResetInterface; /** * @internal */ #[Package('fundamentals@after-sales')] -class MappingServiceV2 +class MappingServiceV2 implements ResetInterface { public const PLACEHOLDER = 'uuid-mapping-promise'; /** - * @var list + * indexed by entityName + oldIdentifier + * the value is always a list with at least one promise + * + * @var array> */ private array $unfulfilledPromises = []; public function __construct( protected readonly Connection $connection, + protected readonly LoggerInterface $logger, ) { } @@ -31,7 +38,12 @@ public function &getMapping( $ref = self::PLACEHOLDER; $promise = new MappingPromise($ref, $entityName, $oldIdentifier, $shouldCreate); - $this->unfulfilledPromises[] = $promise; + + $key = $entityName . $oldIdentifier; + if (!isset($this->unfulfilledPromises[$key])) { + $this->unfulfilledPromises[$key] = []; + } + $this->unfulfilledPromises[$key][] = $promise; return $ref; } @@ -39,8 +51,30 @@ public function &getMapping( public function resolvePromises( string $connectionId, ): void { - $sql = ' - SELECT id, + if ($this->unfulfilledPromises === []) { + return; + } + + // todo: clean up the SQL query shenanigans + $mappingLookups = array_map( + fn ($mappingPromises) => [$mappingPromises[0]->entity, $mappingPromises[0]->sourceId], + $this->unfulfilledPromises, + ); + + $placeholders = []; + $params = []; + $types = []; + + foreach ($mappingLookups as $pair) { + $placeholders[] = '(?, ?)'; + $params[] = $pair[0]; + $types[] = 'string'; + $params[] = $pair[1]; + $types[] = 'string'; + } + + $sql = sprintf( + 'SELECT id, connection_id AS connectionId, entity, old_identifier AS oldIdentifier, @@ -49,29 +83,79 @@ public function resolvePromises( checksum, additional_data AS additionalData FROM swag_migration_mapping - WHERE connection_id = :connectionId - AND (entity, old_identifier) IN :mappingLookups; - '; - - $mappingLookups = array_map( - fn ($mappingPromise) => [$mappingPromise->entity, $mappingPromise->sourceId], - $this->unfulfilledPromises, + WHERE connection_id = :connectionId AND (entity, old_identifier) IN (%s)', + implode(', ', $placeholders) ); + $mappings = $this->connection->fetchAssociative( $sql, [ 'connectionId' => Uuid::fromHexToBytes($connectionId), - 'mappingLookups' => $mappingLookups, + ...$params, ], [ 'connectionId' => 'binary', - 'mappingLookups' => 'array', + ...$types, ] ); - // todo: iterate DB mappings and apply them + if (is_array($mappings)) { + foreach ($mappings as $mapping) { + $key = $mapping['entity'] . $mapping['oldIdentifier']; + $promises = $this->unfulfilledPromises[$key]; + foreach ($promises as $promise) { + $promise->resolve($mapping['entityId']); + } + unset($this->unfulfilledPromises[$key]); + } + } + + // the remaining unfulfilled promises don't have a DB mapping yet + foreach ($this->unfulfilledPromises as $promises) { + $shouldCreate = false; + foreach ($promises as $promise) { + if ($promise->shouldCreate) { + $shouldCreate = true; + break; + } + } + + if (!$shouldCreate) { + // todo: proper error handling + throw new \RuntimeException('Could not resolve mapping promise ' . $promises[0]->sourceId . ' for ' . $promises[0]->entity); + } + + $newEntityId = Uuid::randomHex(); + $promise->resolve($newEntityId); // todo: should this only execute after the DB insert? + + $createMapping = [ + 'id' => Uuid::randomHex(), + 'connection_id' => Uuid::fromHexToBytes($connectionId), + 'entity' => $promises[0]->entity, + 'old_identifier' => $promises[0]->sourceId, + 'entity_id' => Uuid::fromHexToBytes($newEntityId), + 'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT), + ]; + // todo: bulk insert? + $this->connection->insert('swag_migration_mapping', $createMapping, [ + 'id' => 'binary', + 'connection_id' => 'binary', + 'entity_id' => 'binary', + ]); + } + + unset($this->unfulfilledPromises); + $this->unfulfilledPromises = []; + } + + public function reset(): void + { + if ($this->unfulfilledPromises !== []) { + $this->logger->error('Resetting MappingServiceV2 without resolving all promises'); + } - // todo: also create new mappings if necessary based on the promises + unset($this->unfulfilledPromises); + $this->unfulfilledPromises = []; } } diff --git a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php index d255bf9d9..124738812 100644 --- a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php +++ b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php @@ -99,7 +99,7 @@ public function process( // todo: remove benchmarking code if ($progress->getCurrentEntity() === 'product' && $progress->getCurrentEntityProgress() === 0) { // end profiling - $profile = $blackfire->endProbe($probe); + //$profile = $blackfire->endProbe($probe); } $this->bus->dispatch(new MigrationProcessMessage($context, $migrationContext->getRunUuid())); diff --git a/src/Migration/Service/MigrationDataConverter.php b/src/Migration/Service/MigrationDataConverter.php index 92a56366e..3e59b9cec 100644 --- a/src/Migration/Service/MigrationDataConverter.php +++ b/src/Migration/Service/MigrationDataConverter.php @@ -23,6 +23,7 @@ use SwagMigrationAssistant\Migration\Logging\LoggingServiceInterface; use SwagMigrationAssistant\Migration\Mapping\MappingDeltaResult; use SwagMigrationAssistant\Migration\Mapping\MappingServiceInterface; +use SwagMigrationAssistant\Migration\Mapping\MappingServiceV2; use SwagMigrationAssistant\Migration\Media\MediaFileServiceInterface; use SwagMigrationAssistant\Migration\MigrationContextInterface; use SwagMigrationAssistant\Migration\Validation\MigrationEntityValidationService; @@ -38,6 +39,7 @@ public function __construct( private readonly EntityDefinition $dataDefinition, private readonly MappingServiceInterface $mappingService, private readonly MigrationEntityValidationService $validationService, + private readonly MappingServiceV2 $mappingServiceV2, ) { } @@ -108,6 +110,8 @@ private function convertData( $convertFailureFlag = empty($convertStruct->getConverted()); + // todo: this needs to be called after the whole batch was converted + $this->mappingServiceV2->resolvePromises($migrationContext->getConnection()->getId()); $this->validationService->validate( $migrationContext, $context, diff --git a/src/Profile/Shopware/Converter/ProductConverter.php b/src/Profile/Shopware/Converter/ProductConverter.php index e9d534751..4eac51355 100644 --- a/src/Profile/Shopware/Converter/ProductConverter.php +++ b/src/Profile/Shopware/Converter/ProductConverter.php @@ -32,6 +32,7 @@ use SwagMigrationAssistant\Migration\Mapping\Lookup\MediaDefaultFolderLookup; use SwagMigrationAssistant\Migration\Mapping\Lookup\TaxLookup; use SwagMigrationAssistant\Migration\Mapping\MappingServiceInterface; +use SwagMigrationAssistant\Migration\Mapping\MappingServiceV2; use SwagMigrationAssistant\Migration\Media\MediaFileServiceInterface; use SwagMigrationAssistant\Migration\MigrationContextInterface; use SwagMigrationAssistant\Migration\Validation\Log\MigrationValidationRequiredFieldMissingLog; @@ -80,6 +81,7 @@ public function __construct( protected readonly MediaDefaultFolderLookup $mediaFolderLookup, protected readonly LanguageLookup $languageLookup, protected readonly DeliveryTimeLookup $deliveryTimeLookup, + protected readonly MappingServiceV2 $mappingServiceV2, ) { parent::__construct($mappingService, $loggingService); } @@ -187,11 +189,8 @@ public function convert( if (empty($returnData)) { $returnData = null; } - $this->updateMainMapping($migrationContext, $context); - $mainMapping = $this->mainMapping['id'] ?? null; - - return new ConvertStruct($converted, $returnData, $mainMapping); + return new ConvertStruct($converted, $returnData, null); } /** @@ -199,17 +198,14 @@ public function convert( */ protected function convertMainProduct(array $data): ConvertStruct { - $containerMapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $containerUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_CONTAINER, $data['id'], - $this->context + true, ); - $containerUuid = $containerMapping['entityId']; $converted = []; $converted['id'] = $containerUuid; - $this->mappingIds[] = $containerMapping['id']; unset($data['detail']['articleID']); $converted = $this->getProductData($data, $converted); @@ -218,14 +214,8 @@ protected function convertMainProduct(array $data): ConvertStruct $converted['productNumber'] .= 'M'; // Remove options from product container as in core unset($converted['options']); - $this->mainMapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, - DefaultEntities::PRODUCT, - $this->oldProductId, - $this->context, - $this->checksum - ); - $converted['children'][0]['id'] = $this->mainMapping['entityId']; + $productUuid = &$this->mappingServiceV2->getMapping(DefaultEntities::PRODUCT, $this->oldProductId, true); + $converted['children'][0]['id'] = $productUuid; if (isset($converted['children'][0]['media'])) { if (isset($converted['children'][0]['cover'])) { @@ -240,7 +230,7 @@ protected function convertMainProduct(array $data): ConvertStruct ); $productMediaRelationUuid = $productMediaRelationMapping['entityId']; $this->mappingIds[] = $productMediaRelationMapping['id']; - $media['productId'] = $this->mainMapping['entityId']; + $media['productId'] = $productUuid; $media['id'] = $productMediaRelationUuid; if (isset($coverMediaUuid) && $media['media']['id'] === $coverMediaUuid) { @@ -271,7 +261,7 @@ protected function convertMainProduct(array $data): ConvertStruct } $this->updateMainMapping($this->migrationContext, $this->context); - return new ConvertStruct($converted, $returnData, $this->mainMapping['id'] ?? null); + return new ConvertStruct($converted, $returnData, null); } /** @@ -281,29 +271,16 @@ protected function convertMainProduct(array $data): ConvertStruct */ protected function convertVariantProduct(array $data): ConvertStruct { - $parentMapping = $this->mappingService->getMapping( - $this->connectionId, - DefaultEntities::PRODUCT_CONTAINER, - $data['detail']['articleID'], - $this->context - ); - - if ($parentMapping === null) { - throw MigrationException::parentEntityForChildNotFound(DefaultEntities::PRODUCT, $this->oldProductId); - } - - $this->mainMapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $converted = []; + $converted['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT, $this->oldProductId, - $this->context, - $this->checksum + true + ); + $converted['parentId'] = &$this->mappingServiceV2->getMapping( + DefaultEntities::PRODUCT_CONTAINER, + $data['detail']['articleID'] ); - - $converted = []; - $converted['id'] = $this->mainMapping['entityId']; - $converted['parentId'] = $parentMapping['entityId']; - $this->mappingIds[] = $parentMapping['id']; $converted = $this->getProductData($data, $converted); unset($data['detail']['id'], $data['detail']['articleID'], $data['categories']); @@ -315,11 +292,8 @@ protected function convertVariantProduct(array $data): ConvertStruct if (empty($returnData)) { $returnData = null; } - $this->updateMainMapping($this->migrationContext, $this->context); - $mainMapping = $this->mainMapping['id'] ?? null; - - return new ConvertStruct($converted, $returnData, $mainMapping); + return new ConvertStruct($converted, $returnData, null); } /** @@ -329,17 +303,15 @@ protected function convertVariantProduct(array $data): ConvertStruct */ private function getUuidForProduct(array &$data): array { - $this->mainMapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $converted = []; + $converted['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT, $this->oldProductId, - $this->context, - $this->checksum + true, ); - $converted = []; - $converted['id'] = $this->mainMapping['entityId']; - + // todo: what was the purpose of this? + // todo: looks like it writes back a created mapping to a different entity + source id combination $mapping = $this->mappingService->getOrCreateMapping( $this->connectionId, DefaultEntities::PRODUCT_MAIN, @@ -349,7 +321,6 @@ private function getUuidForProduct(array &$data): array null, $converted['id'] ); - $this->mappingIds[] = $mapping['id']; return $converted; } @@ -707,15 +678,12 @@ private function applyOptions(array &$converted, array &$data): void */ private function getManufacturer(array $data): array { - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $manufacturer = []; + $manufacturer['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_MANUFACTURER, $data['id'], - $this->context + true, ); - $manufacturer = []; - $manufacturer['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $this->applyManufacturerTranslation($manufacturer, $data); $this->convertValue($manufacturer, 'link', $data, 'link'); diff --git a/tests/Migration/Mapping/MappingServiceV2Test.php b/tests/Migration/Mapping/MappingServiceV2Test.php new file mode 100644 index 000000000..a0c5407fd --- /dev/null +++ b/tests/Migration/Mapping/MappingServiceV2Test.php @@ -0,0 +1,22 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SwagMigrationAssistant\Test\Migration\Mapping; + +use Doctrine\DBAL\Connection; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Shopware\Core\Framework\Log\Package; +use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; + +#[Package('fundamentals@after-sales')] +class MappingServiceV2Test extends TestCase +{ + use IntegrationTestBehaviour; + + // todo +} From 873f4d4ec3af4578637ac5b2dae4b357a9adef7e Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Fri, 6 Mar 2026 15:08:35 +0100 Subject: [PATCH 4/9] feat: further progress --- src/Migration/Mapping/MappingPromise.php | 9 + src/Migration/Mapping/MappingServiceV2.php | 269 ++++++++++++++---- .../Mapping/SwagMigrationMappingEntity.php | 12 +- .../Handler/Processor/FetchingProcessor.php | 2 +- .../Shopware/Converter/ProductConverter.php | 15 +- .../Mapping/MappingServiceV2Test.php | 2 - 6 files changed, 234 insertions(+), 75 deletions(-) diff --git a/src/Migration/Mapping/MappingPromise.php b/src/Migration/Mapping/MappingPromise.php index bb57ef81f..08cd559f5 100644 --- a/src/Migration/Mapping/MappingPromise.php +++ b/src/Migration/Mapping/MappingPromise.php @@ -1,4 +1,9 @@ + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace SwagMigrationAssistant\Migration\Mapping; @@ -34,6 +39,10 @@ public function __construct( * if true, the mapping will be created in DB if it does not exist */ public bool $shouldCreate = false, + /** + * if creating, use this Uuid to map to + */ + public ?string $createWith = null, ) { } diff --git a/src/Migration/Mapping/MappingServiceV2.php b/src/Migration/Mapping/MappingServiceV2.php index bcac32b32..2d03a3d5a 100644 --- a/src/Migration/Mapping/MappingServiceV2.php +++ b/src/Migration/Mapping/MappingServiceV2.php @@ -1,4 +1,9 @@ + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace SwagMigrationAssistant\Migration\Mapping; @@ -30,14 +35,32 @@ public function __construct( ) { } + /** + * Lookup of a mapping with optional creation parameters if it doesn't exist yet. + * + * Important: this method returns a reference to a string which is also internally stored. + * It must be called like this: + * + * $data['newId'] = &$mappingService->getMapping('entity', 'oldId'); + * + * The use of '&' is very important, otherwise it will only copy the placeholder string. + * More info on this can be found in the PHP docs: + * https://www.php.net/manual/en/language.references.return.php + * + * @param bool $shouldCreate if the mapping should be created if it doesn't exist yet + * @param string|null $createWith if creating, use this Uuid to map to. All the same lookups will resolve to this + * + * @return string placeholder string which will be resolved later by calling @see MappingServiceV2->resolvePromises() + */ public function &getMapping( string $entityName, string $oldIdentifier, bool $shouldCreate = false, + ?string $createWith = null, ): string { $ref = self::PLACEHOLDER; - $promise = new MappingPromise($ref, $entityName, $oldIdentifier, $shouldCreate); + $promise = new MappingPromise($ref, $entityName, $oldIdentifier, $shouldCreate, $createWith); $key = $entityName . $oldIdentifier; if (!isset($this->unfulfilledPromises[$key])) { @@ -55,25 +78,108 @@ public function resolvePromises( return; } - // todo: clean up the SQL query shenanigans $mappingLookups = array_map( - fn ($mappingPromises) => [$mappingPromises[0]->entity, $mappingPromises[0]->sourceId], + fn ($mappingPromises) => [ + 'entity' => $mappingPromises[0]->entity, + 'oldIdentifier' => $mappingPromises[0]->sourceId, + ], $this->unfulfilledPromises, ); + $dbMappings = $this->fetchDbMappings($connectionId, $mappingLookups); + + foreach ($dbMappings as $dbMapping) { + $key = $dbMapping['entity'] . $dbMapping['oldIdentifier']; + $promises = $this->unfulfilledPromises[$key]; + foreach ($promises as $promise) { + $promise->resolve($dbMapping['entityId']); + } + unset($this->unfulfilledPromises[$key]); + } + + // the remaining unfulfilled promises don't have a DB mapping yet + $createDbMappings = []; + foreach ($this->unfulfilledPromises as $promises) { + // search if this particular lookup is allowed to create a mapping + // and if exactly one createWith value was provided + $shouldCreate = false; + $createWith = null; + foreach ($promises as $promise) { + if ($promise->shouldCreate) { + $shouldCreate = true; + } + if ($promise->createWith) { + if ($createWith) { + // todo: proper error handling + throw new \RuntimeException('resolving mapping promise failed with multiple createWith values for ' . $promises[0]->sourceId . ' for ' . $promises[0]->entity); + } + + $createWith = $promise->createWith; + } + } + + if (!$shouldCreate) { + // todo: proper error handling + throw new \RuntimeException('Could not resolve mapping promise ' . $promises[0]->sourceId . ' for ' . $promises[0]->entity); + } + + // it's fine to create the corresponding mapping and resolve all promises with its value + $newEntityId = $createWith ?? Uuid::randomHex(); + + $dbMapping = new SwagMigrationMappingEntity(); + $dbMapping->setId(Uuid::randomHex()); + $dbMapping->setConnectionId($connectionId); + $dbMapping->setEntity($promises[0]->entity); + $dbMapping->setOldIdentifier($promises[0]->sourceId); + $dbMapping->setEntityId($newEntityId); + $dbMapping->setCreatedAt(new \DateTime()); + + $createDbMappings[] = $dbMapping; + + // resolve promises + // todo: should this only execute after successful DB insert? + foreach ($promises as $promise) { + $promise->resolve($newEntityId); + } + } + + unset($this->unfulfilledPromises); + $this->unfulfilledPromises = []; + + $this->bulkCreateDbMappings($createDbMappings); + } + + public function reset(): void + { + if ($this->unfulfilledPromises !== []) { + $this->logger->error('Resetting MappingServiceV2 without resolving all promises'); + } + + unset($this->unfulfilledPromises); + $this->unfulfilledPromises = []; + } + + // todo: move SQL methods below into own class and try to clean them up further + /** + * @param list $lookups + * + * @return list + */ + private function fetchDbMappings(string $connectionId, array $lookups): array + { + if ($lookups === []) { + return []; + } $placeholders = []; $params = []; - $types = []; - foreach ($mappingLookups as $pair) { + foreach ($lookups as $pair) { $placeholders[] = '(?, ?)'; - $params[] = $pair[0]; - $types[] = 'string'; - $params[] = $pair[1]; - $types[] = 'string'; + $params[] = $pair['entity']; + $params[] = $pair['oldIdentifier']; } - $sql = sprintf( + $sql = \sprintf( 'SELECT id, connection_id AS connectionId, entity, @@ -87,8 +193,7 @@ public function resolvePromises( implode(', ', $placeholders) ); - - $mappings = $this->connection->fetchAssociative( + $mappings = $this->connection->fetchAllAssociative( $sql, [ 'connectionId' => Uuid::fromHexToBytes($connectionId), @@ -96,66 +201,114 @@ public function resolvePromises( ], [ 'connectionId' => 'binary', - ...$types, ] ); - if (is_array($mappings)) { - foreach ($mappings as $mapping) { - $key = $mapping['entity'] . $mapping['oldIdentifier']; - $promises = $this->unfulfilledPromises[$key]; - foreach ($promises as $promise) { - $promise->resolve($mapping['entityId']); - } - unset($this->unfulfilledPromises[$key]); - } + return array_map( + function (array $mapping): SwagMigrationMappingEntity { + $mapping['id'] = Uuid::fromBytesToHex($mapping['id']); + $mapping['connectionId'] = Uuid::fromBytesToHex($mapping['connectionId']); + $mapping['entityId'] = isset($mapping['entityId']) ? Uuid::fromBytesToHex($mapping['entityId']) : null; + + $entity = new SwagMigrationMappingEntity(); + $entity->assign($mapping); + + return $entity; + }, + $mappings, + ); + } + + /** + * @param list $mappings + */ + private function bulkCreateDbMappings(array $mappings): void + { + if ($mappings === []) { + return; } - // the remaining unfulfilled promises don't have a DB mapping yet - foreach ($this->unfulfilledPromises as $promises) { - $shouldCreate = false; - foreach ($promises as $promise) { - if ($promise->shouldCreate) { - $shouldCreate = true; - break; + try { + $isFirstInsert = true; + $insertSql = 'INSERT INTO swag_migration_mapping (id, connection_id, entity, old_identifier, entity_id, entity_value, checksum, additional_data, created_at) VALUES '; + $insertParams = []; + $updateSql = ' ON DUPLICATE KEY + UPDATE entity = VALUES(entity), + old_identifier = VALUES(old_identifier), + entity_id = VALUES(entity_id), + entity_value = VALUES(entity_value), + checksum = VALUES(checksum), + additional_data = VALUES(additional_data), + updated_at = VALUES(created_at);'; + foreach ($mappings as $index => $writeMapping) { + if ($isFirstInsert) { + $isFirstInsert = false; + } else { + $insertSql .= ', '; } - } - if (!$shouldCreate) { - // todo: proper error handling - throw new \RuntimeException('Could not resolve mapping promise ' . $promises[0]->sourceId . ' for ' . $promises[0]->entity); + $insertSql .= \sprintf('(:id%d, :connectionId%d, :entity%d, :oldIdentifier%d, :entityId%d, :entityValue%d, :checksum%d, :additionalData%d, :createdAt%d)', $index, $index, $index, $index, $index, $index, $index, $index, $index); + + $insertParams['id' . $index] = Uuid::fromHexToBytes($writeMapping->getId()); + $insertParams['connectionId' . $index] = Uuid::fromHexToBytes($writeMapping->getConnectionId()); + $insertParams['entity' . $index] = $writeMapping->getEntity(); + $insertParams['oldIdentifier' . $index] = $writeMapping->getOldIdentifier(); + $insertParams['entityId' . $index] = $writeMapping->getEntityId() === null ? null : Uuid::fromHexToBytes($writeMapping->getEntityId()); + $insertParams['entityValue' . $index] = $writeMapping->getEntityValue(); + $insertParams['checksum' . $index] = $writeMapping->getChecksum(); + $insertParams['additionalData' . $index] = \json_encode($writeMapping->getAdditionalData()); + $insertParams['createdAt' . $index] = $writeMapping->getCreatedAt()->format(Defaults::STORAGE_DATE_TIME_FORMAT); } - $newEntityId = Uuid::randomHex(); - $promise->resolve($newEntityId); // todo: should this only execute after the DB insert? - - $createMapping = [ - 'id' => Uuid::randomHex(), - 'connection_id' => Uuid::fromHexToBytes($connectionId), - 'entity' => $promises[0]->entity, - 'old_identifier' => $promises[0]->sourceId, - 'entity_id' => Uuid::fromHexToBytes($newEntityId), - 'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT), - ]; - // todo: bulk insert? - $this->connection->insert('swag_migration_mapping', $createMapping, [ - 'id' => 'binary', - 'connection_id' => 'binary', - 'entity_id' => 'binary', - ]); + $this->connection->executeStatement($insertSql . $updateSql, $insertParams); + } catch (\Exception) { + $this->createDbMappingsIndividually($mappings); } - - unset($this->unfulfilledPromises); - $this->unfulfilledPromises = []; } - public function reset(): void + /** + * @param list $mappings + */ + private function createDbMappingsIndividually(array $mappings): void { - if ($this->unfulfilledPromises !== []) { - $this->logger->error('Resetting MappingServiceV2 without resolving all promises'); + if ($mappings === []) { + return; } - unset($this->unfulfilledPromises); - $this->unfulfilledPromises = []; + foreach ($mappings as $writeMapping) { + try { + $insertSql = 'INSERT INTO swag_migration_mapping (id, connection_id, entity, old_identifier, entity_id, entity_value, checksum, additional_data, created_at) + VALUES (:id, :connectionId, :entity, :oldIdentifier, :entityId, :entityValue, :checksum, :additionalData, :createdAt) + ON DUPLICATE KEY + UPDATE entity = VALUES(entity), + old_identifier = VALUES(old_identifier), + entity_id = VALUES(entity_id), + entity_value = VALUES(entity_value), + checksum = VALUES(checksum), + additional_data = VALUES(additional_data), + updated_at = VALUES(created_at);'; + + $insertParams = []; + $insertParams['id'] = Uuid::fromHexToBytes($writeMapping->getId()); + $insertParams['connectionId'] = Uuid::fromHexToBytes($writeMapping->getConnectionId()); + $insertParams['entity'] = $writeMapping->getEntity(); + $insertParams['oldIdentifier'] = $writeMapping->getOldIdentifier(); + $insertParams['entityId'] = $writeMapping->getEntityId() === null ? null : Uuid::fromHexToBytes($writeMapping->getEntityId()); + $insertParams['entityValue'] = $writeMapping->getEntityValue(); + $insertParams['checksum'] = $writeMapping->getChecksum(); + $insertParams['additionalData'] = \json_encode($writeMapping->getAdditionalData()); + $insertParams['createdAt'] = $writeMapping->getCreatedAt()->format(Defaults::STORAGE_DATE_TIME_FORMAT); + + $this->connection->executeStatement($insertSql, $insertParams); + } catch (\Exception $e) { + $this->logger->error( + 'SwagMigrationAssistant: Error while writing migration mapping', + [ + 'error' => $e->getMessage(), + 'mapping' => $writeMapping, + ] + ); + } + } } } diff --git a/src/Migration/Mapping/SwagMigrationMappingEntity.php b/src/Migration/Mapping/SwagMigrationMappingEntity.php index 284487165..20a06a0d3 100644 --- a/src/Migration/Mapping/SwagMigrationMappingEntity.php +++ b/src/Migration/Mapping/SwagMigrationMappingEntity.php @@ -21,20 +21,20 @@ class SwagMigrationMappingEntity extends Entity protected ?SwagMigrationConnectionEntity $connection = null; - protected ?string $entity; + protected ?string $entity = null; - protected ?string $oldIdentifier; + protected ?string $oldIdentifier = null; - protected ?string $entityId; + protected ?string $entityId = null; - protected ?string $entityValue; + protected ?string $entityValue = null; - protected ?string $checksum; + protected ?string $checksum = null; /** * @var array|null */ - protected ?array $additionalData; + protected ?array $additionalData = null; public function getConnectionId(): string { diff --git a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php index 124738812..a1c03d7b9 100644 --- a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php +++ b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php @@ -99,7 +99,7 @@ public function process( // todo: remove benchmarking code if ($progress->getCurrentEntity() === 'product' && $progress->getCurrentEntityProgress() === 0) { // end profiling - //$profile = $blackfire->endProbe($probe); + // $profile = $blackfire->endProbe($probe); } $this->bus->dispatch(new MigrationProcessMessage($context, $migrationContext->getRunUuid())); diff --git a/src/Profile/Shopware/Converter/ProductConverter.php b/src/Profile/Shopware/Converter/ProductConverter.php index 4eac51355..f0b390582 100644 --- a/src/Profile/Shopware/Converter/ProductConverter.php +++ b/src/Profile/Shopware/Converter/ProductConverter.php @@ -304,22 +304,21 @@ protected function convertVariantProduct(array $data): ConvertStruct private function getUuidForProduct(array &$data): array { $converted = []; + $newUuid = Uuid::randomHex(); $converted['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT, $this->oldProductId, true, + $newUuid, ); - // todo: what was the purpose of this? - // todo: looks like it writes back a created mapping to a different entity + source id combination - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + // create another mapping that resolves to the same UUID as the mapping above + // but uses a different lookup pair + $this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_MAIN, $data['detail']['articleID'], - $this->context, - null, - null, - $converted['id'] + true, + $newUuid ); return $converted; diff --git a/tests/Migration/Mapping/MappingServiceV2Test.php b/tests/Migration/Mapping/MappingServiceV2Test.php index a0c5407fd..94fcefa5f 100644 --- a/tests/Migration/Mapping/MappingServiceV2Test.php +++ b/tests/Migration/Mapping/MappingServiceV2Test.php @@ -7,9 +7,7 @@ namespace SwagMigrationAssistant\Test\Migration\Mapping; -use Doctrine\DBAL\Connection; use PHPUnit\Framework\TestCase; -use Psr\Log\NullLogger; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; From e69653daeb0df510abc162efdedcc5daba74cdb6 Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Fri, 6 Mar 2026 15:53:03 +0100 Subject: [PATCH 5/9] fix: few bugs to get to working migration again --- src/Migration/Mapping/MappingServiceV2.php | 4 ++-- .../Shopware/Converter/ProductConverter.php | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Migration/Mapping/MappingServiceV2.php b/src/Migration/Mapping/MappingServiceV2.php index 2d03a3d5a..d64e55daa 100644 --- a/src/Migration/Mapping/MappingServiceV2.php +++ b/src/Migration/Mapping/MappingServiceV2.php @@ -88,10 +88,10 @@ public function resolvePromises( $dbMappings = $this->fetchDbMappings($connectionId, $mappingLookups); foreach ($dbMappings as $dbMapping) { - $key = $dbMapping['entity'] . $dbMapping['oldIdentifier']; + $key = $dbMapping->getEntity() . $dbMapping->getOldIdentifier(); $promises = $this->unfulfilledPromises[$key]; foreach ($promises as $promise) { - $promise->resolve($dbMapping['entityId']); + $promise->resolve($dbMapping->getEntityId()); } unset($this->unfulfilledPromises[$key]); } diff --git a/src/Profile/Shopware/Converter/ProductConverter.php b/src/Profile/Shopware/Converter/ProductConverter.php index f0b390582..067018899 100644 --- a/src/Profile/Shopware/Converter/ProductConverter.php +++ b/src/Profile/Shopware/Converter/ProductConverter.php @@ -205,7 +205,7 @@ protected function convertMainProduct(array $data): ConvertStruct ); $converted = []; - $converted['id'] = $containerUuid; + $converted['id'] = &$containerUuid; unset($data['detail']['articleID']); $converted = $this->getProductData($data, $converted); @@ -215,7 +215,7 @@ protected function convertMainProduct(array $data): ConvertStruct // Remove options from product container as in core unset($converted['options']); $productUuid = &$this->mappingServiceV2->getMapping(DefaultEntities::PRODUCT, $this->oldProductId, true); - $converted['children'][0]['id'] = $productUuid; + $converted['children'][0]['id'] = &$productUuid; if (isset($converted['children'][0]['media'])) { if (isset($converted['children'][0]['cover'])) { @@ -230,7 +230,7 @@ protected function convertMainProduct(array $data): ConvertStruct ); $productMediaRelationUuid = $productMediaRelationMapping['entityId']; $this->mappingIds[] = $productMediaRelationMapping['id']; - $media['productId'] = $productUuid; + $media['productId'] = &$productUuid; $media['id'] = $productMediaRelationUuid; if (isset($coverMediaUuid) && $media['media']['id'] === $coverMediaUuid) { @@ -238,7 +238,7 @@ protected function convertMainProduct(array $data): ConvertStruct } } } - $converted['children'][0]['parentId'] = $containerUuid; + $converted['children'][0]['parentId'] = &$containerUuid; unset($data['detail']['id'], $converted['children'][0]['translations'], $converted['children'][0]['customFields']); if (isset($data['categories'])) { @@ -845,7 +845,7 @@ private function getEsdFiles(array $esdFiles, string $oldVariantId, array $conve ); $newProductMedia['id'] = $mapping['entityId']; $this->mappingIds[] = $mapping['id']; - $newProductMedia['productId'] = $converted['id']; + $newProductMedia['productId'] = &$converted['id']; /** @var array $newMedia */ $newMedia = []; @@ -974,7 +974,7 @@ private function getMedia(array $media, string $oldVariantId, array $converted): ); $newProductMedia['id'] = $mapping['entityId']; $this->mappingIds[] = $mapping['id']; - $newProductMedia['productId'] = $converted['id']; + $newProductMedia['productId'] = &$converted['id']; $this->convertValue($newProductMedia, 'position', $mediaData, 'position', self::TYPE_INTEGER); $newMedia = []; @@ -1360,7 +1360,7 @@ private function getPrices(array $priceData, array $converted): array $data = [ 'id' => $productPriceRuleUuid, - 'productId' => $converted['id'], + 'productId' => &$converted['id'], 'rule' => [ 'id' => $priceRuleUuid, 'name' => $price['customergroup']['description'], @@ -1443,7 +1443,7 @@ private function setGivenProductTranslation(array &$data, array &$converted): vo $localeTranslation = []; - $localeTranslation['productId'] = $converted['id']; + $localeTranslation['productId'] = &$converted['id']; $this->convertValue($localeTranslation, 'name', $originalData, 'name'); $this->convertValue($localeTranslation, 'keywords', $originalData, 'keywords'); $this->convertValue($localeTranslation, 'description', $originalData, 'description_long'); @@ -1571,7 +1571,7 @@ private function getVisibilities(array $converted, array $shops): array $this->mappingIds[] = $mapping['id']; $visibilities[] = [ 'id' => (string) $mapping['entityId'], - 'productId' => $converted['id'], + 'productId' => &$converted['id'], 'salesChannelId' => $salesChannelUuid, 'visibility' => ProductVisibilityDefinition::VISIBILITY_ALL, ]; From 6764217d41172da70a72773a68c68714f5a48a81 Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Mon, 9 Mar 2026 11:09:33 +0100 Subject: [PATCH 6/9] feat: further progress on productConverter refactor --- src/Migration/Mapping/MappingServiceV2.php | 9 +- src/Migration/Media/MediaFileService.php | 1 + .../Shopware/Converter/ProductConverter.php | 353 ++++++------------ 3 files changed, 128 insertions(+), 235 deletions(-) diff --git a/src/Migration/Mapping/MappingServiceV2.php b/src/Migration/Mapping/MappingServiceV2.php index d64e55daa..16e3e5032 100644 --- a/src/Migration/Mapping/MappingServiceV2.php +++ b/src/Migration/Mapping/MappingServiceV2.php @@ -58,7 +58,14 @@ public function &getMapping( bool $shouldCreate = false, ?string $createWith = null, ): string { - $ref = self::PLACEHOLDER; + // todo: figure out a nice dev experience for debugging + $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2); + // [0] = current function + // [1] = caller + $caller = $trace[1] ?? []; + $callerString = ($caller['file'] ?? '') . '::' . ($caller['line'] ?? ''); + + $ref = self::PLACEHOLDER . '(' . $entityName . ',' . $oldIdentifier . ')+++' . $callerString; $promise = new MappingPromise($ref, $entityName, $oldIdentifier, $shouldCreate, $createWith); diff --git a/src/Migration/Media/MediaFileService.php b/src/Migration/Media/MediaFileService.php index a3bce2867..f9ec762c2 100644 --- a/src/Migration/Media/MediaFileService.php +++ b/src/Migration/Media/MediaFileService.php @@ -75,6 +75,7 @@ public function writeMediaFile(Context $context): void public function saveMediaFile(array $mediaFile): void { + // todo: this is likely broken with MappingServiceV2 and needs to be checked, maybe refactored $mediaId = $mediaFile['mediaId']; if (isset($this->uuids[$mediaId])) { return; diff --git a/src/Profile/Shopware/Converter/ProductConverter.php b/src/Profile/Shopware/Converter/ProductConverter.php index 067018899..503a7d64e 100644 --- a/src/Profile/Shopware/Converter/ProductConverter.php +++ b/src/Profile/Shopware/Converter/ProductConverter.php @@ -222,16 +222,12 @@ protected function convertMainProduct(array $data): ConvertStruct $coverMediaUuid = $converted['children'][0]['cover']['media']['id']; } foreach ($converted['children'][0]['media'] as &$media) { - $productMediaRelationMapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $media['productId'] = &$productUuid; + $media['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_MEDIA, $media['id'], - $this->context + true, ); - $productMediaRelationUuid = $productMediaRelationMapping['entityId']; - $this->mappingIds[] = $productMediaRelationMapping['id']; - $media['productId'] = &$productUuid; - $media['id'] = $productMediaRelationUuid; if (isset($coverMediaUuid) && $media['media']['id'] === $coverMediaUuid) { $converted['children'][0]['cover'] = $media; @@ -494,10 +490,6 @@ private function getProductData(array &$data, array $converted): array */ private function setPurchasePrices(array &$data, array &$converted): void { - if ($this->currencyUuid === null) { - return; - } - $purchasePrice = 0.0; $purchasePriceGross = 0.0; if (isset($data['detail']['purchaseprice'])) { @@ -507,6 +499,8 @@ private function setPurchasePrices(array &$data, array &$converted): void unset($data['detail']['purchaseprice']); $price = []; + // todo: validate this is fine + /* if ($this->currencyUuid !== Defaults::CURRENCY) { $price[] = [ 'currencyId' => Defaults::CURRENCY, @@ -515,9 +509,10 @@ private function setPurchasePrices(array &$data, array &$converted): void 'linked' => true, ]; } + */ $price[] = [ - 'currencyId' => $this->currencyUuid, + 'currencyId' => &$this->currencyUuid, 'gross' => $purchasePriceGross, 'net' => $purchasePrice, 'linked' => true, @@ -566,19 +561,6 @@ private function getDeliveryTime(string $shippingTime): ?array } } - $mapping = $this->mappingService->getMapping( - $this->connectionId, - DefaultEntities::DELIVERY_TIME, - $shippingTime, - $this->context - ); - - if ($mapping !== null) { - $convertedDeliveryTime['id'] = $mapping['entityId']; - - return $convertedDeliveryTime; - } - $convertedDeliveryTime['id'] = $this->deliveryTimeLookup->get( $convertedDeliveryTime['min'], $convertedDeliveryTime['max'], @@ -587,18 +569,13 @@ private function getDeliveryTime(string $shippingTime): ?array ); if ($convertedDeliveryTime['id'] === null) { - $convertedDeliveryTime['id'] = Uuid::randomHex(); + $convertedDeliveryTime['id'] = &$this->mappingServiceV2->getMapping( + DefaultEntities::DELIVERY_TIME, + $shippingTime, + true, + ); } - $this->mappingService->createMapping( - $this->connectionId, - DefaultEntities::DELIVERY_TIME, - $shippingTime, - null, - null, - $convertedDeliveryTime['id'], - ); - return $convertedDeliveryTime; } @@ -632,24 +609,20 @@ private function applyOptions(array &$converted, array &$data): void } foreach ($data['configuratorOptions'] as $option) { - $optionMapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $optionUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::PROPERTY_GROUP_OPTION, Hasher::hash(\mb_strtolower($option['name'] . '_' . $option['group']['name']), 'md5'), - $this->context + true, ); - $this->mappingIds[] = $optionMapping['id']; - $optionGroupMapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $optionGroupUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::PROPERTY_GROUP, Hasher::hash(\mb_strtolower($option['group']['name']), 'md5'), - $this->context + true, ); - $this->mappingIds[] = $optionGroupMapping['id']; $optionElement = [ - 'id' => $optionMapping['entityId'], + 'id' => &$optionUuid, 'group' => [ - 'id' => $optionGroupMapping['entityId'], + 'id' => &$optionGroupUuid, ], ]; @@ -722,14 +695,11 @@ private function applyManufacturerTranslation(array &$manufacturer, array $data) $this->convertValue($localeTranslation, 'name', $data, 'name'); $this->convertValue($localeTranslation, 'description', $data, 'description'); - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $localeTranslation['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_MANUFACTURER_TRANSLATION, $data['id'] . ':' . $this->locale, - $this->context + true, ); - $localeTranslation['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $languageUuid = $this->languageLookup->get($this->locale, $this->context); if ($languageUuid !== null) { @@ -748,21 +718,19 @@ private function getTax(array $taxData): array $taxRate = (float) $taxData['tax']; $taxUuid = $this->taxLookup->get($taxRate, $this->context); if (empty($taxUuid)) { - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $taxUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::TAX, $taxData['id'], - $this->context + true, ); - $taxUuid = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; } - return [ - 'id' => $taxUuid, - 'taxRate' => $taxRate, - 'name' => $taxData['description'], - ]; + $tax = []; + $tax['id'] = &$taxUuid; + $tax['taxRate'] = $taxRate; + $tax['name'] = $taxData['description']; + + return $tax; } /** @@ -773,14 +741,11 @@ private function getTax(array $taxData): array private function getUnit(array $data): array { $unit = []; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $unit['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::UNIT, $data['id'], - $this->context + true, ); - $unit['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $this->applyUnitTranslation($unit, $data); $this->convertValue($unit, 'shortCode', $data, 'unit'); @@ -810,14 +775,11 @@ private function applyUnitTranslation(array &$unit, array $data): void $this->convertValue($localeTranslation, 'shortCode', $data, 'unit'); $this->convertValue($localeTranslation, 'name', $data, 'description'); - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $localeTranslation['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::UNIT_TRANSLATION, $data['id'] . ':' . $this->locale, - $this->context + true, ); - $localeTranslation['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $languageUuid = $this->languageLookup->get($this->locale, $this->context); if ($languageUuid !== null) { @@ -837,28 +799,21 @@ private function getEsdFiles(array $esdFiles, string $oldVariantId, array $conve $mediaObjects = []; foreach ($esdFiles as $esdFile) { $newProductMedia = []; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $newProductMedia['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_DOWNLOAD, $oldVariantId . '_' . $esdFile['id'], - $this->context + true, ); - $newProductMedia['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $newProductMedia['productId'] = &$converted['id']; /** @var array $newMedia */ $newMedia = []; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $newMedia['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::MEDIA, 'esd_' . $esdFile['id'], - $this->context + true, ); - $newMedia['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; - if (empty($esdFile['name'])) { $this->loggingService->log( MigrationLogBuilder::fromMigrationContext($this->migrationContext) @@ -898,7 +853,7 @@ private function getEsdFiles(array $esdFiles, string $oldVariantId, array $conve 'uri' => $path . '/' . $esdFile['name'], 'fileName' => $esdFile['name'], 'fileSize' => 0, - 'mediaId' => $newMedia['id'], + 'mediaId' => &$newMedia['id'], ] ); @@ -966,26 +921,20 @@ private function getMedia(array $media, string $oldVariantId, array $converted): } $newProductMedia = []; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $newProductMedia['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_MEDIA, $oldVariantId . $mediaData['id'], - $this->context + true, ); - $newProductMedia['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $newProductMedia['productId'] = &$converted['id']; $this->convertValue($newProductMedia, 'position', $mediaData, 'position', self::TYPE_INTEGER); $newMedia = []; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $newMedia['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::MEDIA, $mediaData['media']['id'], - $this->context + true, ); - $newMedia['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; if (empty($mediaData['media']['name'])) { $mediaData['media']['name'] = $newMedia['id']; @@ -998,7 +947,7 @@ private function getMedia(array $media, string $oldVariantId, array $converted): 'uri' => $mediaData['media']['uri'] ?? $mediaData['media']['path'], 'fileName' => $mediaData['media']['name'], 'fileSize' => (int) $mediaData['media']['file_size'], - 'mediaId' => $newMedia['id'], + 'mediaId' => &$newMedia['id'], ] ); @@ -1006,16 +955,11 @@ private function getMedia(array $media, string $oldVariantId, array $converted): $this->convertValue($newMedia, 'title', $mediaData['media'], 'name'); $this->convertValue($newMedia, 'alt', $mediaData, 'description'); - $albumMapping = $this->mappingService->getMapping( - $this->connectionId, - DefaultEntities::MEDIA_FOLDER, - $mediaData['media']['albumID'], - $this->context - ); - - if ($albumMapping !== null) { - $newMedia['mediaFolderId'] = $albumMapping['entityId']; - $this->mappingIds[] = $albumMapping['id']; + if (!empty($mediaData['media']['albumID'])) { + $newMedia['mediaFolderId'] = &$this->mappingServiceV2->getMapping( + DefaultEntities::MEDIA_FOLDER, + $mediaData['media']['albumID'], + ); } $newProductMedia['media'] = $newMedia; @@ -1110,14 +1054,11 @@ private function applyMediaTranslation(array &$media, array $data): void $this->convertValue($localeTranslation, 'title', $data['media'], 'name'); $this->convertValue($localeTranslation, 'alt', $data, 'description'); - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $localeTranslation['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::MEDIA_TRANSLATION, $data['media']['id'] . ':' . $this->locale, - $this->context + true, ); - $localeTranslation['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $languageUuid = $this->languageLookup->get($this->locale, $this->context); if ($languageUuid !== null) { @@ -1133,15 +1074,12 @@ private function applyMediaTranslation(array &$media, array $data): void */ private function getManufacturerMedia(array $media): array { - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $manufacturerMedia = []; + $manufacturerMedia['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::MEDIA, $media['id'], - $this->context + true, ); - $manufacturerMedia = []; - $manufacturerMedia['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; if (empty($media['name'])) { $media['name'] = $manufacturerMedia['id']; @@ -1149,16 +1087,11 @@ private function getManufacturerMedia(array $media): array $this->applyMediaTranslation($manufacturerMedia, ['media' => $media]); - $albumMapping = $this->mappingService->getMapping( - $this->connectionId, - DefaultEntities::MEDIA_FOLDER, - $media['albumID'], - $this->context - ); - - if ($albumMapping !== null) { - $manufacturerMedia['mediaFolderId'] = $albumMapping['entityId']; - $this->mappingIds[] = $albumMapping['id']; + if (!empty($media['albumID'])) { + $manufacturerMedia['mediaFolderId'] = &$this->mappingServiceV2->getMapping( + DefaultEntities::MEDIA_FOLDER, + $media['albumID'], + ); } $this->mediaFileService->saveMediaFile( @@ -1168,7 +1101,7 @@ private function getManufacturerMedia(array $media): array 'uri' => $media['uri'] ?? $media['path'], 'fileName' => $media['name'], 'fileSize' => (int) $media['file_size'], - 'mediaId' => $manufacturerMedia['id'], + 'mediaId' => &$manufacturerMedia['id'], ] ); @@ -1186,26 +1119,20 @@ private function applyOptionTranslation(array &$option, array $data): void $localeOptionTranslation['languageId'] = $languageUuid; $localeGroupTranslation = $localeOptionTranslation; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $localeOptionTranslation['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PROPERTY_GROUP_OPTION_TRANSLATION, Hasher::hash(\mb_strtolower($data['name'] . '_' . $data['group']['name']), 'md5') . ':' . $this->locale, - $this->context + true, ); - $localeOptionTranslation['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $this->convertValue($localeOptionTranslation, 'name', $data, 'name'); $this->convertValue($localeOptionTranslation, 'position', $data, 'position', self::TYPE_INTEGER); - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $localeGroupTranslation['id'] = &$this->mappingServiceV2->getMapping( DefaultEntities::PROPERTY_GROUP_TRANSLATION, Hasher::hash(\mb_strtolower($data['group']['name']), 'md5') . ':' . $this->locale, - $this->context + true, ); - $localeGroupTranslation['id'] = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $this->convertValue($localeGroupTranslation, 'name', $data['group'], 'name'); $this->convertValue($localeGroupTranslation, 'description', $data['group'], 'description'); @@ -1226,20 +1153,15 @@ private function getPrice(array $priceData, float $taxRate): array $gross = \round((float) $priceData['price'] * (1 + $taxRate / 100), $this->context->getRounding()->getDecimals()); if (isset($priceData['currencyShortName'])) { - $currencyMapping = $this->mappingService->getMapping( - $this->connectionId, + $this->currencyUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::CURRENCY, $priceData['currencyShortName'], - $this->context ); } - if (!isset($currencyMapping)) { - return []; - } - $this->currencyUuid = $currencyMapping['entityId']; - $this->mappingIds[] = $currencyMapping['id']; $price = []; + // todo: validate that removing this is fine + /* if ($this->currencyUuid !== Defaults::CURRENCY) { $price[] = [ 'currencyId' => Defaults::CURRENCY, @@ -1248,9 +1170,10 @@ private function getPrice(array $priceData, float $taxRate): array 'linked' => true, ]; } + */ $price[] = [ - 'currencyId' => $this->currencyUuid, + 'currencyId' => &$this->currencyUuid, 'gross' => $gross, 'net' => (float) $priceData['price'], 'linked' => true, @@ -1260,7 +1183,7 @@ private function getPrice(array $priceData, float $taxRate): array if ($listPrice > 0) { $listPriceGross = \round((float) $priceData['pseudoprice'] * (1 + $taxRate / 100), $this->context->getRounding()->getDecimals()); $price[0]['listPrice'] = [ - 'currencyId' => $this->currencyUuid, + 'currencyId' => &$this->currencyUuid, 'gross' => $listPriceGross, 'net' => $listPrice, 'linked' => true, @@ -1284,63 +1207,40 @@ private function getPrices(array $priceData, array $converted): array continue; } - $customerGroupMapping = $this->mappingService->getMapping( - $this->connectionId, + $customerGroupUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::CUSTOMER_GROUP, $price['customergroup']['id'], - $this->context ); - if ($customerGroupMapping === null) { - continue; - } - $customerGroupUuid = $customerGroupMapping['entityId']; - $this->mappingIds[] = $customerGroupMapping['id']; - - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $productPriceRuleUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::RULE, 'customerGroupRule_productPriceRule_' . $price['id'] . '_' . $price['customergroup']['id'], - $this->context + true, ); - $productPriceRuleUuid = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $priceRuleUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::RULE, 'customerGroupRule_' . $price['customergroup']['id'], - $this->context + true, ); - $priceRuleUuid = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $orContainerUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::RULE, 'customerGroupRule_orContainer_' . $price['customergroup']['id'], - $this->context + true, ); - $orContainerUuid = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $andContainerUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::RULE, 'customerGroupRule_andContainer_' . $price['customergroup']['id'], - $this->context + true, ); - $andContainerUuid = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $conditionUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::RULE, 'customerGroupRule_condition_' . $price['customergroup']['id'], - $this->context + true, ); - $conditionUuid = $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; $priceArray = $this->getPrice($price, $converted['tax']['taxRate']); @@ -1359,10 +1259,10 @@ private function getPrices(array $priceData, array $converted): array } $data = [ - 'id' => $productPriceRuleUuid, + 'id' => &$productPriceRuleUuid, 'productId' => &$converted['id'], 'rule' => [ - 'id' => $priceRuleUuid, + 'id' => &$priceRuleUuid, 'name' => $price['customergroup']['description'], 'priority' => 0, 'moduleTypes' => [ @@ -1372,26 +1272,26 @@ private function getPrices(array $priceData, array $converted): array ], 'conditions' => [ [ - 'id' => $orContainerUuid, + 'id' => &$orContainerUuid, 'type' => (new OrRule())->getName(), 'value' => [], ], [ - 'id' => $andContainerUuid, + 'id' => &$andContainerUuid, 'type' => (new AndRule())->getName(), - 'parentId' => $orContainerUuid, + 'parentId' => &$orContainerUuid, 'value' => [], ], [ - 'id' => $conditionUuid, + 'id' => &$conditionUuid, 'type' => 'customerCustomerGroup', - 'parentId' => $andContainerUuid, + 'parentId' => &$andContainerUuid, 'position' => 1, 'value' => [ 'customerGroupIds' => [ - $customerGroupUuid, + &$customerGroupUuid, ], 'operator' => '=', ], @@ -1450,13 +1350,12 @@ private function setGivenProductTranslation(array &$data, array &$converted): vo $this->convertValue($localeTranslation, 'metaTitle', $originalData, 'metaTitle'); $this->convertValue($localeTranslation, 'packUnit', $originalData['detail'], 'packunit'); - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, + // only create and persist this mapping, no use in this converter + $this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_TRANSLATION, $this->oldProductId . ':' . $this->locale, - $this->context + true, ); - $this->mappingIds[] = $mapping['id']; $languageUuid = $this->languageLookup->get($this->locale, $this->context); $localeTranslation['languageId'] = $languageUuid; @@ -1479,35 +1378,32 @@ private function createMainCategoriesMapping(array $categories): array { $mainCategories = []; foreach ($categories as $category) { - $id = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $id = &$this->mappingServiceV2->getMapping( DefaultEntities::PRODUCT_MAIN_CATEGORY, $category['id'], - $this->context - )['entityId']; + true, + ); - $categoryId = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $categoryId = &$this->mappingServiceV2->getMapping( DefaultEntities::CATEGORY, $category['categoryId'], - $this->context - )['entityId']; + true, + ); - $salesChannelId = $this->mappingService->getOrCreateMapping( - $this->connectionId, + $salesChannelId = &$this->mappingServiceV2->getMapping( DefaultEntities::SALES_CHANNEL, $category['shopId'], - $this->context - )['entityId']; + true, + ); if (!$id || !$categoryId || !$salesChannelId) { continue; } $mainCategories[] = [ - 'id' => $id, - 'categoryId' => $categoryId, - 'salesChannelId' => $salesChannelId, + 'id' => &$id, + 'categoryId' => &$categoryId, + 'salesChannelId' => &$salesChannelId, ]; } @@ -1524,18 +1420,14 @@ private function getCategoryMapping(array $categories): array $categoryMapping = []; foreach ($categories as $category) { - $mapping = $this->mappingService->getMapping( - $this->connectionId, + $mappingUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::CATEGORY, $category['id'], - $this->context ); - if ($mapping === null) { - continue; - } - $categoryMapping[] = ['id' => (string) $mapping['entityId']]; - $this->mappingIds[] = $mapping['id']; + $categoryMapping[] = [ + 'id' => &$mappingUuid, + ]; } return $categoryMapping; @@ -1552,30 +1444,23 @@ private function getVisibilities(array $converted, array $shops): array $visibilities = []; foreach ($shops as $shop) { - $mapping = $this->mappingService->getMapping( - $this->connectionId, + $salesChannelUuid = &$this->mappingServiceV2->getMapping( DefaultEntities::SALES_CHANNEL, $shop, - $this->context ); - if ($mapping !== null) { - $salesChannelUuid = (string) $mapping['entityId']; - $this->mappingIds[] = $mapping['id']; - $mapping = $this->mappingService->getOrCreateMapping( - $this->connectionId, - DefaultEntities::PRODUCT_VISIBILITY, - $this->oldProductId . '_' . $shop, - $this->context - ); - $this->mappingIds[] = $mapping['id']; - $visibilities[] = [ - 'id' => (string) $mapping['entityId'], - 'productId' => &$converted['id'], - 'salesChannelId' => $salesChannelUuid, - 'visibility' => ProductVisibilityDefinition::VISIBILITY_ALL, - ]; - } + $productVisibilityUuid = &$this->mappingServiceV2->getMapping( + DefaultEntities::PRODUCT_VISIBILITY, + $this->oldProductId . '_' . $shop, + true, + ); + + $visibilities[] = [ + 'id' => &$productVisibilityUuid, + 'productId' => &$converted['id'], + 'salesChannelId' => &$salesChannelUuid, + 'visibility' => ProductVisibilityDefinition::VISIBILITY_ALL, + ]; } return $visibilities; From e67e360432e238a08df559281c92aaa339725235 Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Mon, 9 Mar 2026 16:33:00 +0100 Subject: [PATCH 7/9] feat: ProductConverter and migration working again --- src/Migration/Mapping/MappingServiceV2.php | 9 ++++----- .../Shopware/Converter/ProductConverter.php | 2 +- .../Mapping/MappingServiceV2Test.php | 20 ------------------- 3 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 tests/Migration/Mapping/MappingServiceV2Test.php diff --git a/src/Migration/Mapping/MappingServiceV2.php b/src/Migration/Mapping/MappingServiceV2.php index 16e3e5032..2a99eca30 100644 --- a/src/Migration/Mapping/MappingServiceV2.php +++ b/src/Migration/Mapping/MappingServiceV2.php @@ -59,13 +59,12 @@ public function &getMapping( ?string $createWith = null, ): string { // todo: figure out a nice dev experience for debugging - $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2); - // [0] = current function - // [1] = caller - $caller = $trace[1] ?? []; + // todo: this is already a good start? + $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1); + $caller = $trace[0] ?? []; $callerString = ($caller['file'] ?? '') . '::' . ($caller['line'] ?? ''); - $ref = self::PLACEHOLDER . '(' . $entityName . ',' . $oldIdentifier . ')+++' . $callerString; + $ref = self::PLACEHOLDER . '(\'' . $entityName . '\', \'' . $oldIdentifier . '\') +++ ' . $callerString; $promise = new MappingPromise($ref, $entityName, $oldIdentifier, $shouldCreate, $createWith); diff --git a/src/Profile/Shopware/Converter/ProductConverter.php b/src/Profile/Shopware/Converter/ProductConverter.php index 503a7d64e..c5effb900 100644 --- a/src/Profile/Shopware/Converter/ProductConverter.php +++ b/src/Profile/Shopware/Converter/ProductConverter.php @@ -690,7 +690,7 @@ private function applyManufacturerTranslation(array &$manufacturer, array $data) } $localeTranslation = []; - $localeTranslation['productManufacturerId'] = $manufacturer['id']; + $localeTranslation['productManufacturerId'] = &$manufacturer['id']; $this->convertValue($localeTranslation, 'name', $data, 'name'); $this->convertValue($localeTranslation, 'description', $data, 'description'); diff --git a/tests/Migration/Mapping/MappingServiceV2Test.php b/tests/Migration/Mapping/MappingServiceV2Test.php deleted file mode 100644 index 94fcefa5f..000000000 --- a/tests/Migration/Mapping/MappingServiceV2Test.php +++ /dev/null @@ -1,20 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SwagMigrationAssistant\Test\Migration\Mapping; - -use PHPUnit\Framework\TestCase; -use Shopware\Core\Framework\Log\Package; -use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; - -#[Package('fundamentals@after-sales')] -class MappingServiceV2Test extends TestCase -{ - use IntegrationTestBehaviour; - - // todo -} From 59df48e3a6672332fe18a0a3b5cef122538e518e Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Mon, 9 Mar 2026 16:50:25 +0100 Subject: [PATCH 8/9] feat: apply workaround to resolve mappings on whole batch and do validation afterwards --- .../Service/MigrationDataConverter.php | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/Migration/Service/MigrationDataConverter.php b/src/Migration/Service/MigrationDataConverter.php index 3e59b9cec..fc9f13015 100644 --- a/src/Migration/Service/MigrationDataConverter.php +++ b/src/Migration/Service/MigrationDataConverter.php @@ -93,6 +93,7 @@ private function convertData( ): array { $runUuid = $migrationContext->getRunUuid(); + $convertData = []; $createData = []; foreach ($data as $item) { try { @@ -108,10 +109,43 @@ private function convertData( continue; } - $convertFailureFlag = empty($convertStruct->getConverted()); + $convertData[] = [ + 'convertStruct' => $convertStruct, + 'item' => $item, + ]; + } catch (\Throwable $exception) { + $this->loggingService->log( + MigrationLogBuilder::fromMigrationContext($migrationContext) + ->withExceptionMessage($exception->getMessage()) + ->withExceptionTrace($exception->getTrace()) + ->withEntityName($dataSet::getEntity()) + ->withSourceData($item) + ->build(RunExceptionLog::class) + ); - // todo: this needs to be called after the whole batch was converted - $this->mappingServiceV2->resolvePromises($migrationContext->getConnection()->getId()); + $createData[] = [ + 'entity' => $dataSet::getEntity(), + 'runId' => $runUuid, + 'raw' => $item, + 'converted' => null, + 'unmapped' => $item, + 'mappingUuid' => null, + 'convertFailure' => true, + ]; + } + } + + // todo: this can throw as well + $this->mappingServiceV2->resolvePromises($migrationContext->getConnection()->getId()); + + // todo: validation needs to be called after the whole batch was converted + mappings resolved + // todo: should be refactored to work with batches and be outsourced like MigrationDataFetcher + MigrationDataConverter + // todo: for now this is a temporary ugly workaround + foreach ($convertData as $convertItem) { + $convertStruct = $convertItem['convertStruct']; + $item = $convertItem['item']; + + try { $this->validationService->validate( $migrationContext, $context, @@ -120,6 +154,8 @@ private function convertData( $item ); + $convertFailureFlag = empty($convertStruct->getConverted()); + $createData[] = [ 'entity' => $dataSet::getEntity(), 'runId' => $runUuid, @@ -148,8 +184,6 @@ private function convertData( 'mappingUuid' => null, 'convertFailure' => true, ]; - - continue; } } From 0afb86cedf491a8e5c5eaed5b1ad2005e75ffbd6 Mon Sep 17 00:00:00 2001 From: Malte Janz Date: Tue, 10 Mar 2026 09:37:02 +0100 Subject: [PATCH 9/9] chore: enable profiling code --- .../MessageQueue/Handler/Processor/FetchingProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php index a1c03d7b9..d255bf9d9 100644 --- a/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php +++ b/src/Migration/MessageQueue/Handler/Processor/FetchingProcessor.php @@ -99,7 +99,7 @@ public function process( // todo: remove benchmarking code if ($progress->getCurrentEntity() === 'product' && $progress->getCurrentEntityProgress() === 0) { // end profiling - // $profile = $blackfire->endProbe($probe); + $profile = $blackfire->endProbe($probe); } $this->bus->dispatch(new MigrationProcessMessage($context, $migrationContext->getRunUuid()));