From 1fc5d467e1da9f4daafb61010b2421819de64ee5 Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Fri, 19 Dec 2025 10:13:16 +0100 Subject: [PATCH 1/4] refactor: filter optional database fields --- src/DependencyInjection/migration.xml | 1 + src/Exception/MigrationException.php | 14 +- .../Validation/MigrationValidationService.php | 176 ++++++++++++------ 3 files changed, 136 insertions(+), 55 deletions(-) diff --git a/src/DependencyInjection/migration.xml b/src/DependencyInjection/migration.xml index b008b1b85..297e01376 100644 --- a/src/DependencyInjection/migration.xml +++ b/src/DependencyInjection/migration.xml @@ -426,6 +426,7 @@ + diff --git a/src/Exception/MigrationException.php b/src/Exception/MigrationException.php index ff1c28442..95340b2e8 100644 --- a/src/Exception/MigrationException.php +++ b/src/Exception/MigrationException.php @@ -95,7 +95,9 @@ class MigrationException extends HttpException final public const INVALID_ID = 'SWAG_MIGRATION__INVALID_ID'; - public const DUPLICATE_SOURCE_CONNECTION = 'SWAG_MIGRATION__DUPLICATE_SOURCE_CONNECTION'; + final public const DUPLICATE_SOURCE_CONNECTION = 'SWAG_MIGRATION__DUPLICATE_SOURCE_CONNECTION'; + + final public const TABLE_NOT_FOUND = 'SWAG_MIGRATION__TABLE_NOT_FOUND'; public static function associationEntityRequiredMissing(string $entity, string $missingEntity): self { @@ -500,4 +502,14 @@ public static function duplicateSourceConnection(): self 'A connection to this source system already exists.', ); } + + public static function tableNotFound(string $tableName): self + { + return new self( + Response::HTTP_NOT_FOUND, + self::TABLE_NOT_FOUND, + 'The table "{{ tableName }}" was not found.', + ['tableName' => $tableName] + ); + } } diff --git a/src/Migration/Validation/MigrationValidationService.php b/src/Migration/Validation/MigrationValidationService.php index e1d939c2a..fd7acbcd3 100644 --- a/src/Migration/Validation/MigrationValidationService.php +++ b/src/Migration/Validation/MigrationValidationService.php @@ -7,11 +7,14 @@ namespace SwagMigrationAssistant\Migration\Validation; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Exception; use Shopware\Core\Framework\Context; +use Shopware\Core\Framework\DataAbstractionLayer\CompiledFieldCollection; use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry; -use Shopware\Core\Framework\DataAbstractionLayer\Field\Field; use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField; use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required; +use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware; use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommandQueue; use Shopware\Core\Framework\DataAbstractionLayer\Write\DataStack\KeyValuePair; use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence; @@ -37,13 +40,19 @@ * @internal */ #[Package('fundamentals@after-sales')] -readonly class MigrationValidationService +final class MigrationValidationService { + /** + * @var array> + */ + private array $requiredColumnsCache = []; + public function __construct( - private DefinitionInstanceRegistry $definitionRegistry, - private EventDispatcherInterface $eventDispatcher, - private LoggingServiceInterface $loggingService, - private MappingServiceInterface $mappingService, + private readonly DefinitionInstanceRegistry $definitionRegistry, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly LoggingServiceInterface $loggingService, + private readonly MappingServiceInterface $mappingService, + private readonly Connection $connection, ) { } @@ -83,7 +92,7 @@ public function validate( } catch (\Throwable $exception) { $validationContext->getValidationResult()->addLog( MigrationLogBuilder::fromMigrationContext($validationContext->getMigrationContext()) - ->withEntityName($validationContext->getEntityDefinition()->getEntityName()) + ->withEntityName($entityDefinition->getEntityName()) ->withSourceData($validationContext->getSourceData()) ->withConvertedData($validationContext->getConvertedData()) ->withExceptionMessage($exception->getMessage()) @@ -108,23 +117,33 @@ public function validate( private function validateEntityStructure(MigrationValidationContext $validationContext): void { - $fields = $validationContext->getEntityDefinition()->getFields(); + $entityDefinition = $validationContext->getEntityDefinition(); + + $fields = $entityDefinition->getFields(); + $entityName = $entityDefinition->getEntityName(); - $requiredFields = array_values(array_map( - static fn (Field $field) => $field->getPropertyName(), - $fields->filterByFlag(Required::class)->getElements() - )); + $convertedData = $validationContext->getConvertedData(); + $validationResult = $validationContext->getValidationResult(); - $convertedFieldNames = array_keys($validationContext->getConvertedData()); - $missingRequiredFields = array_diff($requiredFields, $convertedFieldNames); + $requiredDatabaseColumns = $this->getRequiredDatabaseColumns($entityName); + $requiredFields = $this->filterRequiredFields( + $fields, + $requiredDatabaseColumns + ); + + $convertedFieldNames = array_keys($convertedData); + $missingRequiredFields = array_diff( + $requiredFields, + $convertedFieldNames + ); foreach ($missingRequiredFields as $missingField) { - $validationContext->getValidationResult()->addLog( + $validationResult->addLog( MigrationLogBuilder::fromMigrationContext($validationContext->getMigrationContext()) - ->withEntityName($validationContext->getEntityDefinition()->getEntityName()) + ->withEntityName($entityName) ->withFieldName($missingField) - ->withConvertedData($validationContext->getConvertedData()) - ->withEntityId($validationContext->getConvertedData()['id'] ?? null) + ->withConvertedData($convertedData) + ->withEntityId($convertedData['id'] ?? null) ->build(MigrationValidationMissingRequiredFieldLog::class) ); } @@ -132,12 +151,12 @@ private function validateEntityStructure(MigrationValidationContext $validationC $unexpectedFields = array_diff($convertedFieldNames, array_keys($fields->getElements())); foreach ($unexpectedFields as $unexpectedField) { - $validationContext->getValidationResult()->addLog( + $validationResult->addLog( MigrationLogBuilder::fromMigrationContext($validationContext->getMigrationContext()) - ->withEntityName($validationContext->getEntityDefinition()->getEntityName()) + ->withEntityName($entityName) ->withFieldName($unexpectedField) - ->withConvertedData($validationContext->getConvertedData()) - ->withEntityId($validationContext->getConvertedData()['id'] ?? null) + ->withConvertedData($convertedData) + ->withEntityId($convertedData['id'] ?? null) ->build(MigrationValidationUnexpectedFieldLog::class) ); } @@ -145,29 +164,35 @@ private function validateEntityStructure(MigrationValidationContext $validationC private function validateFields(MigrationValidationContext $validationContext): void { - $fields = $validationContext->getEntityDefinition()->getFields(); + $entityDefinition = $validationContext->getEntityDefinition(); + $fields = $entityDefinition->getFields(); + + $convertedData = $validationContext->getConvertedData(); + $validationResult = $validationContext->getValidationResult(); + + $id = $convertedData['id'] ?? null; - if (!isset($validationContext->getConvertedData()['id'])) { + if ($id === null) { throw MigrationException::unexpectedNullValue('id'); } - if (!Uuid::isValid($validationContext->getConvertedData()['id'])) { - throw MigrationException::invalidId($validationContext->getConvertedData()['id'], $validationContext->getEntityDefinition()->getEntityName()); + if (!Uuid::isValid($id)) { + throw MigrationException::invalidId($id, $entityDefinition->getEntityName()); } $entityExistence = EntityExistence::createForEntity( - $validationContext->getEntityDefinition()->getEntityName(), - ['id' => $validationContext->getConvertedData()['id']], + $entityDefinition->getEntityName(), + ['id' => $id], ); $parameters = new WriteParameterBag( - $validationContext->getEntityDefinition(), + $entityDefinition, WriteContext::createFromContext($validationContext->getContext()), '', new WriteCommandQueue(), ); - foreach ($validationContext->getConvertedData() as $fieldName => $value) { + foreach ($convertedData as $fieldName => $value) { if (!$fields->has($fieldName)) { continue; } @@ -185,15 +210,15 @@ private function validateFields(MigrationValidationContext $validationContext): $serializer = $field->getSerializer(); \iterator_to_array($serializer->encode($field, $entityExistence, $keyValue, $parameters), false); } catch (\Throwable $e) { - $validationContext->getValidationResult()->addLog( + $validationResult->addLog( MigrationLogBuilder::fromMigrationContext($validationContext->getMigrationContext()) - ->withEntityName($validationContext->getEntityDefinition()->getEntityName()) + ->withEntityName($entityDefinition->getEntityName()) ->withFieldName($fieldName) ->withConvertedData([$fieldName => $value]) ->withSourceData($validationContext->getSourceData()) ->withExceptionMessage($e->getMessage()) ->withExceptionTrace($e->getTrace()) - ->withEntityId($validationContext->getConvertedData()['id'] ?? null) + ->withEntityId($id) ->build(MigrationValidationInvalidFieldValueLog::class) ); } @@ -202,30 +227,21 @@ private function validateFields(MigrationValidationContext $validationContext): private function validateAssociations(MigrationValidationContext $validationContext): void { - $fields = $validationContext->getEntityDefinition()->getFields(); - - $fkFields = array_values(array_map( - static fn (Field $field) => $field->getPropertyName(), - $fields->filterInstance(FkField::class)->getElements() - )); + $entityDefinition = $validationContext->getEntityDefinition(); + $fkFields = $entityDefinition->getFields()->filterInstance(FkField::class); - foreach ($fkFields as $fkFieldName) { - if (!isset($validationContext->getConvertedData()[$fkFieldName])) { - continue; - } + $convertedData = $validationContext->getConvertedData(); + $validationResult = $validationContext->getValidationResult(); - $fkValue = $validationContext->getConvertedData()[$fkFieldName]; + /** @var FkField $fkField */ + foreach ($fkFields as $fkField) { + $fkFieldName = $fkField->getPropertyName(); + $fkValue = $convertedData[$fkFieldName] ?? null; - if ($fkValue === '') { + if ($fkValue === null || $fkValue === '') { continue; } - $fkField = $fields->get($fkFieldName); - - if (!$fkField instanceof FkField) { - throw MigrationException::unexpectedNullValue($fkFieldName); - } - $referenceEntity = $fkField->getReferenceEntity(); if (!$referenceEntity) { @@ -240,16 +256,68 @@ private function validateAssociations(MigrationValidationContext $validationCont ); if (!$hasMapping) { - $validationContext->getValidationResult()->addLog( + $validationResult->addLog( MigrationLogBuilder::fromMigrationContext($validationContext->getMigrationContext()) - ->withEntityName($validationContext->getEntityDefinition()->getEntityName()) + ->withEntityName($entityDefinition->getEntityName()) ->withFieldName($fkFieldName) ->withConvertedData([$fkFieldName => $fkValue]) ->withSourceData($validationContext->getSourceData()) - ->withEntityId($validationContext->getConvertedData()['id'] ?? null) + ->withEntityId($convertedData['id'] ?? null) ->build(MigrationValidationInvalidForeignKeyLog::class) ); } } } + + /** + * @param array $requiredDbColumns + * + * @return array + */ + private function filterRequiredFields(CompiledFieldCollection $fields, array $requiredDbColumns): array + { + $requiredFields = []; + + foreach ($fields->filterByFlag(Required::class) as $field) { + if (!($field instanceof StorageAware)) { + $requiredFields[] = $field->getPropertyName(); + + continue; + } + + if (!\in_array($field->getStorageName(), $requiredDbColumns, true)) { + continue; + } + + $requiredFields[] = $field->getPropertyName(); + } + + return $requiredFields; + } + + /** + * @return list + */ + private function getRequiredDatabaseColumns(string $entityName): array + { + if (isset($this->requiredColumnsCache[$entityName])) { + return $this->requiredColumnsCache[$entityName]; + } + + $this->requiredColumnsCache[$entityName] = []; + + try { + $columns = $this->connection->createSchemaManager()->listTableColumns($entityName); + } catch (Exception) { + throw MigrationException::tableNotFound($entityName); + } + + foreach ($columns as $column) { + if ($column->getNotnull() && $column->getDefault() === null && !$column->getAutoincrement()) { + $this->requiredColumnsCache[$entityName][] = $column->getName(); + } + } + + return $this->requiredColumnsCache[$entityName]; + } } From 9d5ab1358cc7b489ec18200fdcb82f867d38900b Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Fri, 19 Dec 2025 11:42:31 +0100 Subject: [PATCH 2/4] test: nullable validation fields --- src/Exception/MigrationException.php | 12 -- .../Validation/MigrationValidationService.php | 24 ++- tests/MigrationServicesTrait.php | 4 +- .../MigrationValidationServiceTest.php | 137 ++++++++++++++---- 4 files changed, 129 insertions(+), 48 deletions(-) diff --git a/src/Exception/MigrationException.php b/src/Exception/MigrationException.php index 95340b2e8..65bbf8a1c 100644 --- a/src/Exception/MigrationException.php +++ b/src/Exception/MigrationException.php @@ -97,8 +97,6 @@ class MigrationException extends HttpException final public const DUPLICATE_SOURCE_CONNECTION = 'SWAG_MIGRATION__DUPLICATE_SOURCE_CONNECTION'; - final public const TABLE_NOT_FOUND = 'SWAG_MIGRATION__TABLE_NOT_FOUND'; - public static function associationEntityRequiredMissing(string $entity, string $missingEntity): self { return new self( @@ -502,14 +500,4 @@ public static function duplicateSourceConnection(): self 'A connection to this source system already exists.', ); } - - public static function tableNotFound(string $tableName): self - { - return new self( - Response::HTTP_NOT_FOUND, - self::TABLE_NOT_FOUND, - 'The table "{{ tableName }}" was not found.', - ['tableName' => $tableName] - ); - } } diff --git a/src/Migration/Validation/MigrationValidationService.php b/src/Migration/Validation/MigrationValidationService.php index fd7acbcd3..6a48ba283 100644 --- a/src/Migration/Validation/MigrationValidationService.php +++ b/src/Migration/Validation/MigrationValidationService.php @@ -8,7 +8,6 @@ namespace SwagMigrationAssistant\Migration\Validation; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Exception; use Shopware\Core\Framework\Context; use Shopware\Core\Framework\DataAbstractionLayer\CompiledFieldCollection; use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry; @@ -40,7 +39,7 @@ * @internal */ #[Package('fundamentals@after-sales')] -final class MigrationValidationService +class MigrationValidationService { /** * @var array> @@ -115,6 +114,10 @@ public function validate( return $validationContext->getValidationResult(); } + /** + * Validates that all required fields are present and that no unexpected fields exist. + * Required fields are determined by checking which database columns are non-nullable without a default value + */ private function validateEntityStructure(MigrationValidationContext $validationContext): void { $entityDefinition = $validationContext->getEntityDefinition(); @@ -162,6 +165,9 @@ private function validateEntityStructure(MigrationValidationContext $validationC } } + /** + * Validates that all field values conform to their field definitions by attempting to serialize them. + */ private function validateFields(MigrationValidationContext $validationContext): void { $entityDefinition = $validationContext->getEntityDefinition(); @@ -225,6 +231,9 @@ private function validateFields(MigrationValidationContext $validationContext): } } + /** + * Validates that all foreign key fields reference existing entities by checking the mapping service. + */ private function validateAssociations(MigrationValidationContext $validationContext): void { $entityDefinition = $validationContext->getEntityDefinition(); @@ -296,6 +305,9 @@ private function filterRequiredFields(CompiledFieldCollection $fields, array $re } /** + * Gets the list of required database columns for the given entity and caches the result for future calls. + * A required database column is defined as a column that is non-nullable, has no default value, and is not auto-incrementing. + * * @return list */ private function getRequiredDatabaseColumns(string $entityName): array @@ -306,11 +318,9 @@ private function getRequiredDatabaseColumns(string $entityName): array $this->requiredColumnsCache[$entityName] = []; - try { - $columns = $this->connection->createSchemaManager()->listTableColumns($entityName); - } catch (Exception) { - throw MigrationException::tableNotFound($entityName); - } + $columns = $this->connection + ->createSchemaManager() + ->listTableColumns($entityName); foreach ($columns as $column) { if ($column->getNotnull() && $column->getDefault() === null && !$column->getAutoincrement()) { diff --git a/tests/MigrationServicesTrait.php b/tests/MigrationServicesTrait.php index 9135d0557..ab565ba14 100644 --- a/tests/MigrationServicesTrait.php +++ b/tests/MigrationServicesTrait.php @@ -7,6 +7,7 @@ namespace SwagMigrationAssistant\Test; +use Doctrine\DBAL\Connection; use Psr\Log\NullLogger; use Shopware\Core\Checkout\Cart\Tax\TaxCalculator; use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryStates; @@ -212,6 +213,7 @@ protected function getMigrationDataConverter( $this->getContainer()->get('event_dispatcher'), $loggingService, $mappingService, + $this->getContainer()->get(Connection::class), ); return new MigrationDataConverter( @@ -221,7 +223,7 @@ protected function getMigrationDataConverter( $loggingService, $dataDefinition, new DummyMappingService(), - $validationService + $validationService, ); } diff --git a/tests/integration/Migration/Validation/MigrationValidationServiceTest.php b/tests/integration/Migration/Validation/MigrationValidationServiceTest.php index fa4748df4..da00c5600 100644 --- a/tests/integration/Migration/Validation/MigrationValidationServiceTest.php +++ b/tests/integration/Migration/Validation/MigrationValidationServiceTest.php @@ -17,6 +17,7 @@ use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; use Shopware\Core\Framework\Uuid\Uuid; +use SwagMigrationAssistant\Exception\MigrationException; use SwagMigrationAssistant\Migration\Connection\SwagMigrationConnectionEntity; use SwagMigrationAssistant\Migration\Logging\SwagMigrationLoggingCollection; use SwagMigrationAssistant\Migration\Logging\SwagMigrationLoggingDefinition; @@ -47,6 +48,8 @@ class MigrationValidationServiceTest extends TestCase private const CONNECTION_ID = '01991554142d73348ea58793d98f1989'; + private MigrationContext $migrationContext; + private MigrationValidationService $validationService; /** @@ -76,7 +79,21 @@ protected function setUp(): void $this->mappingRepo = static::getContainer()->get(SwagMigrationMappingDefinition::ENTITY_NAME . '.repository'); $this->context = Context::createDefaultContext(); + $connection = new SwagMigrationConnectionEntity(); + $connection->setId(self::CONNECTION_ID); + $connection->setProfileName(Shopware54Profile::PROFILE_NAME); + $connection->setGatewayName(DummyLocalGateway::GATEWAY_NAME); + $this->runId = Uuid::randomHex(); + + $this->migrationContext = new MigrationContext( + $connection, + new Shopware54Profile(), + new DummyLocalGateway(), + null, + $this->runId, + ); + static::getContainer()->get('swag_migration_connection.repository')->create( [ [ @@ -134,21 +151,8 @@ public function testShouldEarlyReturnNullWhenConvertedDataIsEmpty(): void #[DataProvider('entityStructureAndFieldProvider')] public function testShouldValidateStructureAndFieldsValues(array $convertedData, array $expectedLogs): void { - $connection = new SwagMigrationConnectionEntity(); - $connection->setId(self::CONNECTION_ID); - $connection->setProfileName(Shopware54Profile::PROFILE_NAME); - $connection->setGatewayName(DummyLocalGateway::GATEWAY_NAME); - - $migrationContext = new MigrationContext( - $connection, - new Shopware54Profile(), - new DummyLocalGateway(), - null, - $this->runId, - ); - $result = $this->validationService->validate( - $migrationContext, + $this->migrationContext, $this->context, $convertedData, SwagMigrationLoggingDefinition::ENTITY_NAME, @@ -170,6 +174,96 @@ public function testShouldValidateStructureAndFieldsValues(array $convertedData, static::assertSame($expectedLogs, $logCodes); } + public function testShouldFilterNullableFields(): void + { + $result = $this->validationService->validate( + $this->migrationContext, + $this->context, + [ + 'id' => Uuid::randomHex(), + ], + ProductDefinition::ENTITY_NAME, + [] + ); + + static::assertInstanceOf(MigrationValidationResult::class, $result); + + $missingFields = \array_map(fn ($log) => $log->getFieldName(), $result->getLogs()); + static::assertCount(3, $missingFields); + + $expectedMissingFields = [ + 'active', // has no required flag + 'price', // is nullable, but has required flag + 'cmsPageVersionId', // has default value, but has required flag + ]; + + static::assertCount( + 0, + \array_intersect($expectedMissingFields, $missingFields) + ); + } + + public function testShouldLogWhenEntityHasNowId(): void + { + $result = $this->validationService->validate( + $this->migrationContext, + $this->context, + [ + 'level' => 'error', + 'code' => 'some_code', + 'userFixable' => true, + 'createdAt' => (new \DateTime())->format(\DATE_ATOM), + ], + SwagMigrationLoggingDefinition::ENTITY_NAME, + [] + ); + + static::assertInstanceOf(MigrationValidationResult::class, $result); + + $logs = \array_filter($result->getLogs(), fn ($log) => $log instanceof MigrationValidationExceptionLog); + static::assertCount(1, $logs); + + $exceptionLog = array_values($logs)[0]; + static::assertInstanceOf(MigrationValidationExceptionLog::class, $exceptionLog); + + static::assertSame( + MigrationException::unexpectedNullValue('id')->getMessage(), + $exceptionLog->getExceptionMessage() + ); + } + + public function testShouldLogWhenEntityHasInvalidId(): void + { + $id = 'invalid-uuid'; + + $result = $this->validationService->validate( + $this->migrationContext, + $this->context, + [ + 'id' => $id, + 'level' => 'error', + 'code' => 'some_code', + 'userFixable' => true, + 'createdAt' => (new \DateTime())->format(\DATE_ATOM), + ], + SwagMigrationLoggingDefinition::ENTITY_NAME, + [] + ); + + static::assertInstanceOf(MigrationValidationResult::class, $result); + + $logs = \array_filter($result->getLogs(), fn ($log) => $log instanceof MigrationValidationExceptionLog); + static::assertCount(1, $logs); + + $exceptionLog = array_values($logs)[0]; + static::assertInstanceOf(MigrationValidationExceptionLog::class, $exceptionLog); + + static::assertSame( + MigrationException::invalidId($id, SwagMigrationLoggingDefinition::ENTITY_NAME)->getMessage(), + $exceptionLog->getExceptionMessage(), + ); + } + /** * @param array $convertedData * @param array> $mappings @@ -178,25 +272,12 @@ public function testShouldValidateStructureAndFieldsValues(array $convertedData, #[DataProvider('associationProvider')] public function testValidateAssociations(array $convertedData, array $mappings, array $expectedLogs): void { - $connection = new SwagMigrationConnectionEntity(); - $connection->setId(self::CONNECTION_ID); - $connection->setProfileName(Shopware54Profile::PROFILE_NAME); - $connection->setGatewayName(DummyLocalGateway::GATEWAY_NAME); - - $migrationContext = new MigrationContext( - $connection, - new Shopware54Profile(), - new DummyLocalGateway(), - null, - $this->runId, - ); - if (!empty($mappings)) { $this->mappingRepo->create($mappings, $this->context); } $result = $this->validationService->validate( - $migrationContext, + $this->migrationContext, $this->context, $convertedData, SwagMigrationLoggingDefinition::ENTITY_NAME, From 5bfc5e45e6be9137027d5f8da432b044358858a6 Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Fri, 19 Dec 2025 15:54:37 +0100 Subject: [PATCH 3/4] refactor: implement reset interface --- src/DependencyInjection/migration.xml | 2 ++ .../Service/MediaFileProcessorService.php | 1 + src/Migration/Service/MigrationDataWriter.php | 5 +++-- .../Validation/MigrationValidationService.php | 14 +++++++++++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/DependencyInjection/migration.xml b/src/DependencyInjection/migration.xml index 297e01376..071e8101c 100644 --- a/src/DependencyInjection/migration.xml +++ b/src/DependencyInjection/migration.xml @@ -427,6 +427,8 @@ + + diff --git a/src/Migration/Service/MediaFileProcessorService.php b/src/Migration/Service/MediaFileProcessorService.php index d4c8c2c56..ce3983fcc 100644 --- a/src/Migration/Service/MediaFileProcessorService.php +++ b/src/Migration/Service/MediaFileProcessorService.php @@ -108,6 +108,7 @@ private function getMediaFiles(MigrationContextInterface $migrationContext): arr ->from('swag_migration_media_file') ->where('run_id = :runId') ->andWhere('written = 1') + ->andWhere('processed = 0') ->orderBy('entity, file_size') ->setFirstResult($migrationContext->getOffset()) ->setMaxResults($migrationContext->getLimit()) diff --git a/src/Migration/Service/MigrationDataWriter.php b/src/Migration/Service/MigrationDataWriter.php index c62982dd0..40c9e5cf4 100644 --- a/src/Migration/Service/MigrationDataWriter.php +++ b/src/Migration/Service/MigrationDataWriter.php @@ -73,6 +73,7 @@ public function writeData(MigrationContextInterface $migrationContext, Context $ $criteria->addFilter(new EqualsFilter('entity', $dataSet::getEntity())); $criteria->addFilter(new EqualsFilter('runId', $migrationContext->getRunUuid())); $criteria->addFilter(new EqualsFilter('convertFailure', false)); + $criteria->addFilter(new EqualsFilter('written', false)); $criteria->setOffset($migrationContext->getOffset()); $criteria->setLimit($migrationContext->getLimit()); $criteria->addSorting(new FieldSorting('autoIncrement', FieldSorting::ASCENDING)); @@ -133,7 +134,7 @@ public function writeData(MigrationContextInterface $migrationContext, Context $ } unset($data); - return $migrationData->getTotal(); + return $migrationData->count(); } catch (WriteException $exception) { $this->handleWriteException( $exception, @@ -165,7 +166,7 @@ public function writeData(MigrationContextInterface $migrationContext, Context $ $context ); - return $migrationData->getTotal(); + return $migrationData->count(); } /** diff --git a/src/Migration/Validation/MigrationValidationService.php b/src/Migration/Validation/MigrationValidationService.php index 6a48ba283..fa23c3f67 100644 --- a/src/Migration/Validation/MigrationValidationService.php +++ b/src/Migration/Validation/MigrationValidationService.php @@ -34,12 +34,13 @@ use SwagMigrationAssistant\Migration\Validation\Log\MigrationValidationMissingRequiredFieldLog; use SwagMigrationAssistant\Migration\Validation\Log\MigrationValidationUnexpectedFieldLog; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Service\ResetInterface; /** * @internal */ #[Package('fundamentals@after-sales')] -class MigrationValidationService +class MigrationValidationService implements ResetInterface { /** * @var array> @@ -55,6 +56,11 @@ public function __construct( ) { } + public function reset(): void + { + $this->requiredColumnsCache = []; + } + /** * @param array|null $convertedEntity * @param array $sourceData @@ -204,6 +210,12 @@ private function validateFields(MigrationValidationContext $validationContext): } $field = clone $fields->get($fieldName); + + /** + * Forces validation to run even for null values. + * Without Required, AbstractFieldSerializer::requiresValidation() returns false + * for null values on optional fields, skipping type/format validation entirely. + */ $field->setFlags(new Required()); $keyValue = new KeyValuePair( From 2260f76428eb6f7c88deecbd6e36646058f66609 Mon Sep 17 00:00:00 2001 From: Lars Kemper Date: Mon, 22 Dec 2025 10:20:11 +0100 Subject: [PATCH 4/4] fix: search not flushed mappings for associations --- src/Migration/Mapping/MappingService.php | 9 +++++++++ .../Log/MigrationValidationUnexpectedFieldLog.php | 2 +- src/Migration/Validation/MigrationValidationService.php | 5 ++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Migration/Mapping/MappingService.php b/src/Migration/Mapping/MappingService.php index d84ab7b04..dd1e30812 100644 --- a/src/Migration/Mapping/MappingService.php +++ b/src/Migration/Mapping/MappingService.php @@ -216,6 +216,15 @@ public function getMappings(string $connectionId, string $entityName, array $ids public function hasValidMappingByEntityId(string $connectionId, string $entityName, string $entityId, Context $context): bool { + // check in write array first to avoid unnecessary db calls and find not yet written mappings + foreach ($this->writeArray as $writeMapping) { + if ($writeMapping['connectionId'] !== $connectionId || $writeMapping['entityId'] !== $entityId) { + continue; + } + + return $writeMapping['oldIdentifier'] !== null; + } + $criteria = new Criteria(); $criteria->addFilter( new EqualsFilter('connectionId', $connectionId), diff --git a/src/Migration/Validation/Log/MigrationValidationUnexpectedFieldLog.php b/src/Migration/Validation/Log/MigrationValidationUnexpectedFieldLog.php index d363bf846..491f5bc83 100644 --- a/src/Migration/Validation/Log/MigrationValidationUnexpectedFieldLog.php +++ b/src/Migration/Validation/Log/MigrationValidationUnexpectedFieldLog.php @@ -15,7 +15,7 @@ { public function isUserFixable(): bool { - return true; + return false; } public function getLevel(): string diff --git a/src/Migration/Validation/MigrationValidationService.php b/src/Migration/Validation/MigrationValidationService.php index fa23c3f67..d783ce48c 100644 --- a/src/Migration/Validation/MigrationValidationService.php +++ b/src/Migration/Validation/MigrationValidationService.php @@ -212,9 +212,8 @@ private function validateFields(MigrationValidationContext $validationContext): $field = clone $fields->get($fieldName); /** - * Forces validation to run even for null values. - * Without Required, AbstractFieldSerializer::requiresValidation() returns false - * for null values on optional fields, skipping type/format validation entirely. + * The required flag controls flow in AbstractFieldSerializer::requiresValidation(). + * Without it, the serializer will skip validation for the field. */ $field->setFlags(new Required());