From 68c446e241e40c04230a5188f32f0dcd4abb7b76 Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Thu, 23 Apr 2026 09:06:05 +0200 Subject: [PATCH 1/5] fix: resolve missing customer language and default address ids --- .../Shopware/Converter/CustomerConverter.php | 8 ++-- .../Gateway/Local/Reader/LanguageReader.php | 16 +++++++- .../Gateway/Local/LanguageReaderTest.php | 40 ++++++++++++++++++- .../Converter/CustomerConverterTest.php | 25 ++++++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/Profile/Shopware/Converter/CustomerConverter.php b/src/Profile/Shopware/Converter/CustomerConverter.php index 7b975b20d..54c692858 100644 --- a/src/Profile/Shopware/Converter/CustomerConverter.php +++ b/src/Profile/Shopware/Converter/CustomerConverter.php @@ -325,10 +325,6 @@ protected function applyAddresses(array &$originalData, array &$converted): void $newAddress = []; $salutationUuid = $this->getSalutation($address['salutation']); - if ($salutationUuid === null) { - continue; - } - $addressMapping = $this->mappingService->getOrCreateMapping( $this->connectionId, DefaultEntities::CUSTOMER_ADDRESS, @@ -337,7 +333,9 @@ protected function applyAddresses(array &$originalData, array &$converted): void ); $newAddress['id'] = $addressMapping['entityId']; $this->mappingIds[] = $addressMapping['id']; - $newAddress['salutationId'] = $salutationUuid; + if ($salutationUuid !== null) { + $newAddress['salutationId'] = $salutationUuid; + } if (isset($originalData['default_billing_address_id']) && $address['id'] === $originalData['default_billing_address_id']) { $converted['defaultBillingAddressId'] = $newAddress['id']; diff --git a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php index 8e4c2edf2..ddd5de525 100644 --- a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php +++ b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php @@ -40,7 +40,10 @@ public function supports(MigrationContextInterface $migrationContext): bool public function read(MigrationContextInterface $migrationContext): array { - $fetchedShopLocaleIds = \array_unique($this->fetchShopLocaleIds($migrationContext)); + $fetchedShopLocaleIds = \array_unique(\array_merge( + $this->fetchShopLocaleIds($migrationContext), + $this->fetchCustomerLocaleIds($migrationContext) + )); $locales = $this->fetchLocales($fetchedShopLocaleIds, $migrationContext); return $this->appendAssociatedData($locales, $migrationContext); @@ -76,6 +79,17 @@ private function fetchShopLocaleIds(MigrationContextInterface $migrationContext) return $query->fetchFirstColumn(); } + private function fetchCustomerLocaleIds(MigrationContextInterface $migrationContext): array + { + $connection = $this->getConnection($migrationContext); + $query = $connection->createQueryBuilder(); + $query->from('s_user', 'customer'); + $query->addSelect('customer.language'); + $query->where('customer.language IS NOT NULL'); + + return $query->executeQuery()->fetchFirstColumn(); + } + private function fetchLocales(array $fetchedShopLocaleIds, MigrationContextInterface $migrationContext): array { $connection = $this->getConnection($migrationContext); diff --git a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php index 4b8d1c35e..bce846ad5 100644 --- a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php +++ b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php @@ -7,6 +7,7 @@ namespace SwagMigrationAssistant\Test\Profile\Shopware\Gateway\Local; +use Doctrine\DBAL\Connection; use PHPUnit\Framework\TestCase; use Shopware\Core\Framework\Log\Package; use SwagMigrationAssistant\Migration\MigrationContext; @@ -25,11 +26,16 @@ class LanguageReaderTest extends TestCase private MigrationContext $migrationContext; + private Connection $dbConnection; + + private int $customerLocaleId = 9999; + protected function setUp(): void { $this->connectionSetup(); - $this->languageReader = new LanguageReader(new ConnectionFactory()); + $connectionFactory = new ConnectionFactory(); + $this->languageReader = new LanguageReader($connectionFactory); $this->migrationContext = new MigrationContext( $this->connection, @@ -42,6 +48,16 @@ protected function setUp(): void ); $this->migrationContext->setGateway(new DummyLocalGateway()); + $this->dbConnection = $connectionFactory->createDatabaseConnection($this->migrationContext); + } + + protected function tearDown(): void + { + $this->dbConnection->executeStatement('UPDATE s_user SET language = :language WHERE id = :id', [ + 'language' => 1, + 'id' => 1, + ]); + $this->dbConnection->executeStatement('DELETE FROM s_core_locales WHERE id = :id', ['id' => $this->customerLocaleId]); } public function testRead(): void @@ -63,4 +79,26 @@ public function testRead(): void static::assertSame('de_DE', $data[1]['translations'][0]['locale']); static::assertSame('en_GB', $data[1]['translations'][1]['locale']); } + + public function testReadIncludesCustomerLocalesOutsideShopLocales(): void + { + $this->dbConnection->executeStatement( + 'INSERT INTO s_core_locales (id, locale, language, territory) VALUES (:id, :locale, :language, :territory)', + [ + 'id' => $this->customerLocaleId, + 'locale' => 'ar_EG', + 'language' => 'Arabic', + 'territory' => 'Egypt', + ] + ); + $this->dbConnection->executeStatement('UPDATE s_user SET language = :language WHERE id = :id', [ + 'language' => $this->customerLocaleId, + 'id' => 1, + ]); + + $data = $this->languageReader->read($this->migrationContext); + + static::assertCount(3, $data); + static::assertSame('ar-EG', $data[0]['locale']); + } } diff --git a/tests/Profile/Shopware55/Converter/CustomerConverterTest.php b/tests/Profile/Shopware55/Converter/CustomerConverterTest.php index 82cbf5a28..ec0b66661 100644 --- a/tests/Profile/Shopware55/Converter/CustomerConverterTest.php +++ b/tests/Profile/Shopware55/Converter/CustomerConverterTest.php @@ -372,4 +372,29 @@ public function testConvertBusinessCustomer(): void static::assertSame('Shopware AG', $converted['company']); static::assertSame(CustomerEntity::ACCOUNT_TYPE_BUSINESS, $converted['accountType']); } + + public function testConvertKeepsDefaultAddressesWhenAddressSalutationIsUnknown(): void + { + $customerData = require __DIR__ . '/../../../_fixtures/customer_data.php'; + $customerData = $customerData[0]; + $customerData['addresses'][0]['salutation'] = 'unknown-salutation'; + + $context = Context::createDefaultContext(); + $convertResult = $this->customerConverter->convert( + $customerData, + $context, + $this->migrationContext + ); + + $converted = $convertResult->getConverted(); + static::assertNotNull($converted); + static::assertCount(3, $converted['addresses']); + static::assertArrayNotHasKey('salutationId', $converted['addresses'][0]); + static::assertSame($converted['addresses'][0]['id'], $converted['defaultBillingAddressId']); + static::assertSame($converted['addresses'][1]['id'], $converted['defaultShippingAddressId']); + + $logs = $this->loggingService->getLoggingArray(); + static::assertCount(1, $logs); + static::assertSame(ConvertEntityUnknownLog::getCode(), $logs[0]['code']); + } } From 877073ffa34001749e59913d85029837a345374f Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Thu, 23 Apr 2026 09:20:21 +0200 Subject: [PATCH 2/5] fix: resolve customer languageId from migration mapping --- .../Shopware/Converter/CustomerConverter.php | 2 +- .../Shopware/Converter/ShopwareConverter.php | 20 +++++++++ .../Gateway/Local/Reader/LanguageReader.php | 3 ++ .../Gateway/Local/LanguageReaderTest.php | 39 +++++++++++----- .../Converter/CustomerConverterTest.php | 45 +++++++++++++++++++ 5 files changed, 98 insertions(+), 11 deletions(-) diff --git a/src/Profile/Shopware/Converter/CustomerConverter.php b/src/Profile/Shopware/Converter/CustomerConverter.php index 54c692858..57666f9e8 100644 --- a/src/Profile/Shopware/Converter/CustomerConverter.php +++ b/src/Profile/Shopware/Converter/CustomerConverter.php @@ -218,7 +218,7 @@ public function convert( unset($data['attributes']); if (isset($data['customerlanguage']['locale'])) { - $languageUuid = $this->languageLookup->get($data['customerlanguage']['locale'], $context); + $languageUuid = $this->resolveLanguageId($data['customerlanguage']['locale'], $context, $this->languageLookup); if ($languageUuid !== null) { $converted['languageId'] = $languageUuid; } diff --git a/src/Profile/Shopware/Converter/ShopwareConverter.php b/src/Profile/Shopware/Converter/ShopwareConverter.php index cc936e378..fc17b0ffa 100644 --- a/src/Profile/Shopware/Converter/ShopwareConverter.php +++ b/src/Profile/Shopware/Converter/ShopwareConverter.php @@ -11,6 +11,8 @@ use Shopware\Core\Framework\Log\Package; use SwagMigrationAssistant\Migration\Connection\Helper\ConnectionNameSanitizer; use SwagMigrationAssistant\Migration\Converter\Converter; +use SwagMigrationAssistant\Migration\DataSelection\DefaultEntities; +use SwagMigrationAssistant\Migration\Mapping\Lookup\LanguageLookup; use SwagMigrationAssistant\Migration\MigrationContextInterface; #[Package('fundamentals@after-sales')] @@ -30,6 +32,24 @@ public function getSourceIdentifier(array $data): string return $data['id']; } + protected function resolveLanguageId(string $locale, Context $context, LanguageLookup $languageLookup): ?string + { + $mapping = $this->mappingService->getMapping( + $this->migrationContext->getConnection()->getId(), + DefaultEntities::LANGUAGE, + $locale, + $context + ); + + if (isset($mapping['entityId'])) { + $this->mappingIds[] = $mapping['id']; + + return $mapping['entityId']; + } + + return $languageLookup->get($locale, $context); + } + /** * @param array $newData * @param array $sourceData diff --git a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php index ddd5de525..771cfb7b2 100644 --- a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php +++ b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php @@ -79,6 +79,9 @@ private function fetchShopLocaleIds(MigrationContextInterface $migrationContext) return $query->fetchFirstColumn(); } + /** + * @return list + */ private function fetchCustomerLocaleIds(MigrationContextInterface $migrationContext): array { $connection = $this->getConnection($migrationContext); diff --git a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php index bce846ad5..3ae93b3e6 100644 --- a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php +++ b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php @@ -30,6 +30,8 @@ class LanguageReaderTest extends TestCase private int $customerLocaleId = 9999; + private bool $customerLocaleInserted = false; + protected function setUp(): void { $this->connectionSetup(); @@ -49,6 +51,10 @@ protected function setUp(): void $this->migrationContext->setGateway(new DummyLocalGateway()); $this->dbConnection = $connectionFactory->createDatabaseConnection($this->migrationContext); + $this->dbConnection->executeStatement('UPDATE s_user SET language = :language WHERE id = :id', [ + 'language' => 1, + 'id' => 1, + ]); } protected function tearDown(): void @@ -57,7 +63,9 @@ protected function tearDown(): void 'language' => 1, 'id' => 1, ]); - $this->dbConnection->executeStatement('DELETE FROM s_core_locales WHERE id = :id', ['id' => $this->customerLocaleId]); + if ($this->customerLocaleInserted) { + $this->dbConnection->executeStatement('DELETE FROM s_core_locales WHERE id = :id', ['id' => $this->customerLocaleId]); + } } public function testRead(): void @@ -82,15 +90,26 @@ public function testRead(): void public function testReadIncludesCustomerLocalesOutsideShopLocales(): void { - $this->dbConnection->executeStatement( - 'INSERT INTO s_core_locales (id, locale, language, territory) VALUES (:id, :locale, :language, :territory)', - [ - 'id' => $this->customerLocaleId, - 'locale' => 'ar_EG', - 'language' => 'Arabic', - 'territory' => 'Egypt', - ] + $existingLocaleId = $this->dbConnection->fetchOne( + 'SELECT id FROM s_core_locales WHERE locale = :locale', + ['locale' => 'ar_EG'] ); + + if ($existingLocaleId === false) { + $this->dbConnection->executeStatement( + 'INSERT INTO s_core_locales (id, locale, language, territory) VALUES (:id, :locale, :language, :territory)', + [ + 'id' => $this->customerLocaleId, + 'locale' => 'ar_EG', + 'language' => 'Arabic', + 'territory' => 'Egypt', + ] + ); + $this->customerLocaleInserted = true; + } else { + $this->customerLocaleId = (int) $existingLocaleId; + } + $this->dbConnection->executeStatement('UPDATE s_user SET language = :language WHERE id = :id', [ 'language' => $this->customerLocaleId, 'id' => 1, @@ -99,6 +118,6 @@ public function testReadIncludesCustomerLocalesOutsideShopLocales(): void $data = $this->languageReader->read($this->migrationContext); static::assertCount(3, $data); - static::assertSame('ar-EG', $data[0]['locale']); + static::assertContains('ar-EG', \array_column($data, 'locale')); } } diff --git a/tests/Profile/Shopware55/Converter/CustomerConverterTest.php b/tests/Profile/Shopware55/Converter/CustomerConverterTest.php index ec0b66661..f6d5ac967 100644 --- a/tests/Profile/Shopware55/Converter/CustomerConverterTest.php +++ b/tests/Profile/Shopware55/Converter/CustomerConverterTest.php @@ -397,4 +397,49 @@ public function testConvertKeepsDefaultAddressesWhenAddressSalutationIsUnknown() static::assertCount(1, $logs); static::assertSame(ConvertEntityUnknownLog::getCode(), $logs[0]['code']); } + + public function testConvertUsesLanguageMappingWhenLanguageDoesNotExistYet(): void + { + $customerData = require __DIR__ . '/../../../_fixtures/customer_data.php'; + $customerData = $customerData[0]; + $customerData['customerlanguage']['locale'] = 'ar-EG'; + + $languageId = Uuid::randomHex(); + $context = Context::createDefaultContext(); + $this->mappingService->getOrCreateMapping( + $this->connectionId, + DefaultEntities::LANGUAGE, + 'ar-EG', + $context, + null, + [], + $languageId + ); + + $languageLookup = $this->createMock(LanguageLookup::class); + $languageLookup->expects($this->never())->method('get'); + + $validator = static::getContainer()->get('validator'); + $salesChannelRepo = static::getContainer()->get('sales_channel.repository'); + + $customerConverter = new Shopware55CustomerConverter( + $this->mappingService, + $this->loggingService, + $validator, + $salesChannelRepo, + static::getContainer()->get(CountryLookup::class), + $languageLookup, + static::getContainer()->get(CountryStateLookup::class), + ); + + $convertResult = $customerConverter->convert( + $customerData, + $context, + $this->migrationContext + ); + + $converted = $convertResult->getConverted(); + static::assertNotNull($converted); + static::assertSame($languageId, $converted['languageId']); + } } From 6fe3f71526e227c3911f760e86fda2420b1f85c9 Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Thu, 23 Apr 2026 09:48:35 +0200 Subject: [PATCH 3/5] refactor: cleanup --- src/Profile/Shopware/Converter/CustomerConverter.php | 9 ++++++++- src/Profile/Shopware/Converter/ShopwareConverter.php | 2 +- .../Shopware/Gateway/Local/Reader/LanguageReader.php | 1 + .../Shopware/Gateway/Local/LanguageReaderTest.php | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Profile/Shopware/Converter/CustomerConverter.php b/src/Profile/Shopware/Converter/CustomerConverter.php index 57666f9e8..2c3b5d889 100644 --- a/src/Profile/Shopware/Converter/CustomerConverter.php +++ b/src/Profile/Shopware/Converter/CustomerConverter.php @@ -218,7 +218,12 @@ public function convert( unset($data['attributes']); if (isset($data['customerlanguage']['locale'])) { - $languageUuid = $this->resolveLanguageId($data['customerlanguage']['locale'], $context, $this->languageLookup); + $languageUuid = $this->resolveLanguageId( + $data['customerlanguage']['locale'], + $this->languageLookup, + $context, + ); + if ($languageUuid !== null) { $converted['languageId'] = $languageUuid; } @@ -331,8 +336,10 @@ protected function applyAddresses(array &$originalData, array &$converted): void $address['id'], $this->context ); + $newAddress['id'] = $addressMapping['entityId']; $this->mappingIds[] = $addressMapping['id']; + if ($salutationUuid !== null) { $newAddress['salutationId'] = $salutationUuid; } diff --git a/src/Profile/Shopware/Converter/ShopwareConverter.php b/src/Profile/Shopware/Converter/ShopwareConverter.php index fc17b0ffa..da99d641a 100644 --- a/src/Profile/Shopware/Converter/ShopwareConverter.php +++ b/src/Profile/Shopware/Converter/ShopwareConverter.php @@ -32,7 +32,7 @@ public function getSourceIdentifier(array $data): string return $data['id']; } - protected function resolveLanguageId(string $locale, Context $context, LanguageLookup $languageLookup): ?string + protected function resolveLanguageId(string $locale, LanguageLookup $languageLookup, Context $context): ?string { $mapping = $this->mappingService->getMapping( $this->migrationContext->getConnection()->getId(), diff --git a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php index 771cfb7b2..a6aa0a6c8 100644 --- a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php +++ b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php @@ -44,6 +44,7 @@ public function read(MigrationContextInterface $migrationContext): array $this->fetchShopLocaleIds($migrationContext), $this->fetchCustomerLocaleIds($migrationContext) )); + $locales = $this->fetchLocales($fetchedShopLocaleIds, $migrationContext); return $this->appendAssociatedData($locales, $migrationContext); diff --git a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php index 3ae93b3e6..4ea038da5 100644 --- a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php +++ b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php @@ -50,6 +50,7 @@ protected function setUp(): void ); $this->migrationContext->setGateway(new DummyLocalGateway()); + $this->dbConnection = $connectionFactory->createDatabaseConnection($this->migrationContext); $this->dbConnection->executeStatement('UPDATE s_user SET language = :language WHERE id = :id', [ 'language' => 1, @@ -63,6 +64,7 @@ protected function tearDown(): void 'language' => 1, 'id' => 1, ]); + if ($this->customerLocaleInserted) { $this->dbConnection->executeStatement('DELETE FROM s_core_locales WHERE id = :id', ['id' => $this->customerLocaleId]); } @@ -105,6 +107,7 @@ public function testReadIncludesCustomerLocalesOutsideShopLocales(): void 'territory' => 'Egypt', ] ); + $this->customerLocaleInserted = true; } else { $this->customerLocaleId = (int) $existingLocaleId; From 97f219bc1cc4b2ed7d2b23ec6aa9fdb89398aa4e Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Thu, 23 Apr 2026 09:58:24 +0200 Subject: [PATCH 4/5] fix: add distinct --- src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php | 3 ++- tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php index a6aa0a6c8..f1cff213a 100644 --- a/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php +++ b/src/Profile/Shopware/Gateway/Local/Reader/LanguageReader.php @@ -81,7 +81,7 @@ private function fetchShopLocaleIds(MigrationContextInterface $migrationContext) } /** - * @return list + * @return list */ private function fetchCustomerLocaleIds(MigrationContextInterface $migrationContext): array { @@ -89,6 +89,7 @@ private function fetchCustomerLocaleIds(MigrationContextInterface $migrationCont $query = $connection->createQueryBuilder(); $query->from('s_user', 'customer'); $query->addSelect('customer.language'); + $query->distinct(); $query->where('customer.language IS NOT NULL'); return $query->executeQuery()->fetchFirstColumn(); diff --git a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php index 4ea038da5..c0bd1d22b 100644 --- a/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php +++ b/tests/Profile/Shopware/Gateway/Local/LanguageReaderTest.php @@ -60,6 +60,7 @@ protected function setUp(): void protected function tearDown(): void { + // reset customer to the fixture default locale $this->dbConnection->executeStatement('UPDATE s_user SET language = :language WHERE id = :id', [ 'language' => 1, 'id' => 1, From 943e0f35fd9cdccf738d12966da5ec5f16b11c72 Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Thu, 23 Apr 2026 11:41:06 +0200 Subject: [PATCH 5/5] docs: update changelog --- CHANGELOG.md | 4 ++++ CHANGELOG_de-DE.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f6048b7..e454bbfa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# NEXT + +- Fixed Shopware 5 customer migration with custom locales by resolving `languageId` from existing migration mappings during the same run + # 16.2.0 - #15568 - Switched to PHP Symfony service definitions diff --git a/CHANGELOG_de-DE.md b/CHANGELOG_de-DE.md index ea3b5a9fd..1217c7b3e 100644 --- a/CHANGELOG_de-DE.md +++ b/CHANGELOG_de-DE.md @@ -1,3 +1,7 @@ +# NEXT + +- Fehler bei der Migration von Shopware-5-Kunden mit benutzerdefinierten Sprachen behoben, indem die `languageId` im selben Lauf aus vorhandenen Migrations-Mappings aufgelöst wird + # 16.2.0 - #15568 - Umstellung auf PHP-Symfony-Service-Definitionen