Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1fc5d46
refactor: filter optional database fields
larskemper Dec 19, 2025
9d5ab13
test: nullable validation fields
larskemper Dec 19, 2025
5bfc5e4
refactor: implement reset interface
larskemper Dec 19, 2025
2260f76
fix: search not flushed mappings for associations
larskemper Dec 22, 2025
e35fa66
Merge branch 'feature/migration-logging-refactor' into refactor/filte…
larskemper Dec 24, 2025
b13a0b2
refactor: validation of migration
larskemper Jan 6, 2026
99968d0
test: fix
larskemper Jan 6, 2026
49d4f79
refactor: validation
larskemper Jan 6, 2026
54de53b
feat: cleanup and add tests
larskemper Jan 7, 2026
2be5679
fix: null order custom field
larskemper Jan 7, 2026
89d8147
Merge branch 'feature/migration-logging-refactor' into refactor/filte…
larskemper Jan 7, 2026
9526887
fix: test
larskemper Jan 7, 2026
9036ca7
feat: add validation of resolution values
larskemper Dec 23, 2025
9cb5a7a
test: fix
larskemper Dec 23, 2025
6e7a457
feat: add defaults to examples
larskemper Dec 23, 2025
7dd489b
test: error resolution controller
larskemper Dec 24, 2025
4b4a984
test: jest
larskemper Dec 29, 2025
82a5c79
refactor: unify controller annoations
larskemper Jan 7, 2026
ec3f5da
refactor: rebase & adjust to changes
larskemper Jan 7, 2026
0de2bed
fix: typo
larskemper Jan 7, 2026
f29f329
fix: card border
larskemper Jan 8, 2026
6b22087
feat: add nested field validation
larskemper Jan 21, 2026
4a4ddef
refactor: validation
larskemper Jan 21, 2026
6b242c5
test: refine php tests
larskemper Jan 21, 2026
534d4d1
refactor: simplify
larskemper Jan 21, 2026
2f81945
test: add indexed generator keys
larskemper Jan 22, 2026
9583ee2
fix: remove old invalid code
larskemper Jan 22, 2026
a2554ff
Merge remote-tracking branch 'origin/feature/migration-logging-refact…
larskemper Jan 22, 2026
3e0ea79
fix: resolve threads
larskemper Jan 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,12 @@

- [BREAKING] [#51](https://github.com/shopware/SwagMigrationAssistant/pull/51) - feat!: add migration validation of converted data
- [BREAKING] Added validation check of converted data to `convertData(...)` method in `SwagMigrationAssistant\Migration\Service\MigrationDataConverter`
- Added `hasValidMappingByEntityId(...)` to `SwagMigrationAssistant\Migration\Mapping\MappingService` and `SwagMigrationAssistant\Migration\Mapping\MappingServiceInterface` to check if a mapping exists and is valid for a given entity and source id
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationValidationService` to validate converted data against Shopware's data definitions
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationEntityValidationService` to validate converted data against Shopware's data definitions
- Added new events `SwagMigrationAssistant\Migration\Validation\Event\MigrationPreValidationEvent` and `SwagMigrationAssistant\Migration\Validation\Event\MigrationPostValidationEvent` to allow extensions to hook into the validation process
- Added new log classes `ValidationInvalidFieldValueLog`, `ValidationInvalidForeignKeyLog`, `ValidationMissingRequiredFieldLog` and `ValidationUnexpectedFieldLog` to log validation errors
- Added new context class `SwagMigrationAssistant\Migration\Validation\MigrationValidationContext` to pass validation related data
- Added new result class `SwagMigrationAssistant\Migration\Validation\MigrationValidationResult` to collect validation results
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationValidationService` to validate converted data against Shopware's data definitions in three steps:
- Added new service `SwagMigrationAssistant\Migration\Validation\MigrationEntityValidationService` to validate converted data against Shopware's data definitions in three steps:
- Entity structure: Check for unexpected fields
- Field Validation: Check for missing required fields and invalid field values
- Association Validation: Check for invalid foreign keys
Expand Down
16 changes: 9 additions & 7 deletions src/Controller/DataProviderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Routing\RoutingException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\DataProvider\Provider\ProviderRegistryInterface;
use SwagMigrationAssistant\DataProvider\Service\EnvironmentServiceInterface;
use SwagMigrationAssistant\Exception\MigrationException;
Expand All @@ -32,7 +34,7 @@
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: ['_routeScope' => ['api']])]
#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class DataProviderController extends AbstractController
{
Expand All @@ -54,7 +56,7 @@ public function __construct(
#[Route(
path: '/api/_action/data-provider/get-environment',
name: 'api.admin.data-provider.get-environment',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getEnvironment(Context $context): JsonResponse
Expand All @@ -67,7 +69,7 @@ public function getEnvironment(Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/get-data',
name: 'api.admin.data-provider.get-data',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getData(Request $request, Context $context): JsonResponse
Expand All @@ -89,7 +91,7 @@ public function getData(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/get-total',
name: 'api.admin.data-provider.get-total',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getTotal(Request $request, Context $context): JsonResponse
Expand All @@ -107,7 +109,7 @@ public function getTotal(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/get-table',
name: 'api.admin.data-provider.get-table',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function getTable(Request $request, Context $context): JsonResponse
Expand All @@ -127,7 +129,7 @@ public function getTable(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/data-provider/generate-document',
name: 'api.admin.data-provider.generate-document',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function generateDocument(Request $request, Context $context): JsonResponse
Expand All @@ -154,7 +156,7 @@ public function generateDocument(Request $request, Context $context): JsonRespon
#[Route(
path: '/api/_action/data-provider/download-private-file/{file}',
name: 'api.admin.data-provider.download-private-file',
defaults: ['_acl' => ['admin']],
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['admin']],
methods: [Request::METHOD_GET]
)]
public function downloadPrivateFile(Request $request, Context $context): StreamedResponse|RedirectResponse
Expand Down
147 changes: 147 additions & 0 deletions src/Controller/ErrorResolutionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php declare(strict_types=1);
/*
* (c) shopware AG <info@shopware.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace SwagMigrationAssistant\Controller;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\Exception\MigrationException;
use SwagMigrationAssistant\Migration\ErrorResolution\MigrationFieldExampleGenerator;
use SwagMigrationAssistant\Migration\Validation\Exception\MigrationValidationException;
use SwagMigrationAssistant\Migration\Validation\MigrationFieldValidationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class ErrorResolutionController extends AbstractController
{
/**
* @internal
*/
public function __construct(
private readonly MigrationFieldValidationService $fieldValidationService,
) {
}

#[Route(
path: '/api/_action/migration/error-resolution/validate',
name: 'api.admin.migration.error-resolution.validate',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST]
)]
public function validateResolution(Request $request, Context $context): JsonResponse
{
$entityName = (string) $request->request->get('entityName');
$fieldName = (string) $request->request->get('fieldName');

if ($entityName === '') {
throw MigrationException::missingRequestParameter('entityName');
}

if ($fieldName === '') {
throw MigrationException::missingRequestParameter('fieldName');
}

$fieldValue = $this->decodeFieldValue($request->request->all()['fieldValue'] ?? null);

if ($fieldValue === null) {
throw MigrationException::missingRequestParameter('fieldValue');
}

try {
$this->fieldValidationService->validateField(
$entityName,
$fieldName,
$fieldValue,
$context,
);
} catch (MigrationValidationException $exception) {
$previous = $exception->getPrevious();

if ($previous instanceof WriteConstraintViolationException) {
return new JsonResponse([
'valid' => false,
'violations' => $previous->toArray(),
]);
}

return new JsonResponse([
'valid' => false,
'violations' => [['message' => $exception->getMessage()]],
]);
} catch (\Throwable $exception) {
return new JsonResponse([
'valid' => false,
'violations' => [['message' => $exception->getMessage()]],
]);
}

return new JsonResponse([
'valid' => true,
'violations' => [],
]);
}

#[Route(
path: '/api/_action/migration/error-resolution/example-field-structure',
name: 'api.admin.migration.error-resolution.example-field-structure',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST]
)]
public function getExampleFieldStructure(Request $request): JsonResponse
{
$entityName = (string) $request->request->get('entityName');
$fieldName = (string) $request->request->get('fieldName');

if ($entityName === '') {
throw MigrationException::missingRequestParameter('entityName');
}

if ($fieldName === '') {
throw MigrationException::missingRequestParameter('fieldName');
}

$resolved = $this->fieldValidationService->resolveFieldPath($entityName, $fieldName);

if ($resolved === null) {
throw MigrationValidationException::entityFieldNotFound($entityName, $fieldName);
}

[, $field] = $resolved;

$response = [
'fieldType' => MigrationFieldExampleGenerator::getFieldType($field),
'example' => MigrationFieldExampleGenerator::generateExample($field),
];

return new JsonResponse($response);
}

/**
* @return array<array-key, mixed>|bool|float|int|string|null
*/
private function decodeFieldValue(mixed $value): array|bool|float|int|string|null
{
if ($value === null || $value === '' || $value === []) {
return null;
}

if (!\is_string($value)) {
return $value;
}

$decoded = \json_decode($value, true);

return \json_last_error() === \JSON_ERROR_NONE ? $decoded : $value;
}
}
40 changes: 31 additions & 9 deletions src/Controller/HistoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Routing\RoutingException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\Exception\MigrationException;
use SwagMigrationAssistant\Migration\History\HistoryServiceInterface;
use SwagMigrationAssistant\Migration\History\LogGroupingService;
Expand All @@ -22,7 +24,7 @@
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

#[Route(defaults: ['_routeScope' => ['api']])]
#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class HistoryController extends AbstractController
{
Expand All @@ -35,7 +37,12 @@ public function __construct(
) {
}

#[Route(path: '/api/_action/migration/get-grouped-logs-of-run', name: 'api.admin.migration.get-grouped-logs-of-run', methods: ['GET'], defaults: ['_acl' => ['swag_migration.viewer']])]
#[Route(
path: '/api/_action/migration/get-grouped-logs-of-run',
name: 'api.admin.migration.get-grouped-logs-of-run',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_GET],
)]
public function getGroupedLogsOfRun(Request $request, Context $context): JsonResponse
{
$runUuid = $request->query->getAlnum('runUuid');
Expand All @@ -60,7 +67,12 @@ public function getGroupedLogsOfRun(Request $request, Context $context): JsonRes
]);
}

#[Route(path: '/api/_action/migration/download-logs-of-run', name: 'api.admin.migration.download-logs-of-run', methods: ['POST'], defaults: ['auth_required' => false, '_acl' => ['swag_migration.viewer']])]
#[Route(
path: '/api/_action/migration/download-logs-of-run',
name: 'api.admin.migration.download-logs-of-run',
defaults: ['auth_required' => false, PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST],
)]
public function downloadLogsOfRun(Request $request, Context $context): StreamedResponse
{
$runUuid = $request->request->getAlnum('runUuid');
Expand All @@ -86,7 +98,12 @@ public function downloadLogsOfRun(Request $request, Context $context): StreamedR
return $response;
}

#[Route(path: '/api/_action/migration/clear-data-of-run', name: 'api.admin.migration.clear-data-of-run', methods: ['POST'], defaults: ['_acl' => ['swag_migration.deleter']])]
#[Route(
path: '/api/_action/migration/clear-data-of-run',
name: 'api.admin.migration.clear-data-of-run',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.deleter']],
methods: [Request::METHOD_POST],
)]
public function clearDataOfRun(Request $request, Context $context): Response
{
$runUuid = $request->request->getAlnum('runUuid');
Expand All @@ -104,7 +121,12 @@ public function clearDataOfRun(Request $request, Context $context): Response
return new Response();
}

#[Route(path: '/api/_action/migration/is-media-processing', name: 'api.admin.migration.is-media-processing', methods: ['GET'], defaults: ['_acl' => ['swag_migration_history:read']])]
#[Route(
path: '/api/_action/migration/is-media-processing',
name: 'api.admin.migration.is-media-processing',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration_history:read']],
methods: [Request::METHOD_GET],
)]
public function isMediaProcessing(): JsonResponse
{
$result = $this->historyService->isMediaProcessing();
Expand All @@ -115,8 +137,8 @@ public function isMediaProcessing(): JsonResponse
#[Route(
path: '/api/_action/migration/get-log-groups',
name: 'api.admin.migration.get-log-groups',
methods: ['GET'],
defaults: ['_acl' => ['swag_migration.viewer']]
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_GET],
)]
public function getLogGroups(Request $request, Context $context): JsonResponse
{
Expand Down Expand Up @@ -166,8 +188,8 @@ public function getLogGroups(Request $request, Context $context): JsonResponse
#[Route(
path: '/api/_action/migration/get-all-log-ids',
name: 'api.admin.migration.get-all-log-ids',
methods: ['POST'],
defaults: ['_acl' => ['swag_migration.viewer']]
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.viewer']],
methods: [Request::METHOD_POST],
)]
public function getAllLogIds(Request $request): JsonResponse
{
Expand Down
18 changes: 15 additions & 3 deletions src/Controller/PremappingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Routing\ApiRouteScope;
use Shopware\Core\Framework\Routing\RoutingException;
use Shopware\Core\PlatformRequest;
use SwagMigrationAssistant\Migration\MigrationContextFactoryInterface;
use SwagMigrationAssistant\Migration\Service\PremappingServiceInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
Expand All @@ -18,7 +20,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: ['_routeScope' => ['api']])]
#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
#[Package('fundamentals@after-sales')]
class PremappingController extends AbstractController
{
Expand All @@ -31,7 +33,12 @@ public function __construct(
) {
}

#[Route(path: '/api/_action/migration/generate-premapping', name: 'api.admin.migration.generate-premapping', methods: ['POST'], defaults: ['_acl' => ['swag_migration.editor']])]
#[Route(
path: '/api/_action/migration/generate-premapping',
name: 'api.admin.migration.generate-premapping',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.editor']],
methods: [Request::METHOD_POST],
)]
public function generatePremapping(Request $request, Context $context): JsonResponse
{
$dataSelectionIds = $request->request->all('dataSelectionIds');
Expand All @@ -44,7 +51,12 @@ public function generatePremapping(Request $request, Context $context): JsonResp
return new JsonResponse($this->premappingService->generatePremapping($context, $migrationContext, $dataSelectionIds));
}

#[Route(path: '/api/_action/migration/write-premapping', name: 'api.admin.migration.write-premapping', methods: ['POST'], defaults: ['_acl' => ['swag_migration.editor']])]
#[Route(
path: '/api/_action/migration/write-premapping',
name: 'api.admin.migration.write-premapping',
defaults: [PlatformRequest::ATTRIBUTE_ACL => ['swag_migration.editor']],
methods: [Request::METHOD_POST],
)]
public function writePremapping(Request $request, Context $context): Response
{
$premapping = $request->request->all('premapping');
Expand Down
Loading
Loading