Completed
Pull Request — master (#1376)
by Bilge
30:49 queued 28:17
created

UnitOfWork::hasScheduledCollections()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

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