Skip to content

Commit 5ca00af

Browse files
authored
Fix errors with nullable typed associations (#2302)
* Fix initialising nullable associations * Fix error when merging documents with uninitialised typed properties
1 parent 18d9ac7 commit 5ca00af

File tree

4 files changed

+114
-75
lines changed

4 files changed

+114
-75
lines changed

lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -250,18 +250,20 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla
250250
<<<EOF
251251
252252
/** @ReferenceOne */
253-
if (isset(\$data['%1\$s'])) {
254-
\$reference = \$data['%1\$s'];
253+
if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
254+
\$return = \$data['%1\$s'];
255+
if (\$return !== null) {
256+
if (\$this->class->fieldMappings['%2\$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array(\$return)) {
257+
throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$return));
258+
}
255259
256-
if (\$this->class->fieldMappings['%2\$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array(\$reference)) {
257-
throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$reference));
260+
\$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$return);
261+
\$identifier = ClassMetadata::getReferenceId(\$return, \$this->class->fieldMappings['%2\$s']['storeAs']);
262+
\$targetMetadata = \$this->dm->getClassMetadata(\$className);
263+
\$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
264+
\$return = \$this->dm->getReference(\$className, \$id);
258265
}
259266
260-
\$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
261-
\$identifier = ClassMetadata::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']);
262-
\$targetMetadata = \$this->dm->getClassMetadata(\$className);
263-
\$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
264-
\$return = \$this->dm->getReference(\$className, \$id);
265267
\$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
266268
\$hydratedData['%2\$s'] = \$return;
267269
}
@@ -344,24 +346,27 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla
344346
<<<EOF
345347
346348
/** @EmbedOne */
347-
if (isset(\$data['%1\$s'])) {
348-
\$embeddedDocument = \$data['%1\$s'];
349+
if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
350+
\$return = \$data['%1\$s'];
351+
if (\$return !== null) {
352+
\$embeddedDocument = \$return;
349353
350-
if (! is_array(\$embeddedDocument)) {
351-
throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$embeddedDocument));
352-
}
354+
if (! is_array(\$embeddedDocument)) {
355+
throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$embeddedDocument));
356+
}
353357
354-
\$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
355-
\$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
356-
\$return = \$embeddedMetadata->newInstance();
358+
\$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
359+
\$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
360+
\$return = \$embeddedMetadata->newInstance();
357361
358-
\$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
362+
\$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
359363
360-
\$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
361-
\$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
364+
\$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
365+
\$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
362366
363-
if (empty(\$hints[Query::HINT_READ_ONLY])) {
364-
\$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
367+
if (empty(\$hints[Query::HINT_READ_ONLY])) {
368+
\$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
369+
}
365370
}
366371
367372
\$this->class->reflFields['%2\$s']->setValue(\$document, \$return);

lib/Doctrine/ODM/MongoDB/UnitOfWork.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,6 +1830,11 @@ private function doMerge(object $document, array &$visited, ?object $prevManaged
18301830
$name = $nativeReflection->name;
18311831
$prop = $this->reflectionService->getAccessibleProperty($class->name, $name);
18321832
assert($prop instanceof ReflectionProperty);
1833+
1834+
if (method_exists($prop, 'isInitialized') && ! $prop->isInitialized($document)) {
1835+
continue;
1836+
}
1837+
18331838
if (! isset($class->associationMappings[$name])) {
18341839
if (! $class->isIdentifier($name)) {
18351840
$prop->setValue($managedCopy, $prop->getValue($document));

tests/Doctrine/ODM/MongoDB/Tests/Functional/TypedPropertiesTest.php

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,52 +26,96 @@ public function setUp(): void
2626

2727
public function testPersistNew(): void
2828
{
29-
$doc = new TypedDocument();
30-
$doc->setName('Maciej');
31-
$doc->setEmbedOne(new TypedEmbeddedDocument('The answer', 42));
29+
$ref = new TypedDocument();
30+
$ref->name = 'alcaeus';
31+
$this->dm->persist($ref);
32+
33+
$doc = new TypedDocument();
34+
$doc->name = 'Maciej';
35+
$doc->embedOne = new TypedEmbeddedDocument('The answer', 42);
36+
$doc->referenceOne = $ref;
3237
$doc->getEmbedMany()->add(new TypedEmbeddedDocument('Lucky number', 7));
3338
$this->dm->persist($doc);
3439
$this->dm->flush();
3540
$this->dm->clear();
3641

37-
$saved = $this->dm->find(TypedDocument::class, $doc->getId());
42+
$ref = $this->dm->find(TypedDocument::class, $ref->id);
43+
$saved = $this->dm->find(TypedDocument::class, $doc->id);
3844
assert($saved instanceof TypedDocument);
39-
$this->assertEquals($doc->getId(), $saved->getId());
40-
$this->assertSame($doc->getName(), $saved->getName());
41-
$this->assertEquals($doc->getEmbedOne(), $saved->getEmbedOne());
45+
$this->assertEquals($doc->id, $saved->id);
46+
$this->assertSame($doc->name, $saved->name);
47+
$this->assertEquals($doc->embedOne, $saved->embedOne);
48+
$this->assertSame($ref, $saved->referenceOne);
4249
$this->assertEquals($doc->getEmbedMany()->getValues(), $saved->getEmbedMany()->getValues());
4350
}
4451

4552
public function testMerge(): void
4653
{
47-
$doc = new TypedDocument();
48-
$doc->setId((string) new ObjectId());
49-
$doc->setName('Maciej');
50-
$doc->setEmbedOne(new TypedEmbeddedDocument('The answer', 42));
54+
$ref = new TypedDocument();
55+
$ref->name = 'alcaeus';
56+
$this->dm->persist($ref);
57+
58+
$doc = new TypedDocument();
59+
$doc->id = (string) new ObjectId();
60+
$doc->name = 'Maciej';
61+
$doc->embedOne = new TypedEmbeddedDocument('The answer', 42);
62+
$doc->referenceOne = $ref;
63+
$doc->getEmbedMany()->add(new TypedEmbeddedDocument('Lucky number', 7));
64+
65+
$merged = $this->dm->merge($doc);
66+
assert($merged instanceof TypedDocument);
67+
$this->assertEquals($doc->id, $merged->id);
68+
$this->assertSame($doc->name, $merged->name);
69+
$this->assertEquals($doc->embedOne, $merged->embedOne);
70+
$this->assertEquals($doc->referenceOne, $merged->referenceOne);
71+
$this->assertEquals($doc->getEmbedMany()->getValues(), $merged->getEmbedMany()->getValues());
72+
}
73+
74+
public function testMergeWithUninitializedAssociations(): void
75+
{
76+
$doc = new TypedDocument();
77+
$doc->id = (string) new ObjectId();
78+
$doc->name = 'Maciej';
5179
$doc->getEmbedMany()->add(new TypedEmbeddedDocument('Lucky number', 7));
5280

5381
$merged = $this->dm->merge($doc);
54-
$this->assertEquals($doc->getId(), $merged->getId());
55-
$this->assertSame($doc->getName(), $merged->getName());
56-
$this->assertEquals($doc->getEmbedOne(), $merged->getEmbedOne());
82+
assert($merged instanceof TypedDocument);
83+
$this->assertEquals($doc->id, $merged->id);
84+
$this->assertSame($doc->name, $merged->name);
5785
$this->assertEquals($doc->getEmbedMany()->getValues(), $merged->getEmbedMany()->getValues());
5886
}
5987

6088
public function testProxying(): void
6189
{
62-
$doc = new TypedDocument();
63-
$doc->setName('Maciej');
64-
$doc->setEmbedOne(new TypedEmbeddedDocument('The answer', 42));
90+
$doc = new TypedDocument();
91+
$doc->name = 'Maciej';
92+
$doc->embedOne = new TypedEmbeddedDocument('The answer', 42);
6593
$doc->getEmbedMany()->add(new TypedEmbeddedDocument('Lucky number', 7));
6694
$this->dm->persist($doc);
6795
$this->dm->flush();
6896
$this->dm->clear();
6997

70-
$proxy = $this->dm->getReference(TypedDocument::class, $doc->getId());
98+
$proxy = $this->dm->getReference(TypedDocument::class, $doc->id);
7199
assert($proxy instanceof TypedDocument);
72-
$this->assertEquals($doc->getId(), $proxy->getId());
73-
$this->assertSame($doc->getName(), $proxy->getName());
74-
$this->assertEquals($doc->getEmbedOne(), $proxy->getEmbedOne());
100+
$this->assertEquals($doc->id, $proxy->id);
101+
$this->assertSame($doc->name, $proxy->name);
102+
$this->assertEquals($doc->embedOne, $proxy->embedOne);
75103
$this->assertEquals($doc->getEmbedMany()->getValues(), $proxy->getEmbedMany()->getValues());
76104
}
105+
106+
public function testNullableProperties(): void
107+
{
108+
$doc = new TypedDocument();
109+
$doc->name = 'webmozart';
110+
$this->dm->persist($doc);
111+
$this->dm->flush();
112+
$this->dm->clear();
113+
114+
$saved = $this->dm->find(TypedDocument::class, $doc->id);
115+
assert($saved instanceof TypedDocument);
116+
$this->assertNull($saved->nullableEmbedOne);
117+
$this->assertNull($saved->initializedNullableEmbedOne);
118+
$this->assertNull($saved->nullableReferenceOne);
119+
$this->assertNull($saved->initializedNullableReferenceOne);
120+
}
77121
}

tests/Documents74/TypedDocument.php

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,35 @@
1414
class TypedDocument
1515
{
1616
/** @ODM\Id() */
17-
private string $id;
17+
public string $id;
1818

1919
/** @ODM\Field(type="string") */
20-
private string $name;
20+
public string $name;
2121

2222
/** @ODM\EmbedOne(targetDocument=TypedEmbeddedDocument::class) */
23-
private TypedEmbeddedDocument $embedOne;
23+
public TypedEmbeddedDocument $embedOne;
2424

25-
/** @ODM\EmbedMany(targetDocument=TypedEmbeddedDocument::class) */
26-
private Collection $embedMany;
25+
/** @ODM\EmbedOne(targetDocument=TypedEmbeddedDocument::class, nullable=true) */
26+
public ?TypedEmbeddedDocument $nullableEmbedOne;
2727

28-
public function __construct()
29-
{
30-
$this->embedMany = new ArrayCollection();
31-
}
28+
/** @ODM\EmbedOne(targetDocument=TypedEmbeddedDocument::class, nullable=true) */
29+
public ?TypedEmbeddedDocument $initializedNullableEmbedOne = null;
3230

33-
public function getId(): string
34-
{
35-
return $this->id;
36-
}
31+
/** @ODM\ReferenceOne(targetDocument=TypedDocument::class) */
32+
public TypedDocument $referenceOne;
3733

38-
public function setId(string $id): void
39-
{
40-
$this->id = $id;
41-
}
34+
/** @ODM\ReferenceOne(targetDocument=TypedDocument::class, nullable=true) */
35+
public ?TypedDocument $nullableReferenceOne;
4236

43-
public function getName(): string
44-
{
45-
return $this->name;
46-
}
37+
/** @ODM\ReferenceOne(targetDocument=TypedDocument::class, nullable=true) */
38+
public ?TypedDocument $initializedNullableReferenceOne = null;
4739

48-
public function setName(string $name): void
49-
{
50-
$this->name = $name;
51-
}
52-
53-
public function getEmbedOne(): TypedEmbeddedDocument
54-
{
55-
return $this->embedOne;
56-
}
40+
/** @ODM\EmbedMany(targetDocument=TypedEmbeddedDocument::class) */
41+
private Collection $embedMany;
5742

58-
public function setEmbedOne(TypedEmbeddedDocument $embedOne): void
43+
public function __construct()
5944
{
60-
$this->embedOne = $embedOne;
45+
$this->embedMany = new ArrayCollection();
6146
}
6247

6348
public function getEmbedMany(): Collection

0 commit comments

Comments
 (0)