These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Doctrine\ODM\MongoDB; |
||
6 | |||
7 | use Doctrine\Common\Collections\ArrayCollection; |
||
8 | use Doctrine\Common\Collections\Collection; |
||
9 | use Doctrine\Common\EventManager; |
||
10 | use Doctrine\Common\NotifyPropertyChanged; |
||
11 | use Doctrine\Common\PropertyChangedListener; |
||
12 | use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory; |
||
13 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
||
14 | use Doctrine\ODM\MongoDB\Mapping\MappingException; |
||
15 | use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; |
||
16 | use Doctrine\ODM\MongoDB\Persisters\CollectionPersister; |
||
17 | use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder; |
||
18 | use Doctrine\ODM\MongoDB\Proxy\Proxy; |
||
19 | use Doctrine\ODM\MongoDB\Query\Query; |
||
20 | use Doctrine\ODM\MongoDB\Types\DateType; |
||
21 | use Doctrine\ODM\MongoDB\Types\Type; |
||
22 | use Doctrine\ODM\MongoDB\Utility\CollectionHelper; |
||
23 | use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager; |
||
24 | use MongoDB\BSON\UTCDateTime; |
||
25 | use function array_filter; |
||
26 | use function count; |
||
27 | use function get_class; |
||
28 | use function in_array; |
||
29 | use function is_array; |
||
30 | use function is_object; |
||
31 | use function method_exists; |
||
32 | use function preg_match; |
||
33 | use function serialize; |
||
34 | use function spl_object_hash; |
||
35 | use function sprintf; |
||
36 | |||
37 | /** |
||
38 | * The UnitOfWork is responsible for tracking changes to objects during an |
||
39 | * "object-level" transaction and for writing out changes to the database |
||
40 | * in the correct order. |
||
41 | * |
||
42 | */ |
||
43 | class UnitOfWork implements PropertyChangedListener |
||
44 | { |
||
45 | /** |
||
46 | * A document is in MANAGED state when its persistence is managed by a DocumentManager. |
||
47 | */ |
||
48 | public const STATE_MANAGED = 1; |
||
49 | |||
50 | /** |
||
51 | * A document is new if it has just been instantiated (i.e. using the "new" operator) |
||
52 | * and is not (yet) managed by a DocumentManager. |
||
53 | */ |
||
54 | public const STATE_NEW = 2; |
||
55 | |||
56 | /** |
||
57 | * A detached document is an instance with a persistent identity that is not |
||
58 | * (or no longer) associated with a DocumentManager (and a UnitOfWork). |
||
59 | */ |
||
60 | public const STATE_DETACHED = 3; |
||
61 | |||
62 | /** |
||
63 | * A removed document instance is an instance with a persistent identity, |
||
64 | * associated with a DocumentManager, whose persistent state has been |
||
65 | * deleted (or is scheduled for deletion). |
||
66 | */ |
||
67 | public const STATE_REMOVED = 4; |
||
68 | |||
69 | /** |
||
70 | * The identity map holds references to all managed documents. |
||
71 | * |
||
72 | * Documents are grouped by their class name, and then indexed by the |
||
73 | * serialized string of their database identifier field or, if the class |
||
74 | * has no identifier, the SPL object hash. Serializing the identifier allows |
||
75 | * differentiation of values that may be equal (via type juggling) but not |
||
76 | * identical. |
||
77 | * |
||
78 | * Since all classes in a hierarchy must share the same identifier set, |
||
79 | * we always take the root class name of the hierarchy. |
||
80 | * |
||
81 | * @var array |
||
82 | */ |
||
83 | private $identityMap = []; |
||
84 | |||
85 | /** |
||
86 | * Map of all identifiers of managed documents. |
||
87 | * Keys are object ids (spl_object_hash). |
||
88 | * |
||
89 | * @var array |
||
90 | */ |
||
91 | private $documentIdentifiers = []; |
||
92 | |||
93 | /** |
||
94 | * Map of the original document data of managed documents. |
||
95 | * Keys are object ids (spl_object_hash). This is used for calculating changesets |
||
96 | * at commit time. |
||
97 | * |
||
98 | * @var array |
||
99 | * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage. |
||
100 | * A value will only really be copied if the value in the document is modified |
||
101 | * by the user. |
||
102 | */ |
||
103 | private $originalDocumentData = []; |
||
104 | |||
105 | /** |
||
106 | * Map of document changes. Keys are object ids (spl_object_hash). |
||
107 | * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end. |
||
108 | * |
||
109 | * @var array |
||
110 | */ |
||
111 | private $documentChangeSets = []; |
||
112 | |||
113 | /** |
||
114 | * The (cached) states of any known documents. |
||
115 | * Keys are object ids (spl_object_hash). |
||
116 | * |
||
117 | * @var array |
||
118 | */ |
||
119 | private $documentStates = []; |
||
120 | |||
121 | /** |
||
122 | * Map of documents that are scheduled for dirty checking at commit time. |
||
123 | * |
||
124 | * Documents are grouped by their class name, and then indexed by their SPL |
||
125 | * object hash. This is only used for documents with a change tracking |
||
126 | * policy of DEFERRED_EXPLICIT. |
||
127 | * |
||
128 | * @var array |
||
129 | * @todo rename: scheduledForSynchronization |
||
130 | */ |
||
131 | private $scheduledForDirtyCheck = []; |
||
132 | |||
133 | /** |
||
134 | * A list of all pending document insertions. |
||
135 | * |
||
136 | * @var array |
||
137 | */ |
||
138 | private $documentInsertions = []; |
||
139 | |||
140 | /** |
||
141 | * A list of all pending document updates. |
||
142 | * |
||
143 | * @var array |
||
144 | */ |
||
145 | private $documentUpdates = []; |
||
146 | |||
147 | /** |
||
148 | * A list of all pending document upserts. |
||
149 | * |
||
150 | * @var array |
||
151 | */ |
||
152 | private $documentUpserts = []; |
||
153 | |||
154 | /** |
||
155 | * A list of all pending document deletions. |
||
156 | * |
||
157 | * @var array |
||
158 | */ |
||
159 | private $documentDeletions = []; |
||
160 | |||
161 | /** |
||
162 | * All pending collection deletions. |
||
163 | * |
||
164 | * @var array |
||
165 | */ |
||
166 | private $collectionDeletions = []; |
||
167 | |||
168 | /** |
||
169 | * All pending collection updates. |
||
170 | * |
||
171 | * @var array |
||
172 | */ |
||
173 | private $collectionUpdates = []; |
||
174 | |||
175 | /** |
||
176 | * A list of documents related to collections scheduled for update or deletion |
||
177 | * |
||
178 | * @var array |
||
179 | */ |
||
180 | private $hasScheduledCollections = []; |
||
181 | |||
182 | /** |
||
183 | * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork. |
||
184 | * At the end of the UnitOfWork all these collections will make new snapshots |
||
185 | * of their data. |
||
186 | * |
||
187 | * @var array |
||
188 | */ |
||
189 | private $visitedCollections = []; |
||
190 | |||
191 | /** |
||
192 | * The DocumentManager that "owns" this UnitOfWork instance. |
||
193 | * |
||
194 | * @var DocumentManager |
||
195 | */ |
||
196 | private $dm; |
||
197 | |||
198 | /** |
||
199 | * The EventManager used for dispatching events. |
||
200 | * |
||
201 | * @var EventManager |
||
202 | */ |
||
203 | private $evm; |
||
204 | |||
205 | /** |
||
206 | * Additional documents that are scheduled for removal. |
||
207 | * |
||
208 | * @var array |
||
209 | */ |
||
210 | private $orphanRemovals = []; |
||
211 | |||
212 | /** |
||
213 | * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents. |
||
214 | * |
||
215 | * @var HydratorFactory |
||
216 | */ |
||
217 | private $hydratorFactory; |
||
218 | |||
219 | /** |
||
220 | * The document persister instances used to persist document instances. |
||
221 | * |
||
222 | * @var array |
||
223 | */ |
||
224 | private $persisters = []; |
||
225 | |||
226 | /** |
||
227 | * The collection persister instance used to persist changes to collections. |
||
228 | * |
||
229 | * @var Persisters\CollectionPersister |
||
230 | */ |
||
231 | private $collectionPersister; |
||
232 | |||
233 | /** |
||
234 | * The persistence builder instance used in DocumentPersisters. |
||
235 | * |
||
236 | * @var PersistenceBuilder |
||
237 | */ |
||
238 | private $persistenceBuilder; |
||
239 | |||
240 | /** |
||
241 | * Array of parent associations between embedded documents. |
||
242 | * |
||
243 | * @var array |
||
244 | */ |
||
245 | private $parentAssociations = []; |
||
246 | |||
247 | /** @var LifecycleEventManager */ |
||
248 | private $lifecycleEventManager; |
||
249 | |||
250 | /** |
||
251 | * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash |
||
252 | * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents |
||
253 | * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized. |
||
254 | * |
||
255 | * @var array |
||
256 | */ |
||
257 | private $embeddedDocumentsRegistry = []; |
||
258 | |||
259 | /** @var int */ |
||
260 | private $commitsInProgress = 0; |
||
261 | |||
262 | /** |
||
263 | * Initializes a new UnitOfWork instance, bound to the given DocumentManager. |
||
264 | * |
||
265 | */ |
||
266 | 1633 | public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory) |
|
267 | { |
||
268 | 1633 | $this->dm = $dm; |
|
269 | 1633 | $this->evm = $evm; |
|
270 | 1633 | $this->hydratorFactory = $hydratorFactory; |
|
271 | 1633 | $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm); |
|
272 | 1633 | } |
|
273 | |||
274 | /** |
||
275 | * Factory for returning new PersistenceBuilder instances used for preparing data into |
||
276 | * queries for insert persistence. |
||
277 | */ |
||
278 | 1123 | public function getPersistenceBuilder(): PersistenceBuilder |
|
279 | { |
||
280 | 1123 | if (! $this->persistenceBuilder) { |
|
281 | 1123 | $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this); |
|
282 | } |
||
283 | 1123 | return $this->persistenceBuilder; |
|
284 | } |
||
285 | |||
286 | /** |
||
287 | * Sets the parent association for a given embedded document. |
||
288 | */ |
||
289 | 202 | public function setParentAssociation(object $document, array $mapping, object $parent, string $propertyPath): void |
|
290 | { |
||
291 | 202 | $oid = spl_object_hash($document); |
|
292 | 202 | $this->embeddedDocumentsRegistry[$oid] = $document; |
|
293 | 202 | $this->parentAssociations[$oid] = [$mapping, $parent, $propertyPath]; |
|
294 | 202 | } |
|
295 | |||
296 | /** |
||
297 | * Gets the parent association for a given embedded document. |
||
298 | * |
||
299 | * <code> |
||
300 | * list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument); |
||
301 | * </code> |
||
302 | */ |
||
303 | 226 | public function getParentAssociation(object $document): ?array |
|
304 | { |
||
305 | 226 | $oid = spl_object_hash($document); |
|
306 | |||
307 | 226 | return $this->parentAssociations[$oid] ?? null; |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * Get the document persister instance for the given document name |
||
312 | */ |
||
313 | 1121 | public function getDocumentPersister(string $documentName): Persisters\DocumentPersister |
|
314 | { |
||
315 | 1121 | if (! isset($this->persisters[$documentName])) { |
|
316 | 1108 | $class = $this->dm->getClassMetadata($documentName); |
|
317 | 1108 | $pb = $this->getPersistenceBuilder(); |
|
318 | 1108 | $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class); |
|
319 | } |
||
320 | 1121 | return $this->persisters[$documentName]; |
|
321 | } |
||
322 | |||
323 | /** |
||
324 | * Get the collection persister instance. |
||
325 | */ |
||
326 | 1121 | public function getCollectionPersister(): CollectionPersister |
|
327 | { |
||
328 | 1121 | if (! isset($this->collectionPersister)) { |
|
329 | 1121 | $pb = $this->getPersistenceBuilder(); |
|
330 | 1121 | $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this); |
|
331 | } |
||
332 | 1121 | return $this->collectionPersister; |
|
333 | } |
||
334 | |||
335 | /** |
||
336 | * Set the document persister instance to use for the given document name |
||
337 | */ |
||
338 | 13 | public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister): void |
|
339 | { |
||
340 | 13 | $this->persisters[$documentName] = $persister; |
|
341 | 13 | } |
|
342 | |||
343 | /** |
||
344 | * Commits the UnitOfWork, executing all operations that have been postponed |
||
345 | * up to this point. The state of all managed documents will be synchronized with |
||
346 | * the database. |
||
347 | * |
||
348 | * The operations are executed in the following order: |
||
349 | * |
||
350 | * 1) All document insertions |
||
351 | * 2) All document updates |
||
352 | * 3) All document deletions |
||
353 | * |
||
354 | * @param array $options Array of options to be used with batchInsert(), update() and remove() |
||
355 | */ |
||
356 | 603 | public function commit(array $options = []): void |
|
357 | { |
||
358 | // Raise preFlush |
||
359 | 603 | if ($this->evm->hasListeners(Events::preFlush)) { |
|
360 | $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm)); |
||
361 | } |
||
362 | |||
363 | // Compute changes done since last commit. |
||
364 | 603 | $this->computeChangeSets(); |
|
365 | |||
366 | 602 | if (! ($this->documentInsertions || |
|
367 | 255 | $this->documentUpserts || |
|
0 ignored issues
–
show
|
|||
368 | 213 | $this->documentDeletions || |
|
0 ignored issues
–
show
The expression
$this->documentDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||
369 | 196 | $this->documentUpdates || |
|
0 ignored issues
–
show
The expression
$this->documentUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||
370 | 22 | $this->collectionUpdates || |
|
0 ignored issues
–
show
The expression
$this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||
371 | 22 | $this->collectionDeletions || |
|
0 ignored issues
–
show
The expression
$this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||
372 | 602 | $this->orphanRemovals) |
|
0 ignored issues
–
show
The expression
$this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||
373 | ) { |
||
374 | 22 | return; // Nothing to do. |
|
375 | } |
||
376 | |||
377 | 599 | $this->commitsInProgress++; |
|
378 | 599 | if ($this->commitsInProgress > 1) { |
|
379 | throw MongoDBException::commitInProgress(); |
||
380 | } |
||
381 | try { |
||
382 | 599 | if ($this->orphanRemovals) { |
|
383 | 50 | foreach ($this->orphanRemovals as $removal) { |
|
384 | 50 | $this->remove($removal); |
|
385 | } |
||
386 | } |
||
387 | |||
388 | // Raise onFlush |
||
389 | 599 | if ($this->evm->hasListeners(Events::onFlush)) { |
|
390 | 5 | $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm)); |
|
391 | } |
||
392 | |||
393 | 598 | foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) { |
|
394 | 86 | list($class, $documents) = $classAndDocuments; |
|
395 | 86 | $this->executeUpserts($class, $documents, $options); |
|
396 | } |
||
397 | |||
398 | 598 | foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) { |
|
399 | 522 | list($class, $documents) = $classAndDocuments; |
|
400 | 522 | $this->executeInserts($class, $documents, $options); |
|
401 | } |
||
402 | |||
403 | 597 | foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) { |
|
404 | 228 | list($class, $documents) = $classAndDocuments; |
|
405 | 228 | $this->executeUpdates($class, $documents, $options); |
|
406 | } |
||
407 | |||
408 | 597 | foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) { |
|
409 | 74 | list($class, $documents) = $classAndDocuments; |
|
410 | 74 | $this->executeDeletions($class, $documents, $options); |
|
411 | } |
||
412 | |||
413 | // Raise postFlush |
||
414 | 597 | if ($this->evm->hasListeners(Events::postFlush)) { |
|
415 | $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm)); |
||
416 | } |
||
417 | |||
418 | // Clear up |
||
419 | 597 | $this->documentInsertions = |
|
420 | 597 | $this->documentUpserts = |
|
421 | 597 | $this->documentUpdates = |
|
422 | 597 | $this->documentDeletions = |
|
423 | 597 | $this->documentChangeSets = |
|
424 | 597 | $this->collectionUpdates = |
|
425 | 597 | $this->collectionDeletions = |
|
426 | 597 | $this->visitedCollections = |
|
427 | 597 | $this->scheduledForDirtyCheck = |
|
428 | 597 | $this->orphanRemovals = |
|
429 | 597 | $this->hasScheduledCollections = []; |
|
430 | 597 | } finally { |
|
431 | 599 | $this->commitsInProgress--; |
|
432 | } |
||
433 | 597 | } |
|
434 | |||
435 | /** |
||
436 | * Groups a list of scheduled documents by their class. |
||
437 | */ |
||
438 | 598 | private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false): array |
|
439 | { |
||
440 | 598 | if (empty($documents)) { |
|
441 | 598 | return []; |
|
442 | } |
||
443 | 597 | $divided = []; |
|
444 | 597 | $embeds = []; |
|
445 | 597 | foreach ($documents as $oid => $d) { |
|
446 | 597 | $className = get_class($d); |
|
447 | 597 | if (isset($embeds[$className])) { |
|
448 | 74 | continue; |
|
449 | } |
||
450 | 597 | if (isset($divided[$className])) { |
|
451 | 159 | $divided[$className][1][$oid] = $d; |
|
452 | 159 | continue; |
|
453 | } |
||
454 | 597 | $class = $this->dm->getClassMetadata($className); |
|
455 | 597 | if ($class->isEmbeddedDocument && ! $includeEmbedded) { |
|
456 | 176 | $embeds[$className] = true; |
|
457 | 176 | continue; |
|
458 | } |
||
459 | 597 | if (empty($divided[$class->name])) { |
|
460 | 597 | $divided[$class->name] = [$class, [$oid => $d]]; |
|
461 | } else { |
||
462 | 597 | $divided[$class->name][1][$oid] = $d; |
|
463 | } |
||
464 | } |
||
465 | 597 | return $divided; |
|
466 | } |
||
467 | |||
468 | /** |
||
469 | * Compute changesets of all documents scheduled for insertion. |
||
470 | * |
||
471 | * Embedded documents will not be processed. |
||
472 | */ |
||
473 | 608 | private function computeScheduleInsertsChangeSets(): void |
|
474 | { |
||
475 | 608 | foreach ($this->documentInsertions as $document) { |
|
476 | 535 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
477 | 535 | if ($class->isEmbeddedDocument) { |
|
478 | 158 | continue; |
|
479 | } |
||
480 | |||
481 | 529 | $this->computeChangeSet($class, $document); |
|
482 | } |
||
483 | 607 | } |
|
484 | |||
485 | /** |
||
486 | * Compute changesets of all documents scheduled for upsert. |
||
487 | * |
||
488 | * Embedded documents will not be processed. |
||
489 | */ |
||
490 | 607 | private function computeScheduleUpsertsChangeSets(): void |
|
491 | { |
||
492 | 607 | foreach ($this->documentUpserts as $document) { |
|
493 | 85 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
494 | 85 | if ($class->isEmbeddedDocument) { |
|
495 | continue; |
||
496 | } |
||
497 | |||
498 | 85 | $this->computeChangeSet($class, $document); |
|
499 | } |
||
500 | 607 | } |
|
501 | |||
502 | /** |
||
503 | * Gets the changeset for a document. |
||
504 | * |
||
505 | * @return array array('property' => array(0 => mixed|null, 1 => mixed|null)) |
||
506 | */ |
||
507 | 603 | public function getDocumentChangeSet(object $document): array |
|
508 | { |
||
509 | 603 | $oid = spl_object_hash($document); |
|
510 | |||
511 | 603 | return $this->documentChangeSets[$oid] ?? []; |
|
512 | } |
||
513 | |||
514 | /** |
||
515 | * INTERNAL: |
||
516 | * Sets the changeset for a document. |
||
517 | */ |
||
518 | 1 | public function setDocumentChangeSet(object $document, array $changeset): void |
|
519 | { |
||
520 | 1 | $this->documentChangeSets[spl_object_hash($document)] = $changeset; |
|
521 | 1 | } |
|
522 | |||
523 | /** |
||
524 | * Get a documents actual data, flattening all the objects to arrays. |
||
525 | * |
||
526 | * @return array |
||
527 | */ |
||
528 | 608 | public function getDocumentActualData(object $document): array |
|
529 | { |
||
530 | 608 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
531 | 608 | $actualData = []; |
|
532 | 608 | foreach ($class->reflFields as $name => $refProp) { |
|
533 | 608 | $mapping = $class->fieldMappings[$name]; |
|
534 | // skip not saved fields |
||
535 | 608 | if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) { |
|
536 | 48 | continue; |
|
537 | } |
||
538 | 608 | $value = $refProp->getValue($document); |
|
539 | 608 | if ((isset($mapping['association']) && $mapping['type'] === 'many') |
|
540 | 608 | && $value !== null && ! ($value instanceof PersistentCollectionInterface)) { |
|
541 | // If $actualData[$name] is not a Collection then use an ArrayCollection. |
||
542 | 394 | if (! $value instanceof Collection) { |
|
543 | 146 | $value = new ArrayCollection($value); |
|
544 | } |
||
545 | |||
546 | // Inject PersistentCollection |
||
547 | 394 | $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value); |
|
548 | 394 | $coll->setOwner($document, $mapping); |
|
549 | 394 | $coll->setDirty(! $value->isEmpty()); |
|
550 | 394 | $class->reflFields[$name]->setValue($document, $coll); |
|
551 | 394 | $actualData[$name] = $coll; |
|
552 | } else { |
||
553 | 608 | $actualData[$name] = $value; |
|
554 | } |
||
555 | } |
||
556 | 608 | return $actualData; |
|
557 | } |
||
558 | |||
559 | /** |
||
560 | * Computes the changes that happened to a single document. |
||
561 | * |
||
562 | * Modifies/populates the following properties: |
||
563 | * |
||
564 | * {@link originalDocumentData} |
||
565 | * If the document is NEW or MANAGED but not yet fully persisted (only has an id) |
||
566 | * then it was not fetched from the database and therefore we have no original |
||
567 | * document data yet. All of the current document data is stored as the original document data. |
||
568 | * |
||
569 | * {@link documentChangeSets} |
||
570 | * The changes detected on all properties of the document are stored there. |
||
571 | * A change is a tuple array where the first entry is the old value and the second |
||
572 | * entry is the new value of the property. Changesets are used by persisters |
||
573 | * to INSERT/UPDATE the persistent document state. |
||
574 | * |
||
575 | * {@link documentUpdates} |
||
576 | * If the document is already fully MANAGED (has been fetched from the database before) |
||
577 | * and any changes to its properties are detected, then a reference to the document is stored |
||
578 | * there to mark it for an update. |
||
579 | */ |
||
580 | 604 | public function computeChangeSet(ClassMetadata $class, object $document): void |
|
581 | { |
||
582 | 604 | if (! $class->isInheritanceTypeNone()) { |
|
583 | 183 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
584 | } |
||
585 | |||
586 | // Fire PreFlush lifecycle callbacks |
||
587 | 604 | if (! empty($class->lifecycleCallbacks[Events::preFlush])) { |
|
588 | 11 | $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]); |
|
589 | } |
||
590 | |||
591 | 604 | $this->computeOrRecomputeChangeSet($class, $document); |
|
592 | 603 | } |
|
593 | |||
594 | /** |
||
595 | * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet |
||
596 | */ |
||
597 | 604 | private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false): void |
|
598 | { |
||
599 | 604 | $oid = spl_object_hash($document); |
|
600 | 604 | $actualData = $this->getDocumentActualData($document); |
|
601 | 604 | $isNewDocument = ! isset($this->originalDocumentData[$oid]); |
|
602 | 604 | if ($isNewDocument) { |
|
603 | // Document is either NEW or MANAGED but not yet fully persisted (only has an id). |
||
604 | // These result in an INSERT. |
||
605 | 603 | $this->originalDocumentData[$oid] = $actualData; |
|
606 | 603 | $changeSet = []; |
|
607 | 603 | foreach ($actualData as $propName => $actualValue) { |
|
608 | /* At this PersistentCollection shouldn't be here, probably it |
||
609 | * was cloned and its ownership must be fixed |
||
610 | */ |
||
611 | 603 | if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) { |
|
612 | $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName); |
||
613 | $actualValue = $actualData[$propName]; |
||
614 | } |
||
615 | // ignore inverse side of reference relationship |
||
616 | 603 | if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) { |
|
617 | 188 | continue; |
|
618 | } |
||
619 | 603 | $changeSet[$propName] = [null, $actualValue]; |
|
620 | } |
||
621 | 603 | $this->documentChangeSets[$oid] = $changeSet; |
|
622 | } else { |
||
623 | 288 | if ($class->isReadOnly) { |
|
624 | 2 | return; |
|
625 | } |
||
626 | // Document is "fully" MANAGED: it was already fully persisted before |
||
627 | // and we have a copy of the original data |
||
628 | 286 | $originalData = $this->originalDocumentData[$oid]; |
|
629 | 286 | $isChangeTrackingNotify = $class->isChangeTrackingNotify(); |
|
630 | 286 | if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) { |
|
631 | 2 | $changeSet = $this->documentChangeSets[$oid]; |
|
632 | } else { |
||
633 | 286 | $changeSet = []; |
|
634 | } |
||
635 | |||
636 | 286 | $gridFSMetadataProperty = null; |
|
637 | |||
638 | 286 | if ($class->isFile) { |
|
639 | try { |
||
640 | 3 | $gridFSMetadata = $class->getFieldMappingByDbFieldName('metadata'); |
|
641 | 2 | $gridFSMetadataProperty = $gridFSMetadata['fieldName']; |
|
642 | 1 | } catch (MappingException $e) { |
|
643 | } |
||
644 | } |
||
645 | |||
646 | 286 | foreach ($actualData as $propName => $actualValue) { |
|
647 | // skip not saved fields |
||
648 | 286 | if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) || |
|
649 | 286 | ($class->isFile && $propName !== $gridFSMetadataProperty)) { |
|
650 | 3 | continue; |
|
651 | } |
||
652 | |||
653 | 285 | $orgValue = $originalData[$propName] ?? null; |
|
654 | |||
655 | // skip if value has not changed |
||
656 | 285 | if ($orgValue === $actualValue) { |
|
657 | 283 | if (! $actualValue instanceof PersistentCollectionInterface) { |
|
658 | 283 | continue; |
|
659 | } |
||
660 | |||
661 | 199 | if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) { |
|
662 | // consider dirty collections as changed as well |
||
663 | 175 | continue; |
|
664 | } |
||
665 | } |
||
666 | |||
667 | // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations |
||
668 | 246 | if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') { |
|
669 | 14 | if ($orgValue !== null) { |
|
670 | 8 | $this->scheduleOrphanRemoval($orgValue); |
|
671 | } |
||
672 | 14 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
673 | 14 | continue; |
|
674 | } |
||
675 | |||
676 | // if owning side of reference-one relationship |
||
677 | 239 | if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) { |
|
678 | 13 | if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) { |
|
679 | 1 | $this->scheduleOrphanRemoval($orgValue); |
|
680 | } |
||
681 | |||
682 | 13 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
683 | 13 | continue; |
|
684 | } |
||
685 | |||
686 | 232 | if ($isChangeTrackingNotify) { |
|
687 | 3 | continue; |
|
688 | } |
||
689 | |||
690 | // ignore inverse side of reference relationship |
||
691 | 230 | if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) { |
|
692 | 6 | continue; |
|
693 | } |
||
694 | |||
695 | // Persistent collection was exchanged with the "originally" |
||
696 | // created one. This can only mean it was cloned and replaced |
||
697 | // on another document. |
||
698 | 228 | if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) { |
|
699 | 6 | $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName); |
|
700 | } |
||
701 | |||
702 | // if embed-many or reference-many relationship |
||
703 | 228 | if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') { |
|
704 | 119 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
705 | /* If original collection was exchanged with a non-empty value |
||
706 | * and $set will be issued, there is no need to $unset it first |
||
707 | */ |
||
708 | 119 | if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) { |
|
709 | 27 | continue; |
|
710 | } |
||
711 | 100 | if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) { |
|
712 | 18 | $this->scheduleCollectionDeletion($orgValue); |
|
713 | } |
||
714 | 100 | continue; |
|
715 | } |
||
716 | |||
717 | // skip equivalent date values |
||
718 | 146 | if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') { |
|
719 | /** @var DateType $dateType */ |
||
720 | 37 | $dateType = Type::getType('date'); |
|
721 | 37 | $dbOrgValue = $dateType->convertToDatabaseValue($orgValue); |
|
722 | 37 | $dbActualValue = $dateType->convertToDatabaseValue($actualValue); |
|
723 | |||
724 | 37 | $orgTimestamp = $dbOrgValue instanceof UTCDateTime ? $dbOrgValue->toDateTime()->getTimestamp() : null; |
|
725 | 37 | $actualTimestamp = $dbActualValue instanceof UTCDateTime ? $dbActualValue->toDateTime()->getTimestamp() : null; |
|
726 | |||
727 | 37 | if ($orgTimestamp === $actualTimestamp) { |
|
728 | 30 | continue; |
|
729 | } |
||
730 | } |
||
731 | |||
732 | // regular field |
||
733 | 129 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
734 | } |
||
735 | 286 | if ($changeSet) { |
|
736 | 235 | $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid]) |
|
737 | 19 | ? $changeSet + $this->documentChangeSets[$oid] |
|
738 | 232 | : $changeSet; |
|
739 | |||
740 | 235 | $this->originalDocumentData[$oid] = $actualData; |
|
741 | 235 | $this->scheduleForUpdate($document); |
|
742 | } |
||
743 | } |
||
744 | |||
745 | // Look for changes in associations of the document |
||
746 | 604 | $associationMappings = array_filter( |
|
747 | 604 | $class->associationMappings, |
|
748 | function ($assoc) { |
||
749 | 465 | return empty($assoc['notSaved']); |
|
750 | 604 | } |
|
751 | ); |
||
752 | |||
753 | 604 | foreach ($associationMappings as $mapping) { |
|
754 | 465 | $value = $class->reflFields[$mapping['fieldName']]->getValue($document); |
|
755 | |||
756 | 465 | if ($value === null) { |
|
757 | 323 | continue; |
|
758 | } |
||
759 | |||
760 | 446 | $this->computeAssociationChanges($document, $mapping, $value); |
|
761 | |||
762 | 445 | if (isset($mapping['reference'])) { |
|
763 | 337 | continue; |
|
764 | } |
||
765 | |||
766 | 347 | $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap(); |
|
767 | |||
768 | 347 | foreach ($values as $obj) { |
|
769 | 181 | $oid2 = spl_object_hash($obj); |
|
770 | |||
771 | 181 | if (isset($this->documentChangeSets[$oid2])) { |
|
772 | 179 | if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) { |
|
773 | // instance of $value is the same as it was previously otherwise there would be |
||
774 | // change set already in place |
||
775 | 41 | $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value]; |
|
776 | } |
||
777 | |||
778 | 179 | if (! $isNewDocument) { |
|
779 | 81 | $this->scheduleForUpdate($document); |
|
780 | } |
||
781 | |||
782 | 347 | break; |
|
783 | } |
||
784 | } |
||
785 | } |
||
786 | 603 | } |
|
787 | |||
788 | /** |
||
789 | * Computes all the changes that have been done to documents and collections |
||
790 | * since the last commit and stores these changes in the _documentChangeSet map |
||
791 | * temporarily for access by the persisters, until the UoW commit is finished. |
||
792 | */ |
||
793 | 608 | public function computeChangeSets(): void |
|
794 | { |
||
795 | 608 | $this->computeScheduleInsertsChangeSets(); |
|
796 | 607 | $this->computeScheduleUpsertsChangeSets(); |
|
797 | |||
798 | // Compute changes for other MANAGED documents. Change tracking policies take effect here. |
||
799 | 607 | foreach ($this->identityMap as $className => $documents) { |
|
800 | 607 | $class = $this->dm->getClassMetadata($className); |
|
801 | 607 | if ($class->isEmbeddedDocument) { |
|
802 | /* we do not want to compute changes to embedded documents up front |
||
803 | * in case embedded document was replaced and its changeset |
||
804 | * would corrupt data. Embedded documents' change set will |
||
805 | * be calculated by reachability from owning document. |
||
806 | */ |
||
807 | 171 | continue; |
|
808 | } |
||
809 | |||
810 | // If change tracking is explicit or happens through notification, then only compute |
||
811 | // changes on document of that type that are explicitly marked for synchronization. |
||
812 | switch (true) { |
||
813 | 607 | case ($class->isChangeTrackingDeferredImplicit()): |
|
814 | 606 | $documentsToProcess = $documents; |
|
815 | 606 | break; |
|
816 | |||
817 | 4 | case (isset($this->scheduledForDirtyCheck[$className])): |
|
818 | 3 | $documentsToProcess = $this->scheduledForDirtyCheck[$className]; |
|
819 | 3 | break; |
|
820 | |||
821 | default: |
||
822 | 4 | $documentsToProcess = []; |
|
823 | } |
||
824 | |||
825 | 607 | foreach ($documentsToProcess as $document) { |
|
826 | // Ignore uninitialized proxy objects |
||
827 | 602 | if ($document instanceof Proxy && ! $document->__isInitialized__) { |
|
828 | 10 | continue; |
|
829 | } |
||
830 | // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here. |
||
831 | 602 | $oid = spl_object_hash($document); |
|
832 | 602 | if (isset($this->documentInsertions[$oid]) |
|
833 | 329 | || isset($this->documentUpserts[$oid]) |
|
834 | 283 | || isset($this->documentDeletions[$oid]) |
|
835 | 602 | || ! isset($this->documentStates[$oid]) |
|
836 | ) { |
||
837 | 600 | continue; |
|
838 | } |
||
839 | |||
840 | 607 | $this->computeChangeSet($class, $document); |
|
841 | } |
||
842 | } |
||
843 | 607 | } |
|
844 | |||
845 | /** |
||
846 | * Computes the changes of an association. |
||
847 | * |
||
848 | * @param mixed $value The value of the association. |
||
849 | * @throws \InvalidArgumentException |
||
850 | */ |
||
851 | 446 | private function computeAssociationChanges(object $parentDocument, array $assoc, $value): void |
|
852 | { |
||
853 | 446 | $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]); |
|
854 | 446 | $class = $this->dm->getClassMetadata(get_class($parentDocument)); |
|
855 | 446 | $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument); |
|
856 | |||
857 | 446 | if ($value instanceof Proxy && ! $value->__isInitialized__) { |
|
858 | 7 | return; |
|
859 | } |
||
860 | |||
861 | 445 | if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) { |
|
862 | 251 | if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) { |
|
863 | 247 | $this->scheduleCollectionUpdate($value); |
|
864 | } |
||
865 | 251 | $topmostOwner = $this->getOwningDocument($value->getOwner()); |
|
866 | 251 | $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value; |
|
867 | 251 | if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) { |
|
868 | 144 | $value->initialize(); |
|
869 | 144 | foreach ($value->getDeletedDocuments() as $orphan) { |
|
870 | 23 | $this->scheduleOrphanRemoval($orphan); |
|
871 | } |
||
872 | } |
||
873 | } |
||
874 | |||
875 | // Look through the documents, and in any of their associations, |
||
876 | // for transient (new) documents, recursively. ("Persistence by reachability") |
||
877 | // Unwrap. Uninitialized collections will simply be empty. |
||
878 | 445 | $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? [$value] : $value->unwrap(); |
|
879 | |||
880 | 445 | $count = 0; |
|
881 | 445 | foreach ($unwrappedValue as $key => $entry) { |
|
882 | 361 | if (! is_object($entry)) { |
|
883 | 1 | throw new \InvalidArgumentException( |
|
884 | 1 | sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name']) |
|
885 | ); |
||
886 | } |
||
887 | |||
888 | 360 | $targetClass = $this->dm->getClassMetadata(get_class($entry)); |
|
889 | |||
890 | 360 | $state = $this->getDocumentState($entry, self::STATE_NEW); |
|
891 | |||
892 | // Handle "set" strategy for multi-level hierarchy |
||
893 | 360 | $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key; |
|
894 | 360 | $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name']; |
|
895 | |||
896 | 360 | $count++; |
|
897 | |||
898 | switch ($state) { |
||
899 | 360 | case self::STATE_NEW: |
|
900 | 67 | if (! $assoc['isCascadePersist']) { |
|
901 | throw new \InvalidArgumentException('A new document was found through a relationship that was not' |
||
902 | . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.' |
||
903 | . ' Explicitly persist the new document or configure cascading persist operations' |
||
904 | . ' on the relationship.'); |
||
905 | } |
||
906 | |||
907 | 67 | $this->persistNew($targetClass, $entry); |
|
908 | 67 | $this->setParentAssociation($entry, $assoc, $parentDocument, $path); |
|
909 | 67 | $this->computeChangeSet($targetClass, $entry); |
|
910 | 67 | break; |
|
911 | |||
912 | 356 | case self::STATE_MANAGED: |
|
913 | 356 | if ($targetClass->isEmbeddedDocument) { |
|
914 | 172 | list(, $knownParent, ) = $this->getParentAssociation($entry); |
|
915 | 172 | if ($knownParent && $knownParent !== $parentDocument) { |
|
916 | 6 | $entry = clone $entry; |
|
917 | 6 | if ($assoc['type'] === ClassMetadata::ONE) { |
|
918 | 3 | $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry); |
|
919 | 3 | $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry); |
|
920 | 3 | $poid = spl_object_hash($parentDocument); |
|
921 | 3 | if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) { |
|
922 | 3 | $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry; |
|
923 | } |
||
924 | } else { |
||
925 | // must use unwrapped value to not trigger orphan removal |
||
926 | 4 | $unwrappedValue[$key] = $entry; |
|
927 | } |
||
928 | 6 | $this->persistNew($targetClass, $entry); |
|
929 | } |
||
930 | 172 | $this->setParentAssociation($entry, $assoc, $parentDocument, $path); |
|
931 | 172 | $this->computeChangeSet($targetClass, $entry); |
|
932 | } |
||
933 | 356 | break; |
|
934 | |||
935 | 1 | case self::STATE_REMOVED: |
|
936 | // Consume the $value as array (it's either an array or an ArrayAccess) |
||
937 | // and remove the element from Collection. |
||
938 | 1 | if ($assoc['type'] === ClassMetadata::MANY) { |
|
939 | unset($value[$key]); |
||
940 | } |
||
941 | 1 | break; |
|
942 | |||
943 | case self::STATE_DETACHED: |
||
944 | // Can actually not happen right now as we assume STATE_NEW, |
||
945 | // so the exception will be raised from the DBAL layer (constraint violation). |
||
946 | throw new \InvalidArgumentException('A detached document was found through a ' |
||
947 | . 'relationship during cascading a persist operation.'); |
||
948 | |||
949 | 360 | default: |
|
950 | // MANAGED associated documents are already taken into account |
||
951 | // during changeset calculation anyway, since they are in the identity map. |
||
952 | } |
||
953 | } |
||
954 | 444 | } |
|
955 | |||
956 | /** |
||
957 | * INTERNAL: |
||
958 | * Computes the changeset of an individual document, independently of the |
||
959 | * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit(). |
||
960 | * |
||
961 | * The passed document must be a managed document. If the document already has a change set |
||
962 | * because this method is invoked during a commit cycle then the change sets are added. |
||
963 | * whereby changes detected in this method prevail. |
||
964 | * |
||
965 | * @ignore |
||
966 | * @throws \InvalidArgumentException If the passed document is not MANAGED. |
||
967 | */ |
||
968 | 19 | public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document): void |
|
969 | { |
||
970 | // Ignore uninitialized proxy objects |
||
971 | 19 | if ($document instanceof Proxy && ! $document->__isInitialized__) { |
|
972 | 1 | return; |
|
973 | } |
||
974 | |||
975 | 18 | $oid = spl_object_hash($document); |
|
976 | |||
977 | 18 | if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) { |
|
978 | throw new \InvalidArgumentException('Document must be managed.'); |
||
979 | } |
||
980 | |||
981 | 18 | if (! $class->isInheritanceTypeNone()) { |
|
982 | 2 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
983 | } |
||
984 | |||
985 | 18 | $this->computeOrRecomputeChangeSet($class, $document, true); |
|
986 | 18 | } |
|
987 | |||
988 | /** |
||
989 | * @throws \InvalidArgumentException If there is something wrong with document's identifier. |
||
990 | */ |
||
991 | 633 | private function persistNew(ClassMetadata $class, object $document): void |
|
992 | { |
||
993 | 633 | $this->lifecycleEventManager->prePersist($class, $document); |
|
994 | 633 | $oid = spl_object_hash($document); |
|
995 | 633 | $upsert = false; |
|
996 | 633 | if ($class->identifier) { |
|
997 | 633 | $idValue = $class->getIdentifierValue($document); |
|
998 | 633 | $upsert = ! $class->isEmbeddedDocument && $idValue !== null; |
|
999 | |||
1000 | 633 | if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) { |
|
1001 | 3 | throw new \InvalidArgumentException(sprintf( |
|
1002 | 3 | '%s uses NONE identifier generation strategy but no identifier was provided when persisting.', |
|
1003 | 3 | get_class($document) |
|
1004 | )); |
||
1005 | } |
||
1006 | |||
1007 | 632 | if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) { |
|
1008 | 1 | throw new \InvalidArgumentException(sprintf( |
|
1009 | 1 | '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.', |
|
1010 | 1 | get_class($document) |
|
1011 | )); |
||
1012 | } |
||
1013 | |||
1014 | 631 | if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) { |
|
1015 | 552 | $idValue = $class->idGenerator->generate($this->dm, $document); |
|
1016 | 552 | $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue)); |
|
1017 | 552 | $class->setIdentifierValue($document, $idValue); |
|
1018 | } |
||
1019 | |||
1020 | 631 | $this->documentIdentifiers[$oid] = $idValue; |
|
1021 | } else { |
||
1022 | // this is for embedded documents without identifiers |
||
1023 | 151 | $this->documentIdentifiers[$oid] = $oid; |
|
1024 | } |
||
1025 | |||
1026 | 631 | $this->documentStates[$oid] = self::STATE_MANAGED; |
|
1027 | |||
1028 | 631 | if ($upsert) { |
|
1029 | 89 | $this->scheduleForUpsert($class, $document); |
|
1030 | } else { |
||
1031 | 561 | $this->scheduleForInsert($class, $document); |
|
1032 | } |
||
1033 | 631 | } |
|
1034 | |||
1035 | /** |
||
1036 | * Executes all document insertions for documents of the specified type. |
||
1037 | */ |
||
1038 | 522 | private function executeInserts(ClassMetadata $class, array $documents, array $options = []): void |
|
1039 | { |
||
1040 | 522 | $persister = $this->getDocumentPersister($class->name); |
|
1041 | |||
1042 | 522 | foreach ($documents as $oid => $document) { |
|
1043 | 522 | $persister->addInsert($document); |
|
1044 | 522 | unset($this->documentInsertions[$oid]); |
|
1045 | } |
||
1046 | |||
1047 | 522 | $persister->executeInserts($options); |
|
1048 | |||
1049 | 521 | foreach ($documents as $document) { |
|
1050 | 521 | $this->lifecycleEventManager->postPersist($class, $document); |
|
1051 | } |
||
1052 | 521 | } |
|
1053 | |||
1054 | /** |
||
1055 | * Executes all document upserts for documents of the specified type. |
||
1056 | */ |
||
1057 | 86 | private function executeUpserts(ClassMetadata $class, array $documents, array $options = []): void |
|
1058 | { |
||
1059 | 86 | $persister = $this->getDocumentPersister($class->name); |
|
1060 | |||
1061 | 86 | foreach ($documents as $oid => $document) { |
|
1062 | 86 | $persister->addUpsert($document); |
|
1063 | 86 | unset($this->documentUpserts[$oid]); |
|
1064 | } |
||
1065 | |||
1066 | 86 | $persister->executeUpserts($options); |
|
1067 | |||
1068 | 86 | foreach ($documents as $document) { |
|
1069 | 86 | $this->lifecycleEventManager->postPersist($class, $document); |
|
1070 | } |
||
1071 | 86 | } |
|
1072 | |||
1073 | /** |
||
1074 | * Executes all document updates for documents of the specified type. |
||
1075 | */ |
||
1076 | 228 | private function executeUpdates(ClassMetadata $class, array $documents, array $options = []): void |
|
1077 | { |
||
1078 | 228 | if ($class->isReadOnly) { |
|
1079 | return; |
||
1080 | } |
||
1081 | |||
1082 | 228 | $className = $class->name; |
|
1083 | 228 | $persister = $this->getDocumentPersister($className); |
|
1084 | |||
1085 | 228 | foreach ($documents as $oid => $document) { |
|
1086 | 228 | $this->lifecycleEventManager->preUpdate($class, $document); |
|
1087 | |||
1088 | 228 | if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) { |
|
1089 | 227 | $persister->update($document, $options); |
|
1090 | } |
||
1091 | |||
1092 | 222 | unset($this->documentUpdates[$oid]); |
|
1093 | |||
1094 | 222 | $this->lifecycleEventManager->postUpdate($class, $document); |
|
1095 | } |
||
1096 | 221 | } |
|
1097 | |||
1098 | /** |
||
1099 | * Executes all document deletions for documents of the specified type. |
||
1100 | */ |
||
1101 | 74 | private function executeDeletions(ClassMetadata $class, array $documents, array $options = []): void |
|
1102 | { |
||
1103 | 74 | $persister = $this->getDocumentPersister($class->name); |
|
1104 | |||
1105 | 74 | foreach ($documents as $oid => $document) { |
|
1106 | 74 | if (! $class->isEmbeddedDocument) { |
|
1107 | 36 | $persister->delete($document, $options); |
|
1108 | } |
||
1109 | unset( |
||
1110 | 72 | $this->documentDeletions[$oid], |
|
1111 | 72 | $this->documentIdentifiers[$oid], |
|
1112 | 72 | $this->originalDocumentData[$oid] |
|
1113 | ); |
||
1114 | |||
1115 | // Clear snapshot information for any referenced PersistentCollection |
||
1116 | // http://www.doctrine-project.org/jira/browse/MODM-95 |
||
1117 | 72 | foreach ($class->associationMappings as $fieldMapping) { |
|
1118 | 48 | if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) { |
|
1119 | 38 | continue; |
|
1120 | } |
||
1121 | |||
1122 | 28 | $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document); |
|
1123 | 28 | if (! ($value instanceof PersistentCollectionInterface)) { |
|
1124 | 7 | continue; |
|
1125 | } |
||
1126 | |||
1127 | 24 | $value->clearSnapshot(); |
|
1128 | } |
||
1129 | |||
1130 | // Document with this $oid after deletion treated as NEW, even if the $oid |
||
1131 | // is obtained by a new document because the old one went out of scope. |
||
1132 | 72 | $this->documentStates[$oid] = self::STATE_NEW; |
|
1133 | |||
1134 | 72 | $this->lifecycleEventManager->postRemove($class, $document); |
|
1135 | } |
||
1136 | 72 | } |
|
1137 | |||
1138 | /** |
||
1139 | * Schedules a document for insertion into the database. |
||
1140 | * If the document already has an identifier, it will be added to the |
||
1141 | * identity map. |
||
1142 | * |
||
1143 | * @throws \InvalidArgumentException |
||
1144 | */ |
||
1145 | 564 | public function scheduleForInsert(ClassMetadata $class, object $document): void |
|
1146 | { |
||
1147 | 564 | $oid = spl_object_hash($document); |
|
1148 | |||
1149 | 564 | if (isset($this->documentUpdates[$oid])) { |
|
1150 | throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.'); |
||
1151 | } |
||
1152 | 564 | if (isset($this->documentDeletions[$oid])) { |
|
1153 | throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.'); |
||
1154 | } |
||
1155 | 564 | if (isset($this->documentInsertions[$oid])) { |
|
1156 | throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.'); |
||
1157 | } |
||
1158 | |||
1159 | 564 | $this->documentInsertions[$oid] = $document; |
|
1160 | |||
1161 | 564 | if (! isset($this->documentIdentifiers[$oid])) { |
|
1162 | 3 | return; |
|
1163 | } |
||
1164 | |||
1165 | 561 | $this->addToIdentityMap($document); |
|
1166 | 561 | } |
|
1167 | |||
1168 | /** |
||
1169 | * Schedules a document for upsert into the database and adds it to the |
||
1170 | * identity map |
||
1171 | * |
||
1172 | * @throws \InvalidArgumentException |
||
1173 | */ |
||
1174 | 92 | public function scheduleForUpsert(ClassMetadata $class, object $document): void |
|
1175 | { |
||
1176 | 92 | $oid = spl_object_hash($document); |
|
1177 | |||
1178 | 92 | if ($class->isEmbeddedDocument) { |
|
1179 | throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.'); |
||
1180 | } |
||
1181 | 92 | if (isset($this->documentUpdates[$oid])) { |
|
1182 | throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.'); |
||
1183 | } |
||
1184 | 92 | if (isset($this->documentDeletions[$oid])) { |
|
1185 | throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.'); |
||
1186 | } |
||
1187 | 92 | if (isset($this->documentUpserts[$oid])) { |
|
1188 | throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.'); |
||
1189 | } |
||
1190 | |||
1191 | 92 | $this->documentUpserts[$oid] = $document; |
|
1192 | 92 | $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document); |
|
1193 | 92 | $this->addToIdentityMap($document); |
|
1194 | 92 | } |
|
1195 | |||
1196 | /** |
||
1197 | * Checks whether a document is scheduled for insertion. |
||
1198 | */ |
||
1199 | 104 | public function isScheduledForInsert(object $document): bool |
|
1200 | { |
||
1201 | 104 | return isset($this->documentInsertions[spl_object_hash($document)]); |
|
1202 | } |
||
1203 | |||
1204 | /** |
||
1205 | * Checks whether a document is scheduled for upsert. |
||
1206 | */ |
||
1207 | 5 | public function isScheduledForUpsert(object $document): bool |
|
1208 | { |
||
1209 | 5 | return isset($this->documentUpserts[spl_object_hash($document)]); |
|
1210 | } |
||
1211 | |||
1212 | /** |
||
1213 | * Schedules a document for being updated. |
||
1214 | * |
||
1215 | * @throws \InvalidArgumentException |
||
1216 | */ |
||
1217 | 236 | public function scheduleForUpdate(object $document): void |
|
1218 | { |
||
1219 | 236 | $oid = spl_object_hash($document); |
|
1220 | 236 | if (! isset($this->documentIdentifiers[$oid])) { |
|
1221 | throw new \InvalidArgumentException('Document has no identity.'); |
||
1222 | } |
||
1223 | |||
1224 | 236 | if (isset($this->documentDeletions[$oid])) { |
|
1225 | throw new \InvalidArgumentException('Document is removed.'); |
||
1226 | } |
||
1227 | |||
1228 | 236 | if (isset($this->documentUpdates[$oid]) |
|
1229 | 236 | || isset($this->documentInsertions[$oid]) |
|
1230 | 236 | || isset($this->documentUpserts[$oid])) { |
|
1231 | 98 | return; |
|
1232 | } |
||
1233 | |||
1234 | 234 | $this->documentUpdates[$oid] = $document; |
|
1235 | 234 | } |
|
1236 | |||
1237 | /** |
||
1238 | * Checks whether a document is registered as dirty in the unit of work. |
||
1239 | * Note: Is not very useful currently as dirty documents are only registered |
||
1240 | * at commit time. |
||
1241 | */ |
||
1242 | 21 | public function isScheduledForUpdate(object $document): bool |
|
1243 | { |
||
1244 | 21 | return isset($this->documentUpdates[spl_object_hash($document)]); |
|
1245 | } |
||
1246 | |||
1247 | 1 | public function isScheduledForDirtyCheck(object $document): bool |
|
1248 | { |
||
1249 | 1 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1250 | 1 | return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]); |
|
1251 | } |
||
1252 | |||
1253 | /** |
||
1254 | * INTERNAL: |
||
1255 | * Schedules a document for deletion. |
||
1256 | */ |
||
1257 | 79 | public function scheduleForDelete(object $document): void |
|
1258 | { |
||
1259 | 79 | $oid = spl_object_hash($document); |
|
1260 | |||
1261 | 79 | if (isset($this->documentInsertions[$oid])) { |
|
1262 | 2 | if ($this->isInIdentityMap($document)) { |
|
1263 | 2 | $this->removeFromIdentityMap($document); |
|
1264 | } |
||
1265 | 2 | unset($this->documentInsertions[$oid]); |
|
1266 | 2 | return; // document has not been persisted yet, so nothing more to do. |
|
1267 | } |
||
1268 | |||
1269 | 78 | if (! $this->isInIdentityMap($document)) { |
|
1270 | 2 | return; // ignore |
|
1271 | } |
||
1272 | |||
1273 | 77 | $this->removeFromIdentityMap($document); |
|
1274 | 77 | $this->documentStates[$oid] = self::STATE_REMOVED; |
|
1275 | |||
1276 | 77 | if (isset($this->documentUpdates[$oid])) { |
|
1277 | unset($this->documentUpdates[$oid]); |
||
1278 | } |
||
1279 | 77 | if (isset($this->documentDeletions[$oid])) { |
|
1280 | return; |
||
1281 | } |
||
1282 | |||
1283 | 77 | $this->documentDeletions[$oid] = $document; |
|
1284 | 77 | } |
|
1285 | |||
1286 | /** |
||
1287 | * Checks whether a document is registered as removed/deleted with the unit |
||
1288 | * of work. |
||
1289 | */ |
||
1290 | 5 | public function isScheduledForDelete(object $document): bool |
|
1291 | { |
||
1292 | 5 | return isset($this->documentDeletions[spl_object_hash($document)]); |
|
1293 | } |
||
1294 | |||
1295 | /** |
||
1296 | * Checks whether a document is scheduled for insertion, update or deletion. |
||
1297 | */ |
||
1298 | 250 | public function isDocumentScheduled(object $document): bool |
|
1299 | { |
||
1300 | 250 | $oid = spl_object_hash($document); |
|
1301 | 250 | return isset($this->documentInsertions[$oid]) || |
|
1302 | 132 | isset($this->documentUpserts[$oid]) || |
|
1303 | 122 | isset($this->documentUpdates[$oid]) || |
|
1304 | 250 | isset($this->documentDeletions[$oid]); |
|
1305 | } |
||
1306 | |||
1307 | /** |
||
1308 | * INTERNAL: |
||
1309 | * Registers a document in the identity map. |
||
1310 | * |
||
1311 | * Note that documents in a hierarchy are registered with the class name of |
||
1312 | * the root document. Identifiers are serialized before being used as array |
||
1313 | * keys to allow differentiation of equal, but not identical, values. |
||
1314 | * |
||
1315 | * @ignore |
||
1316 | */ |
||
1317 | 672 | public function addToIdentityMap(object $document): bool |
|
1318 | { |
||
1319 | 672 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1320 | 672 | $id = $this->getIdForIdentityMap($document); |
|
1321 | |||
1322 | 672 | if (isset($this->identityMap[$class->name][$id])) { |
|
1323 | 46 | return false; |
|
1324 | } |
||
1325 | |||
1326 | 672 | $this->identityMap[$class->name][$id] = $document; |
|
1327 | |||
1328 | 672 | if ($document instanceof NotifyPropertyChanged && |
|
1329 | 672 | ( ! $document instanceof Proxy || $document->__isInitialized())) { |
|
1330 | 3 | $document->addPropertyChangedListener($this); |
|
1331 | } |
||
1332 | |||
1333 | 672 | return true; |
|
1334 | } |
||
1335 | |||
1336 | /** |
||
1337 | * Gets the state of a document with regard to the current unit of work. |
||
1338 | * |
||
1339 | * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). |
||
1340 | * This parameter can be set to improve performance of document state detection |
||
1341 | * by potentially avoiding a database lookup if the distinction between NEW and DETACHED |
||
1342 | * is either known or does not matter for the caller of the method. |
||
1343 | */ |
||
1344 | 637 | public function getDocumentState(object $document, ?int $assume = null): int |
|
1345 | { |
||
1346 | 637 | $oid = spl_object_hash($document); |
|
1347 | |||
1348 | 637 | if (isset($this->documentStates[$oid])) { |
|
1349 | 398 | return $this->documentStates[$oid]; |
|
1350 | } |
||
1351 | |||
1352 | 636 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1353 | |||
1354 | 636 | if ($class->isEmbeddedDocument) { |
|
1355 | 185 | return self::STATE_NEW; |
|
1356 | } |
||
1357 | |||
1358 | 633 | if ($assume !== null) { |
|
1359 | 631 | return $assume; |
|
1360 | } |
||
1361 | |||
1362 | /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are |
||
1363 | * known. Note that you cannot remember the NEW or DETACHED state in |
||
1364 | * _documentStates since the UoW does not hold references to such |
||
1365 | * objects and the object hash can be reused. More generally, because |
||
1366 | * the state may "change" between NEW/DETACHED without the UoW being |
||
1367 | * aware of it. |
||
1368 | */ |
||
1369 | 3 | $id = $class->getIdentifierObject($document); |
|
1370 | |||
1371 | 3 | if ($id === null) { |
|
1372 | 2 | return self::STATE_NEW; |
|
1373 | } |
||
1374 | |||
1375 | // Check for a version field, if available, to avoid a DB lookup. |
||
1376 | 2 | if ($class->isVersioned) { |
|
1377 | return $class->getFieldValue($document, $class->versionField) |
||
1378 | ? self::STATE_DETACHED |
||
1379 | : self::STATE_NEW; |
||
1380 | } |
||
1381 | |||
1382 | // Last try before DB lookup: check the identity map. |
||
1383 | 2 | if ($this->tryGetById($id, $class)) { |
|
1384 | 1 | return self::STATE_DETACHED; |
|
1385 | } |
||
1386 | |||
1387 | // DB lookup |
||
1388 | 2 | if ($this->getDocumentPersister($class->name)->exists($document)) { |
|
1389 | 1 | return self::STATE_DETACHED; |
|
1390 | } |
||
1391 | |||
1392 | 1 | return self::STATE_NEW; |
|
1393 | } |
||
1394 | |||
1395 | /** |
||
1396 | * INTERNAL: |
||
1397 | * Removes a document from the identity map. This effectively detaches the |
||
1398 | * document from the persistence management of Doctrine. |
||
1399 | * |
||
1400 | * @ignore |
||
1401 | * @throws \InvalidArgumentException |
||
1402 | */ |
||
1403 | 91 | public function removeFromIdentityMap(object $document): bool |
|
1404 | { |
||
1405 | 91 | $oid = spl_object_hash($document); |
|
1406 | |||
1407 | // Check if id is registered first |
||
1408 | 91 | if (! isset($this->documentIdentifiers[$oid])) { |
|
1409 | return false; |
||
1410 | } |
||
1411 | |||
1412 | 91 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1413 | 91 | $id = $this->getIdForIdentityMap($document); |
|
1414 | |||
1415 | 91 | if (isset($this->identityMap[$class->name][$id])) { |
|
1416 | 91 | unset($this->identityMap[$class->name][$id]); |
|
1417 | 91 | $this->documentStates[$oid] = self::STATE_DETACHED; |
|
1418 | 91 | return true; |
|
1419 | } |
||
1420 | |||
1421 | return false; |
||
1422 | } |
||
1423 | |||
1424 | /** |
||
1425 | * INTERNAL: |
||
1426 | * Gets a document in the identity map by its identifier hash. |
||
1427 | * |
||
1428 | * @ignore |
||
1429 | * @param mixed $id Document identifier |
||
1430 | * @throws InvalidArgumentException If the class does not have an identifier. |
||
1431 | */ |
||
1432 | 40 | public function getById($id, ClassMetadata $class): object |
|
1433 | { |
||
1434 | 40 | if (! $class->identifier) { |
|
1435 | throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name)); |
||
1436 | } |
||
1437 | |||
1438 | 40 | $serializedId = serialize($class->getDatabaseIdentifierValue($id)); |
|
1439 | |||
1440 | 40 | return $this->identityMap[$class->name][$serializedId]; |
|
1441 | } |
||
1442 | |||
1443 | /** |
||
1444 | * INTERNAL: |
||
1445 | * Tries to get a document by its identifier hash. If no document is found |
||
1446 | * for the given hash, FALSE is returned. |
||
1447 | * |
||
1448 | * @ignore |
||
1449 | * @param mixed $id Document identifier |
||
1450 | * @return mixed The found document or FALSE. |
||
1451 | * @throws InvalidArgumentException If the class does not have an identifier. |
||
1452 | */ |
||
1453 | 319 | public function tryGetById($id, ClassMetadata $class) |
|
1454 | { |
||
1455 | 319 | if (! $class->identifier) { |
|
1456 | throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name)); |
||
1457 | } |
||
1458 | |||
1459 | 319 | $serializedId = serialize($class->getDatabaseIdentifierValue($id)); |
|
1460 | |||
1461 | 319 | return $this->identityMap[$class->name][$serializedId] ?? false; |
|
1462 | } |
||
1463 | |||
1464 | /** |
||
1465 | * Schedules a document for dirty-checking at commit-time. |
||
1466 | * |
||
1467 | * @todo Rename: scheduleForSynchronization |
||
1468 | */ |
||
1469 | 3 | public function scheduleForDirtyCheck(object $document): void |
|
1470 | { |
||
1471 | 3 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1472 | 3 | $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document; |
|
1473 | 3 | } |
|
1474 | |||
1475 | /** |
||
1476 | * Checks whether a document is registered in the identity map. |
||
1477 | */ |
||
1478 | 87 | public function isInIdentityMap(object $document): bool |
|
1479 | { |
||
1480 | 87 | $oid = spl_object_hash($document); |
|
1481 | |||
1482 | 87 | if (! isset($this->documentIdentifiers[$oid])) { |
|
1483 | 6 | return false; |
|
1484 | } |
||
1485 | |||
1486 | 85 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1487 | 85 | $id = $this->getIdForIdentityMap($document); |
|
1488 | |||
1489 | 85 | return isset($this->identityMap[$class->name][$id]); |
|
1490 | } |
||
1491 | |||
1492 | 672 | private function getIdForIdentityMap(object $document): string |
|
1493 | { |
||
1494 | 672 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1495 | |||
1496 | 672 | if (! $class->identifier) { |
|
1497 | 157 | $id = spl_object_hash($document); |
|
1498 | } else { |
||
1499 | 671 | $id = $this->documentIdentifiers[spl_object_hash($document)]; |
|
1500 | 671 | $id = serialize($class->getDatabaseIdentifierValue($id)); |
|
1501 | } |
||
1502 | |||
1503 | 672 | return $id; |
|
1504 | } |
||
1505 | |||
1506 | /** |
||
1507 | * INTERNAL: |
||
1508 | * Checks whether an identifier exists in the identity map. |
||
1509 | * |
||
1510 | * @ignore |
||
1511 | */ |
||
1512 | public function containsId($id, string $rootClassName): bool |
||
1513 | { |
||
1514 | return isset($this->identityMap[$rootClassName][serialize($id)]); |
||
1515 | } |
||
1516 | |||
1517 | /** |
||
1518 | * Persists a document as part of the current unit of work. |
||
1519 | * |
||
1520 | * @throws MongoDBException If trying to persist MappedSuperclass. |
||
1521 | * @throws \InvalidArgumentException If there is something wrong with document's identifier. |
||
1522 | */ |
||
1523 | 634 | public function persist(object $document): void |
|
1524 | { |
||
1525 | 634 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1526 | 634 | if ($class->isMappedSuperclass || $class->isQueryResultDocument) { |
|
1527 | 1 | throw MongoDBException::cannotPersistMappedSuperclass($class->name); |
|
1528 | } |
||
1529 | 633 | $visited = []; |
|
1530 | 633 | $this->doPersist($document, $visited); |
|
1531 | 628 | } |
|
1532 | |||
1533 | /** |
||
1534 | * Saves a document as part of the current unit of work. |
||
1535 | * This method is internally called during save() cascades as it tracks |
||
1536 | * the already visited documents to prevent infinite recursions. |
||
1537 | * |
||
1538 | * NOTE: This method always considers documents that are not yet known to |
||
1539 | * this UnitOfWork as NEW. |
||
1540 | * |
||
1541 | * @throws \InvalidArgumentException |
||
1542 | * @throws MongoDBException |
||
1543 | */ |
||
1544 | 633 | private function doPersist(object $document, array &$visited): void |
|
1545 | { |
||
1546 | 633 | $oid = spl_object_hash($document); |
|
1547 | 633 | if (isset($visited[$oid])) { |
|
1548 | 25 | return; // Prevent infinite recursion |
|
1549 | } |
||
1550 | |||
1551 | 633 | $visited[$oid] = $document; // Mark visited |
|
1552 | |||
1553 | 633 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1554 | |||
1555 | 633 | $documentState = $this->getDocumentState($document, self::STATE_NEW); |
|
1556 | switch ($documentState) { |
||
1557 | 633 | case self::STATE_MANAGED: |
|
1558 | // Nothing to do, except if policy is "deferred explicit" |
||
1559 | 53 | if ($class->isChangeTrackingDeferredExplicit()) { |
|
1560 | $this->scheduleForDirtyCheck($document); |
||
1561 | } |
||
1562 | 53 | break; |
|
1563 | 633 | case self::STATE_NEW: |
|
1564 | 633 | if ($class->isFile) { |
|
1565 | 1 | throw MongoDBException::cannotPersistGridFSFile($class->name); |
|
1566 | } |
||
1567 | |||
1568 | 632 | $this->persistNew($class, $document); |
|
1569 | 630 | break; |
|
1570 | |||
1571 | 2 | case self::STATE_REMOVED: |
|
1572 | // Document becomes managed again |
||
1573 | 2 | unset($this->documentDeletions[$oid]); |
|
1574 | |||
1575 | 2 | $this->documentStates[$oid] = self::STATE_MANAGED; |
|
1576 | 2 | break; |
|
1577 | |||
1578 | case self::STATE_DETACHED: |
||
1579 | throw new \InvalidArgumentException( |
||
1580 | 'Behavior of persist() for a detached document is not yet defined.' |
||
1581 | ); |
||
1582 | |||
1583 | default: |
||
1584 | throw MongoDBException::invalidDocumentState($documentState); |
||
1585 | } |
||
1586 | |||
1587 | 630 | $this->cascadePersist($document, $visited); |
|
1588 | 628 | } |
|
1589 | |||
1590 | /** |
||
1591 | * Deletes a document as part of the current unit of work. |
||
1592 | */ |
||
1593 | 78 | public function remove(object $document) |
|
1594 | { |
||
1595 | 78 | $visited = []; |
|
1596 | 78 | $this->doRemove($document, $visited); |
|
1597 | 78 | } |
|
1598 | |||
1599 | /** |
||
1600 | * Deletes a document as part of the current unit of work. |
||
1601 | * |
||
1602 | * This method is internally called during delete() cascades as it tracks |
||
1603 | * the already visited documents to prevent infinite recursions. |
||
1604 | * |
||
1605 | * @throws MongoDBException |
||
1606 | */ |
||
1607 | 78 | private function doRemove(object $document, array &$visited): void |
|
1608 | { |
||
1609 | 78 | $oid = spl_object_hash($document); |
|
1610 | 78 | if (isset($visited[$oid])) { |
|
1611 | 1 | return; // Prevent infinite recursion |
|
1612 | } |
||
1613 | |||
1614 | 78 | $visited[$oid] = $document; // mark visited |
|
1615 | |||
1616 | /* Cascade first, because scheduleForDelete() removes the entity from |
||
1617 | * the identity map, which can cause problems when a lazy Proxy has to |
||
1618 | * be initialized for the cascade operation. |
||
1619 | */ |
||
1620 | 78 | $this->cascadeRemove($document, $visited); |
|
1621 | |||
1622 | 78 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1623 | 78 | $documentState = $this->getDocumentState($document); |
|
1624 | switch ($documentState) { |
||
1625 | 78 | case self::STATE_NEW: |
|
1626 | 78 | case self::STATE_REMOVED: |
|
1627 | // nothing to do |
||
1628 | 1 | break; |
|
1629 | 78 | case self::STATE_MANAGED: |
|
1630 | 78 | $this->lifecycleEventManager->preRemove($class, $document); |
|
1631 | 78 | $this->scheduleForDelete($document); |
|
1632 | 78 | break; |
|
1633 | case self::STATE_DETACHED: |
||
1634 | throw MongoDBException::detachedDocumentCannotBeRemoved(); |
||
1635 | default: |
||
1636 | throw MongoDBException::invalidDocumentState($documentState); |
||
1637 | } |
||
1638 | 78 | } |
|
1639 | |||
1640 | /** |
||
1641 | * Merges the state of the given detached document into this UnitOfWork. |
||
1642 | */ |
||
1643 | 12 | public function merge(object $document): object |
|
1644 | { |
||
1645 | 12 | $visited = []; |
|
1646 | |||
1647 | 12 | return $this->doMerge($document, $visited); |
|
1648 | } |
||
1649 | |||
1650 | /** |
||
1651 | * Executes a merge operation on a document. |
||
1652 | * |
||
1653 | * @throws InvalidArgumentException If the entity instance is NEW. |
||
1654 | * @throws LockException If the document uses optimistic locking through a |
||
1655 | * version attribute and the version check against the |
||
1656 | * managed copy fails. |
||
1657 | */ |
||
1658 | 12 | private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null): object |
|
1659 | { |
||
1660 | 12 | $oid = spl_object_hash($document); |
|
1661 | |||
1662 | 12 | if (isset($visited[$oid])) { |
|
1663 | 1 | return $visited[$oid]; // Prevent infinite recursion |
|
1664 | } |
||
1665 | |||
1666 | 12 | $visited[$oid] = $document; // mark visited |
|
1667 | |||
1668 | 12 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1669 | |||
1670 | /* First we assume DETACHED, although it can still be NEW but we can |
||
1671 | * avoid an extra DB round trip this way. If it is not MANAGED but has |
||
1672 | * an identity, we need to fetch it from the DB anyway in order to |
||
1673 | * merge. MANAGED documents are ignored by the merge operation. |
||
1674 | */ |
||
1675 | 12 | $managedCopy = $document; |
|
1676 | |||
1677 | 12 | if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) { |
|
1678 | 12 | if ($document instanceof Proxy && ! $document->__isInitialized()) { |
|
1679 | $document->__load(); |
||
1680 | } |
||
1681 | |||
1682 | 12 | $identifier = $class->getIdentifier(); |
|
1683 | // We always have one element in the identifier array but it might be null |
||
1684 | 12 | $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null; |
|
1685 | 12 | $managedCopy = null; |
|
1686 | |||
1687 | // Try to fetch document from the database |
||
1688 | 12 | if (! $class->isEmbeddedDocument && $id !== null) { |
|
1689 | 12 | $managedCopy = $this->dm->find($class->name, $id); |
|
1690 | |||
1691 | // Managed copy may be removed in which case we can't merge |
||
1692 | 12 | if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) { |
|
1693 | throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.'); |
||
1694 | } |
||
1695 | |||
1696 | 12 | if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) { |
|
1697 | $managedCopy->__load(); |
||
1698 | } |
||
1699 | } |
||
1700 | |||
1701 | 12 | if ($managedCopy === null) { |
|
1702 | // Create a new managed instance |
||
1703 | 4 | $managedCopy = $class->newInstance(); |
|
1704 | 4 | if ($id !== null) { |
|
1705 | 3 | $class->setIdentifierValue($managedCopy, $id); |
|
1706 | } |
||
1707 | 4 | $this->persistNew($class, $managedCopy); |
|
1708 | } |
||
1709 | |||
1710 | 12 | if ($class->isVersioned) { |
|
1711 | $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy); |
||
1712 | $documentVersion = $class->reflFields[$class->versionField]->getValue($document); |
||
1713 | |||
1714 | // Throw exception if versions don't match |
||
1715 | if ($managedCopyVersion !== $documentVersion) { |
||
1716 | throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion); |
||
1717 | } |
||
1718 | } |
||
1719 | |||
1720 | // Merge state of $document into existing (managed) document |
||
1721 | 12 | foreach ($class->reflClass->getProperties() as $prop) { |
|
1722 | 12 | $name = $prop->name; |
|
1723 | 12 | $prop->setAccessible(true); |
|
1724 | 12 | if (! isset($class->associationMappings[$name])) { |
|
1725 | 12 | if (! $class->isIdentifier($name)) { |
|
1726 | 12 | $prop->setValue($managedCopy, $prop->getValue($document)); |
|
1727 | } |
||
1728 | } else { |
||
1729 | 12 | $assoc2 = $class->associationMappings[$name]; |
|
1730 | |||
1731 | 12 | if ($assoc2['type'] === 'one') { |
|
1732 | 6 | $other = $prop->getValue($document); |
|
1733 | |||
1734 | 6 | if ($other === null) { |
|
1735 | 2 | $prop->setValue($managedCopy, null); |
|
1736 | 5 | } elseif ($other instanceof Proxy && ! $other->__isInitialized__) { |
|
1737 | // Do not merge fields marked lazy that have not been fetched |
||
1738 | 1 | continue; |
|
1739 | 4 | } elseif (! $assoc2['isCascadeMerge']) { |
|
1740 | if ($this->getDocumentState($other) === self::STATE_DETACHED) { |
||
1741 | $targetDocument = $assoc2['targetDocument'] ?? get_class($other); |
||
1742 | /** @var ClassMetadata $targetClass */ |
||
1743 | $targetClass = $this->dm->getClassMetadata($targetDocument); |
||
1744 | $relatedId = $targetClass->getIdentifierObject($other); |
||
1745 | |||
1746 | if ($targetClass->subClasses) { |
||
1747 | $other = $this->dm->find($targetClass->name, $relatedId); |
||
1748 | } else { |
||
1749 | $other = $this |
||
1750 | ->dm |
||
1751 | ->getProxyFactory() |
||
1752 | ->getProxy($assoc2['targetDocument'], [$targetClass->identifier => $relatedId]); |
||
1753 | $this->registerManaged($other, $relatedId, []); |
||
1754 | } |
||
1755 | } |
||
1756 | |||
1757 | 5 | $prop->setValue($managedCopy, $other); |
|
1758 | } |
||
1759 | } else { |
||
1760 | 10 | $mergeCol = $prop->getValue($document); |
|
1761 | |||
1762 | 10 | if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) { |
|
1763 | /* Do not merge fields marked lazy that have not |
||
1764 | * been fetched. Keep the lazy persistent collection |
||
1765 | * of the managed copy. |
||
1766 | */ |
||
1767 | 3 | continue; |
|
1768 | } |
||
1769 | |||
1770 | 10 | $managedCol = $prop->getValue($managedCopy); |
|
1771 | |||
1772 | 10 | if (! $managedCol) { |
|
1773 | 1 | $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null); |
|
1774 | 1 | $managedCol->setOwner($managedCopy, $assoc2); |
|
1775 | 1 | $prop->setValue($managedCopy, $managedCol); |
|
1776 | 1 | $this->originalDocumentData[$oid][$name] = $managedCol; |
|
1777 | } |
||
1778 | |||
1779 | /* Note: do not process association's target documents. |
||
1780 | * They will be handled during the cascade. Initialize |
||
1781 | * and, if necessary, clear $managedCol for now. |
||
1782 | */ |
||
1783 | 10 | if ($assoc2['isCascadeMerge']) { |
|
1784 | 10 | $managedCol->initialize(); |
|
1785 | |||
1786 | // If $managedCol differs from the merged collection, clear and set dirty |
||
1787 | 10 | if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) { |
|
1788 | 3 | $managedCol->unwrap()->clear(); |
|
1789 | 3 | $managedCol->setDirty(true); |
|
1790 | |||
1791 | 3 | if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) { |
|
1792 | $this->scheduleForDirtyCheck($managedCopy); |
||
1793 | } |
||
1794 | } |
||
1795 | } |
||
1796 | } |
||
1797 | } |
||
1798 | |||
1799 | 12 | if (! $class->isChangeTrackingNotify()) { |
|
1800 | 12 | continue; |
|
1801 | } |
||
1802 | |||
1803 | // Just treat all properties as changed, there is no other choice. |
||
1804 | $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy)); |
||
1805 | } |
||
1806 | |||
1807 | 12 | if ($class->isChangeTrackingDeferredExplicit()) { |
|
1808 | $this->scheduleForDirtyCheck($document); |
||
1809 | } |
||
1810 | } |
||
1811 | |||
1812 | 12 | if ($prevManagedCopy !== null) { |
|
1813 | 5 | $assocField = $assoc['fieldName']; |
|
1814 | 5 | $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy)); |
|
1815 | |||
1816 | 5 | if ($assoc['type'] === 'one') { |
|
1817 | 3 | $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); |
|
1818 | } else { |
||
1819 | 4 | $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy); |
|
1820 | |||
1821 | 4 | if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) { |
|
1822 | 1 | $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy); |
|
1823 | } |
||
1824 | } |
||
1825 | } |
||
1826 | |||
1827 | // Mark the managed copy visited as well |
||
1828 | 12 | $visited[spl_object_hash($managedCopy)] = $managedCopy; |
|
1829 | |||
1830 | 12 | $this->cascadeMerge($document, $managedCopy, $visited); |
|
1831 | |||
1832 | 12 | return $managedCopy; |
|
1833 | } |
||
1834 | |||
1835 | /** |
||
1836 | * Detaches a document from the persistence management. It's persistence will |
||
1837 | * no longer be managed by Doctrine. |
||
1838 | */ |
||
1839 | 11 | public function detach(object $document): void |
|
1840 | { |
||
1841 | 11 | $visited = []; |
|
1842 | 11 | $this->doDetach($document, $visited); |
|
1843 | 11 | } |
|
1844 | |||
1845 | /** |
||
1846 | * Executes a detach operation on the given document. |
||
1847 | * |
||
1848 | * @internal This method always considers documents with an assigned identifier as DETACHED. |
||
1849 | */ |
||
1850 | 17 | private function doDetach(object $document, array &$visited): void |
|
1851 | { |
||
1852 | 17 | $oid = spl_object_hash($document); |
|
1853 | 17 | if (isset($visited[$oid])) { |
|
1854 | 3 | return; // Prevent infinite recursion |
|
1855 | } |
||
1856 | |||
1857 | 17 | $visited[$oid] = $document; // mark visited |
|
1858 | |||
1859 | 17 | switch ($this->getDocumentState($document, self::STATE_DETACHED)) { |
|
1860 | 17 | case self::STATE_MANAGED: |
|
1861 | 17 | $this->removeFromIdentityMap($document); |
|
1862 | unset( |
||
1863 | 17 | $this->documentInsertions[$oid], |
|
1864 | 17 | $this->documentUpdates[$oid], |
|
1865 | 17 | $this->documentDeletions[$oid], |
|
1866 | 17 | $this->documentIdentifiers[$oid], |
|
1867 | 17 | $this->documentStates[$oid], |
|
1868 | 17 | $this->originalDocumentData[$oid], |
|
1869 | 17 | $this->parentAssociations[$oid], |
|
1870 | 17 | $this->documentUpserts[$oid], |
|
1871 | 17 | $this->hasScheduledCollections[$oid], |
|
1872 | 17 | $this->embeddedDocumentsRegistry[$oid] |
|
1873 | ); |
||
1874 | 17 | break; |
|
1875 | 3 | case self::STATE_NEW: |
|
1876 | 3 | case self::STATE_DETACHED: |
|
1877 | 3 | return; |
|
1878 | } |
||
1879 | |||
1880 | 17 | $this->cascadeDetach($document, $visited); |
|
1881 | 17 | } |
|
1882 | |||
1883 | /** |
||
1884 | * Refreshes the state of the given document from the database, overwriting |
||
1885 | * any local, unpersisted changes. |
||
1886 | * |
||
1887 | * @throws \InvalidArgumentException If the document is not MANAGED. |
||
1888 | */ |
||
1889 | 24 | public function refresh(object $document): void |
|
1890 | { |
||
1891 | 24 | $visited = []; |
|
1892 | 24 | $this->doRefresh($document, $visited); |
|
1893 | 23 | } |
|
1894 | |||
1895 | /** |
||
1896 | * Executes a refresh operation on a document. |
||
1897 | * |
||
1898 | * @throws \InvalidArgumentException If the document is not MANAGED. |
||
1899 | */ |
||
1900 | 24 | private function doRefresh(object $document, array &$visited): void |
|
1901 | { |
||
1902 | 24 | $oid = spl_object_hash($document); |
|
1903 | 24 | if (isset($visited[$oid])) { |
|
1904 | return; // Prevent infinite recursion |
||
1905 | } |
||
1906 | |||
1907 | 24 | $visited[$oid] = $document; // mark visited |
|
1908 | |||
1909 | 24 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1910 | |||
1911 | 24 | if (! $class->isEmbeddedDocument) { |
|
1912 | 24 | if ($this->getDocumentState($document) !== self::STATE_MANAGED) { |
|
1913 | 1 | throw new \InvalidArgumentException('Document is not MANAGED.'); |
|
1914 | } |
||
1915 | |||
1916 | 23 | $this->getDocumentPersister($class->name)->refresh($document); |
|
1917 | } |
||
1918 | |||
1919 | 23 | $this->cascadeRefresh($document, $visited); |
|
1920 | 23 | } |
|
1921 | |||
1922 | /** |
||
1923 | * Cascades a refresh operation to associated documents. |
||
1924 | */ |
||
1925 | 23 | private function cascadeRefresh(object $document, array &$visited): void |
|
1926 | { |
||
1927 | 23 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1928 | |||
1929 | 23 | $associationMappings = array_filter( |
|
1930 | 23 | $class->associationMappings, |
|
1931 | function ($assoc) { |
||
1932 | 18 | return $assoc['isCascadeRefresh']; |
|
1933 | 23 | } |
|
1934 | ); |
||
1935 | |||
1936 | 23 | foreach ($associationMappings as $mapping) { |
|
1937 | 15 | $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); |
|
1938 | 15 | if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { |
|
1939 | 15 | if ($relatedDocuments instanceof PersistentCollectionInterface) { |
|
1940 | // Unwrap so that foreach() does not initialize |
||
1941 | 15 | $relatedDocuments = $relatedDocuments->unwrap(); |
|
1942 | } |
||
1943 | 15 | foreach ($relatedDocuments as $relatedDocument) { |
|
1944 | 15 | $this->doRefresh($relatedDocument, $visited); |
|
1945 | } |
||
1946 | 10 | } elseif ($relatedDocuments !== null) { |
|
1947 | 15 | $this->doRefresh($relatedDocuments, $visited); |
|
1948 | } |
||
1949 | } |
||
1950 | 23 | } |
|
1951 | |||
1952 | /** |
||
1953 | * Cascades a detach operation to associated documents. |
||
1954 | */ |
||
1955 | 17 | private function cascadeDetach(object $document, array &$visited): void |
|
1956 | { |
||
1957 | 17 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1958 | 17 | foreach ($class->fieldMappings as $mapping) { |
|
1959 | 17 | if (! $mapping['isCascadeDetach']) { |
|
1960 | 17 | continue; |
|
1961 | } |
||
1962 | 11 | $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); |
|
1963 | 11 | if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { |
|
1964 | 11 | if ($relatedDocuments instanceof PersistentCollectionInterface) { |
|
1965 | // Unwrap so that foreach() does not initialize |
||
1966 | 8 | $relatedDocuments = $relatedDocuments->unwrap(); |
|
1967 | } |
||
1968 | 11 | foreach ($relatedDocuments as $relatedDocument) { |
|
1969 | 11 | $this->doDetach($relatedDocument, $visited); |
|
1970 | } |
||
1971 | 11 | } elseif ($relatedDocuments !== null) { |
|
1972 | 11 | $this->doDetach($relatedDocuments, $visited); |
|
1973 | } |
||
1974 | } |
||
1975 | 17 | } |
|
1976 | /** |
||
1977 | * Cascades a merge operation to associated documents. |
||
1978 | */ |
||
1979 | 12 | private function cascadeMerge(object $document, object $managedCopy, array &$visited): void |
|
1980 | { |
||
1981 | 12 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
1982 | |||
1983 | 12 | $associationMappings = array_filter( |
|
1984 | 12 | $class->associationMappings, |
|
1985 | function ($assoc) { |
||
1986 | 12 | return $assoc['isCascadeMerge']; |
|
1987 | 12 | } |
|
1988 | ); |
||
1989 | |||
1990 | 12 | foreach ($associationMappings as $assoc) { |
|
1991 | 11 | $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document); |
|
1992 | |||
1993 | 11 | if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { |
|
1994 | 8 | if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) { |
|
1995 | // Collections are the same, so there is nothing to do |
||
1996 | 1 | continue; |
|
1997 | } |
||
1998 | |||
1999 | 8 | foreach ($relatedDocuments as $relatedDocument) { |
|
2000 | 8 | $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc); |
|
2001 | } |
||
2002 | 6 | } elseif ($relatedDocuments !== null) { |
|
2003 | 11 | $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc); |
|
2004 | } |
||
2005 | } |
||
2006 | 12 | } |
|
2007 | |||
2008 | /** |
||
2009 | * Cascades the save operation to associated documents. |
||
2010 | */ |
||
2011 | 630 | private function cascadePersist(object $document, array &$visited): void |
|
2012 | { |
||
2013 | 630 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
2014 | |||
2015 | 630 | $associationMappings = array_filter( |
|
2016 | 630 | $class->associationMappings, |
|
2017 | function ($assoc) { |
||
2018 | 487 | return $assoc['isCascadePersist']; |
|
2019 | 630 | } |
|
2020 | ); |
||
2021 | |||
2022 | 630 | foreach ($associationMappings as $fieldName => $mapping) { |
|
2023 | 435 | $relatedDocuments = $class->reflFields[$fieldName]->getValue($document); |
|
2024 | |||
2025 | 435 | if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { |
|
2026 | 364 | if ($relatedDocuments instanceof PersistentCollectionInterface) { |
|
2027 | 12 | if ($relatedDocuments->getOwner() !== $document) { |
|
2028 | 2 | $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']); |
|
2029 | } |
||
2030 | // Unwrap so that foreach() does not initialize |
||
2031 | 12 | $relatedDocuments = $relatedDocuments->unwrap(); |
|
2032 | } |
||
2033 | |||
2034 | 364 | $count = 0; |
|
2035 | 364 | foreach ($relatedDocuments as $relatedKey => $relatedDocument) { |
|
2036 | 194 | if (! empty($mapping['embedded'])) { |
|
2037 | 123 | list(, $knownParent, ) = $this->getParentAssociation($relatedDocument); |
|
2038 | 123 | if ($knownParent && $knownParent !== $document) { |
|
2039 | 1 | $relatedDocument = clone $relatedDocument; |
|
2040 | 1 | $relatedDocuments[$relatedKey] = $relatedDocument; |
|
2041 | } |
||
2042 | 123 | $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey; |
|
2043 | 123 | $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey); |
|
2044 | } |
||
2045 | 364 | $this->doPersist($relatedDocument, $visited); |
|
2046 | } |
||
2047 | 345 | } elseif ($relatedDocuments !== null) { |
|
2048 | 130 | if (! empty($mapping['embedded'])) { |
|
2049 | 69 | list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments); |
|
2050 | 69 | if ($knownParent && $knownParent !== $document) { |
|
2051 | 3 | $relatedDocuments = clone $relatedDocuments; |
|
2052 | 3 | $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments); |
|
2053 | } |
||
2054 | 69 | $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']); |
|
2055 | } |
||
2056 | 435 | $this->doPersist($relatedDocuments, $visited); |
|
2057 | } |
||
2058 | } |
||
2059 | 628 | } |
|
2060 | |||
2061 | /** |
||
2062 | * Cascades the delete operation to associated documents. |
||
2063 | */ |
||
2064 | 78 | private function cascadeRemove(object $document, array &$visited): void |
|
2065 | { |
||
2066 | 78 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
2067 | 78 | foreach ($class->fieldMappings as $mapping) { |
|
2068 | 78 | if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) { |
|
2069 | 77 | continue; |
|
2070 | } |
||
2071 | 38 | if ($document instanceof Proxy && ! $document->__isInitialized__) { |
|
2072 | 3 | $document->__load(); |
|
2073 | } |
||
2074 | |||
2075 | 38 | $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document); |
|
2076 | 38 | if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) { |
|
2077 | // If its a PersistentCollection initialization is intended! No unwrap! |
||
2078 | 26 | foreach ($relatedDocuments as $relatedDocument) { |
|
2079 | 26 | $this->doRemove($relatedDocument, $visited); |
|
2080 | } |
||
2081 | 27 | } elseif ($relatedDocuments !== null) { |
|
2082 | 38 | $this->doRemove($relatedDocuments, $visited); |
|
2083 | } |
||
2084 | } |
||
2085 | 78 | } |
|
2086 | |||
2087 | /** |
||
2088 | * Acquire a lock on the given document. |
||
2089 | * |
||
2090 | * @throws LockException |
||
2091 | * @throws \InvalidArgumentException |
||
2092 | */ |
||
2093 | 8 | public function lock(object $document, int $lockMode, ?int $lockVersion = null): void |
|
2094 | { |
||
2095 | 8 | if ($this->getDocumentState($document) !== self::STATE_MANAGED) { |
|
2096 | 1 | throw new \InvalidArgumentException('Document is not MANAGED.'); |
|
2097 | } |
||
2098 | |||
2099 | 7 | $documentName = get_class($document); |
|
2100 | 7 | $class = $this->dm->getClassMetadata($documentName); |
|
2101 | |||
2102 | 7 | if ($lockMode === LockMode::OPTIMISTIC) { |
|
2103 | 2 | if (! $class->isVersioned) { |
|
2104 | 1 | throw LockException::notVersioned($documentName); |
|
2105 | } |
||
2106 | |||
2107 | 1 | if ($lockVersion !== null) { |
|
2108 | 1 | $documentVersion = $class->reflFields[$class->versionField]->getValue($document); |
|
2109 | 1 | if ($documentVersion !== $lockVersion) { |
|
2110 | 1 | throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion); |
|
2111 | } |
||
2112 | } |
||
2113 | 5 | } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) { |
|
2114 | 5 | $this->getDocumentPersister($class->name)->lock($document, $lockMode); |
|
2115 | } |
||
2116 | 5 | } |
|
2117 | |||
2118 | /** |
||
2119 | * Releases a lock on the given document. |
||
2120 | * |
||
2121 | * @throws \InvalidArgumentException |
||
2122 | */ |
||
2123 | 1 | public function unlock(object $document): void |
|
2124 | { |
||
2125 | 1 | if ($this->getDocumentState($document) !== self::STATE_MANAGED) { |
|
2126 | throw new \InvalidArgumentException('Document is not MANAGED.'); |
||
2127 | } |
||
2128 | 1 | $documentName = get_class($document); |
|
2129 | 1 | $this->getDocumentPersister($documentName)->unlock($document); |
|
2130 | 1 | } |
|
2131 | |||
2132 | /** |
||
2133 | * Clears the UnitOfWork. |
||
2134 | */ |
||
2135 | 391 | public function clear(?string $documentName = null): void |
|
2136 | { |
||
2137 | 391 | if ($documentName === null) { |
|
2138 | 385 | $this->identityMap = |
|
2139 | 385 | $this->documentIdentifiers = |
|
2140 | 385 | $this->originalDocumentData = |
|
2141 | 385 | $this->documentChangeSets = |
|
2142 | 385 | $this->documentStates = |
|
2143 | 385 | $this->scheduledForDirtyCheck = |
|
2144 | 385 | $this->documentInsertions = |
|
2145 | 385 | $this->documentUpserts = |
|
2146 | 385 | $this->documentUpdates = |
|
2147 | 385 | $this->documentDeletions = |
|
2148 | 385 | $this->collectionUpdates = |
|
2149 | 385 | $this->collectionDeletions = |
|
2150 | 385 | $this->parentAssociations = |
|
2151 | 385 | $this->embeddedDocumentsRegistry = |
|
2152 | 385 | $this->orphanRemovals = |
|
2153 | 385 | $this->hasScheduledCollections = []; |
|
2154 | } else { |
||
2155 | 6 | $visited = []; |
|
2156 | 6 | foreach ($this->identityMap as $className => $documents) { |
|
2157 | 6 | if ($className !== $documentName) { |
|
2158 | 3 | continue; |
|
2159 | } |
||
2160 | |||
2161 | 6 | foreach ($documents as $document) { |
|
2162 | 6 | $this->doDetach($document, $visited); |
|
2163 | } |
||
2164 | } |
||
2165 | } |
||
2166 | |||
2167 | 391 | if (! $this->evm->hasListeners(Events::onClear)) { |
|
2168 | 391 | return; |
|
2169 | } |
||
2170 | |||
2171 | $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName)); |
||
2172 | } |
||
2173 | |||
2174 | /** |
||
2175 | * INTERNAL: |
||
2176 | * Schedules an embedded document for removal. The remove() operation will be |
||
2177 | * invoked on that document at the beginning of the next commit of this |
||
2178 | * UnitOfWork. |
||
2179 | * |
||
2180 | * @ignore |
||
2181 | */ |
||
2182 | 53 | public function scheduleOrphanRemoval(object $document): void |
|
2183 | { |
||
2184 | 53 | $this->orphanRemovals[spl_object_hash($document)] = $document; |
|
2185 | 53 | } |
|
2186 | |||
2187 | /** |
||
2188 | * INTERNAL: |
||
2189 | * Unschedules an embedded or referenced object for removal. |
||
2190 | * |
||
2191 | * @ignore |
||
2192 | */ |
||
2193 | 120 | public function unscheduleOrphanRemoval(object $document): void |
|
2194 | { |
||
2195 | 120 | $oid = spl_object_hash($document); |
|
2196 | 120 | unset($this->orphanRemovals[$oid]); |
|
2197 | 120 | } |
|
2198 | |||
2199 | /** |
||
2200 | * Fixes PersistentCollection state if it wasn't used exactly as we had in mind: |
||
2201 | * 1) sets owner if it was cloned |
||
2202 | * 2) clones collection, sets owner, updates document's property and, if necessary, updates originalData |
||
2203 | * 3) NOP if state is OK |
||
2204 | * Returned collection should be used from now on (only important with 2nd point) |
||
2205 | */ |
||
2206 | 8 | private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName): PersistentCollectionInterface |
|
2207 | { |
||
2208 | 8 | $owner = $coll->getOwner(); |
|
2209 | 8 | if ($owner === null) { // cloned |
|
2210 | 6 | $coll->setOwner($document, $class->fieldMappings[$propName]); |
|
2211 | 2 | } elseif ($owner !== $document) { // no clone, we have to fix |
|
2212 | 2 | if (! $coll->isInitialized()) { |
|
2213 | 1 | $coll->initialize(); // we have to do this otherwise the cols share state |
|
2214 | } |
||
2215 | 2 | $newValue = clone $coll; |
|
2216 | 2 | $newValue->setOwner($document, $class->fieldMappings[$propName]); |
|
2217 | 2 | $class->reflFields[$propName]->setValue($document, $newValue); |
|
2218 | 2 | if ($this->isScheduledForUpdate($document)) { |
|
2219 | // @todo following line should be superfluous once collections are stored in change sets |
||
2220 | $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue); |
||
2221 | } |
||
2222 | 2 | return $newValue; |
|
2223 | } |
||
2224 | 6 | return $coll; |
|
2225 | } |
||
2226 | |||
2227 | /** |
||
2228 | * INTERNAL: |
||
2229 | * Schedules a complete collection for removal when this UnitOfWork commits. |
||
2230 | */ |
||
2231 | 43 | public function scheduleCollectionDeletion(PersistentCollectionInterface $coll): void |
|
2232 | { |
||
2233 | 43 | $oid = spl_object_hash($coll); |
|
2234 | 43 | unset($this->collectionUpdates[$oid]); |
|
2235 | 43 | if (isset($this->collectionDeletions[$oid])) { |
|
2236 | return; |
||
2237 | } |
||
2238 | |||
2239 | 43 | $this->collectionDeletions[$oid] = $coll; |
|
2240 | 43 | $this->scheduleCollectionOwner($coll); |
|
2241 | 43 | } |
|
2242 | |||
2243 | /** |
||
2244 | * Checks whether a PersistentCollection is scheduled for deletion. |
||
2245 | */ |
||
2246 | 213 | public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll): bool |
|
2247 | { |
||
2248 | 213 | return isset($this->collectionDeletions[spl_object_hash($coll)]); |
|
2249 | } |
||
2250 | |||
2251 | /** |
||
2252 | * INTERNAL: |
||
2253 | * Unschedules a collection from being deleted when this UnitOfWork commits. |
||
2254 | * |
||
2255 | */ |
||
2256 | 225 | public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll): void |
|
2257 | { |
||
2258 | 225 | $oid = spl_object_hash($coll); |
|
2259 | 225 | if (! isset($this->collectionDeletions[$oid])) { |
|
2260 | 225 | return; |
|
2261 | } |
||
2262 | |||
2263 | 12 | $topmostOwner = $this->getOwningDocument($coll->getOwner()); |
|
2264 | 12 | unset($this->collectionDeletions[$oid]); |
|
2265 | 12 | unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]); |
|
2266 | 12 | } |
|
2267 | |||
2268 | /** |
||
2269 | * INTERNAL: |
||
2270 | * Schedules a collection for update when this UnitOfWork commits. |
||
2271 | * |
||
2272 | */ |
||
2273 | 247 | public function scheduleCollectionUpdate(PersistentCollectionInterface $coll): void |
|
2274 | { |
||
2275 | 247 | $mapping = $coll->getMapping(); |
|
2276 | 247 | if (CollectionHelper::usesSet($mapping['strategy'])) { |
|
2277 | /* There is no need to $unset collection if it will be $set later |
||
2278 | * This is NOP if collection is not scheduled for deletion |
||
2279 | */ |
||
2280 | 40 | $this->unscheduleCollectionDeletion($coll); |
|
2281 | } |
||
2282 | 247 | $oid = spl_object_hash($coll); |
|
2283 | 247 | if (isset($this->collectionUpdates[$oid])) { |
|
2284 | 11 | return; |
|
2285 | } |
||
2286 | |||
2287 | 247 | $this->collectionUpdates[$oid] = $coll; |
|
2288 | 247 | $this->scheduleCollectionOwner($coll); |
|
2289 | 247 | } |
|
2290 | |||
2291 | /** |
||
2292 | * INTERNAL: |
||
2293 | * Unschedules a collection from being updated when this UnitOfWork commits. |
||
2294 | * |
||
2295 | */ |
||
2296 | 225 | public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll): void |
|
2297 | { |
||
2298 | 225 | $oid = spl_object_hash($coll); |
|
2299 | 225 | if (! isset($this->collectionUpdates[$oid])) { |
|
2300 | 45 | return; |
|
2301 | } |
||
2302 | |||
2303 | 215 | $topmostOwner = $this->getOwningDocument($coll->getOwner()); |
|
2304 | 215 | unset($this->collectionUpdates[$oid]); |
|
2305 | 215 | unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]); |
|
2306 | 215 | } |
|
2307 | |||
2308 | /** |
||
2309 | * Checks whether a PersistentCollection is scheduled for update. |
||
2310 | */ |
||
2311 | 133 | public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll): bool |
|
2312 | { |
||
2313 | 133 | return isset($this->collectionUpdates[spl_object_hash($coll)]); |
|
2314 | } |
||
2315 | |||
2316 | /** |
||
2317 | * INTERNAL: |
||
2318 | * Gets PersistentCollections that have been visited during computing change |
||
2319 | * set of $document |
||
2320 | * |
||
2321 | * @return PersistentCollectionInterface[] |
||
2322 | */ |
||
2323 | 583 | public function getVisitedCollections(object $document): array |
|
2324 | { |
||
2325 | 583 | $oid = spl_object_hash($document); |
|
2326 | |||
2327 | 583 | return $this->visitedCollections[$oid] ?? []; |
|
2328 | } |
||
2329 | |||
2330 | /** |
||
2331 | * INTERNAL: |
||
2332 | * Gets PersistentCollections that are scheduled to update and related to $document |
||
2333 | * |
||
2334 | * @return PersistentCollectionInterface[] |
||
2335 | */ |
||
2336 | 583 | public function getScheduledCollections(object $document): array |
|
2337 | { |
||
2338 | 583 | $oid = spl_object_hash($document); |
|
2339 | |||
2340 | 583 | return $this->hasScheduledCollections[$oid] ?? []; |
|
2341 | } |
||
2342 | |||
2343 | /** |
||
2344 | * Checks whether the document is related to a PersistentCollection |
||
2345 | * scheduled for update or deletion. |
||
2346 | */ |
||
2347 | 51 | public function hasScheduledCollections(object $document): bool |
|
2348 | { |
||
2349 | 51 | return isset($this->hasScheduledCollections[spl_object_hash($document)]); |
|
2350 | } |
||
2351 | |||
2352 | /** |
||
2353 | * Marks the PersistentCollection's top-level owner as having a relation to |
||
2354 | * a collection scheduled for update or deletion. |
||
2355 | * |
||
2356 | * If the owner is not scheduled for any lifecycle action, it will be |
||
2357 | * scheduled for update to ensure that versioning takes place if necessary. |
||
2358 | * |
||
2359 | * If the collection is nested within atomic collection, it is immediately |
||
2360 | * unscheduled and atomic one is scheduled for update instead. This makes |
||
2361 | * calculating update data way easier. |
||
2362 | * |
||
2363 | */ |
||
2364 | 249 | private function scheduleCollectionOwner(PersistentCollectionInterface $coll): void |
|
2365 | { |
||
2366 | 249 | $document = $this->getOwningDocument($coll->getOwner()); |
|
2367 | 249 | $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll; |
|
2368 | |||
2369 | 249 | if ($document !== $coll->getOwner()) { |
|
2370 | 25 | $parent = $coll->getOwner(); |
|
2371 | 25 | $mapping = []; |
|
2372 | 25 | while (($parentAssoc = $this->getParentAssociation($parent)) !== null) { |
|
2373 | 25 | list($mapping, $parent, ) = $parentAssoc; |
|
2374 | } |
||
2375 | 25 | if (CollectionHelper::isAtomic($mapping['strategy'])) { |
|
2376 | 8 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
2377 | 8 | $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']); |
|
2378 | 8 | $this->scheduleCollectionUpdate($atomicCollection); |
|
2379 | 8 | $this->unscheduleCollectionDeletion($coll); |
|
2380 | 8 | $this->unscheduleCollectionUpdate($coll); |
|
2381 | } |
||
2382 | } |
||
2383 | |||
2384 | 249 | if ($this->isDocumentScheduled($document)) { |
|
2385 | 244 | return; |
|
2386 | } |
||
2387 | |||
2388 | 50 | $this->scheduleForUpdate($document); |
|
2389 | 50 | } |
|
2390 | |||
2391 | /** |
||
2392 | * Get the top-most owning document of a given document |
||
2393 | * |
||
2394 | * If a top-level document is provided, that same document will be returned. |
||
2395 | * For an embedded document, we will walk through parent associations until |
||
2396 | * we find a top-level document. |
||
2397 | * |
||
2398 | * @throws \UnexpectedValueException When a top-level document could not be found. |
||
2399 | */ |
||
2400 | 251 | public function getOwningDocument(object $document): object |
|
2401 | { |
||
2402 | 251 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
2403 | 251 | while ($class->isEmbeddedDocument) { |
|
2404 | 40 | $parentAssociation = $this->getParentAssociation($document); |
|
2405 | |||
2406 | 40 | if (! $parentAssociation) { |
|
2407 | throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document)); |
||
2408 | } |
||
2409 | |||
2410 | 40 | list(, $document, ) = $parentAssociation; |
|
2411 | 40 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
2412 | } |
||
2413 | |||
2414 | 251 | return $document; |
|
2415 | } |
||
2416 | |||
2417 | /** |
||
2418 | * Gets the class name for an association (embed or reference) with respect |
||
2419 | * to any discriminator value. |
||
2420 | * |
||
2421 | * @param array|object|null $data |
||
2422 | */ |
||
2423 | 240 | public function getClassNameForAssociation(array $mapping, $data): string |
|
2424 | { |
||
2425 | 240 | $discriminatorField = $mapping['discriminatorField'] ?? null; |
|
2426 | |||
2427 | 240 | $discriminatorValue = null; |
|
2428 | 240 | if (isset($discriminatorField, $data[$discriminatorField])) { |
|
2429 | 21 | $discriminatorValue = $data[$discriminatorField]; |
|
2430 | 220 | } elseif (isset($mapping['defaultDiscriminatorValue'])) { |
|
2431 | $discriminatorValue = $mapping['defaultDiscriminatorValue']; |
||
2432 | } |
||
2433 | |||
2434 | 240 | if ($discriminatorValue !== null) { |
|
2435 | 21 | return $mapping['discriminatorMap'][$discriminatorValue] |
|
2436 | 21 | ?? $discriminatorValue; |
|
2437 | } |
||
2438 | |||
2439 | 220 | $class = $this->dm->getClassMetadata($mapping['targetDocument']); |
|
2440 | |||
2441 | 220 | if (isset($class->discriminatorField, $data[$class->discriminatorField])) { |
|
2442 | 15 | $discriminatorValue = $data[$class->discriminatorField]; |
|
2443 | 206 | } elseif ($class->defaultDiscriminatorValue !== null) { |
|
2444 | 1 | $discriminatorValue = $class->defaultDiscriminatorValue; |
|
2445 | } |
||
2446 | |||
2447 | 220 | if ($discriminatorValue !== null) { |
|
2448 | 16 | return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue; |
|
2449 | } |
||
2450 | |||
2451 | 205 | return $mapping['targetDocument']; |
|
2452 | } |
||
2453 | |||
2454 | /** |
||
2455 | * INTERNAL: |
||
2456 | * Creates a document. Used for reconstitution of documents during hydration. |
||
2457 | * |
||
2458 | * @ignore |
||
2459 | * @internal Highly performance-sensitive method. |
||
2460 | */ |
||
2461 | 409 | public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null): object |
|
2462 | { |
||
2463 | 409 | $class = $this->dm->getClassMetadata($className); |
|
2464 | |||
2465 | // @TODO figure out how to remove this |
||
2466 | 409 | $discriminatorValue = null; |
|
2467 | 409 | if (isset($class->discriminatorField, $data[$class->discriminatorField])) { |
|
2468 | 17 | $discriminatorValue = $data[$class->discriminatorField]; |
|
2469 | 401 | } elseif (isset($class->defaultDiscriminatorValue)) { |
|
2470 | 2 | $discriminatorValue = $class->defaultDiscriminatorValue; |
|
2471 | } |
||
2472 | |||
2473 | 409 | if ($discriminatorValue !== null) { |
|
2474 | 18 | $className = $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue; |
|
2475 | |||
2476 | 18 | $class = $this->dm->getClassMetadata($className); |
|
2477 | |||
2478 | 18 | unset($data[$class->discriminatorField]); |
|
2479 | } |
||
2480 | |||
2481 | 409 | if (! empty($hints[Query::HINT_READ_ONLY])) { |
|
2482 | 2 | $document = $class->newInstance(); |
|
2483 | 2 | $this->hydratorFactory->hydrate($document, $data, $hints); |
|
2484 | 2 | return $document; |
|
2485 | } |
||
2486 | |||
2487 | 408 | $isManagedObject = false; |
|
2488 | 408 | $serializedId = null; |
|
2489 | 408 | $id = null; |
|
2490 | 408 | if (! $class->isQueryResultDocument) { |
|
2491 | 405 | $id = $class->getDatabaseIdentifierValue($data['_id']); |
|
2492 | 405 | $serializedId = serialize($id); |
|
2493 | 405 | $isManagedObject = isset($this->identityMap[$class->name][$serializedId]); |
|
2494 | } |
||
2495 | |||
2496 | 408 | $oid = null; |
|
2497 | 408 | if ($isManagedObject) { |
|
2498 | 97 | $document = $this->identityMap[$class->name][$serializedId]; |
|
2499 | 97 | $oid = spl_object_hash($document); |
|
2500 | 97 | if ($document instanceof Proxy && ! $document->__isInitialized__) { |
|
2501 | 16 | $document->__isInitialized__ = true; |
|
2502 | 16 | $overrideLocalValues = true; |
|
2503 | 16 | if ($document instanceof NotifyPropertyChanged) { |
|
2504 | 16 | $document->addPropertyChangedListener($this); |
|
2505 | } |
||
2506 | } else { |
||
2507 | 87 | $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]); |
|
2508 | } |
||
2509 | 97 | if ($overrideLocalValues) { |
|
2510 | 44 | $data = $this->hydratorFactory->hydrate($document, $data, $hints); |
|
2511 | 97 | $this->originalDocumentData[$oid] = $data; |
|
2512 | } |
||
2513 | } else { |
||
2514 | 365 | if ($document === null) { |
|
2515 | 365 | $document = $class->newInstance(); |
|
2516 | } |
||
2517 | |||
2518 | 365 | if (! $class->isQueryResultDocument) { |
|
2519 | 361 | $this->registerManaged($document, $id, $data); |
|
2520 | 361 | $oid = spl_object_hash($document); |
|
2521 | 361 | $this->documentStates[$oid] = self::STATE_MANAGED; |
|
2522 | 361 | $this->identityMap[$class->name][$serializedId] = $document; |
|
2523 | } |
||
2524 | |||
2525 | 365 | $data = $this->hydratorFactory->hydrate($document, $data, $hints); |
|
2526 | |||
2527 | 365 | if (! $class->isQueryResultDocument) { |
|
2528 | 361 | $this->originalDocumentData[$oid] = $data; |
|
2529 | } |
||
2530 | } |
||
2531 | |||
2532 | 408 | return $document; |
|
2533 | } |
||
2534 | |||
2535 | /** |
||
2536 | * Initializes (loads) an uninitialized persistent collection of a document. |
||
2537 | */ |
||
2538 | 184 | public function loadCollection(PersistentCollectionInterface $collection): void |
|
2539 | { |
||
2540 | 184 | $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection); |
|
2541 | 184 | $this->lifecycleEventManager->postCollectionLoad($collection); |
|
2542 | 184 | } |
|
2543 | |||
2544 | /** |
||
2545 | * Gets the identity map of the UnitOfWork. |
||
2546 | */ |
||
2547 | public function getIdentityMap(): array |
||
2548 | { |
||
2549 | return $this->identityMap; |
||
2550 | } |
||
2551 | |||
2552 | /** |
||
2553 | * Gets the original data of a document. The original data is the data that was |
||
2554 | * present at the time the document was reconstituted from the database. |
||
2555 | * |
||
2556 | * @return array |
||
2557 | */ |
||
2558 | 1 | public function getOriginalDocumentData(object $document): array |
|
2559 | { |
||
2560 | 1 | $oid = spl_object_hash($document); |
|
2561 | |||
2562 | 1 | return $this->originalDocumentData[$oid] ?? []; |
|
2563 | } |
||
2564 | |||
2565 | 63 | public function setOriginalDocumentData(object $document, array $data): void |
|
2566 | { |
||
2567 | 63 | $oid = spl_object_hash($document); |
|
2568 | 63 | $this->originalDocumentData[$oid] = $data; |
|
2569 | 63 | unset($this->documentChangeSets[$oid]); |
|
2570 | 63 | } |
|
2571 | |||
2572 | /** |
||
2573 | * INTERNAL: |
||
2574 | * Sets a property value of the original data array of a document. |
||
2575 | * |
||
2576 | * @ignore |
||
2577 | * @param mixed $value |
||
2578 | */ |
||
2579 | 3 | public function setOriginalDocumentProperty(string $oid, string $property, $value): void |
|
2580 | { |
||
2581 | 3 | $this->originalDocumentData[$oid][$property] = $value; |
|
2582 | 3 | } |
|
2583 | |||
2584 | /** |
||
2585 | * Gets the identifier of a document. |
||
2586 | * |
||
2587 | * @return mixed The identifier value |
||
2588 | */ |
||
2589 | 444 | public function getDocumentIdentifier(object $document) |
|
2590 | { |
||
2591 | 444 | return $this->documentIdentifiers[spl_object_hash($document)] ?? null; |
|
2592 | } |
||
2593 | |||
2594 | /** |
||
2595 | * Checks whether the UnitOfWork has any pending insertions. |
||
2596 | * |
||
2597 | * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise. |
||
2598 | */ |
||
2599 | public function hasPendingInsertions(): bool |
||
2600 | { |
||
2601 | return ! empty($this->documentInsertions); |
||
2602 | } |
||
2603 | |||
2604 | /** |
||
2605 | * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the |
||
2606 | * number of documents in the identity map. |
||
2607 | */ |
||
2608 | 2 | public function size(): int |
|
2609 | { |
||
2610 | 2 | $count = 0; |
|
2611 | 2 | foreach ($this->identityMap as $documentSet) { |
|
2612 | 2 | $count += count($documentSet); |
|
2613 | } |
||
2614 | 2 | return $count; |
|
2615 | } |
||
2616 | |||
2617 | /** |
||
2618 | * INTERNAL: |
||
2619 | * Registers a document as managed. |
||
2620 | * |
||
2621 | * TODO: This method assumes that $id is a valid PHP identifier for the |
||
2622 | * document class. If the class expects its database identifier to be an |
||
2623 | * ObjectId, and an incompatible $id is registered (e.g. an integer), the |
||
2624 | * document identifiers map will become inconsistent with the identity map. |
||
2625 | * In the future, we may want to round-trip $id through a PHP and database |
||
2626 | * conversion and throw an exception if it's inconsistent. |
||
2627 | * |
||
2628 | * @param mixed $id The identifier values. |
||
2629 | */ |
||
2630 | 394 | public function registerManaged(object $document, $id, array $data): void |
|
2631 | { |
||
2632 | 394 | $oid = spl_object_hash($document); |
|
2633 | 394 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
2634 | |||
2635 | 394 | if (! $class->identifier || $id === null) { |
|
2636 | 112 | $this->documentIdentifiers[$oid] = $oid; |
|
2637 | } else { |
||
2638 | 388 | $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id); |
|
2639 | } |
||
2640 | |||
2641 | 394 | $this->documentStates[$oid] = self::STATE_MANAGED; |
|
2642 | 394 | $this->originalDocumentData[$oid] = $data; |
|
2643 | 394 | $this->addToIdentityMap($document); |
|
2644 | 394 | } |
|
2645 | |||
2646 | /** |
||
2647 | * INTERNAL: |
||
2648 | * Clears the property changeset of the document with the given OID. |
||
2649 | */ |
||
2650 | public function clearDocumentChangeSet(string $oid) |
||
2651 | { |
||
2652 | $this->documentChangeSets[$oid] = []; |
||
2653 | } |
||
2654 | |||
2655 | /* PropertyChangedListener implementation */ |
||
2656 | |||
2657 | /** |
||
2658 | * Notifies this UnitOfWork of a property change in a document. |
||
2659 | * |
||
2660 | * @param object $document The document that owns the property. |
||
2661 | * @param string $propertyName The name of the property that changed. |
||
2662 | * @param mixed $oldValue The old value of the property. |
||
2663 | * @param mixed $newValue The new value of the property. |
||
2664 | */ |
||
2665 | 2 | public function propertyChanged($document, $propertyName, $oldValue, $newValue) |
|
2666 | { |
||
2667 | 2 | $oid = spl_object_hash($document); |
|
2668 | 2 | $class = $this->dm->getClassMetadata(get_class($document)); |
|
2669 | |||
2670 | 2 | if (! isset($class->fieldMappings[$propertyName])) { |
|
2671 | 1 | return; // ignore non-persistent fields |
|
2672 | } |
||
2673 | |||
2674 | // Update changeset and mark document for synchronization |
||
2675 | 2 | $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; |
|
2676 | 2 | if (isset($this->scheduledForDirtyCheck[$class->name][$oid])) { |
|
2677 | return; |
||
2678 | } |
||
2679 | |||
2680 | 2 | $this->scheduleForDirtyCheck($document); |
|
2681 | 2 | } |
|
2682 | |||
2683 | /** |
||
2684 | * Gets the currently scheduled document insertions in this UnitOfWork. |
||
2685 | */ |
||
2686 | 3 | public function getScheduledDocumentInsertions(): array |
|
2687 | { |
||
2688 | 3 | return $this->documentInsertions; |
|
2689 | } |
||
2690 | |||
2691 | /** |
||
2692 | * Gets the currently scheduled document upserts in this UnitOfWork. |
||
2693 | */ |
||
2694 | 1 | public function getScheduledDocumentUpserts(): array |
|
2695 | { |
||
2696 | 1 | return $this->documentUpserts; |
|
2697 | } |
||
2698 | |||
2699 | /** |
||
2700 | * Gets the currently scheduled document updates in this UnitOfWork. |
||
2701 | */ |
||
2702 | 2 | public function getScheduledDocumentUpdates(): array |
|
2703 | { |
||
2704 | 2 | return $this->documentUpdates; |
|
2705 | } |
||
2706 | |||
2707 | /** |
||
2708 | * Gets the currently scheduled document deletions in this UnitOfWork. |
||
2709 | */ |
||
2710 | public function getScheduledDocumentDeletions(): array |
||
2711 | { |
||
2712 | return $this->documentDeletions; |
||
2713 | } |
||
2714 | |||
2715 | /** |
||
2716 | * Get the currently scheduled complete collection deletions |
||
2717 | */ |
||
2718 | public function getScheduledCollectionDeletions(): array |
||
2719 | { |
||
2720 | return $this->collectionDeletions; |
||
2721 | } |
||
2722 | |||
2723 | /** |
||
2724 | * Gets the currently scheduled collection inserts, updates and deletes. |
||
2725 | */ |
||
2726 | public function getScheduledCollectionUpdates(): array |
||
2727 | { |
||
2728 | return $this->collectionUpdates; |
||
2729 | } |
||
2730 | |||
2731 | /** |
||
2732 | * Helper method to initialize a lazy loading proxy or persistent collection. |
||
2733 | */ |
||
2734 | public function initializeObject(object $obj): void |
||
2735 | { |
||
2736 | if ($obj instanceof Proxy) { |
||
2737 | $obj->__load(); |
||
2738 | } elseif ($obj instanceof PersistentCollectionInterface) { |
||
2739 | $obj->initialize(); |
||
2740 | } |
||
2741 | } |
||
2742 | |||
2743 | private function objToStr(object $obj): string |
||
2744 | { |
||
2745 | return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj); |
||
2746 | } |
||
2747 | } |
||
2748 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.