Skip to content

Commit 8ca5873

Browse files
committed
WIP
1 parent 7e49091 commit 8ca5873

File tree

7 files changed

+145
-109
lines changed

7 files changed

+145
-109
lines changed

config/services/denormalizer.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
66

77
<services>
8+
<service id="sylius_import_export.denormalizer.entity_relation" class="Sylius\ImportExport\Denormalizer\EntityRelationDenormalizer">
9+
<argument type="service" id="doctrine.orm.entity_manager" />
10+
<tag name="serializer.normalizer" priority="64" />
11+
</service>
12+
813
<service id="sylius_import_export.denormalizer.relation_resolver" class="Sylius\ImportExport\Denormalizer\DoctrineRelationResolver">
914
<argument type="service" id="doctrine.orm.entity_manager" />
15+
<argument type="service" id="serializer" />
1016
</service>
1117

1218
<service id="sylius_import_export.denormalizer.default_resource" class="Sylius\ImportExport\Denormalizer\DefaultResourceDenormalizer">
1319
<argument type="service" id="serializer" />
14-
<argument type="service" id="sylius_import_export.denormalizer.relation_resolver" />
15-
<argument type="service" id="doctrine.orm.entity_manager" />
1620
</service>
1721

1822
<service id="sylius_import_export.denormalizer.registry" class="Sylius\ImportExport\Denormalizer\DenormalizerRegistry">

src/Controller/ImportAction.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,4 @@ public function __invoke(Request $request, string $grid): Response
7474

7575
return new RedirectResponse($request->headers->get('referer') ?? '/');
7676
}
77-
}
77+
}

src/Denormalizer/DefaultResourceDenormalizer.php

Lines changed: 8 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,96 +13,32 @@
1313

1414
namespace Sylius\ImportExport\Denormalizer;
1515

16-
use Doctrine\ORM\EntityManagerInterface;
1716
use Sylius\ImportExport\Serializer\ExportAwareItemNormalizer;
17+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
18+
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
1819
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
19-
use Webmozart\Assert\Assert;
2020

2121
final readonly class DefaultResourceDenormalizer implements ResourceDenormalizerInterface
2222
{
2323
public function __construct(
2424
private DenormalizerInterface $denormalizer,
25-
private RelationResolverInterface $relationResolver,
26-
private EntityManagerInterface $entityManager,
2725
) {
2826
}
2927

3028
public function denormalize(array $data, string $resourceClass): object
3129
{
32-
$metadata = $this->entityManager->getClassMetadata($resourceClass);
33-
$processedData = [];
34-
35-
foreach ($data as $field => $value) {
36-
if (null === $value || (is_string($value) && '' === $value)) {
37-
continue;
38-
}
39-
40-
if ($metadata->hasAssociation($field)) {
41-
$associationMapping = $metadata->getAssociationMapping($field);
42-
$targetEntity = $associationMapping['targetEntity'];
43-
Assert::string($targetEntity);
44-
45-
if ($metadata->isCollectionValuedAssociation($field)) {
46-
if (is_array($value) && !empty($value)) {
47-
$resolvedEntities = $this->relationResolver->resolveCollection($targetEntity, $value);
48-
$filteredEntities = array_filter($resolvedEntities); // Remove null values
49-
if (!empty($filteredEntities)) {
50-
$processedData[$field] = $filteredEntities;
51-
}
52-
// Don't add the field at all if no entities found
53-
}
54-
} else {
55-
if (is_array($value) && !empty($value)) {
56-
$resolvedEntity = $this->relationResolver->resolveEntity($targetEntity, $value);
57-
if (null !== $resolvedEntity) {
58-
$processedData[$field] = $resolvedEntity;
59-
}
60-
// Don't add the field at all if entity not found
61-
}
62-
}
63-
} elseif (is_array($value)) {
64-
$processedData[$field] = $this->processNestedArray($value);
65-
} else {
66-
$processedData[$field] = $value;
67-
}
68-
}
69-
70-
// Ensure all entity relationships are properly managed before denormalization
71-
foreach ($processedData as $field => $value) {
72-
if ($metadata->hasAssociation($field)) {
73-
if ($metadata->isCollectionValuedAssociation($field) && is_array($value)) {
74-
foreach ($value as $entity) {
75-
if (is_object($entity) && !$this->entityManager->contains($entity)) {
76-
throw new \RuntimeException(sprintf(
77-
'Entity %s in collection %s is not managed by EntityManager',
78-
get_class($entity),
79-
$field
80-
));
81-
}
82-
}
83-
} elseif (is_object($value) && !$this->entityManager->contains($value)) {
84-
throw new \RuntimeException(sprintf(
85-
'Entity %s in field %s is not managed by EntityManager',
86-
get_class($value),
87-
$field
88-
));
89-
}
90-
}
91-
}
92-
9330
return $this->denormalizer->denormalize(
94-
$processedData,
31+
$data,
9532
$resourceClass,
9633
null,
97-
[ExportAwareItemNormalizer::EXPORT_CONTEXT_KEY => true]
34+
[
35+
ExportAwareItemNormalizer::EXPORT_CONTEXT_KEY => true,
36+
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s',
37+
AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE => true,
38+
],
9839
);
9940
}
10041

101-
private function processNestedArray(array $data): array
102-
{
103-
return $data;
104-
}
105-
10642
public function supports(string $resourceClass): bool
10743
{
10844
return true;

src/Denormalizer/DoctrineRelationResolver.php

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
namespace Sylius\ImportExport\Denormalizer;
1515

1616
use Doctrine\ORM\EntityManagerInterface;
17+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
1718
use Webmozart\Assert\Assert;
1819

1920
final readonly class DoctrineRelationResolver implements RelationResolverInterface
2021
{
2122
public function __construct(
2223
private EntityManagerInterface $entityManager,
24+
private DenormalizerInterface $denormalizer,
2325
) {
2426
}
2527

@@ -30,37 +32,15 @@ public function resolveEntity(string $entityClass, array $data): ?object
3032
}
3133

3234
Assert::classExists($entityClass);
33-
$repository = $this->entityManager->getRepository($entityClass);
34-
$identifierField = $this->getIdentifierField($entityClass, $data);
35-
36-
if (!isset($data[$identifierField])) {
37-
return null;
38-
}
3935

40-
$identifier = $data[$identifierField];
41-
42-
if ('id' === $identifierField) {
43-
$entity = $repository->find($identifier);
44-
} else {
45-
$entity = $repository->findOneBy([$identifierField => $identifier]);
46-
}
47-
48-
// Log if entity not found for debugging
49-
if (null === $entity) {
50-
error_log(sprintf('Import: Skipping relation - %s not found with %s = %s',
51-
substr($entityClass, strrpos($entityClass, '\\') + 1),
52-
$identifierField,
53-
$identifier
54-
));
55-
return null;
36+
// Try to find existing entity first
37+
$existingEntity = $this->findExistingEntity($entityClass, $data);
38+
if (null !== $existingEntity) {
39+
return $existingEntity;
5640
}
5741

58-
// Ensure entity is managed by EntityManager to prevent cascade persist errors
59-
if (!$this->entityManager->contains($entity)) {
60-
$entity = $this->entityManager->merge($entity);
61-
}
62-
63-
return $entity;
42+
// Create new entity if not found
43+
return $this->createNewEntity($entityClass, $data);
6444
}
6545

6646
public function resolveCollection(string $entityClass, array $dataCollection): array
@@ -80,16 +60,26 @@ public function resolveCollection(string $entityClass, array $dataCollection): a
8060
return $entities;
8161
}
8262

83-
private function getIdentifierField(string $entityClass, array $data): string
63+
/**
64+
* @param class-string $entityClass
65+
*/
66+
private function findExistingEntity(string $entityClass, array $data): ?object
8467
{
68+
$repository = $this->entityManager->getRepository($entityClass);
69+
8570
if (isset($data['code'])) {
86-
return 'code';
71+
return $repository->findOneBy(['code' => $data['code']]);
8772
}
8873

8974
if (isset($data['id'])) {
90-
return 'id';
75+
return $repository->find($data['id']);
9176
}
9277

93-
return 'id';
78+
return null;
79+
}
80+
81+
private function createNewEntity(string $entityClass, array $data): object
82+
{
83+
return $this->denormalizer->denormalize($data, $entityClass);
9484
}
9585
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\ImportExport\Denormalizer;
15+
16+
use Doctrine\ORM\EntityManagerInterface;
17+
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
18+
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
19+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
20+
21+
final class EntityRelationDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
22+
{
23+
use DenormalizerAwareTrait;
24+
25+
public function __construct(
26+
private EntityManagerInterface $entityManager,
27+
) {
28+
}
29+
30+
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
31+
{
32+
if (!is_array($data)) {
33+
return $data;
34+
}
35+
36+
// Try to find existing entity by id or code
37+
/** @var class-string $type */
38+
$existingEntity = $this->findExistingEntity($type, $data);
39+
if (null !== $existingEntity) {
40+
// Ensure entity is managed to prevent cascade persist errors
41+
if (!$this->entityManager->contains($existingEntity)) {
42+
// In Doctrine 3.x, merge() was removed. We refresh instead or persist if detached
43+
try {
44+
$this->entityManager->refresh($existingEntity);
45+
} catch (\Exception) {
46+
// If refresh fails, the entity might be detached, so we persist it
47+
$this->entityManager->persist($existingEntity);
48+
}
49+
}
50+
51+
return $existingEntity;
52+
}
53+
54+
// Add context to prevent infinite recursion
55+
$context['entity_relation_denormalizer_skip'] = true;
56+
57+
// Create new entity using the default denormalizer
58+
return $this->denormalizer->denormalize($data, $type, $format, $context);
59+
}
60+
61+
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
62+
{
63+
// Skip if we're already processing to avoid infinite recursion
64+
if (isset($context['entity_relation_denormalizer_skip'])) {
65+
return false;
66+
}
67+
68+
// Only handle entity classes that have Doctrine metadata
69+
if (!is_array($data) || !class_exists($type)) {
70+
return false;
71+
}
72+
73+
try {
74+
$this->entityManager->getClassMetadata($type);
75+
76+
// Only handle if data contains id or code (for existing entities)
77+
return isset($data['id']) || isset($data['code']);
78+
} catch (\Exception) {
79+
return false;
80+
}
81+
}
82+
83+
public function getSupportedTypes(?string $format): array
84+
{
85+
return ['*' => false];
86+
}
87+
88+
/**
89+
* @param class-string $entityClass
90+
*/
91+
private function findExistingEntity(string $entityClass, array $data): ?object
92+
{
93+
$repository = $this->entityManager->getRepository($entityClass);
94+
95+
// Try to find by code first, then by id
96+
if (isset($data['code'])) {
97+
return $repository->findOneBy(['code' => $data['code']]);
98+
}
99+
100+
if (isset($data['id'])) {
101+
return $repository->find($data['id']);
102+
}
103+
104+
return null;
105+
}
106+
}

src/Grid/Listener/ImportActionAdminGridListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ private function getRouteSection(): ?string
7878

7979
return $request->attributes->all()['_sylius']['section'] ?? null;
8080
}
81-
}
81+
}

src/Twig/Component/ImportResourceFormComponent.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ protected function instantiateForm(): FormInterface
3737
{
3838
return $this->formFactory->create($this->formClass, ['resourceClass' => $this->resourceClass]);
3939
}
40-
}
40+
}

0 commit comments

Comments
 (0)