Skip to content

Commit 12a760d

Browse files
committed
Correctly denormalize parent-child relationships in import, when only children not parent fields are given
This fixes issue #1272
1 parent b8d1414 commit 12a760d

3 files changed

Lines changed: 82 additions & 34 deletions

File tree

src/Serializer/StructuralElementDenormalizer.php

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
4242

4343
private const ALREADY_CALLED = 'STRUCTURAL_DENORMALIZER_ALREADY_CALLED';
4444

45+
private const PARENT_ELEMENT = 'STRUCTURAL_DENORMALIZER_PARENT_ELEMENT';
46+
4547
private array $object_cache = [];
4648

4749
public function __construct(
@@ -89,32 +91,55 @@ public function denormalize($data, string $type, ?string $format = null, array $
8991

9092
$context[self::ALREADY_CALLED][] = $data;
9193

92-
93-
/** @var AbstractStructuralDBElement $deserialized_entity */
94-
$deserialized_entity = $this->denormalizer->denormalize($data, $type, $format, $context);
94+
//In the first step, denormalize without children
95+
$context_without_children = $context;
96+
$context_without_children['groups'] = array_filter(
97+
$context_without_children['groups'] ?? [],
98+
static fn($group) => $group !== 'include_children',
99+
);
100+
//Also unset any parent element, to avoid infinite loops. We will set the parent element in the next step, when we denormalize the children
101+
unset($context_without_children[self::PARENT_ELEMENT]);
102+
/** @var AbstractStructuralDBElement $entity */
103+
$entity = $this->denormalizer->denormalize($data, $type, $format, $context_without_children);
104+
105+
//Assign the parent element to the denormalized entity, so it can be used in the denormalization of the children (e.g. for path generation)
106+
if (isset($context[self::PARENT_ELEMENT]) && $context[self::PARENT_ELEMENT] instanceof $entity && $entity->getID() === null) {
107+
$entity->setParent($context[self::PARENT_ELEMENT]);
108+
}
95109

96110
//Check if we already have the entity in the database (via path)
97111
/** @var StructuralDBElementRepository<T> $repo */
98112
$repo = $this->entityManager->getRepository($type);
113+
$deserialized_entity = $entity;
99114

100115
$path = $deserialized_entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
101116
$db_elements = $repo->getEntityByPath($path, AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
102117
if ($db_elements !== []) {
103118
//We already have the entity in the database, so we can return it
104-
return end($db_elements);
119+
$entity = end($db_elements);
105120
}
106121

107122

108123
//Check if we have created the entity in this request before (so we don't create multiple entities for the same path)
109124
//Entities get saved in the cache by type and path
110125
//We use a different cache for this then the objects created by a string value (saved in repo). However, that should not be a problem
111-
//unless the user data has mixed structure between json data and a string path
126+
//unless the user data has mixed structure between JSON data and a string path
112127
if (isset($this->object_cache[$type][$path])) {
113-
return $this->object_cache[$type][$path];
128+
$entity = $this->object_cache[$type][$path];
129+
} else {
130+
//Save the entity in the cache
131+
$this->object_cache[$type][$path] = $deserialized_entity;
114132
}
115133

116-
//Save the entity in the cache
117-
$this->object_cache[$type][$path] = $deserialized_entity;
134+
//In the next step we can denormalize the children, and add our children to the entity.
135+
if (in_array('include_children', $context['groups'], true) && isset($data['children']) && is_array($data['children'])) {
136+
foreach ($data['children'] as $child_data) {
137+
$child_entity = $this->denormalize($child_data, $type, $format, array_merge($context, [self::PARENT_ELEMENT => $entity]));
138+
if ($child_entity !== null && !$entity->getChildren()->contains($child_entity)) {
139+
$entity->addChild($child_entity);
140+
}
141+
}
142+
}
118143

119144
//We don't have the entity in the database, so we have to persist it
120145
$this->entityManager->persist($deserialized_entity);

src/Services/ImportExportSystem/EntityImporter.php

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,6 @@ public function importString(string $data, array $options = [], array &$errors =
219219
$entities = [$entities];
220220
}
221221

222-
//The serializer has only set the children attributes. We also have to change the parent value (the real value in DB)
223-
if ($entities[0] instanceof AbstractStructuralDBElement) {
224-
$this->correctParentEntites($entities, null);
225-
}
226-
227222
//Set the parent of the imported elements to the given options
228223
foreach ($entities as $entity) {
229224
if ($entity instanceof AbstractStructuralDBElement) {
@@ -297,6 +292,14 @@ protected function configureOptions(OptionsResolver $resolver): OptionsResolver
297292
return $resolver;
298293
}
299294

295+
private function persistRecursively(AbstractStructuralDBElement $entity): void
296+
{
297+
$this->em->persist($entity);
298+
foreach ($entity->getChildren() as $child) {
299+
$this->persistRecursively($child);
300+
}
301+
}
302+
300303
/**
301304
* This method deserializes the given file and writes the entities to the database (and flush the db).
302305
* The imported elements will be checked (validated) before written to database.
@@ -322,7 +325,7 @@ public function importFileAndPersistToDB(File $file, array $options = [], array
322325

323326
//Iterate over each $entity write it to DB (the invalid entities were already filtered out).
324327
foreach ($entities as $entity) {
325-
$this->em->persist($entity);
328+
$this->persistRecursively($entity);
326329
}
327330

328331
//Save changes to database, when no error happened, or we should continue on error.
@@ -400,7 +403,7 @@ public function determineFormat(string $extension): ?string
400403
*
401404
* @param File $file The Excel file to convert
402405
* @param string $delimiter The CSV delimiter to use
403-
*
406+
*
404407
* @return string The CSV data as string
405408
*/
406409
protected function convertExcelToCsv(File $file, string $delimiter = ';'): string
@@ -421,7 +424,7 @@ protected function convertExcelToCsv(File $file, string $delimiter = ';'): strin
421424
]);
422425

423426
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
424-
427+
425428
for ($row = 1; $row <= $highestRow; $row++) {
426429
$rowData = [];
427430

@@ -431,7 +434,7 @@ protected function convertExcelToCsv(File $file, string $delimiter = ';'): strin
431434
try {
432435
$cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue();
433436
$rowData[] = $cellValue ?? '';
434-
437+
435438
} catch (\Exception $e) {
436439
$this->logger->warning('Error reading cell value', [
437440
'cell' => "{$col}{$row}",
@@ -484,21 +487,4 @@ protected function convertExcelToCsv(File $file, string $delimiter = ';'): strin
484487
throw $e;
485488
}
486489
}
487-
488-
489-
/**
490-
* This functions corrects the parent setting based on the children value of the parent.
491-
*
492-
* @param iterable $entities the list of entities that should be fixed
493-
* @param AbstractStructuralDBElement|null $parent the parent, to which the entity should be set
494-
*/
495-
protected function correctParentEntites(iterable $entities, ?AbstractStructuralDBElement $parent = null): void
496-
{
497-
foreach ($entities as $entity) {
498-
/** @var AbstractStructuralDBElement $entity */
499-
$entity->setParent($parent);
500-
//Do the same for the children of entity
501-
$this->correctParentEntites($entity->getChildren(), $entity);
502-
}
503-
}
504490
}

tests/Serializer/StructuralElementDenormalizerTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,41 @@ public function testDenormalize(): void
8585
$result2 = $this->service->denormalize($data, Category::class, 'json', ['groups' => ['import']]);
8686
$this->assertSame($result, $result2);
8787
}
88+
89+
public function testDenormalizeViaChildren(): void
90+
{
91+
$data = ['name' => 'Node',
92+
'children' => [
93+
['name' => 'A', 'children' => [['name' => '1'], ['name' => '2']]],
94+
['name' => 'B', 'children' => [['name' => '1'], ['name' => '2']]],
95+
['name' => 'C', 'children' => [['name' => '1'], ['name' => '2'], ['name' => '3']]],
96+
]
97+
];
98+
99+
$result = $this->service->denormalize($data, Category::class, 'json', ['groups' => ['import', 'include_children']]);
100+
$this->assertInstanceOf(Category::class, $result);
101+
102+
$this->assertCount(3, $result->getChildren());
103+
$this->assertSame('A', $result->getChildren()[0]->getName());
104+
$this->assertSame('B', $result->getChildren()[1]->getName());
105+
$this->assertSame('C', $result->getChildren()[2]->getName());
106+
//Parents should be set correctly
107+
$this->assertSame($result, $result->getChildren()[0]->getParent());
108+
$this->assertSame($result, $result->getChildren()[1]->getParent());
109+
$this->assertSame($result, $result->getChildren()[2]->getParent());
110+
111+
$this->assertCount(2, $result->getChildren()[0]->getChildren());
112+
$this->assertSame('1', $result->getChildren()[0]->getChildren()[0]->getName());
113+
$this->assertSame('2', $result->getChildren()[0]->getChildren()[1]->getName());
114+
//Parents should be set correctly
115+
$this->assertSame($result->getChildren()[0], $result->getChildren()[0]->getChildren()[0]->getParent());
116+
$this->assertSame($result->getChildren()[0], $result->getChildren()[0]->getChildren()[1]->getParent());
117+
118+
$this->assertCount(2, $result->getChildren()[1]->getChildren());
119+
$this->assertSame('1', $result->getChildren()[1]->getChildren()[0]->getName());
120+
$this->assertSame('2', $result->getChildren()[1]->getChildren()[1]->getName());
121+
//Must be different instances than the children of A, because we create new elements for the same path, if we don't have them in the DB
122+
$this->assertNotSame($result->getChildren()[0]->getChildren()[0], $result->getChildren()[1]->getChildren()[0]);
123+
124+
}
88125
}

0 commit comments

Comments
 (0)