Skip to content

Commit b579078

Browse files
committed
Add tests and fix preparation of positional operators
1 parent f2c700d commit b579078

File tree

3 files changed

+213
-32
lines changed

3 files changed

+213
-32
lines changed

lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,30 +1258,6 @@ private function prepareQueryElement(string $originalFieldName, $value = null, ?
12581258
return [[$fieldName, $prepareValue ? $this->convertToDatabaseValue($fieldNameParts[0], $value) : $value]];
12591259
}
12601260

1261-
// Don't recurse for references. Instead, prepare the reference directly
1262-
if (! empty($mapping['reference'])) {
1263-
// First part is the name of the reference
1264-
// Second part is either a positional operator, index/key, or the name of a field
1265-
// Third part (if any) is the name of a field
1266-
// That means, we can implode all field parts except the first as the next field name
1267-
if ($fieldNameParts[1] === '$') {
1268-
assert($partCount >= 3);
1269-
$objectProperty = $fieldNameParts[2];
1270-
$referencePrefix = $fieldNamePrefix . $mapping['name'] . '.$';
1271-
} else {
1272-
$objectProperty = $fieldNameParts[1];
1273-
$referencePrefix = $fieldNamePrefix . $mapping['name'];
1274-
}
1275-
1276-
if ($targetClass->hasField($objectProperty) && $targetClass->isIdentifier($objectProperty)) {
1277-
$fieldName = ClassMetadata::getReferenceFieldName($mapping['storeAs'], $referencePrefix);
1278-
1279-
return [[$fieldName, $prepareValue ? $this->prepareQueryReference($value, $targetClass) : $value]];
1280-
}
1281-
1282-
return [[$fieldName, $prepareValue ? $this->convertToDatabaseValue($objectProperty, $value, $targetClass) : $value]];
1283-
}
1284-
12851261
/*
12861262
* 1 element: impossible (because of the dot)
12871263
* 2 elements: fieldName.objectProperty, fieldName.<index>, or fieldName.$. For EmbedMany and ReferenceMany, treat the second element as index if $inNewObj is true and convert the value. Otherwise, recurse.
@@ -1291,9 +1267,20 @@ private function prepareQueryElement(string $originalFieldName, $value = null, ?
12911267
if ($inNewObj || CollectionHelper::isHash($mapping['strategy'])) {
12921268
// When there are only two segments in a hash or when serialising a new object, we seem to be replacing an entire element. Don't recurse, just convert the value.
12931269
if ($partCount === 2) {
1294-
$fieldName = $fieldNamePrefix . $mapping['name'] . '.' . $fieldNameParts[1];
1295-
1296-
return [[$fieldName, $prepareValue ? $this->convertToDatabaseValue($fieldNameParts[0], $value, $targetClass) : $value]];
1270+
// In order to prepare the embedded document value, we need to recurse with the original field name, then append the second segment
1271+
$prepared = $this->prepareQueryElement(
1272+
$mapping['name'],
1273+
$value,
1274+
$targetClass,
1275+
$prepareValue,
1276+
$inNewObj,
1277+
$fieldNamePrefix,
1278+
);
1279+
1280+
$preparedFieldName = $prepared[0][0];
1281+
$preparedValue = $prepared[0][1];
1282+
1283+
return [[$preparedFieldName . '.' . $fieldNameParts[1], $preparedValue]];
12971284
}
12981285

12991286
// When there are more than two segments, treat the second segment (index/key/positional operator) as part of the field name and recurse into the rest

lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Doctrine\ODM\MongoDB\Persisters;
66

7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
79
use Doctrine\ODM\MongoDB\DocumentManager;
810
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
911
use Doctrine\ODM\MongoDB\Mapping\MappingException;
@@ -392,7 +394,12 @@ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDo
392394
break;
393395
}
394396

395-
$value = $this->prepareAssociatedCollectionValue($rawValue, $includeNestedCollections);
397+
// Prepare persistent collection if it's not already one
398+
$collection = $rawValue instanceof PersistentCollectionInterface
399+
? $rawValue
400+
: $this->preparePersistentCollection($mapping, $embeddedDocument, $rawValue);
401+
402+
$value = $this->prepareAssociatedCollectionValue($collection, $includeNestedCollections);
396403
break;
397404

398405
default:
@@ -507,4 +514,23 @@ public function prepareAssociatedCollectionValue(PersistentCollectionInterface $
507514

508515
return $setData;
509516
}
517+
518+
private function preparePersistentCollection(array $mapping, object $owner, mixed $rawValue): PersistentCollectionInterface
519+
{
520+
if ($rawValue instanceof PersistentCollectionInterface) {
521+
return $rawValue;
522+
}
523+
524+
// If $actualData[$name] is not a Collection then use an ArrayCollection.
525+
if (! $rawValue instanceof Collection) {
526+
$rawValue = new ArrayCollection($rawValue);
527+
}
528+
529+
// Inject PersistentCollection
530+
$coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $rawValue);
531+
$coll->setOwner($owner, $mapping);
532+
$coll->setDirty(! $rawValue->isEmpty());
533+
534+
return $coll;
535+
}
510536
}

tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2825Test.php

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44

55
namespace Doctrine\ODM\MongoDB\Tests\Functional\Ticket;
66

7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
79
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
810
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
911
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;
1012
use MongoDB\BSON\ObjectId;
1113

1214
class GH2825Test extends BaseTestCase
1315
{
14-
public function testQueryBuilderUpdatesEmbeddedDocumentCorrectly(): void
16+
public function testQueryBuilderUpdatesEmbedOneCorrectly(): void
1517
{
1618
$document = new GH2825Document('foo');
1719
$document->embedded = new GH2825Embedded('level 1');
@@ -37,7 +39,62 @@ public function testQueryBuilderUpdatesEmbeddedDocumentCorrectly(): void
3739
self::assertSame('level 2', $result['embedded']['embedded']['renamed']);
3840
}
3941

40-
public function testQueryBuilderUpdatesReferencesCorrectly(): void
42+
public function testQueryBuilderUpdatesEmbedManyCorrectly(): void
43+
{
44+
$document = new GH2825Document('foo');
45+
$document->embeddedDocuments[] = new GH2825Embedded('level 1');
46+
47+
$this->dm->persist($document);
48+
$this->dm->flush();
49+
50+
$embedded = new GH2825Embedded('level 2');
51+
52+
$this->dm->persist($embedded);
53+
54+
$this->dm->createQueryBuilder(GH2825Document::class)
55+
->updateOne()
56+
->field('id')->equals($document->id)
57+
->field('embeddedDocuments.property')->equals('level 1')
58+
->field('embeddedDocuments.$.embedded')->set($embedded)
59+
->getQuery()
60+
->execute();
61+
62+
$result = $this->dm->getDocumentCollection(GH2825Document::class)
63+
->findOne(['_id' => new ObjectId($document->id)]);
64+
65+
self::assertIsArray($result['embeddedDocuments']);
66+
self::assertSame('level 1', $result['embeddedDocuments'][0]['renamed']);
67+
self::assertSame('level 2', $result['embeddedDocuments'][0]['embedded']['renamed']);
68+
}
69+
70+
public function testQueryBuilderReplacesEmbedManyCorrectly(): void
71+
{
72+
$document = new GH2825Document('foo');
73+
$document->embeddedDocuments[] = new GH2825Embedded('original');
74+
75+
$this->dm->persist($document);
76+
$this->dm->flush();
77+
78+
$embedded = new GH2825Embedded('replaced');
79+
80+
$this->dm->persist($embedded);
81+
82+
$this->dm->createQueryBuilder(GH2825Document::class)
83+
->updateOne()
84+
->field('id')->equals($document->id)
85+
->field('embeddedDocuments.property')->equals('original')
86+
->field('embeddedDocuments.$')->set($embedded)
87+
->getQuery()
88+
->execute();
89+
90+
$result = $this->dm->getDocumentCollection(GH2825Document::class)
91+
->findOne(['_id' => new ObjectId($document->id)]);
92+
93+
self::assertIsArray($result['embeddedDocuments']);
94+
self::assertSame('replaced', $result['embeddedDocuments'][0]['renamed']);
95+
}
96+
97+
public function testQueryBuilderUpdatesReferenceOneCorrectly(): void
4198
{
4299
$document = new GH2825Document('document');
43100
$document->embedded = new GH2825Embedded('embedded');
@@ -73,6 +130,77 @@ public function testQueryBuilderUpdatesReferencesCorrectly(): void
73130
self::assertEquals(['id' => $referenceId], $result['embedded']['referenceStoreAsRef']);
74131
self::assertEquals(['$ref' => 'GH2825Document', '$id' => $referenceId], $result['embedded']['referenceStoreAsDbRef']);
75132
}
133+
134+
public function testQueryBuilderUpdatesReferenceManyCorrectly(): void
135+
{
136+
$document = new GH2825Document('document');
137+
$document->embeddedDocuments[] = new GH2825Embedded('embedded');
138+
$reference = new GH2825Document('referenced');
139+
140+
$this->dm->persist($document);
141+
$this->dm->persist($reference);
142+
143+
$this->dm->flush();
144+
145+
$this->dm->createQueryBuilder(GH2825Document::class)
146+
->updateOne()
147+
->field('id')->equals($document->id)
148+
->field('embeddedDocuments.property')->equals('embedded')
149+
->field('embeddedDocuments.$.referenceStoreAsId')->set($reference)
150+
->field('embeddedDocuments.$.referenceStoreAsRef')->set($reference)
151+
->field('embeddedDocuments.$.referenceStoreAsDbRef')->set($reference)
152+
->getQuery()
153+
->execute();
154+
155+
$result = $this->dm->getDocumentCollection(GH2825Document::class)
156+
->findOne(['_id' => new ObjectId($document->id)], ['typeMap' => ['root' => 'array', 'document' => 'array']]);
157+
158+
$referenceId = new ObjectId($reference->id);
159+
160+
self::assertIsArray($result['embeddedDocuments']);
161+
162+
self::assertEquals($referenceId, $result['embeddedDocuments'][0]['referenceStoreAsId']);
163+
self::assertEquals(['id' => $referenceId], $result['embeddedDocuments'][0]['referenceStoreAsRef']);
164+
self::assertEquals(['$ref' => 'GH2825Document', '$id' => $referenceId], $result['embeddedDocuments'][0]['referenceStoreAsDbRef']);
165+
}
166+
167+
public function testQueryBuilderReplacesReferenceManyCorrectly(): void
168+
{
169+
$document = new GH2825Document('document');
170+
$reference = new GH2825Document('original');
171+
$otherReference = new GH2825Document('original');
172+
$document->referencedDocumentsStoreAsId[] = $reference;
173+
$document->referencedDocumentsStoreAsRef[] = $reference;
174+
$document->referencedDocumentsStoreAsDbRef[] = $reference;
175+
176+
$this->dm->persist($document);
177+
$this->dm->persist($reference);
178+
$this->dm->persist($otherReference);
179+
180+
$this->dm->flush();
181+
182+
$this->dm->createQueryBuilder(GH2825Document::class)
183+
->updateOne()
184+
->field('id')->equals($document->id)
185+
->field('referencedDocumentsStoreAsId.0')->set($otherReference)
186+
->field('referencedDocumentsStoreAsRef.0')->set($otherReference)
187+
->field('referencedDocumentsStoreAsDbRef.0')->set($otherReference)
188+
->getQuery()
189+
->execute();
190+
191+
$result = $this->dm->getDocumentCollection(GH2825Document::class)
192+
->findOne(['_id' => new ObjectId($document->id)], ['typeMap' => ['root' => 'array', 'document' => 'array']]);
193+
194+
$referenceId = new ObjectId($otherReference->id);
195+
196+
self::assertIsArray($result['referencedDocumentsStoreAsId']);
197+
self::assertIsArray($result['referencedDocumentsStoreAsRef']);
198+
self::assertIsArray($result['referencedDocumentsStoreAsDbRef']);
199+
200+
self::assertEquals($referenceId, $result['referencedDocumentsStoreAsId'][0]);
201+
self::assertEquals(['id' => $referenceId], $result['referencedDocumentsStoreAsRef'][0]);
202+
self::assertEquals(['$ref' => 'GH2825Document', '$id' => $referenceId], $result['referencedDocumentsStoreAsDbRef'][0]);
203+
}
76204
}
77205

78206
#[ODM\Document]
@@ -96,9 +224,29 @@ class GH2825Document
96224
#[ODM\ReferenceOne(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)]
97225
public GH2825Document|null $referenceStoreAsDbRef = null;
98226

227+
/** @var Collection<int, GH2825Embedded> */
228+
#[ODM\EmbedMany(targetDocument: GH2825Embedded::class)]
229+
public Collection $embeddedDocuments;
230+
231+
/** @var Collection<int, GH2825Document> */
232+
#[ODM\ReferenceMany(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_ID)]
233+
public Collection $referencedDocumentsStoreAsId;
234+
235+
/** @var Collection<int, GH2825Document> */
236+
#[ODM\ReferenceMany(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_REF)]
237+
public Collection $referencedDocumentsStoreAsRef;
238+
239+
/** @var Collection<int, GH2825Document> */
240+
#[ODM\ReferenceMany(targetDocument: self::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)]
241+
public Collection $referencedDocumentsStoreAsDbRef;
242+
99243
public function __construct(string $name)
100244
{
101-
$this->name = $name;
245+
$this->name = $name;
246+
$this->embeddedDocuments = new ArrayCollection();
247+
$this->referencedDocumentsStoreAsId = new ArrayCollection();
248+
$this->referencedDocumentsStoreAsRef = new ArrayCollection();
249+
$this->referencedDocumentsStoreAsDbRef = new ArrayCollection();
102250
}
103251
}
104252

@@ -120,8 +268,28 @@ class GH2825Embedded
120268
#[ODM\ReferenceOne(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)]
121269
public GH2825Document|null $referenceStoreAsDbRef = null;
122270

271+
/** @var Collection<int, GH2825Embedded> */
272+
#[ODM\EmbedMany(targetDocument: self::class)]
273+
public Collection $embeddedDocuments;
274+
275+
/** @var Collection<int, GH2825Document> */
276+
#[ODM\ReferenceMany(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_ID)]
277+
public Collection $referencedDocumentsStoreAsId;
278+
279+
/** @var Collection<int, GH2825Document> */
280+
#[ODM\ReferenceMany(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_REF)]
281+
public Collection $referencedDocumentsStoreAsRef;
282+
283+
/** @var Collection<int, GH2825Document> */
284+
#[ODM\ReferenceMany(targetDocument: GH2825Document::class, storeAs: ClassMetadata::REFERENCE_STORE_AS_DB_REF)]
285+
public Collection $referencedDocumentsStoreAsDbRef;
286+
123287
public function __construct(string $property)
124288
{
125-
$this->property = $property;
289+
$this->property = $property;
290+
$this->embeddedDocuments = new ArrayCollection();
291+
$this->referencedDocumentsStoreAsId = new ArrayCollection();
292+
$this->referencedDocumentsStoreAsRef = new ArrayCollection();
293+
$this->referencedDocumentsStoreAsDbRef = new ArrayCollection();
126294
}
127295
}

0 commit comments

Comments
 (0)