Completed
Pull Request — master (#1427)
by Andreas
06:15
created

UnitOfWork::getParentAssociation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
ccs 4
cts 5
cp 0.8
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
crap 2.032
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\EventManager;
25
use Doctrine\Common\NotifyPropertyChanged;
26
use Doctrine\Common\PropertyChangedListener;
27
use Doctrine\MongoDB\GridFSFile;
28
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
29
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
30
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
31
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
32
use Doctrine\ODM\MongoDB\Proxy\Proxy;
33
use Doctrine\ODM\MongoDB\Query\Query;
34
use Doctrine\ODM\MongoDB\Types\Type;
35
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
36
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
37
38
/**
39
 * The UnitOfWork is responsible for tracking changes to objects during an
40
 * "object-level" transaction and for writing out changes to the database
41
 * in the correct order.
42
 *
43
 * @since       1.0
44
 */
45
class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = array();
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = array();
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @var array
101
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
102
     *           A value will only really be copied if the value in the document is modified
103
     *           by the user.
104
     */
105
    private $originalDocumentData = array();
106
107
    /**
108
     * Map of document changes. Keys are object ids (spl_object_hash).
109
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
110
     *
111
     * @var array
112
     */
113
    private $documentChangeSets = array();
114
115
    /**
116
     * The (cached) states of any known documents.
117
     * Keys are object ids (spl_object_hash).
118
     *
119
     * @var array
120
     */
121
    private $documentStates = array();
122
123
    /**
124
     * Map of documents that are scheduled for dirty checking at commit time.
125
     *
126
     * Documents are grouped by their class name, and then indexed by their SPL
127
     * object hash. This is only used for documents with a change tracking
128
     * policy of DEFERRED_EXPLICIT.
129
     *
130
     * @var array
131
     * @todo rename: scheduledForSynchronization
132
     */
133
    private $scheduledForDirtyCheck = array();
134
135
    /**
136
     * A list of all pending document insertions.
137
     *
138
     * @var array
139
     */
140
    private $documentInsertions = array();
141
142
    /**
143
     * A list of all pending document updates.
144
     *
145
     * @var array
146
     */
147
    private $documentUpdates = array();
148
149
    /**
150
     * A list of all pending document upserts.
151
     *
152
     * @var array
153
     */
154
    private $documentUpserts = array();
155
156
    /**
157
     * A list of all pending document deletions.
158
     *
159
     * @var array
160
     */
161
    private $documentDeletions = array();
162
163
    /**
164
     * All pending collection deletions.
165
     *
166
     * @var array
167
     */
168
    private $collectionDeletions = array();
169
170
    /**
171
     * All pending collection updates.
172
     *
173
     * @var array
174
     */
175
    private $collectionUpdates = array();
176
177
    /**
178
     * A list of documents related to collections scheduled for update or deletion
179
     *
180
     * @var array
181
     */
182
    private $hasScheduledCollections = array();
183
184
    /**
185
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
186
     * At the end of the UnitOfWork all these collections will make new snapshots
187
     * of their data.
188
     *
189
     * @var array
190
     */
191
    private $visitedCollections = array();
192
193
    /**
194
     * The DocumentManager that "owns" this UnitOfWork instance.
195
     *
196
     * @var DocumentManager
197
     */
198
    private $dm;
199
200
    /**
201
     * The EventManager used for dispatching events.
202
     *
203
     * @var EventManager
204
     */
205
    private $evm;
206
207
    /**
208
     * Additional documents that are scheduled for removal.
209
     *
210
     * @var array
211
     */
212
    private $orphanRemovals = array();
213
214
    /**
215
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
216
     *
217
     * @var HydratorFactory
218
     */
219
    private $hydratorFactory;
220
221
    /**
222
     * The document persister instances used to persist document instances.
223
     *
224
     * @var array
225
     */
226
    private $persisters = array();
227
228
    /**
229
     * The collection persister instance used to persist changes to collections.
230
     *
231
     * @var Persisters\CollectionPersister
232
     */
233
    private $collectionPersister;
234
235
    /**
236
     * The persistence builder instance used in DocumentPersisters.
237
     *
238
     * @var PersistenceBuilder
239
     */
240
    private $persistenceBuilder;
241
242
    /**
243
     * Array of parent associations between embedded documents
244
     *
245
     * @todo We might need to clean up this array in clear(), doDetach(), etc.
246
     * @var array
247
     */
248
    private $parentAssociations = array();
249
250
    /**
251
     * @var LifecycleEventManager
252
     */
253
    private $lifecycleEventManager;
254
255
    /**
256
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
257
     *
258
     * @param DocumentManager $dm
259
     * @param EventManager $evm
260
     * @param HydratorFactory $hydratorFactory
261
     */
262 977
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
263
    {
264 977
        $this->dm = $dm;
265 977
        $this->evm = $evm;
266 977
        $this->hydratorFactory = $hydratorFactory;
267 977
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
268 977
    }
269
270
    /**
271
     * Factory for returning new PersistenceBuilder instances used for preparing data into
272
     * queries for insert persistence.
273
     *
274
     * @return PersistenceBuilder $pb
275
     */
276 10
    public function getPersistenceBuilder()
277
    {
278 10
        if ( ! $this->persistenceBuilder) {
279 10
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
280
        }
281 10
        return $this->persistenceBuilder;
282
    }
283
284
    /**
285
     * Sets the parent association for a given embedded document.
286
     *
287
     * @param object $document
288
     * @param array $mapping
289
     * @param object $parent
290
     * @param string $propertyPath
291
     */
292 1
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
293
    {
294 1
        $oid = spl_object_hash($document);
295 1
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
296 1
    }
297
298
    /**
299
     * Gets the parent association for a given embedded document.
300
     *
301
     *     <code>
302
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
303
     *     </code>
304
     *
305
     * @param object $document
306
     * @return array $association
307
     */
308 1
    public function getParentAssociation($document)
309
    {
310 1
        $oid = spl_object_hash($document);
311 1
        if ( ! isset($this->parentAssociations[$oid])) {
312
            return null;
313
        }
314 1
        return $this->parentAssociations[$oid];
315
    }
316
317
    /**
318
     * Get the document persister instance for the given document name
319
     *
320
     * @param string $documentName
321
     * @return Persisters\DocumentPersister
322
     */
323 4
    public function getDocumentPersister($documentName)
324
    {
325 4
        if ( ! isset($this->persisters[$documentName])) {
326 4
            $class = $this->dm->getClassMetadata($documentName);
327
            $pb = $this->getPersistenceBuilder();
328
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
329
        }
330
        return $this->persisters[$documentName];
331
    }
332
333
    /**
334
     * Get the collection persister instance.
335
     *
336
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
337
     */
338
    public function getCollectionPersister()
339
    {
340
        if ( ! isset($this->collectionPersister)) {
341
            $pb = $this->getPersistenceBuilder();
342
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
343
        }
344
        return $this->collectionPersister;
345
    }
346
347
    /**
348
     * Set the document persister instance to use for the given document name
349
     *
350
     * @param string $documentName
351
     * @param Persisters\DocumentPersister $persister
352
     */
353
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
354
    {
355
        $this->persisters[$documentName] = $persister;
356
    }
357
358
    /**
359
     * Commits the UnitOfWork, executing all operations that have been postponed
360
     * up to this point. The state of all managed documents will be synchronized with
361
     * the database.
362
     *
363
     * The operations are executed in the following order:
364
     *
365
     * 1) All document insertions
366
     * 2) All document updates
367
     * 3) All document deletions
368
     *
369
     * @param object $document
370
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
371
     */
372 1
    public function commit($document = null, array $options = array())
373
    {
374
        // Raise preFlush
375 1
        if ($this->evm->hasListeners(Events::preFlush)) {
376
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
377
        }
378
379
        // Compute changes done since last commit.
380 1
        if ($document === null) {
381
            $this->computeChangeSets();
382 1
        } elseif (is_object($document)) {
383 1
            $this->computeSingleDocumentChangeSet($document);
384
        } elseif (is_array($document)) {
385
            foreach ($document as $object) {
386
                $this->computeSingleDocumentChangeSet($object);
387
            }
388
        }
389
390
        if ( ! ($this->documentInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentInsertions 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 empty(..) or ! empty(...) instead.

Loading history...
391
            $this->documentUpserts ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentUpserts 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 empty(..) or ! empty(...) instead.

Loading history...
392
            $this->documentDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
393
            $this->documentUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
394
            $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
395
            $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
396
            $this->orphanRemovals)
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
397
        ) {
398
            return; // Nothing to do.
399
        }
400
401
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
402
            foreach ($this->orphanRemovals as $removal) {
403
                $this->remove($removal);
404
            }
405
        }
406
407
        // Raise onFlush
408
        if ($this->evm->hasListeners(Events::onFlush)) {
409
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
410
        }
411
412
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
413
            list($class, $documents) = $classAndDocuments;
414
            $this->executeUpserts($class, $documents, $options);
415
        }
416
417
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
418
            list($class, $documents) = $classAndDocuments;
419
            $this->executeInserts($class, $documents, $options);
420
        }
421
422
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
423
            list($class, $documents) = $classAndDocuments;
424
            $this->executeUpdates($class, $documents, $options);
425
        }
426
427
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
428
            list($class, $documents) = $classAndDocuments;
429
            $this->executeDeletions($class, $documents, $options);
430
        }
431
432
        // Raise postFlush
433
        if ($this->evm->hasListeners(Events::postFlush)) {
434
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
435
        }
436
437
        // Clear up
438
        $this->documentInsertions =
439
        $this->documentUpserts =
440
        $this->documentUpdates =
441
        $this->documentDeletions =
442
        $this->documentChangeSets =
443
        $this->collectionUpdates =
444
        $this->collectionDeletions =
445
        $this->visitedCollections =
446
        $this->scheduledForDirtyCheck =
447
        $this->orphanRemovals =
448
        $this->hasScheduledCollections = array();
449
    }
450
451
    /**
452
     * Groups a list of scheduled documents by their class.
453
     *
454
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
455
     * @param bool $includeEmbedded
456
     * @return array Tuples of ClassMetadata and a corresponding array of objects
457
     */
458
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
459
    {
460
        if (empty($documents)) {
461
            return array();
462
        }
463
        $divided = array();
464
        $embeds = array();
465
        foreach ($documents as $oid => $d) {
466
            $className = get_class($d);
467
            if (isset($embeds[$className])) {
468
                continue;
469
            }
470
            if (isset($divided[$className])) {
471
                $divided[$className][1][$oid] = $d;
472
                continue;
473
            }
474
            $class = $this->dm->getClassMetadata($className);
475
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
476
                $embeds[$className] = true;
477
                continue;
478
            }
479
            if (empty($divided[$class->name])) {
480
                $divided[$class->name] = array($class, array($oid => $d));
481
            } else {
482
                $divided[$class->name][1][$oid] = $d;
483
            }
484
        }
485
        return $divided;
486
    }
487
488
    /**
489
     * Compute changesets of all documents scheduled for insertion.
490
     *
491
     * Embedded documents will not be processed.
492
     */
493 View Code Duplication
    private function computeScheduleInsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
494
    {
495
        foreach ($this->documentInsertions as $document) {
496
            $class = $this->dm->getClassMetadata(get_class($document));
497
            if ( ! $class->isEmbeddedDocument) {
498
                $this->computeChangeSet($class, $document);
499
            }
500
        }
501
    }
502
503
    /**
504
     * Compute changesets of all documents scheduled for upsert.
505
     *
506
     * Embedded documents will not be processed.
507
     */
508 View Code Duplication
    private function computeScheduleUpsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
509
    {
510
        foreach ($this->documentUpserts as $document) {
511
            $class = $this->dm->getClassMetadata(get_class($document));
512
            if ( ! $class->isEmbeddedDocument) {
513
                $this->computeChangeSet($class, $document);
514
            }
515
        }
516
    }
517
518
    /**
519
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
520
     *
521
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
522
     * 2. Proxies are skipped.
523
     * 3. Only if document is properly managed.
524
     *
525
     * @param  object $document
526
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
527
     * @return void
528
     */
529 1
    private function computeSingleDocumentChangeSet($document)
530
    {
531 1
        $state = $this->getDocumentState($document);
532
533
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
534
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
535
        }
536
537
        $class = $this->dm->getClassMetadata(get_class($document));
538
539
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
540
            $this->persist($document);
541
        }
542
543
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
544
        $this->computeScheduleInsertsChangeSets();
545
        $this->computeScheduleUpsertsChangeSets();
546
547
        // Ignore uninitialized proxy objects
548
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
549
            return;
550
        }
551
552
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
553
        $oid = spl_object_hash($document);
554
555 View Code Duplication
        if ( ! isset($this->documentInsertions[$oid])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
556
            && ! isset($this->documentUpserts[$oid])
557
            && ! isset($this->documentDeletions[$oid])
558
            && isset($this->documentStates[$oid])
559
        ) {
560
            $this->computeChangeSet($class, $document);
561
        }
562
    }
563
564
    /**
565
     * Gets the changeset for a document.
566
     *
567
     * @param object $document
568
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
569
     */
570
    public function getDocumentChangeSet($document)
571
    {
572
        $oid = spl_object_hash($document);
573
        if (isset($this->documentChangeSets[$oid])) {
574
            return $this->documentChangeSets[$oid];
575
        }
576
        return array();
577
    }
578
579
    /**
580
     * INTERNAL:
581
     * Sets the changeset for a document.
582
     *
583
     * @param object $document
584
     * @param array $changeset
585
     */
586
    public function setDocumentChangeSet($document, $changeset)
587
    {
588
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
589
    }
590
591
    /**
592
     * Get a documents actual data, flattening all the objects to arrays.
593
     *
594
     * @param object $document
595
     * @return array
596
     */
597
    public function getDocumentActualData($document)
598
    {
599
        $class = $this->dm->getClassMetadata(get_class($document));
600
        $actualData = array();
601
        foreach ($class->reflFields as $name => $refProp) {
602
            $mapping = $class->fieldMappings[$name];
603
            // skip not saved fields
604
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
605
                continue;
606
            }
607
            $value = $refProp->getValue($document);
608
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
609
                $value = new GridFSFile($value);
610
                $class->reflFields[$name]->setValue($document, $value);
611
                $actualData[$name] = $value;
612
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
613
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
614
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
615
                if ( ! $value instanceof Collection) {
616
                    $value = new ArrayCollection($value);
617
                }
618
619
                // Inject PersistentCollection
620
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
621
                $coll->setOwner($document, $mapping);
622
                $coll->setDirty( ! $value->isEmpty());
623
                $class->reflFields[$name]->setValue($document, $coll);
624
                $actualData[$name] = $coll;
625
            } else {
626
                $actualData[$name] = $value;
627
            }
628
        }
629
        return $actualData;
630
    }
631
632
    /**
633
     * Computes the changes that happened to a single document.
634
     *
635
     * Modifies/populates the following properties:
636
     *
637
     * {@link originalDocumentData}
638
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
639
     * then it was not fetched from the database and therefore we have no original
640
     * document data yet. All of the current document data is stored as the original document data.
641
     *
642
     * {@link documentChangeSets}
643
     * The changes detected on all properties of the document are stored there.
644
     * A change is a tuple array where the first entry is the old value and the second
645
     * entry is the new value of the property. Changesets are used by persisters
646
     * to INSERT/UPDATE the persistent document state.
647
     *
648
     * {@link documentUpdates}
649
     * If the document is already fully MANAGED (has been fetched from the database before)
650
     * and any changes to its properties are detected, then a reference to the document is stored
651
     * there to mark it for an update.
652
     *
653
     * @param ClassMetadata $class The class descriptor of the document.
654
     * @param object $document The document for which to compute the changes.
655
     */
656
    public function computeChangeSet(ClassMetadata $class, $document)
657
    {
658
        if ( ! $class->isInheritanceTypeNone()) {
659
            $class = $this->dm->getClassMetadata(get_class($document));
660
        }
661
662
        // Fire PreFlush lifecycle callbacks
663 View Code Duplication
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
664
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
665
        }
666
667
        $this->computeOrRecomputeChangeSet($class, $document);
668
    }
669
670
    /**
671
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
672
     *
673
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
674
     * @param object $document
675
     * @param boolean $recompute
676
     */
677
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
678
    {
679
        $oid = spl_object_hash($document);
680
        $actualData = $this->getDocumentActualData($document);
681
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
682
        if ($isNewDocument) {
683
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
684
            // These result in an INSERT.
685
            $this->originalDocumentData[$oid] = $actualData;
686
            $changeSet = array();
687
            foreach ($actualData as $propName => $actualValue) {
688
                /* At this PersistentCollection shouldn't be here, probably it
689
                 * was cloned and its ownership must be fixed
690
                 */
691
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
692
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
693
                    $actualValue = $actualData[$propName];
694
                }
695
                // ignore inverse side of reference relationship
696 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
697
                    continue;
698
                }
699
                $changeSet[$propName] = array(null, $actualValue);
700
            }
701
            $this->documentChangeSets[$oid] = $changeSet;
702
        } else {
703
            // Document is "fully" MANAGED: it was already fully persisted before
704
            // and we have a copy of the original data
705
            $originalData = $this->originalDocumentData[$oid];
706
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
707
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
708
                $changeSet = $this->documentChangeSets[$oid];
709
            } else {
710
                $changeSet = array();
711
            }
712
713
            foreach ($actualData as $propName => $actualValue) {
714
                // skip not saved fields
715
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
716
                    continue;
717
                }
718
719
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
720
721
                // skip if value has not changed
722
                if ($orgValue === $actualValue) {
723
                    if ($actualValue instanceof PersistentCollectionInterface) {
724
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
725
                            // consider dirty collections as changed as well
726
                            continue;
727
                        }
728
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
729
                        // but consider dirty GridFSFile instances as changed
730
                        continue;
731
                    }
732
                }
733
734
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
735
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
736
                    if ($orgValue !== null) {
737
                        $this->scheduleOrphanRemoval($orgValue);
738
                    }
739
740
                    $changeSet[$propName] = array($orgValue, $actualValue);
741
                    continue;
742
                }
743
744
                // if owning side of reference-one relationship
745
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
746
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
747
                        $this->scheduleOrphanRemoval($orgValue);
748
                    }
749
750
                    $changeSet[$propName] = array($orgValue, $actualValue);
751
                    continue;
752
                }
753
754
                if ($isChangeTrackingNotify) {
755
                    continue;
756
                }
757
758
                // ignore inverse side of reference relationship
759 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
760
                    continue;
761
                }
762
763
                // Persistent collection was exchanged with the "originally"
764
                // created one. This can only mean it was cloned and replaced
765
                // on another document.
766
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
767
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
768
                }
769
770
                // if embed-many or reference-many relationship
771
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
772
                    $changeSet[$propName] = array($orgValue, $actualValue);
773
                    /* If original collection was exchanged with a non-empty value
774
                     * and $set will be issued, there is no need to $unset it first
775
                     */
776
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
777
                        continue;
778
                    }
779
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
780
                        $this->scheduleCollectionDeletion($orgValue);
781
                    }
782
                    continue;
783
                }
784
785
                // skip equivalent date values
786
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
787
                    $dateType = Type::getType('date');
788
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
789
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
790
791
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
792
                        continue;
793
                    }
794
                }
795
796
                // regular field
797
                $changeSet[$propName] = array($orgValue, $actualValue);
798
            }
799
            if ($changeSet) {
800
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
801
                    ? $changeSet + $this->documentChangeSets[$oid]
802
                    : $changeSet;
803
804
                $this->originalDocumentData[$oid] = $actualData;
805
                $this->scheduleForUpdate($document);
806
            }
807
        }
808
809
        // Look for changes in associations of the document
810
        $associationMappings = array_filter(
811
            $class->associationMappings,
812
            function ($assoc) { return empty($assoc['notSaved']); }
813
        );
814
815
        foreach ($associationMappings as $mapping) {
816
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
817
818
            if ($value === null) {
819
                continue;
820
            }
821
822
            $this->computeAssociationChanges($document, $mapping, $value);
823
824
            if (isset($mapping['reference'])) {
825
                continue;
826
            }
827
828
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
829
830
            foreach ($values as $obj) {
831
                $oid2 = spl_object_hash($obj);
832
833
                if (isset($this->documentChangeSets[$oid2])) {
834
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
835
836
                    if ( ! $isNewDocument) {
837
                        $this->scheduleForUpdate($document);
838
                    }
839
840
                    break;
841
                }
842
            }
843
        }
844
    }
845
846
    /**
847
     * Computes all the changes that have been done to documents and collections
848
     * since the last commit and stores these changes in the _documentChangeSet map
849
     * temporarily for access by the persisters, until the UoW commit is finished.
850
     */
851
    public function computeChangeSets()
852
    {
853
        $this->computeScheduleInsertsChangeSets();
854
        $this->computeScheduleUpsertsChangeSets();
855
856
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
857
        foreach ($this->identityMap as $className => $documents) {
858
            $class = $this->dm->getClassMetadata($className);
859
            if ($class->isEmbeddedDocument) {
860
                /* we do not want to compute changes to embedded documents up front
861
                 * in case embedded document was replaced and its changeset
862
                 * would corrupt data. Embedded documents' change set will
863
                 * be calculated by reachability from owning document.
864
                 */
865
                continue;
866
            }
867
868
            // If change tracking is explicit or happens through notification, then only compute
869
            // changes on document of that type that are explicitly marked for synchronization.
870
            switch (true) {
871
                case ($class->isChangeTrackingDeferredImplicit()):
872
                    $documentsToProcess = $documents;
873
                    break;
874
875
                case (isset($this->scheduledForDirtyCheck[$className])):
876
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
877
                    break;
878
879
                default:
880
                    $documentsToProcess = array();
881
882
            }
883
884
            foreach ($documentsToProcess as $document) {
885
                // Ignore uninitialized proxy objects
886
                if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
887
                    continue;
888
                }
889
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
890
                $oid = spl_object_hash($document);
891 View Code Duplication
                if ( ! isset($this->documentInsertions[$oid])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
892
                    && ! isset($this->documentUpserts[$oid])
893
                    && ! isset($this->documentDeletions[$oid])
894
                    && isset($this->documentStates[$oid])
895
                ) {
896
                    $this->computeChangeSet($class, $document);
897
                }
898
            }
899
        }
900
    }
901
902
    /**
903
     * Computes the changes of an association.
904
     *
905
     * @param object $parentDocument
906
     * @param array $assoc
907
     * @param mixed $value The value of the association.
908
     * @throws \InvalidArgumentException
909
     */
910
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
911
    {
912
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
913
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
914
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
915
916
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
917
            return;
918
        }
919
920
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
921
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
922
                $this->scheduleCollectionUpdate($value);
923
            }
924
            $topmostOwner = $this->getOwningDocument($value->getOwner());
925
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
926
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
927
                $value->initialize();
928
                foreach ($value->getDeletedDocuments() as $orphan) {
929
                    $this->scheduleOrphanRemoval($orphan);
930
                }
931
            }
932
        }
933
934
        // Look through the documents, and in any of their associations,
935
        // for transient (new) documents, recursively. ("Persistence by reachability")
936
        // Unwrap. Uninitialized collections will simply be empty.
937
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
938
939
        $count = 0;
940
        foreach ($unwrappedValue as $key => $entry) {
941
            if ( ! is_object($entry)) {
942
                throw new \InvalidArgumentException(
943
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
944
                );
945
            }
946
947
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
948
949
            $state = $this->getDocumentState($entry, self::STATE_NEW);
950
951
            // Handle "set" strategy for multi-level hierarchy
952
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
953
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
954
955
            $count++;
956
957
            switch ($state) {
958
                case self::STATE_NEW:
959
                    if ( ! $assoc['isCascadePersist']) {
960
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
961
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
962
                            . ' Explicitly persist the new document or configure cascading persist operations'
963
                            . ' on the relationship.');
964
                    }
965
966
                    $this->persistNew($targetClass, $entry);
967
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
968
                    $this->computeChangeSet($targetClass, $entry);
969
                    break;
970
971
                case self::STATE_MANAGED:
972
                    if ($targetClass->isEmbeddedDocument) {
973
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
974
                        if ($knownParent && $knownParent !== $parentDocument) {
975
                            $entry = clone $entry;
976
                            if ($assoc['type'] === ClassMetadata::ONE) {
977
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
978
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
979
                            } else {
980
                                // must use unwrapped value to not trigger orphan removal
981
                                $unwrappedValue[$key] = $entry;
982
                            }
983
                            $this->persistNew($targetClass, $entry);
984
                        }
985
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
986
                        $this->computeChangeSet($targetClass, $entry);
987
                    }
988
                    break;
989
990
                case self::STATE_REMOVED:
991
                    // Consume the $value as array (it's either an array or an ArrayAccess)
992
                    // and remove the element from Collection.
993
                    if ($assoc['type'] === ClassMetadata::MANY) {
994
                        unset($value[$key]);
995
                    }
996
                    break;
997
998
                case self::STATE_DETACHED:
999
                    // Can actually not happen right now as we assume STATE_NEW,
1000
                    // so the exception will be raised from the DBAL layer (constraint violation).
1001
                    throw new \InvalidArgumentException('A detached document was found through a '
1002
                        . 'relationship during cascading a persist operation.');
1003
1004
                default:
1005
                    // MANAGED associated documents are already taken into account
1006
                    // during changeset calculation anyway, since they are in the identity map.
1007
1008
            }
1009
        }
1010
    }
1011
1012
    /**
1013
     * INTERNAL:
1014
     * Computes the changeset of an individual document, independently of the
1015
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1016
     *
1017
     * The passed document must be a managed document. If the document already has a change set
1018
     * because this method is invoked during a commit cycle then the change sets are added.
1019
     * whereby changes detected in this method prevail.
1020
     *
1021
     * @ignore
1022
     * @param ClassMetadata $class The class descriptor of the document.
1023
     * @param object $document The document for which to (re)calculate the change set.
1024
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1025
     */
1026
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1027
    {
1028
        // Ignore uninitialized proxy objects
1029
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1030
            return;
1031
        }
1032
1033
        $oid = spl_object_hash($document);
1034
1035
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1036
            throw new \InvalidArgumentException('Document must be managed.');
1037
        }
1038
1039
        if ( ! $class->isInheritanceTypeNone()) {
1040
            $class = $this->dm->getClassMetadata(get_class($document));
1041
        }
1042
1043
        $this->computeOrRecomputeChangeSet($class, $document, true);
1044
    }
1045
1046
    /**
1047
     * @param ClassMetadata $class
1048
     * @param object $document
1049
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1050
     */
1051
    private function persistNew(ClassMetadata $class, $document)
1052
    {
1053
        $this->lifecycleEventManager->prePersist($class, $document);
1054
        $oid = spl_object_hash($document);
1055
        $upsert = false;
1056
        if ($class->identifier) {
1057
            $idValue = $class->getIdentifierValue($document);
1058
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1059
1060
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1061
                throw new \InvalidArgumentException(sprintf(
1062
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1063
                    get_class($document)
1064
                ));
1065
            }
1066
1067
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1068
                throw new \InvalidArgumentException(sprintf(
1069
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1070
                    get_class($document)
1071
                ));
1072
            }
1073
1074
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1075
                $idValue = $class->idGenerator->generate($this->dm, $document);
1076
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1077
                $class->setIdentifierValue($document, $idValue);
1078
            }
1079
1080
            $this->documentIdentifiers[$oid] = $idValue;
1081
        } else {
1082
            // this is for embedded documents without identifiers
1083
            $this->documentIdentifiers[$oid] = $oid;
1084
        }
1085
1086
        $this->documentStates[$oid] = self::STATE_MANAGED;
1087
1088
        if ($upsert) {
1089
            $this->scheduleForUpsert($class, $document);
1090
        } else {
1091
            $this->scheduleForInsert($class, $document);
1092
        }
1093
    }
1094
1095
    /**
1096
     * Executes all document insertions for documents of the specified type.
1097
     *
1098
     * @param ClassMetadata $class
1099
     * @param array $documents Array of documents to insert
1100
     * @param array $options Array of options to be used with batchInsert()
1101
     */
1102 View Code Duplication
    private function executeInserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1103
    {
1104
        $persister = $this->getDocumentPersister($class->name);
1105
1106
        foreach ($documents as $oid => $document) {
1107
            $persister->addInsert($document);
1108
            unset($this->documentInsertions[$oid]);
1109
        }
1110
1111
        $persister->executeInserts($options);
1112
1113
        foreach ($documents as $document) {
1114
            $this->lifecycleEventManager->postPersist($class, $document);
1115
        }
1116
    }
1117
1118
    /**
1119
     * Executes all document upserts for documents of the specified type.
1120
     *
1121
     * @param ClassMetadata $class
1122
     * @param array $documents Array of documents to upsert
1123
     * @param array $options Array of options to be used with batchInsert()
1124
     */
1125 View Code Duplication
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1126
    {
1127
        $persister = $this->getDocumentPersister($class->name);
1128
1129
1130
        foreach ($documents as $oid => $document) {
1131
            $persister->addUpsert($document);
1132
            unset($this->documentUpserts[$oid]);
1133
        }
1134
1135
        $persister->executeUpserts($options);
1136
1137
        foreach ($documents as $document) {
1138
            $this->lifecycleEventManager->postPersist($class, $document);
1139
        }
1140
    }
1141
1142
    /**
1143
     * Executes all document updates for documents of the specified type.
1144
     *
1145
     * @param Mapping\ClassMetadata $class
1146
     * @param array $documents Array of documents to update
1147
     * @param array $options Array of options to be used with update()
1148
     */
1149
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1150
    {
1151
        $className = $class->name;
1152
        $persister = $this->getDocumentPersister($className);
1153
1154
        foreach ($documents as $oid => $document) {
1155
            $this->lifecycleEventManager->preUpdate($class, $document);
1156
1157
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1158
                $persister->update($document, $options);
1159
            }
1160
1161
            unset($this->documentUpdates[$oid]);
1162
1163
            $this->lifecycleEventManager->postUpdate($class, $document);
1164
        }
1165
    }
1166
1167
    /**
1168
     * Executes all document deletions for documents of the specified type.
1169
     *
1170
     * @param ClassMetadata $class
1171
     * @param array $documents Array of documents to delete
1172
     * @param array $options Array of options to be used with remove()
1173
     */
1174
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1175
    {
1176
        $persister = $this->getDocumentPersister($class->name);
1177
1178
        foreach ($documents as $oid => $document) {
1179
            if ( ! $class->isEmbeddedDocument) {
1180
                $persister->delete($document, $options);
1181
            }
1182
            unset(
1183
                $this->documentDeletions[$oid],
1184
                $this->documentIdentifiers[$oid],
1185
                $this->originalDocumentData[$oid]
1186
            );
1187
1188
            // Clear snapshot information for any referenced PersistentCollection
1189
            // http://www.doctrine-project.org/jira/browse/MODM-95
1190
            foreach ($class->associationMappings as $fieldMapping) {
1191
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1192
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1193
                    if ($value instanceof PersistentCollectionInterface) {
1194
                        $value->clearSnapshot();
1195
                    }
1196
                }
1197
            }
1198
1199
            // Document with this $oid after deletion treated as NEW, even if the $oid
1200
            // is obtained by a new document because the old one went out of scope.
1201
            $this->documentStates[$oid] = self::STATE_NEW;
1202
1203
            $this->lifecycleEventManager->postRemove($class, $document);
1204
        }
1205
    }
1206
1207
    /**
1208
     * Schedules a document for insertion into the database.
1209
     * If the document already has an identifier, it will be added to the
1210
     * identity map.
1211
     *
1212
     * @param ClassMetadata $class
1213
     * @param object $document The document to schedule for insertion.
1214
     * @throws \InvalidArgumentException
1215
     */
1216
    public function scheduleForInsert(ClassMetadata $class, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1217
    {
1218
        $oid = spl_object_hash($document);
1219
1220
        if (isset($this->documentUpdates[$oid])) {
1221
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1222
        }
1223
        if (isset($this->documentDeletions[$oid])) {
1224
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1225
        }
1226
        if (isset($this->documentInsertions[$oid])) {
1227
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1228
        }
1229
1230
        $this->documentInsertions[$oid] = $document;
1231
1232
        if (isset($this->documentIdentifiers[$oid])) {
1233
            $this->addToIdentityMap($document);
1234
        }
1235
    }
1236
1237
    /**
1238
     * Schedules a document for upsert into the database and adds it to the
1239
     * identity map
1240
     *
1241
     * @param ClassMetadata $class
1242
     * @param object $document The document to schedule for upsert.
1243
     * @throws \InvalidArgumentException
1244
     */
1245
    public function scheduleForUpsert(ClassMetadata $class, $document)
1246
    {
1247
        $oid = spl_object_hash($document);
1248
1249
        if ($class->isEmbeddedDocument) {
1250
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1251
        }
1252
        if (isset($this->documentUpdates[$oid])) {
1253
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1254
        }
1255
        if (isset($this->documentDeletions[$oid])) {
1256
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1257
        }
1258
        if (isset($this->documentUpserts[$oid])) {
1259
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1260
        }
1261
1262
        $this->documentUpserts[$oid] = $document;
1263
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1264
        $this->addToIdentityMap($document);
1265
    }
1266
1267
    /**
1268
     * Checks whether a document is scheduled for insertion.
1269
     *
1270
     * @param object $document
1271
     * @return boolean
1272
     */
1273
    public function isScheduledForInsert($document)
1274
    {
1275
        return isset($this->documentInsertions[spl_object_hash($document)]);
1276
    }
1277
1278
    /**
1279
     * Checks whether a document is scheduled for upsert.
1280
     *
1281
     * @param object $document
1282
     * @return boolean
1283
     */
1284
    public function isScheduledForUpsert($document)
1285
    {
1286
        return isset($this->documentUpserts[spl_object_hash($document)]);
1287
    }
1288
1289
    /**
1290
     * Schedules a document for being updated.
1291
     *
1292
     * @param object $document The document to schedule for being updated.
1293
     * @throws \InvalidArgumentException
1294
     */
1295
    public function scheduleForUpdate($document)
1296
    {
1297
        $oid = spl_object_hash($document);
1298
        if ( ! isset($this->documentIdentifiers[$oid])) {
1299
            throw new \InvalidArgumentException('Document has no identity.');
1300
        }
1301
1302
        if (isset($this->documentDeletions[$oid])) {
1303
            throw new \InvalidArgumentException('Document is removed.');
1304
        }
1305
1306
        if ( ! isset($this->documentUpdates[$oid])
1307
            && ! isset($this->documentInsertions[$oid])
1308
            && ! isset($this->documentUpserts[$oid])) {
1309
            $this->documentUpdates[$oid] = $document;
1310
        }
1311
    }
1312
1313
    /**
1314
     * Checks whether a document is registered as dirty in the unit of work.
1315
     * Note: Is not very useful currently as dirty documents are only registered
1316
     * at commit time.
1317
     *
1318
     * @param object $document
1319
     * @return boolean
1320
     */
1321
    public function isScheduledForUpdate($document)
1322
    {
1323
        return isset($this->documentUpdates[spl_object_hash($document)]);
1324
    }
1325
1326
    public function isScheduledForDirtyCheck($document)
1327
    {
1328
        $class = $this->dm->getClassMetadata(get_class($document));
1329
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1330
    }
1331
1332
    /**
1333
     * INTERNAL:
1334
     * Schedules a document for deletion.
1335
     *
1336
     * @param object $document
1337
     */
1338 1
    public function scheduleForDelete($document)
1339
    {
1340 1
        $oid = spl_object_hash($document);
1341
1342 1
        if (isset($this->documentInsertions[$oid])) {
1343
            if ($this->isInIdentityMap($document)) {
1344
                $this->removeFromIdentityMap($document);
1345
            }
1346
            unset($this->documentInsertions[$oid]);
1347
            return; // document has not been persisted yet, so nothing more to do.
1348
        }
1349
1350 1
        if ( ! $this->isInIdentityMap($document)) {
1351 1
            return; // ignore
1352
        }
1353
1354
        $this->removeFromIdentityMap($document);
1355
        $this->documentStates[$oid] = self::STATE_REMOVED;
1356
1357
        if (isset($this->documentUpdates[$oid])) {
1358
            unset($this->documentUpdates[$oid]);
1359
        }
1360
        if ( ! isset($this->documentDeletions[$oid])) {
1361
            $this->documentDeletions[$oid] = $document;
1362
        }
1363
    }
1364
1365
    /**
1366
     * Checks whether a document is registered as removed/deleted with the unit
1367
     * of work.
1368
     *
1369
     * @param object $document
1370
     * @return boolean
1371
     */
1372 1
    public function isScheduledForDelete($document)
1373
    {
1374 1
        return isset($this->documentDeletions[spl_object_hash($document)]);
1375
    }
1376
1377
    /**
1378
     * Checks whether a document is scheduled for insertion, update or deletion.
1379
     *
1380
     * @param $document
1381
     * @return boolean
1382
     */
1383
    public function isDocumentScheduled($document)
1384
    {
1385
        $oid = spl_object_hash($document);
1386
        return isset($this->documentInsertions[$oid]) ||
1387
            isset($this->documentUpserts[$oid]) ||
1388
            isset($this->documentUpdates[$oid]) ||
1389
            isset($this->documentDeletions[$oid]);
1390
    }
1391
1392
    /**
1393
     * INTERNAL:
1394
     * Registers a document in the identity map.
1395
     *
1396
     * Note that documents in a hierarchy are registered with the class name of
1397
     * the root document. Identifiers are serialized before being used as array
1398
     * keys to allow differentiation of equal, but not identical, values.
1399
     *
1400
     * @ignore
1401
     * @param object $document  The document to register.
1402
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1403
     *                  the document in question is already managed.
1404
     */
1405 1
    public function addToIdentityMap($document)
1406
    {
1407 1
        $class = $this->dm->getClassMetadata(get_class($document));
1408 1
        $id = $this->getIdForIdentityMap($document);
1409
1410 1
        if (isset($this->identityMap[$class->name][$id])) {
1411
            return false;
1412
        }
1413
1414 1
        $this->identityMap[$class->name][$id] = $document;
1415
1416 1
        if ($document instanceof NotifyPropertyChanged &&
1417 1
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1418
            $document->addPropertyChangedListener($this);
1419
        }
1420
1421 1
        return true;
1422
    }
1423
1424
    /**
1425
     * Gets the state of a document with regard to the current unit of work.
1426
     *
1427
     * @param object   $document
1428
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1429
     *                         This parameter can be set to improve performance of document state detection
1430
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1431
     *                         is either known or does not matter for the caller of the method.
1432
     * @return int The document state.
1433
     */
1434 3
    public function getDocumentState($document, $assume = null)
1435
    {
1436 3
        $oid = spl_object_hash($document);
1437
1438 3
        if (isset($this->documentStates[$oid])) {
1439
            return $this->documentStates[$oid];
1440
        }
1441
1442 3
        $class = $this->dm->getClassMetadata(get_class($document));
1443
1444
        if ($class->isEmbeddedDocument) {
1445
            return self::STATE_NEW;
1446
        }
1447
1448
        if ($assume !== null) {
1449
            return $assume;
1450
        }
1451
1452
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1453
         * known. Note that you cannot remember the NEW or DETACHED state in
1454
         * _documentStates since the UoW does not hold references to such
1455
         * objects and the object hash can be reused. More generally, because
1456
         * the state may "change" between NEW/DETACHED without the UoW being
1457
         * aware of it.
1458
         */
1459
        $id = $class->getIdentifierObject($document);
1460
1461
        if ($id === null) {
1462
            return self::STATE_NEW;
1463
        }
1464
1465
        // Check for a version field, if available, to avoid a DB lookup.
1466
        if ($class->isVersioned) {
1467
            return $class->getFieldValue($document, $class->versionField)
1468
                ? self::STATE_DETACHED
1469
                : self::STATE_NEW;
1470
        }
1471
1472
        // Last try before DB lookup: check the identity map.
1473
        if ($this->tryGetById($id, $class)) {
1474
            return self::STATE_DETACHED;
1475
        }
1476
1477
        // DB lookup
1478
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1479
            return self::STATE_DETACHED;
1480
        }
1481
1482
        return self::STATE_NEW;
1483
    }
1484
1485
    /**
1486
     * INTERNAL:
1487
     * Removes a document from the identity map. This effectively detaches the
1488
     * document from the persistence management of Doctrine.
1489
     *
1490
     * @ignore
1491
     * @param object $document
1492
     * @throws \InvalidArgumentException
1493
     * @return boolean
1494
     */
1495
    public function removeFromIdentityMap($document)
1496
    {
1497
        $oid = spl_object_hash($document);
1498
1499
        // Check if id is registered first
1500
        if ( ! isset($this->documentIdentifiers[$oid])) {
1501
            return false;
1502
        }
1503
1504
        $class = $this->dm->getClassMetadata(get_class($document));
1505
        $id = $this->getIdForIdentityMap($document);
1506
1507
        if (isset($this->identityMap[$class->name][$id])) {
1508
            unset($this->identityMap[$class->name][$id]);
1509
            $this->documentStates[$oid] = self::STATE_DETACHED;
1510
            return true;
1511
        }
1512
1513
        return false;
1514
    }
1515
1516
    /**
1517
     * INTERNAL:
1518
     * Gets a document in the identity map by its identifier hash.
1519
     *
1520
     * @ignore
1521
     * @param mixed         $id    Document identifier
1522
     * @param ClassMetadata $class Document class
1523
     * @return object
1524
     * @throws InvalidArgumentException if the class does not have an identifier
1525
     */
1526
    public function getById($id, ClassMetadata $class)
1527
    {
1528
        if ( ! $class->identifier) {
1529
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1530
        }
1531
1532
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1533
1534
        return $this->identityMap[$class->name][$serializedId];
1535
    }
1536
1537
    /**
1538
     * INTERNAL:
1539
     * Tries to get a document by its identifier hash. If no document is found
1540
     * for the given hash, FALSE is returned.
1541
     *
1542
     * @ignore
1543
     * @param mixed         $id    Document identifier
1544
     * @param ClassMetadata $class Document class
1545
     * @return mixed The found document or FALSE.
1546
     * @throws InvalidArgumentException if the class does not have an identifier
1547
     */
1548
    public function tryGetById($id, ClassMetadata $class)
1549
    {
1550
        if ( ! $class->identifier) {
1551
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1552
        }
1553
1554
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1555
1556
        return isset($this->identityMap[$class->name][$serializedId]) ?
1557
            $this->identityMap[$class->name][$serializedId] : false;
1558
    }
1559
1560
    /**
1561
     * Schedules a document for dirty-checking at commit-time.
1562
     *
1563
     * @param object $document The document to schedule for dirty-checking.
1564
     * @todo Rename: scheduleForSynchronization
1565
     */
1566
    public function scheduleForDirtyCheck($document)
1567
    {
1568
        $class = $this->dm->getClassMetadata(get_class($document));
1569
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1570
    }
1571
1572
    /**
1573
     * Checks whether a document is registered in the identity map.
1574
     *
1575
     * @param object $document
1576
     * @return boolean
1577
     */
1578 1
    public function isInIdentityMap($document)
1579
    {
1580 1
        $oid = spl_object_hash($document);
1581
1582 1
        if ( ! isset($this->documentIdentifiers[$oid])) {
1583 1
            return false;
1584
        }
1585
1586
        $class = $this->dm->getClassMetadata(get_class($document));
1587
        $id = $this->getIdForIdentityMap($document);
1588
1589
        return isset($this->identityMap[$class->name][$id]);
1590
    }
1591
1592
    /**
1593
     * @param object $document
1594
     * @return string
1595
     */
1596 1
    private function getIdForIdentityMap($document)
1597
    {
1598 1
        $class = $this->dm->getClassMetadata(get_class($document));
1599
1600 1
        if ( ! $class->identifier) {
1601 1
            $id = spl_object_hash($document);
1602
        } else {
1603
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1604
            $id = serialize($class->getDatabaseIdentifierValue($id));
1605
        }
1606
1607 1
        return $id;
1608
    }
1609
1610
    /**
1611
     * INTERNAL:
1612
     * Checks whether an identifier exists in the identity map.
1613
     *
1614
     * @ignore
1615
     * @param string $id
1616
     * @param string $rootClassName
1617
     * @return boolean
1618
     */
1619
    public function containsId($id, $rootClassName)
1620
    {
1621
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1622
    }
1623
1624
    /**
1625
     * Persists a document as part of the current unit of work.
1626
     *
1627
     * @param object $document The document to persist.
1628
     * @throws MongoDBException If trying to persist MappedSuperclass.
1629
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1630
     */
1631 562
    public function persist($document)
1632
    {
1633 562
        $class = $this->dm->getClassMetadata(get_class($document));
1634 1
        if ($class->isMappedSuperclass) {
1635 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1636
        }
1637
        $visited = array();
1638
        $this->doPersist($document, $visited);
1639
    }
1640
1641
    /**
1642
     * Saves a document as part of the current unit of work.
1643
     * This method is internally called during save() cascades as it tracks
1644
     * the already visited documents to prevent infinite recursions.
1645
     *
1646
     * NOTE: This method always considers documents that are not yet known to
1647
     * this UnitOfWork as NEW.
1648
     *
1649
     * @param object $document The document to persist.
1650
     * @param array $visited The already visited documents.
1651
     * @throws \InvalidArgumentException
1652
     * @throws MongoDBException
1653
     */
1654
    private function doPersist($document, array &$visited)
1655
    {
1656
        $oid = spl_object_hash($document);
1657
        if (isset($visited[$oid])) {
1658
            return; // Prevent infinite recursion
1659
        }
1660
1661
        $visited[$oid] = $document; // Mark visited
1662
1663
        $class = $this->dm->getClassMetadata(get_class($document));
1664
1665
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1666
        switch ($documentState) {
1667
            case self::STATE_MANAGED:
1668
                // Nothing to do, except if policy is "deferred explicit"
1669
                if ($class->isChangeTrackingDeferredExplicit()) {
1670
                    $this->scheduleForDirtyCheck($document);
1671
                }
1672
                break;
1673
            case self::STATE_NEW:
1674
                $this->persistNew($class, $document);
1675
                break;
1676
1677
            case self::STATE_REMOVED:
1678
                // Document becomes managed again
1679
                unset($this->documentDeletions[$oid]);
1680
1681
                $this->documentStates[$oid] = self::STATE_MANAGED;
1682
                break;
1683
1684
            case self::STATE_DETACHED:
1685
                throw new \InvalidArgumentException(
1686
                    'Behavior of persist() for a detached document is not yet defined.');
1687
1688
            default:
1689
                throw MongoDBException::invalidDocumentState($documentState);
1690
        }
1691
1692
        $this->cascadePersist($document, $visited);
1693
    }
1694
1695
    /**
1696
     * Deletes a document as part of the current unit of work.
1697
     *
1698
     * @param object $document The document to remove.
1699
     */
1700
    public function remove($document)
1701
    {
1702
        $visited = array();
1703
        $this->doRemove($document, $visited);
1704
    }
1705
1706
    /**
1707
     * Deletes a document as part of the current unit of work.
1708
     *
1709
     * This method is internally called during delete() cascades as it tracks
1710
     * the already visited documents to prevent infinite recursions.
1711
     *
1712
     * @param object $document The document to delete.
1713
     * @param array $visited The map of the already visited documents.
1714
     * @throws MongoDBException
1715
     */
1716
    private function doRemove($document, array &$visited)
1717
    {
1718
        $oid = spl_object_hash($document);
1719
        if (isset($visited[$oid])) {
1720
            return; // Prevent infinite recursion
1721
        }
1722
1723
        $visited[$oid] = $document; // mark visited
1724
1725
        /* Cascade first, because scheduleForDelete() removes the entity from
1726
         * the identity map, which can cause problems when a lazy Proxy has to
1727
         * be initialized for the cascade operation.
1728
         */
1729
        $this->cascadeRemove($document, $visited);
1730
1731
        $class = $this->dm->getClassMetadata(get_class($document));
1732
        $documentState = $this->getDocumentState($document);
1733
        switch ($documentState) {
1734
            case self::STATE_NEW:
1735
            case self::STATE_REMOVED:
1736
                // nothing to do
1737
                break;
1738
            case self::STATE_MANAGED:
1739
                $this->lifecycleEventManager->preRemove($class, $document);
1740
                $this->scheduleForDelete($document);
1741
                break;
1742
            case self::STATE_DETACHED:
1743
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1744
            default:
1745
                throw MongoDBException::invalidDocumentState($documentState);
1746
        }
1747
    }
1748
1749
    /**
1750
     * Merges the state of the given detached document into this UnitOfWork.
1751
     *
1752
     * @param object $document
1753
     * @return object The managed copy of the document.
1754
     */
1755 3
    public function merge($document)
1756
    {
1757 3
        $visited = array();
1758
1759 3
        return $this->doMerge($document, $visited);
1760
    }
1761
1762
    /**
1763
     * Executes a merge operation on a document.
1764
     *
1765
     * @param object      $document
1766
     * @param array       $visited
1767
     * @param object|null $prevManagedCopy
1768
     * @param array|null  $assoc
1769
     *
1770
     * @return object The managed copy of the document.
1771
     *
1772
     * @throws InvalidArgumentException If the entity instance is NEW.
1773
     * @throws LockException If the document uses optimistic locking through a
1774
     *                       version attribute and the version check against the
1775
     *                       managed copy fails.
1776
     */
1777 3
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1778
    {
1779 3
        $oid = spl_object_hash($document);
1780
1781 3
        if (isset($visited[$oid])) {
1782
            return $visited[$oid]; // Prevent infinite recursion
1783
        }
1784
1785 3
        $visited[$oid] = $document; // mark visited
1786
1787 3
        $class = $this->dm->getClassMetadata(get_class($document));
1788
1789
        /* First we assume DETACHED, although it can still be NEW but we can
1790
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1791
         * an identity, we need to fetch it from the DB anyway in order to
1792
         * merge. MANAGED documents are ignored by the merge operation.
1793
         */
1794
        $managedCopy = $document;
1795
1796
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1797
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1798
                $document->__load();
1799
            }
1800
1801
            $identifier = $class->getIdentifier();
1802
            // We always have one element in the identifier array but it might be null
1803
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1804
            $managedCopy = null;
1805
1806
            // Try to fetch document from the database
1807
            if (! $class->isEmbeddedDocument && $id !== null) {
1808
                $managedCopy = $this->dm->find($class->name, $id);
1809
1810
                // Managed copy may be removed in which case we can't merge
1811
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1812
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1813
                }
1814
1815
                if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1816
                    $managedCopy->__load();
1817
                }
1818
            }
1819
1820
            if ($managedCopy === null) {
1821
                // Create a new managed instance
1822
                $managedCopy = $class->newInstance();
1823
                if ($id !== null) {
1824
                    $class->setIdentifierValue($managedCopy, $id);
1825
                }
1826
                $this->persistNew($class, $managedCopy);
1827
            }
1828
1829
            if ($class->isVersioned) {
1830
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1831
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1832
1833
                // Throw exception if versions don't match
1834
                if ($managedCopyVersion != $documentVersion) {
1835
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1836
                }
1837
            }
1838
1839
            // Merge state of $document into existing (managed) document
1840
            foreach ($class->reflClass->getProperties() as $prop) {
1841
                $name = $prop->name;
1842
                $prop->setAccessible(true);
1843
                if ( ! isset($class->associationMappings[$name])) {
1844
                    if ( ! $class->isIdentifier($name)) {
1845
                        $prop->setValue($managedCopy, $prop->getValue($document));
1846
                    }
1847
                } else {
1848
                    $assoc2 = $class->associationMappings[$name];
1849
1850
                    if ($assoc2['type'] === 'one') {
1851
                        $other = $prop->getValue($document);
1852
1853
                        if ($other === null) {
1854
                            $prop->setValue($managedCopy, null);
1855
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1856
                            // Do not merge fields marked lazy that have not been fetched
1857
                            continue;
1858
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1859
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1860
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1861
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1862
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1863
                                $relatedId = $targetClass->getIdentifierObject($other);
1864
1865
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses 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 empty(..) or ! empty(...) instead.

Loading history...
1866
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1867
                                } else {
1868
                                    $other = $this
1869
                                        ->dm
1870
                                        ->getProxyFactory()
1871
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1872
                                    $this->registerManaged($other, $relatedId, array());
0 ignored issues
show
Documentation introduced by
$relatedId is of type object<MongoId>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1873
                                }
1874
                            }
1875
1876
                            $prop->setValue($managedCopy, $other);
1877
                        }
1878
                    } else {
1879
                        $mergeCol = $prop->getValue($document);
1880
1881
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1882
                            /* Do not merge fields marked lazy that have not
1883
                             * been fetched. Keep the lazy persistent collection
1884
                             * of the managed copy.
1885
                             */
1886
                            continue;
1887
                        }
1888
1889
                        $managedCol = $prop->getValue($managedCopy);
1890
1891
                        if ( ! $managedCol) {
1892
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1893
                            $managedCol->setOwner($managedCopy, $assoc2);
1894
                            $prop->setValue($managedCopy, $managedCol);
1895
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1896
                        }
1897
1898
                        /* Note: do not process association's target documents.
1899
                         * They will be handled during the cascade. Initialize
1900
                         * and, if necessary, clear $managedCol for now.
1901
                         */
1902
                        if ($assoc2['isCascadeMerge']) {
1903
                            $managedCol->initialize();
1904
1905
                            // If $managedCol differs from the merged collection, clear and set dirty
1906
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1907
                                $managedCol->unwrap()->clear();
1908
                                $managedCol->setDirty(true);
1909
1910
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1911
                                    $this->scheduleForDirtyCheck($managedCopy);
1912
                                }
1913
                            }
1914
                        }
1915
                    }
1916
                }
1917
1918
                if ($class->isChangeTrackingNotify()) {
1919
                    // Just treat all properties as changed, there is no other choice.
1920
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1921
                }
1922
            }
1923
1924
            if ($class->isChangeTrackingDeferredExplicit()) {
1925
                $this->scheduleForDirtyCheck($document);
1926
            }
1927
        }
1928
1929
        if ($prevManagedCopy !== null) {
1930
            $assocField = $assoc['fieldName'];
1931
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1932
1933
            if ($assoc['type'] === 'one') {
1934
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1935
            } else {
1936
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1937
1938
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1939
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1940
                }
1941
            }
1942
        }
1943
1944
        // Mark the managed copy visited as well
1945
        $visited[spl_object_hash($managedCopy)] = true;
1946
1947
        $this->cascadeMerge($document, $managedCopy, $visited);
1948
1949
        return $managedCopy;
1950
    }
1951
1952
    /**
1953
     * Detaches a document from the persistence management. It's persistence will
1954
     * no longer be managed by Doctrine.
1955
     *
1956
     * @param object $document The document to detach.
1957
     */
1958
    public function detach($document)
1959
    {
1960
        $visited = array();
1961
        $this->doDetach($document, $visited);
1962
    }
1963
1964
    /**
1965
     * Executes a detach operation on the given document.
1966
     *
1967
     * @param object $document
1968
     * @param array $visited
1969
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1970
     */
1971
    private function doDetach($document, array &$visited)
1972
    {
1973
        $oid = spl_object_hash($document);
1974
        if (isset($visited[$oid])) {
1975
            return; // Prevent infinite recursion
1976
        }
1977
1978
        $visited[$oid] = $document; // mark visited
1979
1980
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1981
            case self::STATE_MANAGED:
1982
                $this->removeFromIdentityMap($document);
1983
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1984
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1985
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1986
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1987
                    $this->hasScheduledCollections[$oid]);
1988
                break;
1989
            case self::STATE_NEW:
1990
            case self::STATE_DETACHED:
1991
                return;
1992
        }
1993
1994
        $this->cascadeDetach($document, $visited);
1995
    }
1996
1997
    /**
1998
     * Refreshes the state of the given document from the database, overwriting
1999
     * any local, unpersisted changes.
2000
     *
2001
     * @param object $document The document to refresh.
2002
     * @throws \InvalidArgumentException If the document is not MANAGED.
2003
     */
2004
    public function refresh($document)
2005
    {
2006
        $visited = array();
2007
        $this->doRefresh($document, $visited);
2008
    }
2009
2010
    /**
2011
     * Executes a refresh operation on a document.
2012
     *
2013
     * @param object $document The document to refresh.
2014
     * @param array $visited The already visited documents during cascades.
2015
     * @throws \InvalidArgumentException If the document is not MANAGED.
2016
     */
2017
    private function doRefresh($document, array &$visited)
2018
    {
2019
        $oid = spl_object_hash($document);
2020
        if (isset($visited[$oid])) {
2021
            return; // Prevent infinite recursion
2022
        }
2023
2024
        $visited[$oid] = $document; // mark visited
2025
2026
        $class = $this->dm->getClassMetadata(get_class($document));
2027
2028
        if ( ! $class->isEmbeddedDocument) {
2029
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2030
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2031
                $this->getDocumentPersister($class->name)->refresh($id, $document);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ODM\MongoDB\Per...entPersister::refresh() has been deprecated with message: The first argument is deprecated.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2032
            } else {
2033
                throw new \InvalidArgumentException('Document is not MANAGED.');
2034
            }
2035
        }
2036
2037
        $this->cascadeRefresh($document, $visited);
2038
    }
2039
2040
    /**
2041
     * Cascades a refresh operation to associated documents.
2042
     *
2043
     * @param object $document
2044
     * @param array $visited
2045
     */
2046
    private function cascadeRefresh($document, array &$visited)
2047
    {
2048
        $class = $this->dm->getClassMetadata(get_class($document));
2049
2050
        $associationMappings = array_filter(
2051
            $class->associationMappings,
2052
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2053
        );
2054
2055
        foreach ($associationMappings as $mapping) {
2056
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2057
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2058
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2059
                    // Unwrap so that foreach() does not initialize
2060
                    $relatedDocuments = $relatedDocuments->unwrap();
2061
                }
2062
                foreach ($relatedDocuments as $relatedDocument) {
2063
                    $this->doRefresh($relatedDocument, $visited);
2064
                }
2065
            } elseif ($relatedDocuments !== null) {
2066
                $this->doRefresh($relatedDocuments, $visited);
2067
            }
2068
        }
2069
    }
2070
2071
    /**
2072
     * Cascades a detach operation to associated documents.
2073
     *
2074
     * @param object $document
2075
     * @param array $visited
2076
     */
2077 View Code Duplication
    private function cascadeDetach($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2078
    {
2079
        $class = $this->dm->getClassMetadata(get_class($document));
2080
        foreach ($class->fieldMappings as $mapping) {
2081
            if ( ! $mapping['isCascadeDetach']) {
2082
                continue;
2083
            }
2084
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2085
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2086
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2087
                    // Unwrap so that foreach() does not initialize
2088
                    $relatedDocuments = $relatedDocuments->unwrap();
2089
                }
2090
                foreach ($relatedDocuments as $relatedDocument) {
2091
                    $this->doDetach($relatedDocument, $visited);
2092
                }
2093
            } elseif ($relatedDocuments !== null) {
2094
                $this->doDetach($relatedDocuments, $visited);
2095
            }
2096
        }
2097
    }
2098
    /**
2099
     * Cascades a merge operation to associated documents.
2100
     *
2101
     * @param object $document
2102
     * @param object $managedCopy
2103
     * @param array $visited
2104
     */
2105
    private function cascadeMerge($document, $managedCopy, array &$visited)
2106
    {
2107
        $class = $this->dm->getClassMetadata(get_class($document));
2108
2109
        $associationMappings = array_filter(
2110
            $class->associationMappings,
2111
            function ($assoc) { return $assoc['isCascadeMerge']; }
2112
        );
2113
2114
        foreach ($associationMappings as $assoc) {
2115
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2116
2117
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2118
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2119
                    // Collections are the same, so there is nothing to do
2120
                    continue;
2121
                }
2122
2123
                foreach ($relatedDocuments as $relatedDocument) {
2124
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2125
                }
2126
            } elseif ($relatedDocuments !== null) {
2127
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2128
            }
2129
        }
2130
    }
2131
2132
    /**
2133
     * Cascades the save operation to associated documents.
2134
     *
2135
     * @param object $document
2136
     * @param array $visited
2137
     */
2138
    private function cascadePersist($document, array &$visited)
2139
    {
2140
        $class = $this->dm->getClassMetadata(get_class($document));
2141
2142
        $associationMappings = array_filter(
2143
            $class->associationMappings,
2144
            function ($assoc) { return $assoc['isCascadePersist']; }
2145
        );
2146
2147
        foreach ($associationMappings as $fieldName => $mapping) {
2148
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2149
2150
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2151
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2152
                    if ($relatedDocuments->getOwner() !== $document) {
2153
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2154
                    }
2155
                    // Unwrap so that foreach() does not initialize
2156
                    $relatedDocuments = $relatedDocuments->unwrap();
2157
                }
2158
2159
                $count = 0;
2160
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2161
                    if ( ! empty($mapping['embedded'])) {
2162
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2163
                        if ($knownParent && $knownParent !== $document) {
2164
                            $relatedDocument = clone $relatedDocument;
2165
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2166
                        }
2167
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2168
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2169
                    }
2170
                    $this->doPersist($relatedDocument, $visited);
2171
                }
2172
            } elseif ($relatedDocuments !== null) {
2173
                if ( ! empty($mapping['embedded'])) {
2174
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2175
                    if ($knownParent && $knownParent !== $document) {
2176
                        $relatedDocuments = clone $relatedDocuments;
2177
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2178
                    }
2179
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2180
                }
2181
                $this->doPersist($relatedDocuments, $visited);
2182
            }
2183
        }
2184
    }
2185
2186
    /**
2187
     * Cascades the delete operation to associated documents.
2188
     *
2189
     * @param object $document
2190
     * @param array $visited
2191
     */
2192 View Code Duplication
    private function cascadeRemove($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2193
    {
2194
        $class = $this->dm->getClassMetadata(get_class($document));
2195
        foreach ($class->fieldMappings as $mapping) {
2196
            if ( ! $mapping['isCascadeRemove']) {
2197
                continue;
2198
            }
2199
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2200
                $document->__load();
2201
            }
2202
2203
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2204
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2205
                // If its a PersistentCollection initialization is intended! No unwrap!
2206
                foreach ($relatedDocuments as $relatedDocument) {
2207
                    $this->doRemove($relatedDocument, $visited);
2208
                }
2209
            } elseif ($relatedDocuments !== null) {
2210
                $this->doRemove($relatedDocuments, $visited);
2211
            }
2212
        }
2213
    }
2214
2215
    /**
2216
     * Acquire a lock on the given document.
2217
     *
2218
     * @param object $document
2219
     * @param int $lockMode
2220
     * @param int $lockVersion
2221
     * @throws LockException
2222
     * @throws \InvalidArgumentException
2223
     */
2224 1
    public function lock($document, $lockMode, $lockVersion = null)
2225
    {
2226 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2227
            throw new \InvalidArgumentException('Document is not MANAGED.');
2228
        }
2229
2230
        $documentName = get_class($document);
2231
        $class = $this->dm->getClassMetadata($documentName);
2232
2233
        if ($lockMode == LockMode::OPTIMISTIC) {
2234
            if ( ! $class->isVersioned) {
2235
                throw LockException::notVersioned($documentName);
2236
            }
2237
2238
            if ($lockVersion != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $lockVersion of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
2239
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2240
                if ($documentVersion != $lockVersion) {
2241
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2242
                }
2243
            }
2244
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2245
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2246
        }
2247
    }
2248
2249
    /**
2250
     * Releases a lock on the given document.
2251
     *
2252
     * @param object $document
2253
     * @throws \InvalidArgumentException
2254
     */
2255
    public function unlock($document)
2256
    {
2257
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2258
            throw new \InvalidArgumentException("Document is not MANAGED.");
2259
        }
2260
        $documentName = get_class($document);
2261
        $this->getDocumentPersister($documentName)->unlock($document);
2262
    }
2263
2264
    /**
2265
     * Clears the UnitOfWork.
2266
     *
2267
     * @param string|null $documentName if given, only documents of this type will get detached.
2268
     */
2269 6
    public function clear($documentName = null)
2270
    {
2271 6
        if ($documentName === null) {
2272 6
            $this->identityMap =
2273 6
            $this->documentIdentifiers =
2274 6
            $this->originalDocumentData =
2275 6
            $this->documentChangeSets =
2276 6
            $this->documentStates =
2277 6
            $this->scheduledForDirtyCheck =
2278 6
            $this->documentInsertions =
2279 6
            $this->documentUpserts =
2280 6
            $this->documentUpdates =
2281 6
            $this->documentDeletions =
2282 6
            $this->collectionUpdates =
2283 6
            $this->collectionDeletions =
2284 6
            $this->parentAssociations =
2285 6
            $this->orphanRemovals =
2286 6
            $this->hasScheduledCollections = array();
2287
        } else {
2288
            $visited = array();
2289
            foreach ($this->identityMap as $className => $documents) {
2290
                if ($className === $documentName) {
2291
                    foreach ($documents as $document) {
2292
                        $this->doDetach($document, $visited);
2293
                    }
2294
                }
2295
            }
2296
        }
2297
2298 6 View Code Duplication
        if ($this->evm->hasListeners(Events::onClear)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2299
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2300
        }
2301 6
    }
2302
2303
    /**
2304
     * INTERNAL:
2305
     * Schedules an embedded document for removal. The remove() operation will be
2306
     * invoked on that document at the beginning of the next commit of this
2307
     * UnitOfWork.
2308
     *
2309
     * @ignore
2310
     * @param object $document
2311
     */
2312
    public function scheduleOrphanRemoval($document)
2313
    {
2314
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2315
    }
2316
2317
    /**
2318
     * INTERNAL:
2319
     * Unschedules an embedded or referenced object for removal.
2320
     *
2321
     * @ignore
2322
     * @param object $document
2323
     */
2324
    public function unscheduleOrphanRemoval($document)
2325
    {
2326
        $oid = spl_object_hash($document);
2327
        if (isset($this->orphanRemovals[$oid])) {
2328
            unset($this->orphanRemovals[$oid]);
2329
        }
2330
    }
2331
2332
    /**
2333
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2334
     *  1) sets owner if it was cloned
2335
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2336
     *  3) NOP if state is OK
2337
     * Returned collection should be used from now on (only important with 2nd point)
2338
     *
2339
     * @param PersistentCollectionInterface $coll
2340
     * @param object $document
2341
     * @param ClassMetadata $class
2342
     * @param string $propName
2343
     * @return PersistentCollectionInterface
2344
     */
2345
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2346
    {
2347
        $owner = $coll->getOwner();
2348
        if ($owner === null) { // cloned
2349
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2350
        } elseif ($owner !== $document) { // no clone, we have to fix
2351
            if ( ! $coll->isInitialized()) {
2352
                $coll->initialize(); // we have to do this otherwise the cols share state
2353
            }
2354
            $newValue = clone $coll;
2355
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2356
            $class->reflFields[$propName]->setValue($document, $newValue);
2357
            if ($this->isScheduledForUpdate($document)) {
2358
                // @todo following line should be superfluous once collections are stored in change sets
2359
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2360
            }
2361
            return $newValue;
2362
        }
2363
        return $coll;
2364
    }
2365
2366
    /**
2367
     * INTERNAL:
2368
     * Schedules a complete collection for removal when this UnitOfWork commits.
2369
     *
2370
     * @param PersistentCollectionInterface $coll
2371
     */
2372
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2373
    {
2374
        $oid = spl_object_hash($coll);
2375
        unset($this->collectionUpdates[$oid]);
2376
        if ( ! isset($this->collectionDeletions[$oid])) {
2377
            $this->collectionDeletions[$oid] = $coll;
2378
            $this->scheduleCollectionOwner($coll);
2379
        }
2380
    }
2381
2382
    /**
2383
     * Checks whether a PersistentCollection is scheduled for deletion.
2384
     *
2385
     * @param PersistentCollectionInterface $coll
2386
     * @return boolean
2387
     */
2388
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2389
    {
2390
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2391
    }
2392
2393
    /**
2394
     * INTERNAL:
2395
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2396
     *
2397
     * @param PersistentCollectionInterface $coll
2398
     */
2399 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2400
    {
2401
        $oid = spl_object_hash($coll);
2402
        if (isset($this->collectionDeletions[$oid])) {
2403
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2404
            unset($this->collectionDeletions[$oid]);
2405
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2406
        }
2407
    }
2408
2409
    /**
2410
     * INTERNAL:
2411
     * Schedules a collection for update when this UnitOfWork commits.
2412
     *
2413
     * @param PersistentCollectionInterface $coll
2414
     */
2415
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2416
    {
2417
        $mapping = $coll->getMapping();
2418
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2419
            /* There is no need to $unset collection if it will be $set later
2420
             * This is NOP if collection is not scheduled for deletion
2421
             */
2422
            $this->unscheduleCollectionDeletion($coll);
2423
        }
2424
        $oid = spl_object_hash($coll);
2425
        if ( ! isset($this->collectionUpdates[$oid])) {
2426
            $this->collectionUpdates[$oid] = $coll;
2427
            $this->scheduleCollectionOwner($coll);
2428
        }
2429
    }
2430
2431
    /**
2432
     * INTERNAL:
2433
     * Unschedules a collection from being updated when this UnitOfWork commits.
2434
     *
2435
     * @param PersistentCollectionInterface $coll
2436
     */
2437 View Code Duplication
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2438
    {
2439
        $oid = spl_object_hash($coll);
2440
        if (isset($this->collectionUpdates[$oid])) {
2441
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2442
            unset($this->collectionUpdates[$oid]);
2443
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2444
        }
2445
    }
2446
2447
    /**
2448
     * Checks whether a PersistentCollection is scheduled for update.
2449
     *
2450
     * @param PersistentCollectionInterface $coll
2451
     * @return boolean
2452
     */
2453
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2454
    {
2455
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2456
    }
2457
2458
    /**
2459
     * INTERNAL:
2460
     * Gets PersistentCollections that have been visited during computing change
2461
     * set of $document
2462
     *
2463
     * @param object $document
2464
     * @return PersistentCollectionInterface[]
2465
     */
2466
    public function getVisitedCollections($document)
2467
    {
2468
        $oid = spl_object_hash($document);
2469
        return isset($this->visitedCollections[$oid])
2470
                ? $this->visitedCollections[$oid]
2471
                : array();
2472
    }
2473
2474
    /**
2475
     * INTERNAL:
2476
     * Gets PersistentCollections that are scheduled to update and related to $document
2477
     *
2478
     * @param object $document
2479
     * @return array
2480
     */
2481
    public function getScheduledCollections($document)
2482
    {
2483
        $oid = spl_object_hash($document);
2484
        return isset($this->hasScheduledCollections[$oid])
2485
                ? $this->hasScheduledCollections[$oid]
2486
                : array();
2487
    }
2488
2489
    /**
2490
     * Checks whether the document is related to a PersistentCollection
2491
     * scheduled for update or deletion.
2492
     *
2493
     * @param object $document
2494
     * @return boolean
2495
     */
2496
    public function hasScheduledCollections($document)
2497
    {
2498
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2499
    }
2500
2501
    /**
2502
     * Marks the PersistentCollection's top-level owner as having a relation to
2503
     * a collection scheduled for update or deletion.
2504
     *
2505
     * If the owner is not scheduled for any lifecycle action, it will be
2506
     * scheduled for update to ensure that versioning takes place if necessary.
2507
     *
2508
     * If the collection is nested within atomic collection, it is immediately
2509
     * unscheduled and atomic one is scheduled for update instead. This makes
2510
     * calculating update data way easier.
2511
     *
2512
     * @param PersistentCollectionInterface $coll
2513
     */
2514
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2515
    {
2516
        $document = $this->getOwningDocument($coll->getOwner());
2517
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2518
2519
        if ($document !== $coll->getOwner()) {
2520
            $parent = $coll->getOwner();
2521
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2522
                list($mapping, $parent, ) = $parentAssoc;
2523
            }
2524
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2525
                $class = $this->dm->getClassMetadata(get_class($document));
2526
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
0 ignored issues
show
Bug introduced by
The variable $mapping does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2527
                $this->scheduleCollectionUpdate($atomicCollection);
2528
                $this->unscheduleCollectionDeletion($coll);
2529
                $this->unscheduleCollectionUpdate($coll);
2530
            }
2531
        }
2532
2533
        if ( ! $this->isDocumentScheduled($document)) {
2534
            $this->scheduleForUpdate($document);
2535
        }
2536
    }
2537
2538
    /**
2539
     * Get the top-most owning document of a given document
2540
     *
2541
     * If a top-level document is provided, that same document will be returned.
2542
     * For an embedded document, we will walk through parent associations until
2543
     * we find a top-level document.
2544
     *
2545
     * @param object $document
2546
     * @throws \UnexpectedValueException when a top-level document could not be found
2547
     * @return object
2548
     */
2549
    public function getOwningDocument($document)
2550
    {
2551
        $class = $this->dm->getClassMetadata(get_class($document));
2552
        while ($class->isEmbeddedDocument) {
2553
            $parentAssociation = $this->getParentAssociation($document);
2554
2555
            if ( ! $parentAssociation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentAssociation 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 empty(..) or ! empty(...) instead.

Loading history...
2556
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2557
            }
2558
2559
            list(, $document, ) = $parentAssociation;
2560
            $class = $this->dm->getClassMetadata(get_class($document));
2561
        }
2562
2563
        return $document;
2564
    }
2565
2566
    /**
2567
     * Gets the class name for an association (embed or reference) with respect
2568
     * to any discriminator value.
2569
     *
2570
     * @param array      $mapping Field mapping for the association
2571
     * @param array|null $data    Data for the embedded document or reference
2572
     * @return string Class name.
2573
     */
2574 3
    public function getClassNameForAssociation(array $mapping, $data)
2575
    {
2576 3
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2577
2578 3
        $discriminatorValue = null;
2579 3
        if (isset($discriminatorField, $data[$discriminatorField])) {
2580 1
            $discriminatorValue = $data[$discriminatorField];
2581 2
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2582
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2583
        }
2584
2585 3
        if ($discriminatorValue !== null) {
2586 1
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2587 1
                ? $mapping['discriminatorMap'][$discriminatorValue]
2588 1
                : $discriminatorValue;
2589
        }
2590
2591 2
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2592
2593 1 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2594 1
            $discriminatorValue = $data[$class->discriminatorField];
2595
        } elseif ($class->defaultDiscriminatorValue !== null) {
2596
            $discriminatorValue = $class->defaultDiscriminatorValue;
2597
        }
2598
2599 1
        if ($discriminatorValue !== null) {
2600 1
            return isset($class->discriminatorMap[$discriminatorValue])
2601 1
                ? $class->discriminatorMap[$discriminatorValue]
2602 1
                : $discriminatorValue;
2603
        }
2604
2605
        return $mapping['targetDocument'];
2606
    }
2607
2608
    /**
2609
     * INTERNAL:
2610
     * Creates a document. Used for reconstitution of documents during hydration.
2611
     *
2612
     * @ignore
2613
     * @param string $className The name of the document class.
2614
     * @param array $data The data for the document.
2615
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2616
     * @param object $document The document to be hydrated into in case of creation
2617
     * @return object The document instance.
2618
     * @internal Highly performance-sensitive method.
2619
     */
2620
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2621
    {
2622
        $class = $this->dm->getClassMetadata($className);
2623
2624
        // @TODO figure out how to remove this
2625
        $discriminatorValue = null;
2626 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2627
            $discriminatorValue = $data[$class->discriminatorField];
2628
        } elseif (isset($class->defaultDiscriminatorValue)) {
2629
            $discriminatorValue = $class->defaultDiscriminatorValue;
2630
        }
2631
2632
        if ($discriminatorValue !== null) {
2633
            $className = isset($class->discriminatorMap[$discriminatorValue])
2634
                ? $class->discriminatorMap[$discriminatorValue]
2635
                : $discriminatorValue;
2636
2637
            $class = $this->dm->getClassMetadata($className);
2638
2639
            unset($data[$class->discriminatorField]);
2640
        }
2641
        
2642
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2643
            $document = $class->newInstance();
2644
            $this->hydratorFactory->hydrate($document, $data, $hints);
2645
            return $document;
2646
        }
2647
2648
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2649
        $serializedId = serialize($id);
2650
2651
        if (isset($this->identityMap[$class->name][$serializedId])) {
2652
            $document = $this->identityMap[$class->name][$serializedId];
2653
            $oid = spl_object_hash($document);
2654
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2655
                $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2656
                $overrideLocalValues = true;
2657
                if ($document instanceof NotifyPropertyChanged) {
2658
                    $document->addPropertyChangedListener($this);
2659
                }
2660
            } else {
2661
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2662
            }
2663
            if ($overrideLocalValues) {
2664
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2665
                $this->originalDocumentData[$oid] = $data;
2666
            }
2667
        } else {
2668
            if ($document === null) {
2669
                $document = $class->newInstance();
2670
            }
2671
            $this->registerManaged($document, $id, $data);
2672
            $oid = spl_object_hash($document);
2673
            $this->documentStates[$oid] = self::STATE_MANAGED;
2674
            $this->identityMap[$class->name][$serializedId] = $document;
2675
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2676
            $this->originalDocumentData[$oid] = $data;
2677
        }
2678
        return $document;
2679
    }
2680
2681
    /**
2682
     * Initializes (loads) an uninitialized persistent collection of a document.
2683
     *
2684
     * @param PersistentCollectionInterface $collection The collection to initialize.
2685
     */
2686
    public function loadCollection(PersistentCollectionInterface $collection)
2687
    {
2688
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2689
        $this->lifecycleEventManager->postCollectionLoad($collection);
2690
    }
2691
2692
    /**
2693
     * Gets the identity map of the UnitOfWork.
2694
     *
2695
     * @return array
2696
     */
2697
    public function getIdentityMap()
2698
    {
2699
        return $this->identityMap;
2700
    }
2701
2702
    /**
2703
     * Gets the original data of a document. The original data is the data that was
2704
     * present at the time the document was reconstituted from the database.
2705
     *
2706
     * @param object $document
2707
     * @return array
2708
     */
2709
    public function getOriginalDocumentData($document)
2710
    {
2711
        $oid = spl_object_hash($document);
2712
        if (isset($this->originalDocumentData[$oid])) {
2713
            return $this->originalDocumentData[$oid];
2714
        }
2715
        return array();
2716
    }
2717
2718
    /**
2719
     * @ignore
2720
     */
2721
    public function setOriginalDocumentData($document, array $data)
2722
    {
2723
        $oid = spl_object_hash($document);
2724
        $this->originalDocumentData[$oid] = $data;
2725
        unset($this->documentChangeSets[$oid]);
2726
    }
2727
2728
    /**
2729
     * INTERNAL:
2730
     * Sets a property value of the original data array of a document.
2731
     *
2732
     * @ignore
2733
     * @param string $oid
2734
     * @param string $property
2735
     * @param mixed $value
2736
     */
2737
    public function setOriginalDocumentProperty($oid, $property, $value)
2738
    {
2739
        $this->originalDocumentData[$oid][$property] = $value;
2740
    }
2741
2742
    /**
2743
     * Gets the identifier of a document.
2744
     *
2745
     * @param object $document
2746
     * @return mixed The identifier value
2747
     */
2748 1
    public function getDocumentIdentifier($document)
2749
    {
2750 1
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2751 1
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2752
    }
2753
2754
    /**
2755
     * Checks whether the UnitOfWork has any pending insertions.
2756
     *
2757
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2758
     */
2759
    public function hasPendingInsertions()
2760
    {
2761
        return ! empty($this->documentInsertions);
2762
    }
2763
2764
    /**
2765
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2766
     * number of documents in the identity map.
2767
     *
2768
     * @return integer
2769
     */
2770
    public function size()
2771
    {
2772
        $count = 0;
2773
        foreach ($this->identityMap as $documentSet) {
2774
            $count += count($documentSet);
2775
        }
2776
        return $count;
2777
    }
2778
2779
    /**
2780
     * INTERNAL:
2781
     * Registers a document as managed.
2782
     *
2783
     * TODO: This method assumes that $id is a valid PHP identifier for the
2784
     * document class. If the class expects its database identifier to be a
2785
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
2786
     * document identifiers map will become inconsistent with the identity map.
2787
     * In the future, we may want to round-trip $id through a PHP and database
2788
     * conversion and throw an exception if it's inconsistent.
2789
     *
2790
     * @param object $document The document.
2791
     * @param array $id The identifier values.
2792
     * @param array $data The original document data.
2793
     */
2794 3
    public function registerManaged($document, $id, array $data)
2795
    {
2796 3
        $oid = spl_object_hash($document);
2797 3
        $class = $this->dm->getClassMetadata(get_class($document));
2798
2799 1
        if ( ! $class->identifier || $id === null) {
2800 1
            $this->documentIdentifiers[$oid] = $oid;
2801
        } else {
2802
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2803
        }
2804
2805 1
        $this->documentStates[$oid] = self::STATE_MANAGED;
2806 1
        $this->originalDocumentData[$oid] = $data;
2807 1
        $this->addToIdentityMap($document);
2808 1
    }
2809
2810
    /**
2811
     * INTERNAL:
2812
     * Clears the property changeset of the document with the given OID.
2813
     *
2814
     * @param string $oid The document's OID.
2815
     */
2816
    public function clearDocumentChangeSet($oid)
2817
    {
2818
        $this->documentChangeSets[$oid] = array();
2819
    }
2820
2821
    /* PropertyChangedListener implementation */
2822
2823
    /**
2824
     * Notifies this UnitOfWork of a property change in a document.
2825
     *
2826
     * @param object $document The document that owns the property.
2827
     * @param string $propertyName The name of the property that changed.
2828
     * @param mixed $oldValue The old value of the property.
2829
     * @param mixed $newValue The new value of the property.
2830
     */
2831
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2832
    {
2833
        $oid = spl_object_hash($document);
2834
        $class = $this->dm->getClassMetadata(get_class($document));
2835
2836
        if ( ! isset($class->fieldMappings[$propertyName])) {
2837
            return; // ignore non-persistent fields
2838
        }
2839
2840
        // Update changeset and mark document for synchronization
2841
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2842
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2843
            $this->scheduleForDirtyCheck($document);
2844
        }
2845
    }
2846
2847
    /**
2848
     * Gets the currently scheduled document insertions in this UnitOfWork.
2849
     *
2850
     * @return array
2851
     */
2852
    public function getScheduledDocumentInsertions()
2853
    {
2854
        return $this->documentInsertions;
2855
    }
2856
2857
    /**
2858
     * Gets the currently scheduled document upserts in this UnitOfWork.
2859
     *
2860
     * @return array
2861
     */
2862
    public function getScheduledDocumentUpserts()
2863
    {
2864
        return $this->documentUpserts;
2865
    }
2866
2867
    /**
2868
     * Gets the currently scheduled document updates in this UnitOfWork.
2869
     *
2870
     * @return array
2871
     */
2872
    public function getScheduledDocumentUpdates()
2873
    {
2874
        return $this->documentUpdates;
2875
    }
2876
2877
    /**
2878
     * Gets the currently scheduled document deletions in this UnitOfWork.
2879
     *
2880
     * @return array
2881
     */
2882
    public function getScheduledDocumentDeletions()
2883
    {
2884
        return $this->documentDeletions;
2885
    }
2886
2887
    /**
2888
     * Get the currently scheduled complete collection deletions
2889
     *
2890
     * @return array
2891
     */
2892
    public function getScheduledCollectionDeletions()
2893
    {
2894
        return $this->collectionDeletions;
2895
    }
2896
2897
    /**
2898
     * Gets the currently scheduled collection inserts, updates and deletes.
2899
     *
2900
     * @return array
2901
     */
2902
    public function getScheduledCollectionUpdates()
2903
    {
2904
        return $this->collectionUpdates;
2905
    }
2906
2907
    /**
2908
     * Helper method to initialize a lazy loading proxy or persistent collection.
2909
     *
2910
     * @param object
2911
     * @return void
2912
     */
2913
    public function initializeObject($obj)
2914
    {
2915
        if ($obj instanceof Proxy) {
2916
            $obj->__load();
2917
        } elseif ($obj instanceof PersistentCollectionInterface) {
2918
            $obj->initialize();
2919
        }
2920
    }
2921
2922
    private function objToStr($obj)
2923
    {
2924
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2925
    }
2926
}
2927