Completed
Pull Request — 1.0.x (#1420)
by Andreas
10:22 queued 01:18
created

UnitOfWork::setOriginalDocumentProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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