Skip to content

Commit f81f6d0

Browse files
committed
AbstractLazyCollection
1 parent b72727a commit f81f6d0

File tree

3 files changed

+283
-10
lines changed

3 files changed

+283
-10
lines changed

src/PersistentCollection.php

Lines changed: 280 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44

55
namespace Doctrine\ODM\MongoDB;
66

7+
use Doctrine\Common\Collections\AbstractLazyCollection;
78
use Doctrine\Common\Collections\Collection as BaseCollection;
89
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
9-
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionTrait;
10+
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
11+
12+
use function array_combine;
13+
use function array_diff_key;
14+
use function array_map;
15+
use function array_udiff_assoc;
16+
use function array_values;
17+
use function count;
18+
use function is_object;
1019

1120
/**
1221
* A PersistentCollection represents a collection of elements that have persistent state.
@@ -15,16 +24,278 @@
1524
* @template T of object
1625
* @template-implements PersistentCollectionInterface<TKey,T>
1726
*/
18-
final class PersistentCollection implements PersistentCollectionInterface
27+
final class PersistentCollection extends AbstractLazyCollection implements PersistentCollectionInterface
1928
{
20-
/** @use PersistentCollectionTrait<TKey, T> */
21-
use PersistentCollectionTrait;
29+
/**
30+
* A snapshot of the collection at the moment it was fetched from the database.
31+
* This is used to create a diff of the collection at commit time.
32+
*
33+
* @var array<TKey, T>
34+
*/
35+
private array $snapshot = [];
36+
37+
/**
38+
* Collection's owning document
39+
*/
40+
private ?object $owner = null;
41+
42+
/**
43+
* @var array<string, mixed>|null
44+
* @phpstan-var FieldMapping|null
45+
*/
46+
private ?array $mapping = null;
47+
48+
/**
49+
* Whether the collection is dirty and needs to be synchronized with the database
50+
* when the UnitOfWork that manages its persistent state commits.
51+
*/
52+
private bool $isDirty = false;
53+
54+
/**
55+
* The DocumentManager that manages the persistence of the collection.
56+
*/
57+
private DocumentManager $dm;
58+
59+
/**
60+
* The UnitOfWork that manages the persistence of the collection.
61+
*/
62+
private UnitOfWork $uow;
63+
64+
/**
65+
* The raw mongo data that will be used to initialize this collection.
66+
*
67+
* @var mixed[]
68+
*/
69+
private array $mongoData = [];
70+
71+
/**
72+
* Any hints to account for during reconstitution/lookup of the documents.
73+
*
74+
* @var array<int, mixed>
75+
* @phpstan-var Hints
76+
*/
77+
private array $hints = [];
78+
79+
/** @param BaseCollection<TKey, T> $collection */
80+
public function __construct(BaseCollection $collection, DocumentManager $dm, UnitOfWork $uow)
81+
{
82+
$this->collection = $collection;
83+
$this->dm = $dm;
84+
$this->uow = $uow;
85+
}
86+
87+
protected function doInitialize()
88+
{
89+
if (! $this->mapping) {
90+
return;
91+
}
92+
93+
/** @var array<TKey, T> $newObjects */
94+
$newObjects = [];
95+
96+
if ($this->isDirty) {
97+
// Remember any NEW objects added through add()
98+
$newObjects = $this->coll->toArray();
99+
}
100+
101+
$this->collection->clear();
102+
$this->uow->loadCollection($this);
103+
$this->takeSnapshot();
104+
105+
$this->mongoData = [];
106+
107+
// Reattach any NEW objects added through add()
108+
if (! $newObjects) {
109+
return;
110+
}
111+
112+
foreach ($newObjects as $key => $obj) {
113+
if (CollectionHelper::isHash($this->mapping['strategy'])) {
114+
$this->collection->set($key, $obj);
115+
} else {
116+
$this->collection->add($obj);
117+
}
118+
}
119+
120+
$this->isDirty = true;
121+
}
122+
123+
public function setDocumentManager(DocumentManager $dm)
124+
{
125+
$this->dm = $dm;
126+
$this->uow = $dm->getUnitOfWork();
127+
}
22128

23-
/** @param BaseCollection<TKey, T> $coll */
24-
public function __construct(BaseCollection $coll, DocumentManager $dm, UnitOfWork $uow)
129+
public function setMongoData(array $mongoData)
25130
{
26-
$this->coll = $coll;
27-
$this->dm = $dm;
28-
$this->uow = $uow;
131+
$this->mongoData = $mongoData;
132+
}
133+
134+
public function getMongoData()
135+
{
136+
return $this->mongoData;
137+
}
138+
139+
public function setHints(array $hints)
140+
{
141+
$this->hints = $hints;
142+
}
143+
144+
public function getHints()
145+
{
146+
return $this->hints;
147+
}
148+
149+
public function isDirty()
150+
{
151+
if ($this->isDirty) {
152+
return true;
153+
}
154+
155+
if (! $this->initialized && count($this->collection)) {
156+
// not initialized collection with added elements
157+
return true;
158+
}
159+
160+
if ($this->initialized) {
161+
// if initialized let's check with last known snapshot
162+
return $this->collection->toArray() !== $this->snapshot;
163+
}
164+
165+
return false;
166+
}
167+
168+
public function setDirty(bool $dirty)
169+
{
170+
$this->isDirty = $dirty;
171+
}
172+
173+
public function setOwner(object $document, array $mapping)
174+
{
175+
$this->owner = $document;
176+
$this->mapping = $mapping;
177+
}
178+
179+
public function takeSnapshot()
180+
{
181+
if ($this->mapping !== null && CollectionHelper::isList($this->mapping['strategy'])) {
182+
$array = $this->collection->toArray();
183+
$this->collection->clear();
184+
foreach ($array as $document) {
185+
$this->collection->add($document);
186+
}
187+
}
188+
189+
$this->snapshot = $this->collection->toArray();
190+
$this->isDirty = false;
191+
}
192+
193+
public function clearSnapshot()
194+
{
195+
$this->snapshot = [];
196+
$this->isDirty = $this->collection->count() !== 0;
197+
}
198+
199+
public function getSnapshot()
200+
{
201+
return $this->snapshot;
202+
}
203+
204+
public function getDeleteDiff()
205+
{
206+
return array_udiff_assoc(
207+
$this->snapshot,
208+
$this->coll->toArray(),
209+
static fn ($a, $b) => $a === $b ? 0 : 1,
210+
);
211+
}
212+
213+
public function getDeletedDocuments()
214+
{
215+
$collection = $this->collection->toArray();
216+
$loadedObjectsByOid = array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot);
217+
$newObjectsByOid = array_combine(array_map('spl_object_id', $collection), $collection);
218+
219+
return array_values(array_diff_key($loadedObjectsByOid, $newObjectsByOid));
220+
}
221+
222+
public function getInsertDiff()
223+
{
224+
return array_udiff_assoc(
225+
$this->collection->toArray(),
226+
$this->snapshot,
227+
static fn ($a, $b) => $a === $b ? 0 : 1,
228+
);
229+
}
230+
231+
public function getInsertedDocuments()
232+
{
233+
$collection = $this->collection->toArray();
234+
$newObjectsByOid = array_combine(array_map('spl_object_id', $collection), $collection);
235+
$loadedObjectsByOid = array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot);
236+
237+
return array_values(array_diff_key($newObjectsByOid, $loadedObjectsByOid));
238+
}
239+
240+
public function getOwner(): ?object
241+
{
242+
return $this->owner;
243+
}
244+
245+
public function getMapping()
246+
{
247+
return $this->mapping;
248+
}
249+
250+
public function getTypeClass()
251+
{
252+
if (! isset($this->dm)) {
253+
throw new MongoDBException('No DocumentManager is associated with this PersistentCollection, please set one using setDocumentManager method.');
254+
}
255+
256+
if (empty($this->mapping)) {
257+
throw new MongoDBException('No mapping is associated with this PersistentCollection, please set one using setOwner method.');
258+
}
259+
260+
if (empty($this->mapping['targetDocument'])) {
261+
throw new MongoDBException('Specifying targetDocument is required for the ClassMetadata to be obtained.');
262+
}
263+
264+
return $this->dm->getClassMetadata($this->mapping['targetDocument']);
265+
}
266+
267+
public function setInitialized($bool)
268+
{
269+
$this->initialized = $bool;
270+
}
271+
272+
public function unwrap()
273+
{
274+
return $this->collection;
275+
}
276+
277+
/**
278+
* Cleanup internal state of cloned persistent collection.
279+
*
280+
* The following problems have to be prevented:
281+
* 1. Added documents are added to old PersistentCollection
282+
* 2. New collection is not dirty, if reused on other document nothing
283+
* changes.
284+
* 3. Snapshot leads to invalid diffs being generated.
285+
* 4. Lazy loading grabs documents from old owner object.
286+
* 5. New collection is connected to old owner and leads to duplicate keys.
287+
*/
288+
public function __clone()
289+
{
290+
if (is_object($this->collection)) {
291+
$this->collection = clone $this->collection;
292+
}
293+
294+
$this->initialize();
295+
296+
$this->owner = null;
297+
$this->snapshot = [];
298+
299+
$this->changed();
29300
}
30301
}

src/PersistentCollection/PersistentCollectionInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function isDirty();
8888
*
8989
* @return void
9090
*/
91-
public function setDirty($dirty);
91+
public function setDirty(bool $dirty);
9292

9393
/**
9494
* Sets the collection's owning document together with the AssociationMapping that

src/PersistentCollection/PersistentCollectionTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
* @phpstan-import-type FieldMapping from ClassMetadata
3535
* @template TKey of array-key
3636
* @template T of object
37+
*
38+
* @deprecated
3739
*/
3840
trait PersistentCollectionTrait
3941
{

0 commit comments

Comments
 (0)