Completed
Pull Request — master (#1289)
by Jefersson
08:23 queued 02:20
created

UnitOfWork::scheduleForSynchronization()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1
Metric Value
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4286
cc 1
eloc 3
nc 1
nop 1
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
     */
134
    private $scheduledForSynchronization = array();
135
136
    /**
137
     * A list of all pending document insertions.
138
     *
139
     * @var array
140
     */
141
    private $documentInsertions = array();
142
143
    /**
144
     * A list of all pending document updates.
145
     *
146
     * @var array
147
     */
148
    private $documentUpdates = array();
149
150
    /**
151
     * A list of all pending document upserts.
152
     *
153
     * @var array
154
     */
155
    private $documentUpserts = array();
156
157
    /**
158
     * A list of all pending document deletions.
159
     *
160
     * @var array
161
     */
162
    private $documentDeletions = array();
163
164
    /**
165
     * All pending collection deletions.
166
     *
167
     * @var array
168
     */
169
    private $collectionDeletions = array();
170
171
    /**
172
     * All pending collection updates.
173
     *
174
     * @var array
175
     */
176
    private $collectionUpdates = array();
177
    
178
    /**
179
     * A list of documents related to collections scheduled for update or deletion
180
     * 
181
     * @var array
182
     */
183
    private $hasScheduledCollections = array();
184
185
    /**
186
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
187
     * At the end of the UnitOfWork all these collections will make new snapshots
188
     * of their data.
189
     *
190
     * @var array
191
     */
192
    private $visitedCollections = array();
193
194
    /**
195
     * The DocumentManager that "owns" this UnitOfWork instance.
196
     *
197
     * @var DocumentManager
198
     */
199
    private $dm;
200
201
    /**
202
     * The EventManager used for dispatching events.
203
     *
204
     * @var EventManager
205
     */
206
    private $evm;
207
208
    /**
209
     * Embedded documents that are scheduled for removal.
210
     *
211
     * @var array
212
     */
213
    private $orphanRemovals = array();
214
215
    /**
216
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
217
     *
218
     * @var HydratorFactory
219
     */
220
    private $hydratorFactory;
221
222
    /**
223
     * The document persister instances used to persist document instances.
224
     *
225
     * @var array
226
     */
227
    private $persisters = array();
228
229
    /**
230
     * The collection persister instance used to persist changes to collections.
231
     *
232
     * @var Persisters\CollectionPersister
233
     */
234
    private $collectionPersister;
235
236
    /**
237
     * The persistence builder instance used in DocumentPersisters.
238
     *
239
     * @var PersistenceBuilder
240
     */
241
    private $persistenceBuilder;
242
243
    /**
244
     * Array of parent associations between embedded documents
245
     *
246
     * @todo We might need to clean up this array in clear(), doDetach(), etc.
247
     * @var array
248
     */
249
    private $parentAssociations = array();
250
251
    /**
252
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
253
     *
254
     * @param DocumentManager $dm
255
     * @param EventManager $evm
256
     * @param HydratorFactory $hydratorFactory
257
     */
258 911
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
259
    {
260 911
        $this->dm = $dm;
261 911
        $this->evm = $evm;
262 911
        $this->hydratorFactory = $hydratorFactory;
263 911
    }
264
265
    /**
266
     * Factory for returning new PersistenceBuilder instances used for preparing data into
267
     * queries for insert persistence.
268
     *
269
     * @return PersistenceBuilder $pb
270
     */
271 658
    public function getPersistenceBuilder()
272
    {
273 658
        if ( ! $this->persistenceBuilder) {
274 658
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
275 658
        }
276 658
        return $this->persistenceBuilder;
277
    }
278
279
    /**
280
     * Sets the parent association for a given embedded document.
281
     *
282
     * @param object $document
283
     * @param array $mapping
284
     * @param object $parent
285
     * @param string $propertyPath
286
     */
287 174
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
288
    {
289 174
        $oid = spl_object_hash($document);
290 174
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
291 174
    }
292
293
    /**
294
     * Gets the parent association for a given embedded document.
295
     *
296
     *     <code>
297
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
298
     *     </code>
299
     *
300
     * @param object $document
301
     * @return array $association
302
     */
303 201
    public function getParentAssociation($document)
304
    {
305 201
        $oid = spl_object_hash($document);
306 201
        if ( ! isset($this->parentAssociations[$oid])) {
307 197
            return null;
308
        }
309 159
        return $this->parentAssociations[$oid];
310
    }
311
312
    /**
313
     * Get the document persister instance for the given document name
314
     *
315
     * @param string $documentName
316
     * @return Persisters\DocumentPersister
317
     */
318 656
    public function getDocumentPersister($documentName)
319
    {
320 656
        if ( ! isset($this->persisters[$documentName])) {
321 642
            $class = $this->dm->getClassMetadata($documentName);
322 642
            $pb = $this->getPersistenceBuilder();
323 642
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
324 642
        }
325 656
        return $this->persisters[$documentName];
326
    }
327
328
    /**
329
     * Get the collection persister instance.
330
     *
331
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
332
     */
333 656
    public function getCollectionPersister()
334
    {
335 656
        if ( ! isset($this->collectionPersister)) {
336 656
            $pb = $this->getPersistenceBuilder();
337 656
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
338 656
        }
339 656
        return $this->collectionPersister;
340
    }
341
342
    /**
343
     * Set the document persister instance to use for the given document name
344
     *
345
     * @param string $documentName
346
     * @param Persisters\DocumentPersister $persister
347
     */
348 14
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
349
    {
350 14
        $this->persisters[$documentName] = $persister;
351 14
    }
352
353
    /**
354
     * Commits the UnitOfWork, executing all operations that have been postponed
355
     * up to this point. The state of all managed documents will be synchronized with
356
     * the database.
357
     *
358
     * The operations are executed in the following order:
359
     *
360
     * 1) All document insertions
361
     * 2) All document updates
362
     * 3) All document deletions
363
     *
364
     * @param object $document
365
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
366
     */
367 545
    public function commit($document = null, array $options = array())
368
    {
369
        // Raise preFlush
370 545
        if ($this->evm->hasListeners(Events::preFlush)) {
371
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
372
        }
373
374 545
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
375 545
        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...
376
            $options = array_merge($defaultOptions, $options);
377
        } else {
378 545
            $options = $defaultOptions;
379
        }
380
        // Compute changes done since last commit.
381 545
        if ($document === null) {
382 539
            $this->computeChangeSets();
383 544
        } elseif (is_object($document)) {
384 12
            $this->computeSingleDocumentChangeSet($document);
385 12
        } elseif (is_array($document)) {
386 1
            foreach ($document as $object) {
387 1
                $this->computeSingleDocumentChangeSet($object);
388 1
            }
389 1
        }
390
391 543
        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...
392 226
            $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...
393 190
            $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...
394 180
            $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...
395 23
            $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...
396 23
            $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...
397 23
            $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...
398 543
        ) {
399 23
            return; // Nothing to do.
400
        }
401
402 540
        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...
403 45
            foreach ($this->orphanRemovals as $removal) {
404 44
                $this->remove($removal);
405 44
            }
406 44
        }
407
408
        // Raise onFlush
409 540
        if ($this->evm->hasListeners(Events::onFlush)) {
410 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
411 7
        }
412
413 540
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
414 77
            list($class, $documents) = $classAndDocuments;
415 77
            $this->executeUpserts($class, $documents, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by $defaultOptions on line 378 can also be of type boolean; however, Doctrine\ODM\MongoDB\UnitOfWork::executeUpserts() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
416 540
        }
417
418 540
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
419 474
            list($class, $documents) = $classAndDocuments;
420 474
            $this->executeInserts($class, $documents, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by $defaultOptions on line 378 can also be of type boolean; however, Doctrine\ODM\MongoDB\UnitOfWork::executeInserts() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
421 539
        }
422
423 539
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
424 206
            list($class, $documents) = $classAndDocuments;
425 206
            $this->executeUpdates($class, $documents, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by $defaultOptions on line 378 can also be of type boolean; however, Doctrine\ODM\MongoDB\UnitOfWork::executeUpdates() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
426 539
        }
427
428 539
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
429 61
            list($class, $documents) = $classAndDocuments;
430 61
            $this->executeDeletions($class, $documents, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by $defaultOptions on line 378 can also be of type boolean; however, Doctrine\ODM\MongoDB\Uni...ork::executeDeletions() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
431 539
        }
432
433
        // Raise postFlush
434 539
        if ($this->evm->hasListeners(Events::postFlush)) {
435
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
436
        }
437
438
        // Clear up
439 539
        $this->documentInsertions =
440 539
        $this->documentUpserts =
441 539
        $this->documentUpdates =
442 539
        $this->documentDeletions =
443 539
        $this->documentChangeSets =
444 539
        $this->collectionUpdates =
445 539
        $this->collectionDeletions =
446 539
        $this->visitedCollections =
447 539
        $this->scheduledForSynchronization =
448 539
        $this->orphanRemovals = 
449 539
        $this->hasScheduledCollections = array();
450 539
    }
451
452
    /**
453
     * Groups a list of scheduled documents by their class.
454
     *
455
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
456
     * @param bool $includeEmbedded
457
     * @return array Tuples of ClassMetadata and a corresponding array of objects
458
     */
459 540
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
460
    {
461 540
        if (empty($documents)) {
462 540
            return array();
463
        }
464 539
        $divided = array();
465 539
        $embeds = array();
466 539
        foreach ($documents as $oid => $d) {
467 539
            $className = get_class($d);
468 539
            if (isset($embeds[$className])) {
469 62
                continue;
470
            }
471 539
            if (isset($divided[$className])) {
472 132
                $divided[$className][1][$oid] = $d;
473 132
                continue;
474
            }
475 539
            $class = $this->dm->getClassMetadata($className);
476 539
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
477 159
                $embeds[$className] = true;
478 159
                continue;
479
            }
480 539
            if (empty($divided[$class->name])) {
481 539
                $divided[$class->name] = array($class, array($oid => $d));
482 539
            } else {
483 4
                $divided[$class->name][1][$oid] = $d;
484
            }
485 539
        }
486 539
        return $divided;
487
    }
488
489
    /**
490
     * Compute changesets of all documents scheduled for insertion.
491
     *
492
     * Embedded documents will not be processed.
493
     */
494 547 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...
495
    {
496 547
        foreach ($this->documentInsertions as $document) {
497 482
            $class = $this->dm->getClassMetadata(get_class($document));
498 482
            if ( ! $class->isEmbeddedDocument) {
499 479
                $this->computeChangeSet($class, $document);
500 478
            }
501 546
        }
502 546
    }
503
504
    /**
505
     * Compute changesets of all documents scheduled for upsert.
506
     *
507
     * Embedded documents will not be processed.
508
     */
509 546 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...
510
    {
511 546
        foreach ($this->documentUpserts as $document) {
512 76
            $class = $this->dm->getClassMetadata(get_class($document));
513 76
            if ( ! $class->isEmbeddedDocument) {
514 76
                $this->computeChangeSet($class, $document);
515 76
            }
516 546
        }
517 546
    }
518
519
    /**
520
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
521
     *
522
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
523
     * 2. Proxies are skipped.
524
     * 3. Only if document is properly managed.
525
     *
526
     * @param  object $document
527
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
528
     * @return void
529
     */
530 13
    private function computeSingleDocumentChangeSet($document)
531
    {
532 13
        $state = $this->getDocumentState($document);
533
534 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
535 1
            throw new \InvalidArgumentException("Document has to be managed or scheduled for removal for single computation " . self::objToStr($document));
536
        }
537
538 12
        $class = $this->dm->getClassMetadata(get_class($document));
539
540 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
541 9
            $this->persist($document);
542 9
        }
543
544
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
545 12
        $this->computeScheduleInsertsChangeSets();
546 12
        $this->computeScheduleUpsertsChangeSets();
547
548
        // Ignore uninitialized proxy objects
549 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...
550
            return;
551
        }
552
553
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
554 12
        $oid = spl_object_hash($document);
555
556 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...
557 12
            && ! isset($this->documentUpserts[$oid])
558 12
            && ! isset($this->documentDeletions[$oid])
559 12
            && isset($this->documentStates[$oid])
560 12
        ) {
561 8
            $this->computeChangeSet($class, $document);
562 8
        }
563 12
    }
564
565
    /**
566
     * Gets the changeset for a document.
567
     *
568
     * @param object $document
569
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
570
     */
571 530
    public function getDocumentChangeSet($document)
572
    {
573 530
        $oid = spl_object_hash($document);
574 530
        if (isset($this->documentChangeSets[$oid])) {
575 530
            return $this->documentChangeSets[$oid];
576
        }
577 66
        return array();
578
    }
579
580
    /**
581
     * Get a documents actual data, flattening all the objects to arrays.
582
     *
583
     * @param object $document
584
     * @return array
585
     */
586 544
    public function getDocumentActualData($document)
587
    {
588 544
        $class = $this->dm->getClassMetadata(get_class($document));
589 544
        $actualData = array();
590 544
        foreach ($class->reflFields as $name => $refProp) {
591 544
            $mapping = $class->fieldMappings[$name];
592
            // skip not saved fields
593 544
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
594 49
                continue;
595
            }
596 544
            $value = $refProp->getValue($document);
597 544
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
598 5
                $value = new GridFSFile($value);
599 5
                $class->reflFields[$name]->setValue($document, $value);
600 5
                $actualData[$name] = $value;
601 544
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
602 544
                && $value !== null && ! ($value instanceof PersistentCollection)) {
603
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
604 357
                if ( ! $value instanceof Collection) {
605 118
                    $value = new ArrayCollection($value);
606 118
                }
607
608
                // Inject PersistentCollection
609 357
                $coll = new PersistentCollection($value, $this->dm, $this);
610 357
                $coll->setOwner($document, $mapping);
611 357
                $coll->setDirty( ! $value->isEmpty());
612 357
                $class->reflFields[$name]->setValue($document, $coll);
613 357
                $actualData[$name] = $coll;
614 357
            } else {
615 544
                $actualData[$name] = $value;
616
            }
617 544
        }
618 544
        return $actualData;
619
    }
620
621
    /**
622
     * Computes the changes that happened to a single document.
623
     *
624
     * Modifies/populates the following properties:
625
     *
626
     * {@link originalDocumentData}
627
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
628
     * then it was not fetched from the database and therefore we have no original
629
     * document data yet. All of the current document data is stored as the original document data.
630
     *
631
     * {@link documentChangeSets}
632
     * The changes detected on all properties of the document are stored there.
633
     * A change is a tuple array where the first entry is the old value and the second
634
     * entry is the new value of the property. Changesets are used by persisters
635
     * to INSERT/UPDATE the persistent document state.
636
     *
637
     * {@link documentUpdates}
638
     * If the document is already fully MANAGED (has been fetched from the database before)
639
     * and any changes to its properties are detected, then a reference to the document is stored
640
     * there to mark it for an update.
641
     *
642
     * @param ClassMetadata $class The class descriptor of the document.
643
     * @param object $document The document for which to compute the changes.
644
     */
645 544
    public function computeChangeSet(ClassMetadata $class, $document)
646
    {
647 544
        if ( ! $class->isInheritanceTypeNone()) {
648 171
            $class = $this->dm->getClassMetadata(get_class($document));
649 171
        }
650
651
        // Fire PreFlush lifecycle callbacks
652 544
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
653 10
            $class->invokeLifecycleCallbacks(Events::preFlush, $document);
654 10
        }
655
656 544
        $this->computeOrRecomputeChangeSet($class, $document);
657 543
    }
658
659
    /**
660
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
661
     *
662
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
663
     * @param object $document
664
     * @param boolean $recompute
665
     */
666 544
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
667
    {
668 544
        $oid = spl_object_hash($document);
669 544
        $actualData = $this->getDocumentActualData($document);
670 544
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
671 544
        if ($isNewDocument) {
672
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
673
            // These result in an INSERT.
674 544
            $this->originalDocumentData[$oid] = $actualData;
675 544
            $changeSet = array();
676 544
            foreach ($actualData as $propName => $actualValue) {
677
                /* At this PersistentCollection shouldn't be here, probably it
678
                 * was cloned and its ownership must be fixed
679
                 */
680 544
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
681
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
682
                    $actualValue = $actualData[$propName];
683
                }
684 544
                $changeSet[$propName] = array(null, $actualValue);
685 544
            }
686 544
            $this->documentChangeSets[$oid] = $changeSet;
687 544
        } else {
688
            // Document is "fully" MANAGED: it was already fully persisted before
689
            // and we have a copy of the original data
690 266
            $originalData = $this->originalDocumentData[$oid];
691 266
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
692 266
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
693 2
                $changeSet = $this->documentChangeSets[$oid];
694 2
            } else {
695 266
                $changeSet = array();
696
            }
697
698 266
            foreach ($actualData as $propName => $actualValue) {
699
                // skip not saved fields
700 266
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
701
                    continue;
702
                }
703
704 266
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
705
706
                // skip if value has not changed
707 266
                if ($orgValue === $actualValue) {
708
                    // but consider dirty GridFSFile instances as changed
709 265
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
710 265
                        continue;
711
                    }
712 1
                }
713
714
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
715 173
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
716 10
                    if ($orgValue !== null) {
717 5
                        $this->scheduleOrphanRemoval($orgValue);
718 5
                    }
719
720 10
                    $changeSet[$propName] = array($orgValue, $actualValue);
721 10
                    continue;
722
                }
723
724
                // if owning side of reference-one relationship
725 166
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
726 8
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
727 1
                        $this->scheduleOrphanRemoval($orgValue);
728 1
                    }
729
730 8
                    $changeSet[$propName] = array($orgValue, $actualValue);
731 8
                    continue;
732
                }
733
734 160
                if ($isChangeTrackingNotify) {
735 2
                    continue;
736
                }
737
738
                // ignore inverse side of reference-many relationship
739 159
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'many' && $class->fieldMappings[$propName]['isInverseSide']) {
740
                    continue;
741
                }
742
743
                // Persistent collection was exchanged with the "originally"
744
                // created one. This can only mean it was cloned and replaced
745
                // on another document.
746 159
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
747 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
748 6
                }
749
750
                // if embed-many or reference-many relationship
751 159
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
752 25
                    $changeSet[$propName] = array($orgValue, $actualValue);
753
                    /* If original collection was exchanged with a non-empty value
754
                     * and $set will be issued, there is no need to $unset it first
755
                     */
756 25
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
757 7
                        continue;
758
                    }
759 19
                    if ($orgValue instanceof PersistentCollection) {
760 17
                        $this->scheduleCollectionDeletion($orgValue);
761 17
                    }
762 19
                    continue;
763
                }
764
765
                // skip equivalent date values
766 145
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
767 35
                    $dateType = Type::getType('date');
768 35
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
769 35
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
770
771 35
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
772 29
                        continue;
773
                    }
774 9
                }
775
776
                // regular field
777 129
                $changeSet[$propName] = array($orgValue, $actualValue);
778 266
            }
779 266
            if ($changeSet) {
780 159
                $this->documentChangeSets[$oid] = ($recompute && isset($this->documentChangeSets[$oid]))
781 159
                    ? $changeSet + $this->documentChangeSets[$oid]
782 12
                    : $changeSet;
783
784 159
                $this->originalDocumentData[$oid] = $actualData;
785 159
                $this->scheduleForUpdate($document);
786 159
            }
787
        }
788
789
        // Look for changes in associations of the document
790 544
        $associationMappings = array_filter(
791 544
            $class->associationMappings,
792
            function ($assoc) { return empty($assoc['notSaved']); }
793 544
        );
794
795 544
        foreach ($associationMappings as $mapping) {
796 419
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
797
798 419
            if ($value === null) {
799 279
                continue;
800
            }
801
802 410
            $this->computeAssociationChanges($document, $mapping, $value);
803
804 409
            if (isset($mapping['reference'])) {
805 306
                continue;
806
            }
807
808 318
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
809
810 318
            foreach ($values as $obj) {
811 163
                $oid2 = spl_object_hash($obj);
812
813 163
                if (isset($this->documentChangeSets[$oid2])) {
814 161
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
815
816 161
                    if ( ! $isNewDocument) {
817 71
                        $this->scheduleForUpdate($document);
818 71
                    }
819
820 161
                    break;
821
                }
822 318
            }
823 543
        }
824 543
    }
825
826
    /**
827
     * Computes all the changes that have been done to documents and collections
828
     * since the last commit and stores these changes in the _documentChangeSet map
829
     * temporarily for access by the persisters, until the UoW commit is finished.
830
     */
831 542
    public function computeChangeSets()
832
    {
833 542
        $this->computeScheduleInsertsChangeSets();
834 541
        $this->computeScheduleUpsertsChangeSets();
835
836
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
837 541
        foreach ($this->identityMap as $className => $documents) {
838 541
            $class = $this->dm->getClassMetadata($className);
839 541
            if ($class->isEmbeddedDocument) {
840
                /* we do not want to compute changes to embedded documents up front
841
                 * in case embedded document was replaced and its changeset
842
                 * would corrupt data. Embedded documents' change set will
843
                 * be calculated by reachability from owning document.
844
                 */
845 152
                continue;
846
            }
847
848
            // If change tracking is explicit or happens through notification, then only compute
849
            // changes on document of that type that are explicitly marked for synchronization.
850 541
            switch (true) {
851 541
                case ($class->isChangeTrackingDeferredImplicit()):
852 540
                    $documentsToProcess = $documents;
853 540
                    break;
854
855 3
                case (isset($this->scheduledForSynchronization[$className])):
856 2
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
857 2
                    break;
858
859 3
                default:
860 3
                    $documentsToProcess = array();
861
862 3
            }
863
864 541
            foreach ($documentsToProcess as $document) {
865
                // Ignore uninitialized proxy objects
866 537
                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...
867 10
                    continue;
868
                }
869
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
870 537
                $oid = spl_object_hash($document);
871 537 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...
872 537
                    && ! isset($this->documentUpserts[$oid])
873 537
                    && ! isset($this->documentDeletions[$oid])
874 537
                    && isset($this->documentStates[$oid])
875 537
                ) {
876 251
                    $this->computeChangeSet($class, $document);
877 251
                }
878 541
            }
879 541
        }
880 541
    }
881
882
    /**
883
     * Computes the changes of an association.
884
     *
885
     * @param object $parentDocument
886
     * @param array $assoc
887
     * @param mixed $value The value of the association.
888
     * @throws \InvalidArgumentException
889
     */
890 410
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
891
    {
892 410
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
893 410
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
894 410
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
895
896 410
        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...
897 8
            return;
898
        }
899
900 409
        if ($value instanceof PersistentCollection && $value->isDirty() && $assoc['isOwningSide']) {
901 217
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
902 213
                $this->scheduleCollectionUpdate($value);
903 213
            }
904 217
            $topmostOwner = $this->getOwningDocument($value->getOwner());
905 217
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
906 217
        }
907
908
        // Look through the documents, and in any of their associations,
909
        // for transient (new) documents, recursively. ("Persistence by reachability")
910
        // Unwrap. Uninitialized collections will simply be empty.
911 409
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
912
913 409
        $count = 0;
914 409
        foreach ($unwrappedValue as $key => $entry) {
915 314
            if ( ! is_object($entry)) {
916 1
                throw new \InvalidArgumentException(
917 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
918 1
                );
919
            }
920
921 313
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
922
923 313
            $state = $this->getDocumentState($entry, self::STATE_NEW);
924
925
            // Handle "set" strategy for multi-level hierarchy
926 313
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
927 313
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
928
929 313
            $count++;
930
931
            switch ($state) {
932 313
                case self::STATE_NEW:
933 56
                    if ( ! $assoc['isCascadePersist']) {
934
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
935
                            . " configured to cascade persist operations: " . self::objToStr($entry) . "."
936
                            . " Explicitly persist the new document or configure cascading persist operations"
937
                            . " on the relationship.");
938
                    }
939
940 56
                    $this->persistNew($targetClass, $entry);
941 56
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
942 56
                    $this->computeChangeSet($targetClass, $entry);
943 56
                    break;
944
945 309
                case self::STATE_MANAGED:
946 309
                    if ($targetClass->isEmbeddedDocument) {
947 155
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
948 155
                        if ($knownParent && $knownParent !== $parentDocument) {
949 6
                            $entry = clone $entry;
950 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
951 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
952 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
953 3
                            } else {
954
                                // must use unwrapped value to not trigger orphan removal
955 6
                                $unwrappedValue[$key] = $entry;
956
                            }
957 6
                            $this->persistNew($targetClass, $entry);
958 6
                        }
959 155
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
960 155
                        $this->computeChangeSet($targetClass, $entry);
961 155
                    }
962 309
                    break;
963
964 1
                case self::STATE_REMOVED:
965
                    // Consume the $value as array (it's either an array or an ArrayAccess)
966
                    // and remove the element from Collection.
967 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
968
                        unset($value[$key]);
969
                    }
970 1
                    break;
971
972
                case self::STATE_DETACHED:
973
                    // Can actually not happen right now as we assume STATE_NEW,
974
                    // so the exception will be raised from the DBAL layer (constraint violation).
975
                    throw new \InvalidArgumentException("A detached document was found through a "
976
                        . "relationship during cascading a persist operation.");
977
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
978
979
                default:
980
                    // MANAGED associated documents are already taken into account
981
                    // during changeset calculation anyway, since they are in the identity map.
982
983
            }
984 408
        }
985 408
    }
986
987
    /**
988
     * INTERNAL:
989
     * Computes the changeset of an individual document, independently of the
990
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
991
     *
992
     * The passed document must be a managed document. If the document already has a change set
993
     * because this method is invoked during a commit cycle then the change sets are added.
994
     * whereby changes detected in this method prevail.
995
     *
996
     * @ignore
997
     * @param ClassMetadata $class The class descriptor of the document.
998
     * @param object $document The document for which to (re)calculate the change set.
999
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1000
     */
1001 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1002
    {
1003
        // Ignore uninitialized proxy objects
1004 19
        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...
1005 1
            return;
1006
        }
1007
1008 18
        $oid = spl_object_hash($document);
1009
1010 18
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1011
            throw new \InvalidArgumentException('Document must be managed.');
1012
        }
1013
1014 18
        if ( ! $class->isInheritanceTypeNone()) {
1015 2
            $class = $this->dm->getClassMetadata(get_class($document));
1016 2
        }
1017
1018 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
1019 18
    }
1020
1021
    /**
1022
     * @param ClassMetadata $class
1023
     * @param object $document
1024
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1025
     */
1026 562
    private function persistNew(ClassMetadata $class, $document)
1027
    {
1028 562
        $oid = spl_object_hash($document);
1029 562
        if ( ! empty($class->lifecycleCallbacks[Events::prePersist])) {
1030 155
            $class->invokeLifecycleCallbacks(Events::prePersist, $document);
1031 155
        }
1032 562 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...
1033 6
            $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($document, $this->dm));
1034 6
        }
1035
1036 562
        $upsert = false;
1037 562
        if ($class->identifier) {
1038 562
            $idValue = $class->getIdentifierValue($document);
1039 562
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1040
1041 562
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1042 3
                throw new \InvalidArgumentException(sprintf(
1043 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1044 3
                    get_class($document)
1045 3
                ));
1046
            }
1047
1048
            // \MongoId::isValid($idValue) was introduced in 1.5.0 so it's no good
1049 561
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('/^[0-9a-f]{24}$/', $idValue)) {
1050 1
                throw new \InvalidArgumentException(sprintf(
1051 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1052 1
                    get_class($document)
1053 1
                ));
1054
            }
1055
1056 560
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1057 490
                $idValue = $class->idGenerator->generate($this->dm, $document);
1058 490
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1059 490
                $class->setIdentifierValue($document, $idValue);
1060 490
            }
1061
1062 560
            $this->documentIdentifiers[$oid] = $idValue;
1063 560
        } else {
1064
            // this is for embedded documents without identifiers
1065 142
            $this->documentIdentifiers[$oid] = $oid;
1066
        }
1067
1068 560
        $this->documentStates[$oid] = self::STATE_MANAGED;
1069
1070 560
        if ($upsert) {
1071 80
            $this->scheduleForUpsert($class, $document);
1072 80
        } else {
1073 495
            $this->scheduleForInsert($class, $document);
1074
        }
1075 560
    }
1076
1077
    /**
1078
     * Cascades the postPersist events to embedded documents.
1079
     *
1080
     * @param ClassMetadata $class
1081
     * @param object $document
1082
     */
1083 538
    private function cascadePostPersist(ClassMetadata $class, $document)
1084
    {
1085 538
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1086
1087 538
        $embeddedMappings = array_filter(
1088 538
            $class->associationMappings,
1089
            function($assoc) { return ! empty($assoc['embedded']); }
1090 538
        );
1091
1092 538
        foreach ($embeddedMappings as $mapping) {
1093 326
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1094
1095 326
            if ($value === null) {
1096 213
                continue;
1097
            }
1098
1099 308
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1100
1101 308
            if (isset($mapping['targetDocument'])) {
1102 297
                $embeddedClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1103 297
            }
1104
1105 308
            foreach ($values as $embeddedDocument) {
1106 153
                if ( ! isset($mapping['targetDocument'])) {
1107 13
                    $embeddedClass = $this->dm->getClassMetadata(get_class($embeddedDocument));
1108 13
                }
1109
1110 153
                if ( ! empty($embeddedClass->lifecycleCallbacks[Events::postPersist])) {
1111 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...
1112 9
                }
1113 153
                if ($hasPostPersistListeners) {
1114 4
                    $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($embeddedDocument, $this->dm));
1115 4
                }
1116 153
                $this->cascadePostPersist($embeddedClass, $embeddedDocument);
1117 308
            }
1118 538
         }
1119 538
     }
1120
1121
    /**
1122
     * Executes all document insertions for documents of the specified type.
1123
     *
1124
     * @param ClassMetadata $class
1125
     * @param array $documents Array of documents to insert
1126
     * @param array $options Array of options to be used with batchInsert()
1127
     */
1128 474 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...
1129
    {
1130 474
        $persister = $this->getDocumentPersister($class->name);
1131
1132 474
        foreach ($documents as $oid => $document) {
1133 474
            $persister->addInsert($document);
1134 474
            unset($this->documentInsertions[$oid]);
1135 474
        }
1136
1137 474
        $persister->executeInserts($options);
1138
1139 473
        $hasPostPersistLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postPersist]);
1140 473
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1141
1142 473
        foreach ($documents as $document) {
1143 473
            if ($hasPostPersistLifecycleCallbacks) {
1144 9
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1145 9
            }
1146 473
            if ($hasPostPersistListeners) {
1147 5
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1148 5
            }
1149 473
            $this->cascadePostPersist($class, $document);
1150 473
        }
1151 473
    }
1152
1153
    /**
1154
     * Executes all document upserts for documents of the specified type.
1155
     *
1156
     * @param ClassMetadata $class
1157
     * @param array $documents Array of documents to upsert
1158
     * @param array $options Array of options to be used with batchInsert()
1159
     */
1160 77 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...
1161
    {
1162 77
        $persister = $this->getDocumentPersister($class->name);
1163
1164
1165 77
        foreach ($documents as $oid => $document) {
1166 77
            $persister->addUpsert($document);
1167 77
            unset($this->documentUpserts[$oid]);
1168 77
        }
1169
1170 77
        $persister->executeUpserts($options);
1171
1172 77
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
1173 77
        $hasListeners = $this->evm->hasListeners(Events::postPersist);
1174
1175 77
        foreach ($documents as $document) {
1176 77
            if ($hasLifecycleCallbacks) {
1177
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1178
            }
1179 77
            if ($hasListeners) {
1180 2
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1181 2
            }
1182 77
            $this->cascadePostPersist($class, $document);
1183 77
        }
1184 77
    }
1185
1186
    /**
1187
     * Executes all document updates for documents of the specified type.
1188
     *
1189
     * @param Mapping\ClassMetadata $class
1190
     * @param array $documents Array of documents to update
1191
     * @param array $options Array of options to be used with update()
1192
     */
1193 206
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1194
    {
1195 206
        $className = $class->name;
1196 206
        $persister = $this->getDocumentPersister($className);
1197
1198 206
        $hasPreUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::preUpdate]);
1199 206
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1200 206
        $hasPostUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postUpdate]);
1201 206
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1202
1203 206
        foreach ($documents as $oid => $document) {
1204 206
            if ($hasPreUpdateLifecycleCallbacks) {
1205 11
                $class->invokeLifecycleCallbacks(Events::preUpdate, $document);
1206 11
                $this->recomputeSingleDocumentChangeSet($class, $document);
1207 11
            }
1208
1209 206
            if ($hasPreUpdateListeners) {
1210 8
                if ( ! isset($this->documentChangeSets[$oid])) {
1211
                    // only ReferenceMany collection is scheduled for update
1212 1
                    $this->documentChangeSets[$oid] = array();
1213 1
                }
1214 8
                $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1215 8
                    $document, $this->dm, $this->documentChangeSets[$oid])
1216 8
                );
1217 8
            }
1218 206
            $this->cascadePreUpdate($class, $document);
1219
1220 206
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1221 205
                $persister->update($document, $options);
1222 201
            }
1223
1224 202
            unset($this->documentUpdates[$oid]);
1225
1226 202
            if ($hasPostUpdateLifecycleCallbacks) {
1227 6
                $class->invokeLifecycleCallbacks(Events::postUpdate, $document);
1228 6
            }
1229 202
            if ($hasPostUpdateListeners) {
1230 8
                $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($document, $this->dm));
1231 8
            }
1232 202
            $this->cascadePostUpdate($class, $document);
1233 202
        }
1234 201
    }
1235
1236
    /**
1237
     * Cascades the preUpdate event to embedded documents.
1238
     *
1239
     * @param ClassMetadata $class
1240
     * @param object $document
1241
     */
1242 206
    private function cascadePreUpdate(ClassMetadata $class, $document)
1243
    {
1244 206
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1245
1246 206
        $embeddedMappings = array_filter(
1247 206
            $class->associationMappings,
1248
            function ($assoc) { return ! empty($assoc['embedded']); }
1249 206
        );
1250
1251 206
        foreach ($embeddedMappings as $mapping) {
1252 125
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1253
1254 125
            if ($value === null) {
1255 49
                continue;
1256
            }
1257
1258 123
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1259
1260 123
            foreach ($values as $entry) {
1261 78
                $entryOid = spl_object_hash($entry);
1262 78
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1263
1264 78
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1265 41
                    continue;
1266
                }
1267
1268 67
                if (isset($this->documentInsertions[$entryOid])) {
1269 52
                    continue;
1270
                }
1271
1272 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...
1273 5
                    $entryClass->invokeLifecycleCallbacks(Events::preUpdate, $entry);
1274 5
                    $this->recomputeSingleDocumentChangeSet($entryClass, $entry);
1275 5
                }
1276 45
                if ($hasPreUpdateListeners) {
1277 3
                    $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1278 3
                        $entry, $this->dm, $this->documentChangeSets[$entryOid])
1279 3
                    );
1280 3
                }
1281
1282 45
                $this->cascadePreUpdate($entryClass, $entry);
1283 123
            }
1284 206
        }
1285 206
    }
1286
1287
    /**
1288
     * Cascades the postUpdate and postPersist events to embedded documents.
1289
     *
1290
     * @param ClassMetadata $class
1291
     * @param object $document
1292
     */
1293 202
    private function cascadePostUpdate(ClassMetadata $class, $document)
1294
    {
1295 202
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1296 202
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1297
1298 202
        $embeddedMappings = array_filter(
1299 202
            $class->associationMappings,
1300
            function($assoc) { return ! empty($assoc['embedded']); }
1301 202
        );
1302
1303 202
        foreach ($embeddedMappings as $mapping) {
1304 121
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1305
1306 121
            if ($value === null) {
1307 52
                continue;
1308
            }
1309
1310 119
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1311
1312 119
            foreach ($values as $entry) {
1313 78
                $entryOid = spl_object_hash($entry);
1314 78
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1315
1316 78
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1317 41
                    continue;
1318
                }
1319
1320 67
                if (isset($this->documentInsertions[$entryOid])) {
1321 52
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postPersist])) {
1322 1
                        $entryClass->invokeLifecycleCallbacks(Events::postPersist, $entry);
1323 1
                    }
1324 52
                    if ($hasPostPersistListeners) {
1325 3
                        $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entry, $this->dm));
1326 3
                    }
1327 52
                } else {
1328 45
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postUpdate])) {
1329 9
                        $entryClass->invokeLifecycleCallbacks(Events::postUpdate, $entry);
1330 9
                    }
1331 45
                    if ($hasPostUpdateListeners) {
1332 3
                        $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entry, $this->dm));
1333 3
                    }
1334
                }
1335
1336 67
                $this->cascadePostUpdate($entryClass, $entry);
1337 119
            }
1338 202
        }
1339 202
    }
1340
1341
    /**
1342
     * Executes all document deletions for documents of the specified type.
1343
     *
1344
     * @param ClassMetadata $class
1345
     * @param array $documents Array of documents to delete
1346
     * @param array $options Array of options to be used with remove()
1347
     */
1348 61
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1349
    {
1350 61
        $hasPostRemoveLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postRemove]);
1351 61
        $hasPostRemoveListeners = $this->evm->hasListeners(Events::postRemove);
1352
1353 61
        $persister = $this->getDocumentPersister($class->name);
1354
1355 61
        foreach ($documents as $oid => $document) {
1356 61
            if ( ! $class->isEmbeddedDocument) {
1357 28
                $persister->delete($document, $options);
1358 26
            }
1359
            unset(
1360 59
                $this->documentDeletions[$oid],
1361 59
                $this->documentIdentifiers[$oid],
1362 59
                $this->originalDocumentData[$oid]
1363
            );
1364
1365
            // Clear snapshot information for any referenced PersistentCollection
1366
            // http://www.doctrine-project.org/jira/browse/MODM-95
1367 59
            foreach ($class->associationMappings as $fieldMapping) {
1368 41
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1369 26
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1370 26
                    if ($value instanceof PersistentCollection) {
1371 22
                        $value->clearSnapshot();
1372 22
                    }
1373 26
                }
1374 59
            }
1375
1376
            // Document with this $oid after deletion treated as NEW, even if the $oid
1377
            // is obtained by a new document because the old one went out of scope.
1378 59
            $this->documentStates[$oid] = self::STATE_NEW;
1379
1380 59
            if ($hasPostRemoveLifecycleCallbacks) {
1381 8
                $class->invokeLifecycleCallbacks(Events::postRemove, $document);
1382 8
            }
1383 59
            if ($hasPostRemoveListeners) {
1384 2
                $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($document, $this->dm));
1385 2
            }
1386 59
        }
1387 59
    }
1388
1389
    /**
1390
     * Schedules a document for insertion into the database.
1391
     * If the document already has an identifier, it will be added to the
1392
     * identity map.
1393
     *
1394
     * @param ClassMetadata $class
1395
     * @param object $document The document to schedule for insertion.
1396
     * @throws \InvalidArgumentException
1397
     */
1398 498
    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...
1399
    {
1400 498
        $oid = spl_object_hash($document);
1401
1402 498
        if (isset($this->documentUpdates[$oid])) {
1403
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1404
        }
1405 498
        if (isset($this->documentDeletions[$oid])) {
1406
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1407
        }
1408 498
        if (isset($this->documentInsertions[$oid])) {
1409
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1410
        }
1411
1412 498
        $this->documentInsertions[$oid] = $document;
1413
1414 498
        if (isset($this->documentIdentifiers[$oid])) {
1415 495
            $this->addToIdentityMap($document);
1416 495
        }
1417 498
    }
1418
1419
    /**
1420
     * Schedules a document for upsert into the database and adds it to the
1421
     * identity map
1422
     *
1423
     * @param ClassMetadata $class
1424
     * @param object $document The document to schedule for upsert.
1425
     * @throws \InvalidArgumentException
1426
     */
1427 83
    public function scheduleForUpsert(ClassMetadata $class, $document)
1428
    {
1429 83
        $oid = spl_object_hash($document);
1430
1431 83
        if ($class->isEmbeddedDocument) {
1432
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1433
        }
1434 83
        if (isset($this->documentUpdates[$oid])) {
1435
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1436
        }
1437 83
        if (isset($this->documentDeletions[$oid])) {
1438
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1439
        }
1440 83
        if (isset($this->documentUpserts[$oid])) {
1441
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1442
        }
1443
1444 83
        $this->documentUpserts[$oid] = $document;
1445 83
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1446 83
        $this->addToIdentityMap($document);
1447 83
    }
1448
1449
    /**
1450
     * Checks whether a document is scheduled for insertion.
1451
     *
1452
     * @param object $document
1453
     * @return boolean
1454
     */
1455 70
    public function isScheduledForInsert($document)
1456
    {
1457 70
        return isset($this->documentInsertions[spl_object_hash($document)]);
1458
    }
1459
1460
    /**
1461
     * Checks whether a document is scheduled for upsert.
1462
     *
1463
     * @param object $document
1464
     * @return boolean
1465
     */
1466 5
    public function isScheduledForUpsert($document)
1467
    {
1468 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1469
    }
1470
1471
    /**
1472
     * Schedules a document for being updated.
1473
     *
1474
     * @param object $document The document to schedule for being updated.
1475
     * @throws \InvalidArgumentException
1476
     */
1477 215
    public function scheduleForUpdate($document)
1478
    {
1479 215
        $oid = spl_object_hash($document);
1480 215
        if ( ! isset($this->documentIdentifiers[$oid])) {
1481
            throw new \InvalidArgumentException("Document has no identity.");
1482
        }
1483
1484 215
        if (isset($this->documentDeletions[$oid])) {
1485
            throw new \InvalidArgumentException("Document is removed.");
1486
        }
1487
1488 215
        if ( ! isset($this->documentUpdates[$oid])
1489 215
            && ! isset($this->documentInsertions[$oid])
1490 215
            && ! isset($this->documentUpserts[$oid])) {
1491 211
            $this->documentUpdates[$oid] = $document;
1492 211
        }
1493 215
    }
1494
1495
    /**
1496
     * Checks whether a document is registered as dirty in the unit of work.
1497
     * Note: Is not very useful currently as dirty documents are only registered
1498
     * at commit time.
1499
     *
1500
     * @param object $document
1501
     * @return boolean
1502
     */
1503 13
    public function isScheduledForUpdate($document)
1504
    {
1505 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1506
    }
1507
1508 1
    public function isScheduledForSynchronization($document)
1509
    {
1510 1
        $class = $this->dm->getClassMetadata(get_class($document));
1511 1
        return isset($this->scheduledForSynchronization[$class->name][spl_object_hash($document)]);
1512
    }
1513
1514
    /**
1515
     * @deprecated
1516
     */
1517
    public function isScheduledForDirtyCheck($document)
1518
    {
1519
        return $this->isScheduledForSynchronization($document);
1520
    }
1521
1522
    /**
1523
     * INTERNAL:
1524
     * Schedules a document for deletion.
1525
     *
1526
     * @param object $document
1527
     */
1528 66
    public function scheduleForDelete($document)
1529
    {
1530 66
        $oid = spl_object_hash($document);
1531
1532 66
        if (isset($this->documentInsertions[$oid])) {
1533 2
            if ($this->isInIdentityMap($document)) {
1534 2
                $this->removeFromIdentityMap($document);
1535 2
            }
1536 2
            unset($this->documentInsertions[$oid]);
1537 2
            return; // document has not been persisted yet, so nothing more to do.
1538
        }
1539
1540 65
        if ( ! $this->isInIdentityMap($document)) {
1541 1
            return; // ignore
1542
        }
1543
1544 64
        $this->removeFromIdentityMap($document);
1545 64
        $this->documentStates[$oid] = self::STATE_REMOVED;
1546
1547 64
        if (isset($this->documentUpdates[$oid])) {
1548
            unset($this->documentUpdates[$oid]);
1549
        }
1550 64
        if ( ! isset($this->documentDeletions[$oid])) {
1551 64
            $this->documentDeletions[$oid] = $document;
1552 64
        }
1553 64
    }
1554
1555
    /**
1556
     * Checks whether a document is registered as removed/deleted with the unit
1557
     * of work.
1558
     *
1559
     * @param object $document
1560
     * @return boolean
1561
     */
1562 8
    public function isScheduledForDelete($document)
1563
    {
1564 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1565
    }
1566
1567
    /**
1568
     * Checks whether a document is scheduled for insertion, update or deletion.
1569
     *
1570
     * @param $document
1571
     * @return boolean
1572
     */
1573 216
    public function isDocumentScheduled($document)
1574
    {
1575 216
        $oid = spl_object_hash($document);
1576 216
        return isset($this->documentInsertions[$oid]) ||
1577 113
            isset($this->documentUpserts[$oid]) ||
1578 104
            isset($this->documentUpdates[$oid]) ||
1579 216
            isset($this->documentDeletions[$oid]);
1580
    }
1581
1582
    /**
1583
     * INTERNAL:
1584
     * Registers a document in the identity map.
1585
     *
1586
     * Note that documents in a hierarchy are registered with the class name of
1587
     * the root document. Identifiers are serialized before being used as array
1588
     * keys to allow differentiation of equal, but not identical, values.
1589
     *
1590
     * @ignore
1591
     * @param object $document  The document to register.
1592
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1593
     *                  the document in question is already managed.
1594
     */
1595 589
    public function addToIdentityMap($document)
1596
    {
1597 589
        $class = $this->dm->getClassMetadata(get_class($document));
1598 589
        $id = $this->getIdForIdentityMap($document);
1599
1600 589
        if (isset($this->identityMap[$class->name][$id])) {
1601 47
            return false;
1602
        }
1603
1604 589
        $this->identityMap[$class->name][$id] = $document;
1605
1606 589
        if ($document instanceof NotifyPropertyChanged &&
1607 589
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1608 3
            $document->addPropertyChangedListener($this);
1609 3
        }
1610
1611 589
        return true;
1612
    }
1613
1614
    /**
1615
     * Gets the state of a document with regard to the current unit of work.
1616
     *
1617
     * @param object   $document
1618
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1619
     *                         This parameter can be set to improve performance of document state detection
1620
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1621
     *                         is either known or does not matter for the caller of the method.
1622
     * @return int The document state.
1623
     */
1624 565
    public function getDocumentState($document, $assume = null)
1625
    {
1626 565
        $oid = spl_object_hash($document);
1627
1628 565
        if (isset($this->documentStates[$oid])) {
1629 343
            return $this->documentStates[$oid];
1630
        }
1631
1632 565
        $class = $this->dm->getClassMetadata(get_class($document));
1633
1634 565
        if ($class->isEmbeddedDocument) {
1635 169
            return self::STATE_NEW;
1636
        }
1637
1638 562
        if ($assume !== null) {
1639 559
            return $assume;
1640
        }
1641
1642
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1643
         * known. Note that you cannot remember the NEW or DETACHED state in
1644
         * _documentStates since the UoW does not hold references to such
1645
         * objects and the object hash can be reused. More generally, because
1646
         * the state may "change" between NEW/DETACHED without the UoW being
1647
         * aware of it.
1648
         */
1649 4
        $id = $class->getIdentifierObject($document);
1650
1651 4
        if ($id === null) {
1652 2
            return self::STATE_NEW;
1653
        }
1654
1655
        // Check for a version field, if available, to avoid a DB lookup.
1656 2
        if ($class->isVersioned) {
1657
            return ($class->getFieldValue($document, $class->versionField))
1658
                ? self::STATE_DETACHED
1659
                : self::STATE_NEW;
1660
        }
1661
1662
        // Last try before DB lookup: check the identity map.
1663 2
        if ($this->tryGetById($id, $class)) {
1664 1
            return self::STATE_DETACHED;
1665
        }
1666
1667
        // DB lookup
1668 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1669 1
            return self::STATE_DETACHED;
1670
        }
1671
1672 1
        return self::STATE_NEW;
1673
    }
1674
1675
    /**
1676
     * INTERNAL:
1677
     * Removes a document from the identity map. This effectively detaches the
1678
     * document from the persistence management of Doctrine.
1679
     *
1680
     * @ignore
1681
     * @param object $document
1682
     * @throws \InvalidArgumentException
1683
     * @return boolean
1684
     */
1685 75
    public function removeFromIdentityMap($document)
1686
    {
1687 75
        $oid = spl_object_hash($document);
1688
1689
        // Check if id is registered first
1690 75
        if ( ! isset($this->documentIdentifiers[$oid])) {
1691
            return false;
1692
        }
1693
1694 75
        $class = $this->dm->getClassMetadata(get_class($document));
1695 75
        $id = $this->getIdForIdentityMap($document);
1696
1697 75
        if (isset($this->identityMap[$class->name][$id])) {
1698 75
            unset($this->identityMap[$class->name][$id]);
1699 75
            $this->documentStates[$oid] = self::STATE_DETACHED;
1700 75
            return true;
1701
        }
1702
1703
        return false;
1704
    }
1705
1706
    /**
1707
     * INTERNAL:
1708
     * Gets a document in the identity map by its identifier hash.
1709
     *
1710
     * @ignore
1711
     * @param mixed         $id    Document identifier
1712
     * @param ClassMetadata $class Document class
1713
     * @return object
1714
     * @throws InvalidArgumentException if the class does not have an identifier
1715
     */
1716 31
    public function getById($id, ClassMetadata $class)
1717
    {
1718 31
        if ( ! $class->identifier) {
1719
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1720
        }
1721
1722 31
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1723
1724 31
        return $this->identityMap[$class->name][$serializedId];
1725
    }
1726
1727
    /**
1728
     * INTERNAL:
1729
     * Tries to get a document by its identifier hash. If no document is found
1730
     * for the given hash, FALSE is returned.
1731
     *
1732
     * @ignore
1733
     * @param mixed         $id    Document identifier
1734
     * @param ClassMetadata $class Document class
1735
     * @return mixed The found document or FALSE.
1736
     * @throws InvalidArgumentException if the class does not have an identifier
1737
     */
1738 288
    public function tryGetById($id, ClassMetadata $class)
1739
    {
1740 288
        if ( ! $class->identifier) {
1741
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1742
        }
1743
1744 288
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1745
1746 288
        return isset($this->identityMap[$class->name][$serializedId]) ?
1747 288
            $this->identityMap[$class->name][$serializedId] : false;
1748
    }
1749
1750
    /**
1751
     * Schedules a document for dirty-checking at commit-time.
1752
     *
1753
     * @param object $document The document to schedule for dirty-checking.
1754
     */
1755 2
    public function scheduleForSynchronization($document)
1756
    {
1757 2
        $class = $this->dm->getClassMetadata(get_class($document));
1758 2
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1759 2
    }
1760
1761
    /**
1762
     * @deprecated
1763
     */
1764
    public function scheduleForDirtyCheck($document)
1765
    {
1766
        $this->scheduleForSynchronization($document);
1767
    }
1768
1769
    /**
1770
     * Checks whether a document is registered in the identity map.
1771
     *
1772
     * @param object $document
1773
     * @return boolean
1774
     */
1775 75
    public function isInIdentityMap($document)
1776
    {
1777 75
        $oid = spl_object_hash($document);
1778
1779 75
        if ( ! isset($this->documentIdentifiers[$oid])) {
1780 4
            return false;
1781
        }
1782
1783 74
        $class = $this->dm->getClassMetadata(get_class($document));
1784 74
        $id = $this->getIdForIdentityMap($document);
1785
1786 74
        return isset($this->identityMap[$class->name][$id]);
1787
    }
1788
1789
    /**
1790
     * @param object $document
1791
     * @return string
1792
     */
1793 589
    private function getIdForIdentityMap($document)
1794
    {
1795 589
        $class = $this->dm->getClassMetadata(get_class($document));
1796
1797 589
        if ( ! $class->identifier) {
1798 145
            $id = spl_object_hash($document);
1799 145
        } else {
1800 588
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1801 588
            $id = serialize($class->getDatabaseIdentifierValue($id));
1802
        }
1803
1804 589
        return $id;
1805
    }
1806
1807
    /**
1808
     * INTERNAL:
1809
     * Checks whether an identifier exists in the identity map.
1810
     *
1811
     * @ignore
1812
     * @param string $id
1813
     * @param string $rootClassName
1814
     * @return boolean
1815
     */
1816
    public function containsId($id, $rootClassName)
1817
    {
1818
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1819
    }
1820
1821
    /**
1822
     * Persists a document as part of the current unit of work.
1823
     *
1824
     * @param object $document The document to persist.
1825
     * @throws MongoDBException If trying to persist MappedSuperclass.
1826
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1827
     */
1828 560
    public function persist($document)
1829
    {
1830 560
        $class = $this->dm->getClassMetadata(get_class($document));
1831 560
        if ($class->isMappedSuperclass) {
1832 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1833
        }
1834 559
        $visited = array();
1835 559
        $this->doPersist($document, $visited);
1836 555
    }
1837
1838
    /**
1839
     * Saves a document as part of the current unit of work.
1840
     * This method is internally called during save() cascades as it tracks
1841
     * the already visited documents to prevent infinite recursions.
1842
     *
1843
     * NOTE: This method always considers documents that are not yet known to
1844
     * this UnitOfWork as NEW.
1845
     *
1846
     * @param object $document The document to persist.
1847
     * @param array $visited The already visited documents.
1848
     * @throws \InvalidArgumentException
1849
     * @throws MongoDBException
1850
     */
1851 559
    private function doPersist($document, array &$visited)
1852
    {
1853 559
        $oid = spl_object_hash($document);
1854 559
        if (isset($visited[$oid])) {
1855 24
            return; // Prevent infinite recursion
1856
        }
1857
1858 559
        $visited[$oid] = $document; // Mark visited
1859
1860 559
        $class = $this->dm->getClassMetadata(get_class($document));
1861
1862 559
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1863
        switch ($documentState) {
1864 559
            case self::STATE_MANAGED:
1865
                // Nothing to do, except if policy is "deferred explicit"
1866 43
                if ($class->isChangeTrackingDeferredExplicit()) {
1867
                    $this->scheduleForSynchronization($document);
1868
                }
1869 43
                break;
1870 559
            case self::STATE_NEW:
1871 559
                $this->persistNew($class, $document);
1872 557
                break;
1873
1874 2
            case self::STATE_REMOVED:
1875
                // Document becomes managed again
1876 2
                unset($this->documentDeletions[$oid]);
1877
1878 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1879 2
                break;
1880
1881
            case self::STATE_DETACHED:
1882
                throw new \InvalidArgumentException(
1883
                    "Behavior of persist() for a detached document is not yet defined.");
1884
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1885
1886
            default:
1887
                throw MongoDBException::invalidDocumentState($documentState);
1888
        }
1889
1890 557
        $this->cascadePersist($document, $visited);
1891 555
    }
1892
1893
    /**
1894
     * Deletes a document as part of the current unit of work.
1895
     *
1896
     * @param object $document The document to remove.
1897
     */
1898 65
    public function remove($document)
1899
    {
1900 65
        $visited = array();
1901 65
        $this->doRemove($document, $visited);
1902 65
    }
1903
1904
    /**
1905
     * Deletes a document as part of the current unit of work.
1906
     *
1907
     * This method is internally called during delete() cascades as it tracks
1908
     * the already visited documents to prevent infinite recursions.
1909
     *
1910
     * @param object $document The document to delete.
1911
     * @param array $visited The map of the already visited documents.
1912
     * @throws MongoDBException
1913
     */
1914 65
    private function doRemove($document, array &$visited)
1915
    {
1916 65
        $oid = spl_object_hash($document);
1917 65
        if (isset($visited[$oid])) {
1918 1
            return; // Prevent infinite recursion
1919
        }
1920
1921 65
        $visited[$oid] = $document; // mark visited
1922
1923
        /* Cascade first, because scheduleForDelete() removes the entity from
1924
         * the identity map, which can cause problems when a lazy Proxy has to
1925
         * be initialized for the cascade operation.
1926
         */
1927 65
        $this->cascadeRemove($document, $visited);
1928
1929 65
        $class = $this->dm->getClassMetadata(get_class($document));
1930 65
        $documentState = $this->getDocumentState($document);
1931
        switch ($documentState) {
1932 65
            case self::STATE_NEW:
1933 65
            case self::STATE_REMOVED:
1934
                // nothing to do
1935 1
                break;
1936 65
            case self::STATE_MANAGED:
1937 65
                if ( ! empty($class->lifecycleCallbacks[Events::preRemove])) {
1938 8
                    $class->invokeLifecycleCallbacks(Events::preRemove, $document);
1939 8
                }
1940 65 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...
1941 1
                    $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($document, $this->dm));
1942 1
                }
1943 65
                $this->scheduleForDelete($document);
1944 65
                break;
1945
            case self::STATE_DETACHED:
1946
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1947
            default:
1948
                throw MongoDBException::invalidDocumentState($documentState);
1949
        }
1950 65
    }
1951
1952
    /**
1953
     * Merges the state of the given detached document into this UnitOfWork.
1954
     *
1955
     * @param object $document
1956
     * @return object The managed copy of the document.
1957
     */
1958 13
    public function merge($document)
1959
    {
1960 13
        $visited = array();
1961
1962 13
        return $this->doMerge($document, $visited);
1963
    }
1964
1965
    /**
1966
     * Executes a merge operation on a document.
1967
     *
1968
     * @param object      $document
1969
     * @param array       $visited
1970
     * @param object|null $prevManagedCopy
1971
     * @param array|null  $assoc
1972
     *
1973
     * @return object The managed copy of the document.
1974
     *
1975
     * @throws InvalidArgumentException If the entity instance is NEW.
1976
     * @throws LockException If the document uses optimistic locking through a
1977
     *                       version attribute and the version check against the
1978
     *                       managed copy fails.
1979
     */
1980 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1981
    {
1982 13
        $oid = spl_object_hash($document);
1983
1984 13
        if (isset($visited[$oid])) {
1985 1
            return $visited[$oid]; // Prevent infinite recursion
1986
        }
1987
1988 13
        $visited[$oid] = $document; // mark visited
1989
1990 13
        $class = $this->dm->getClassMetadata(get_class($document));
1991
1992
        /* First we assume DETACHED, although it can still be NEW but we can
1993
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1994
         * an identity, we need to fetch it from the DB anyway in order to
1995
         * merge. MANAGED documents are ignored by the merge operation.
1996
         */
1997 13
        $managedCopy = $document;
1998
1999 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2000 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
2001
                $document->__load();
2002
            }
2003
2004
            // Try to look the document up in the identity map.
2005 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
2006
2007 13
            if ($id === null) {
2008
                // If there is no identifier, it is actually NEW.
2009 5
                $managedCopy = $class->newInstance();
2010 5
                $this->persistNew($class, $managedCopy);
2011 5
            } else {
2012 10
                $managedCopy = $this->tryGetById($id, $class);
2013
2014 10
                if ($managedCopy) {
2015
                    // We have the document in memory already, just make sure it is not removed.
2016 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
2017
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
2018
                    }
2019 5
                } else {
2020
                    // We need to fetch the managed copy in order to merge.
2021 7
                    $managedCopy = $this->dm->find($class->name, $id);
2022
                }
2023
2024 10
                if ($managedCopy === null) {
2025
                    // If the identifier is ASSIGNED, it is NEW
2026
                    $managedCopy = $class->newInstance();
2027
                    $class->setIdentifierValue($managedCopy, $id);
2028
                    $this->persistNew($class, $managedCopy);
2029
                } else {
2030 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...
2031
                        $managedCopy->__load();
2032
                    }
2033
                }
2034
            }
2035
2036 13
            if ($class->isVersioned) {
2037
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
2038
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2039
2040
                // Throw exception if versions don't match
2041
                if ($managedCopyVersion != $documentVersion) {
2042
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
2043
                }
2044
            }
2045
2046
            // Merge state of $document into existing (managed) document
2047 13
            foreach ($class->reflClass->getProperties() as $prop) {
2048 13
                $name = $prop->name;
2049 13
                $prop->setAccessible(true);
2050 13
                if ( ! isset($class->associationMappings[$name])) {
2051 13
                    if ( ! $class->isIdentifier($name)) {
2052 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
2053 13
                    }
2054 13
                } else {
2055 13
                    $assoc2 = $class->associationMappings[$name];
2056
2057 13
                    if ($assoc2['type'] === 'one') {
2058 5
                        $other = $prop->getValue($document);
2059
2060 5
                        if ($other === null) {
2061 2
                            $prop->setValue($managedCopy, null);
2062 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...
2063
                            // Do not merge fields marked lazy that have not been fetched
2064 1
                            continue;
2065 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
2066
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
2067
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
2068
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
2069
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
2070
                                $relatedId = $targetClass->getIdentifierObject($other);
2071
2072
                                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...
2073
                                    $other = $this->dm->find($targetClass->name, $relatedId);
2074
                                } else {
2075
                                    $other = $this
2076
                                        ->dm
2077
                                        ->getProxyFactory()
2078
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
2079
                                    $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...
2080
                                }
2081
                            }
2082
2083
                            $prop->setValue($managedCopy, $other);
2084
                        }
2085 4
                    } else {
2086 10
                        $mergeCol = $prop->getValue($document);
2087
2088 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
2089
                            /* Do not merge fields marked lazy that have not
2090
                             * been fetched. Keep the lazy persistent collection
2091
                             * of the managed copy.
2092
                             */
2093 3
                            continue;
2094
                        }
2095
2096 7
                        $managedCol = $prop->getValue($managedCopy);
2097
2098 7
                        if ( ! $managedCol) {
2099 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
2100 2
                            $managedCol->setOwner($managedCopy, $assoc2);
2101 2
                            $prop->setValue($managedCopy, $managedCol);
2102 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
2103 2
                        }
2104
2105
                        /* Note: do not process association's target documents.
2106
                         * They will be handled during the cascade. Initialize
2107
                         * and, if necessary, clear $managedCol for now.
2108
                         */
2109 7
                        if ($assoc2['isCascadeMerge']) {
2110 7
                            $managedCol->initialize();
2111
2112
                            // If $managedCol differs from the merged collection, clear and set dirty
2113 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
2114 2
                                $managedCol->unwrap()->clear();
2115 2
                                $managedCol->setDirty(true);
2116
2117 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
2118
                                    $this->scheduleForSynchronization($managedCopy);
2119
                                }
2120 2
                            }
2121 7
                        }
2122
                    }
2123
                }
2124
2125 13
                if ($class->isChangeTrackingNotify()) {
2126
                    // Just treat all properties as changed, there is no other choice.
2127
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2128
                }
2129 13
            }
2130
2131 13
            if ($class->isChangeTrackingDeferredExplicit()) {
2132
                $this->scheduleForSynchronization($document);
2133
            }
2134 13
        }
2135
2136 13
        if ($prevManagedCopy !== null) {
2137 6
            $assocField = $assoc['fieldName'];
2138 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
2139
2140 6
            if ($assoc['type'] === 'one') {
2141 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
2142 2
            } else {
2143 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
2144
2145 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
2146 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
2147 1
                }
2148
            }
2149 6
        }
2150
2151
        // Mark the managed copy visited as well
2152 13
        $visited[spl_object_hash($managedCopy)] = true;
2153
2154 13
        $this->cascadeMerge($document, $managedCopy, $visited);
2155
2156 13
        return $managedCopy;
2157
    }
2158
2159
    /**
2160
     * Detaches a document from the persistence management. It's persistence will
2161
     * no longer be managed by Doctrine.
2162
     *
2163
     * @param object $document The document to detach.
2164
     */
2165 9
    public function detach($document)
2166
    {
2167 9
        $visited = array();
2168 9
        $this->doDetach($document, $visited);
2169 9
    }
2170
2171
    /**
2172
     * Executes a detach operation on the given document.
2173
     *
2174
     * @param object $document
2175
     * @param array $visited
2176
     * @internal This method always considers documents with an assigned identifier as DETACHED.
2177
     */
2178 12
    private function doDetach($document, array &$visited)
2179
    {
2180 12
        $oid = spl_object_hash($document);
2181 12
        if (isset($visited[$oid])) {
2182 4
            return; // Prevent infinite recursion
2183
        }
2184
2185 12
        $visited[$oid] = $document; // mark visited
2186
2187 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
2188 12
            case self::STATE_MANAGED:
2189 12
                $this->removeFromIdentityMap($document);
2190 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2191 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2192 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2193 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2194 12
                    $this->hasScheduledCollections[$oid]);
2195 12
                break;
2196 4
            case self::STATE_NEW:
2197 4
            case self::STATE_DETACHED:
2198 4
                return;
2199 12
        }
2200
2201 12
        $this->cascadeDetach($document, $visited);
2202 12
    }
2203
2204
    /**
2205
     * Refreshes the state of the given document from the database, overwriting
2206
     * any local, unpersisted changes.
2207
     *
2208
     * @param object $document The document to refresh.
2209
     * @throws \InvalidArgumentException If the document is not MANAGED.
2210
     */
2211 12
    public function refresh($document)
2212
    {
2213 12
        $visited = array();
2214 12
        $this->doRefresh($document, $visited);
2215 11
    }
2216
2217
    /**
2218
     * Executes a refresh operation on a document.
2219
     *
2220
     * @param object $document The document to refresh.
2221
     * @param array $visited The already visited documents during cascades.
2222
     * @throws \InvalidArgumentException If the document is not MANAGED.
2223
     */
2224 12
    private function doRefresh($document, array &$visited)
2225
    {
2226 12
        $oid = spl_object_hash($document);
2227 12
        if (isset($visited[$oid])) {
2228
            return; // Prevent infinite recursion
2229
        }
2230
2231 12
        $visited[$oid] = $document; // mark visited
2232
2233 12
        $class = $this->dm->getClassMetadata(get_class($document));
2234
2235 12
        if ( ! $class->isEmbeddedDocument) {
2236 12
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2237 11
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2238 11
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2239 11
            } else {
2240 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2241
            }
2242 11
        }
2243
2244 11
        $this->cascadeRefresh($document, $visited);
2245 11
    }
2246
2247
    /**
2248
     * Cascades a refresh operation to associated documents.
2249
     *
2250
     * @param object $document
2251
     * @param array $visited
2252
     */
2253 11
    private function cascadeRefresh($document, array &$visited)
2254
    {
2255 11
        $class = $this->dm->getClassMetadata(get_class($document));
2256
2257 11
        $associationMappings = array_filter(
2258 11
            $class->associationMappings,
2259
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2260 11
        );
2261
2262 11
        foreach ($associationMappings as $mapping) {
2263 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2264 7
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2265 7
                if ($relatedDocuments instanceof PersistentCollection) {
2266
                    // Unwrap so that foreach() does not initialize
2267 7
                    $relatedDocuments = $relatedDocuments->unwrap();
2268 7
                }
2269 7
                foreach ($relatedDocuments as $relatedDocument) {
2270
                    $this->doRefresh($relatedDocument, $visited);
2271 7
                }
2272 7
            } elseif ($relatedDocuments !== null) {
2273
                $this->doRefresh($relatedDocuments, $visited);
2274
            }
2275 11
        }
2276 11
    }
2277
2278
    /**
2279
     * Cascades a detach operation to associated documents.
2280
     *
2281
     * @param object $document
2282
     * @param array $visited
2283
     */
2284 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...
2285
    {
2286 12
        $class = $this->dm->getClassMetadata(get_class($document));
2287 12
        foreach ($class->fieldMappings as $mapping) {
2288 12
            if ( ! $mapping['isCascadeDetach']) {
2289 12
                continue;
2290
            }
2291 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2292 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2293 7
                if ($relatedDocuments instanceof PersistentCollection) {
2294
                    // Unwrap so that foreach() does not initialize
2295 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2296 6
                }
2297 7
                foreach ($relatedDocuments as $relatedDocument) {
2298 5
                    $this->doDetach($relatedDocument, $visited);
2299 7
                }
2300 7
            } elseif ($relatedDocuments !== null) {
2301 5
                $this->doDetach($relatedDocuments, $visited);
2302 5
            }
2303 12
        }
2304 12
    }
2305
    /**
2306
     * Cascades a merge operation to associated documents.
2307
     *
2308
     * @param object $document
2309
     * @param object $managedCopy
2310
     * @param array $visited
2311
     */
2312 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2313
    {
2314 13
        $class = $this->dm->getClassMetadata(get_class($document));
2315
2316 13
        $associationMappings = array_filter(
2317 13
            $class->associationMappings,
2318
            function ($assoc) { return $assoc['isCascadeMerge']; }
2319 13
        );
2320
2321 13
        foreach ($associationMappings as $assoc) {
2322 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2323
2324 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2325 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2326
                    // Collections are the same, so there is nothing to do
2327
                    continue;
2328
                }
2329
2330 8
                if ($relatedDocuments instanceof PersistentCollection) {
2331
                    // Unwrap so that foreach() does not initialize
2332 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2333 6
                }
2334
2335 8
                foreach ($relatedDocuments as $relatedDocument) {
2336 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2337 8
                }
2338 12
            } elseif ($relatedDocuments !== null) {
2339 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2340 3
            }
2341 13
        }
2342 13
    }
2343
2344
    /**
2345
     * Cascades the save operation to associated documents.
2346
     *
2347
     * @param object $document
2348
     * @param array $visited
2349
     */
2350 557
    private function cascadePersist($document, array &$visited)
2351
    {
2352 557
        $class = $this->dm->getClassMetadata(get_class($document));
2353
2354 557
        $associationMappings = array_filter(
2355 557
            $class->associationMappings,
2356
            function ($assoc) { return $assoc['isCascadePersist']; }
2357 557
        );
2358
2359 557
        foreach ($associationMappings as $fieldName => $mapping) {
2360 381
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2361
2362 381
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2363 331
                if ($relatedDocuments instanceof PersistentCollection) {
2364 16
                    if ($relatedDocuments->getOwner() !== $document) {
2365 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2366 2
                    }
2367
                    // Unwrap so that foreach() does not initialize
2368 16
                    $relatedDocuments = $relatedDocuments->unwrap();
2369 16
                }
2370
2371 331
                $count = 0;
2372 331
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2373 180
                    if ( ! empty($mapping['embedded'])) {
2374 107
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2375 107
                        if ($knownParent && $knownParent !== $document) {
2376 4
                            $relatedDocument = clone $relatedDocument;
2377 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2378 4
                        }
2379 107
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2380 107
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2381 107
                    }
2382 180
                    $this->doPersist($relatedDocument, $visited);
2383 330
                }
2384 381
            } elseif ($relatedDocuments !== null) {
2385 120
                if ( ! empty($mapping['embedded'])) {
2386 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2387 66
                    if ($knownParent && $knownParent !== $document) {
2388 5
                        $relatedDocuments = clone $relatedDocuments;
2389 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2390 5
                    }
2391 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2392 66
                }
2393 120
                $this->doPersist($relatedDocuments, $visited);
2394 119
            }
2395 556
        }
2396 555
    }
2397
2398
    /**
2399
     * Cascades the delete operation to associated documents.
2400
     *
2401
     * @param object $document
2402
     * @param array $visited
2403
     */
2404 65 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...
2405
    {
2406 65
        $class = $this->dm->getClassMetadata(get_class($document));
2407 65
        foreach ($class->fieldMappings as $mapping) {
2408 65
            if ( ! $mapping['isCascadeRemove']) {
2409 65
                continue;
2410
            }
2411 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...
2412 2
                $document->__load();
2413 2
            }
2414
2415 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2416 33
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2417
                // If its a PersistentCollection initialization is intended! No unwrap!
2418 24
                foreach ($relatedDocuments as $relatedDocument) {
2419 13
                    $this->doRemove($relatedDocument, $visited);
2420 24
                }
2421 33
            } elseif ($relatedDocuments !== null) {
2422 12
                $this->doRemove($relatedDocuments, $visited);
2423 12
            }
2424 65
        }
2425 65
    }
2426
2427
    /**
2428
     * Acquire a lock on the given document.
2429
     *
2430
     * @param object $document
2431
     * @param int $lockMode
2432
     * @param int $lockVersion
2433
     * @throws LockException
2434
     * @throws \InvalidArgumentException
2435
     */
2436 9
    public function lock($document, $lockMode, $lockVersion = null)
2437
    {
2438 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2439 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2440
        }
2441
2442 8
        $documentName = get_class($document);
2443 8
        $class = $this->dm->getClassMetadata($documentName);
2444
2445 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2446 3
            if ( ! $class->isVersioned) {
2447 1
                throw LockException::notVersioned($documentName);
2448
            }
2449
2450 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...
2451 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2452 2
                if ($documentVersion != $lockVersion) {
2453 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2454
                }
2455 1
            }
2456 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2457 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2458 5
        }
2459 6
    }
2460
2461
    /**
2462
     * Releases a lock on the given document.
2463
     *
2464
     * @param object $document
2465
     * @throws \InvalidArgumentException
2466
     */
2467 1
    public function unlock($document)
2468
    {
2469 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2470
            throw new \InvalidArgumentException("Document is not MANAGED.");
2471
        }
2472 1
        $documentName = get_class($document);
2473 1
        $this->getDocumentPersister($documentName)->unlock($document);
2474 1
    }
2475
2476
    /**
2477
     * Clears the UnitOfWork.
2478
     *
2479
     * @param string|null $documentName if given, only documents of this type will get detached.
2480
     */
2481 385
    public function clear($documentName = null)
2482
    {
2483 385
        if ($documentName === null) {
2484 379
            $this->identityMap =
2485 379
            $this->documentIdentifiers =
2486 379
            $this->originalDocumentData =
2487 379
            $this->documentChangeSets =
2488 379
            $this->documentStates =
2489 379
            $this->scheduledForSynchronization =
2490 379
            $this->documentInsertions =
2491 379
            $this->documentUpserts =
2492 379
            $this->documentUpdates =
2493 379
            $this->documentDeletions =
2494 379
            $this->collectionUpdates =
2495 379
            $this->collectionDeletions =
2496 379
            $this->parentAssociations =
2497 379
            $this->orphanRemovals = 
2498 379
            $this->hasScheduledCollections = array();
2499 379
        } else {
2500 6
            $visited = array();
2501 6
            foreach ($this->identityMap as $className => $documents) {
2502 6
                if ($className === $documentName) {
2503 3
                    foreach ($documents as $document) {
2504 3
                        $this->doDetach($document, $visited, true);
0 ignored issues
show
Unused Code introduced by
The call to UnitOfWork::doDetach() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2505 3
                    }
2506 3
                }
2507 6
            }
2508
        }
2509
2510 385 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...
2511
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2512
        }
2513 385
    }
2514
2515
    /**
2516
     * INTERNAL:
2517
     * Schedules an embedded document for removal. The remove() operation will be
2518
     * invoked on that document at the beginning of the next commit of this
2519
     * UnitOfWork.
2520
     *
2521
     * @ignore
2522
     * @param object $document
2523
     */
2524 48
    public function scheduleOrphanRemoval($document)
2525
    {
2526 48
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2527 48
    }
2528
2529
    /**
2530
     * INTERNAL:
2531
     * Unschedules an embedded or referenced object for removal.
2532
     *
2533
     * @ignore
2534
     * @param object $document
2535
     */
2536 95
    public function unscheduleOrphanRemoval($document)
2537
    {
2538 95
        $oid = spl_object_hash($document);
2539 95
        if (isset($this->orphanRemovals[$oid])) {
2540 3
            unset($this->orphanRemovals[$oid]);
2541 3
        }
2542 95
    }
2543
2544
    /**
2545
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2546
     *  1) sets owner if it was cloned
2547
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2548
     *  3) NOP if state is OK
2549
     * Returned collection should be used from now on (only important with 2nd point)
2550
     *
2551
     * @param PersistentCollection $coll
2552
     * @param object $document
2553
     * @param ClassMetadata $class
2554
     * @param string $propName
2555
     * @return PersistentCollection
2556
     */
2557 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2558
    {
2559 8
        $owner = $coll->getOwner();
2560 8
        if ($owner === null) { // cloned
2561 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2562 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2563 2
            if ( ! $coll->isInitialized()) {
2564 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2565 1
            }
2566 2
            $newValue = clone $coll;
2567 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2568 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2569 2
            if ($this->isScheduledForUpdate($document)) {
2570
                // @todo following line should be superfluous once collections are stored in change sets
2571
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2572
            }
2573 2
            return $newValue;
2574
        }
2575 6
        return $coll;
2576
    }
2577
2578
    /**
2579
     * INTERNAL:
2580
     * Schedules a complete collection for removal when this UnitOfWork commits.
2581
     *
2582
     * @param PersistentCollection $coll
2583
     */
2584 41
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2585
    {
2586 41
        $oid = spl_object_hash($coll);
2587 41
        unset($this->collectionUpdates[$oid]);
2588 41
        if ( ! isset($this->collectionDeletions[$oid])) {
2589 41
            $this->collectionDeletions[$oid] = $coll;
2590 41
            $this->scheduleCollectionOwner($coll);
2591 41
        }
2592 41
    }
2593
2594
    /**
2595
     * Checks whether a PersistentCollection is scheduled for deletion.
2596
     *
2597
     * @param PersistentCollection $coll
2598
     * @return boolean
2599
     */
2600 102
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2601
    {
2602 102
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2603
    }
2604
    
2605
    /**
2606
     * INTERNAL:
2607
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2608
     * 
2609
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2610
     */
2611 198 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...
2612
    {
2613 198
        $oid = spl_object_hash($coll);
2614 198
        if (isset($this->collectionDeletions[$oid])) {
2615 11
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2616 11
            unset($this->collectionDeletions[$oid]);
2617 11
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2618 11
        }
2619 198
    }
2620
2621
    /**
2622
     * INTERNAL:
2623
     * Schedules a collection for update when this UnitOfWork commits.
2624
     *
2625
     * @param PersistentCollection $coll
2626
     */
2627 213
    public function scheduleCollectionUpdate(PersistentCollection $coll)
2628
    {
2629 213
        $mapping = $coll->getMapping();
2630 213
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2631
            /* There is no need to $unset collection if it will be $set later
2632
             * This is NOP if collection is not scheduled for deletion
2633
             */
2634 34
            $this->unscheduleCollectionDeletion($coll);
2635 34
        }
2636 213
        $oid = spl_object_hash($coll);
2637 213
        if ( ! isset($this->collectionUpdates[$oid])) {
2638 213
            $this->collectionUpdates[$oid] = $coll;
2639 213
            $this->scheduleCollectionOwner($coll);
2640 213
        }
2641 213
    }
2642
    
2643
    /**
2644
     * INTERNAL:
2645
     * Unschedules a collection from being updated when this UnitOfWork commits.
2646
     * 
2647
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2648
     */
2649 198 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...
2650
    {
2651 198
        $oid = spl_object_hash($coll);
2652 198
        if (isset($this->collectionUpdates[$oid])) {
2653 188
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2654 188
            unset($this->collectionUpdates[$oid]);
2655 188
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2656 188
        }
2657 198
    }
2658
    
2659
    /**
2660
     * Checks whether a PersistentCollection is scheduled for update.
2661
     *
2662
     * @param PersistentCollection $coll
2663
     * @return boolean
2664
     */
2665 114
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2666
    {
2667 114
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2668
    }
2669
2670
    /**
2671
     * INTERNAL:
2672
     * Gets PersistentCollections that have been visited during computing change
2673
     * set of $document
2674
     *
2675
     * @param object $document
2676
     * @return PersistentCollection[]
2677
     */
2678 525
    public function getVisitedCollections($document)
2679
    {
2680 525
        $oid = spl_object_hash($document);
2681 525
        return isset($this->visitedCollections[$oid])
2682 525
                ? $this->visitedCollections[$oid]
2683 525
                : array();
2684
    }
2685
    
2686
    /**
2687
     * INTERNAL:
2688
     * Gets PersistentCollections that are scheduled to update and related to $document
2689
     * 
2690
     * @param object $document
2691
     * @return array
2692
     */
2693 525
    public function getScheduledCollections($document)
2694
    {
2695 525
        $oid = spl_object_hash($document);
2696 525
        return isset($this->hasScheduledCollections[$oid]) 
2697 525
                ? $this->hasScheduledCollections[$oid]
2698 525
                : array();
2699
    }
2700
    
2701
    /**
2702
     * Checks whether the document is related to a PersistentCollection
2703
     * scheduled for update or deletion.
2704
     *
2705
     * @param object $document
2706
     * @return boolean
2707
     */
2708 52
    public function hasScheduledCollections($document)
2709
    {
2710 52
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2711
    }
2712
    
2713
    /**
2714
     * Marks the PersistentCollection's top-level owner as having a relation to
2715
     * a collection scheduled for update or deletion.
2716
     *
2717
     * If the owner is not scheduled for any lifecycle action, it will be
2718
     * scheduled for update to ensure that versioning takes place if necessary.
2719
     *
2720
     * If the collection is nested within atomic collection, it is immediately
2721
     * unscheduled and atomic one is scheduled for update instead. This makes
2722
     * calculating update data way easier.
2723
     * 
2724
     * @param PersistentCollection $coll
2725
     */
2726 215
    private function scheduleCollectionOwner(PersistentCollection $coll)
2727
    {
2728 215
        $document = $this->getOwningDocument($coll->getOwner());
2729 215
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2730
2731 215
        if ($document !== $coll->getOwner()) {
2732 24
            $parent = $coll->getOwner();
2733 24
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2734 24
                list($mapping, $parent, ) = $parentAssoc;
2735 24
            }
2736 24
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2737 7
                $class = $this->dm->getClassMetadata(get_class($document));
2738 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2739 7
                $this->scheduleCollectionUpdate($atomicCollection);
2740 7
                $this->unscheduleCollectionDeletion($coll);
2741 7
                $this->unscheduleCollectionUpdate($coll);
2742 7
            }
2743 24
        }
2744
2745 215
        if ( ! $this->isDocumentScheduled($document)) {
2746 85
            $this->scheduleForUpdate($document);
2747 85
        }
2748 215
    }
2749
2750
    /**
2751
     * Get the top-most owning document of a given document
2752
     *
2753
     * If a top-level document is provided, that same document will be returned.
2754
     * For an embedded document, we will walk through parent associations until
2755
     * we find a top-level document.
2756
     *
2757
     * @param object $document
2758
     * @throws \UnexpectedValueException when a top-level document could not be found
2759
     * @return object
2760
     */
2761 217
    public function getOwningDocument($document)
2762
    {
2763 217
        $class = $this->dm->getClassMetadata(get_class($document));
2764 217
        while ($class->isEmbeddedDocument) {
2765 38
            $parentAssociation = $this->getParentAssociation($document);
2766
2767 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...
2768
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2769
            }
2770
2771 38
            list(, $document, ) = $parentAssociation;
2772 38
            $class = $this->dm->getClassMetadata(get_class($document));
2773 38
        }
2774
2775 217
        return $document;
2776
    }
2777
2778
    /**
2779
     * Gets the class name for an association (embed or reference) with respect
2780
     * to any discriminator value.
2781
     *
2782
     * @param array      $mapping Field mapping for the association
2783
     * @param array|null $data    Data for the embedded document or reference
2784
     */
2785 199
    public function getClassNameForAssociation(array $mapping, $data)
2786
    {
2787 199
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2788
2789 199
        $discriminatorValue = null;
2790 199
        if (isset($discriminatorField, $data[$discriminatorField])) {
2791 21
            $discriminatorValue = $data[$discriminatorField];
2792 199
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2793
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2794
        }
2795
2796 199
        if ($discriminatorValue !== null) {
2797 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2798 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2799 21
                : $discriminatorValue;
2800
        }
2801
2802 179
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2803
2804 179 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...
2805 15
            $discriminatorValue = $data[$class->discriminatorField];
2806 179
        } elseif ($class->defaultDiscriminatorValue !== null) {
2807 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2808 1
        }
2809
2810 179
        if ($discriminatorValue !== null) {
2811 16
            return isset($class->discriminatorMap[$discriminatorValue])
2812 16
                ? $class->discriminatorMap[$discriminatorValue]
2813 16
                : $discriminatorValue;
2814
        }
2815
2816 163
        return $mapping['targetDocument'];
2817
    }
2818
2819
    /**
2820
     * INTERNAL:
2821
     * Creates a document. Used for reconstitution of documents during hydration.
2822
     *
2823
     * @ignore
2824
     * @param string $className The name of the document class.
2825
     * @param array $data The data for the document.
2826
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2827
     * @param object The document to be hydrated into in case of creation
2828
     * @return object The document instance.
2829
     * @internal Highly performance-sensitive method.
2830
     */
2831 380
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2832
    {
2833 380
        $class = $this->dm->getClassMetadata($className);
2834
2835
        // @TODO figure out how to remove this
2836 380
        $discriminatorValue = null;
2837 380 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...
2838 17
            $discriminatorValue = $data[$class->discriminatorField];
2839 380
        } elseif (isset($class->defaultDiscriminatorValue)) {
2840 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2841 2
        }
2842
2843 380
        if ($discriminatorValue !== null) {
2844 18
            $className = isset($class->discriminatorMap[$discriminatorValue])
2845 18
                ? $class->discriminatorMap[$discriminatorValue]
2846 18
                : $discriminatorValue;
2847
2848 18
            $class = $this->dm->getClassMetadata($className);
2849
2850 18
            unset($data[$class->discriminatorField]);
2851 18
        }
2852
2853 380
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2854 380
        $serializedId = serialize($id);
2855
2856 380
        if (isset($this->identityMap[$class->name][$serializedId])) {
2857 89
            $document = $this->identityMap[$class->name][$serializedId];
2858 89
            $oid = spl_object_hash($document);
2859 89
            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...
2860 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...
2861 10
                $overrideLocalValues = true;
2862 10
                if ($document instanceof NotifyPropertyChanged) {
2863
                    $document->addPropertyChangedListener($this);
2864
                }
2865 10
            } else {
2866 85
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2867
            }
2868 89
            if ($overrideLocalValues) {
2869 46
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2870 46
                $this->originalDocumentData[$oid] = $data;
2871 46
            }
2872 89
        } else {
2873 352
            if ($document === null) {
2874 352
                $document = $class->newInstance();
2875 352
            }
2876 352
            $this->registerManaged($document, $id, $data);
2877 352
            $oid = spl_object_hash($document);
2878 352
            $this->documentStates[$oid] = self::STATE_MANAGED;
2879 352
            $this->identityMap[$class->name][$serializedId] = $document;
2880 352
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2881 352
            $this->originalDocumentData[$oid] = $data;
2882
        }
2883 380
        return $document;
2884
    }
2885
2886
    /**
2887
     * Initializes (loads) an uninitialized persistent collection of a document.
2888
     *
2889
     * @param PersistentCollection $collection The collection to initialize.
2890
     */
2891 149
    public function loadCollection(PersistentCollection $collection)
2892
    {
2893 149
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2894 149
    }
2895
2896
    /**
2897
     * Gets the identity map of the UnitOfWork.
2898
     *
2899
     * @return array
2900
     */
2901
    public function getIdentityMap()
2902
    {
2903
        return $this->identityMap;
2904
    }
2905
2906
    /**
2907
     * Gets the original data of a document. The original data is the data that was
2908
     * present at the time the document was reconstituted from the database.
2909
     *
2910
     * @param object $document
2911
     * @return array
2912
     */
2913 1
    public function getOriginalDocumentData($document)
2914
    {
2915 1
        $oid = spl_object_hash($document);
2916 1
        if (isset($this->originalDocumentData[$oid])) {
2917 1
            return $this->originalDocumentData[$oid];
2918
        }
2919
        return array();
2920
    }
2921
2922
    /**
2923
     * @ignore
2924
     */
2925 42
    public function setOriginalDocumentData($document, array $data)
2926
    {
2927 42
        $this->originalDocumentData[spl_object_hash($document)] = $data;
2928 42
    }
2929
2930
    /**
2931
     * INTERNAL:
2932
     * Sets a property value of the original data array of a document.
2933
     *
2934
     * @ignore
2935
     * @param string $oid
2936
     * @param string $property
2937
     * @param mixed $value
2938
     */
2939 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2940
    {
2941 3
        $this->originalDocumentData[$oid][$property] = $value;
2942 3
    }
2943
2944
    /**
2945
     * Gets the identifier of a document.
2946
     *
2947
     * @param object $document
2948
     * @return mixed The identifier value
2949
     */
2950 345
    public function getDocumentIdentifier($document)
2951
    {
2952 345
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2953 345
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2954
    }
2955
2956
    /**
2957
     * Checks whether the UnitOfWork has any pending insertions.
2958
     *
2959
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2960
     */
2961
    public function hasPendingInsertions()
2962
    {
2963
        return ! empty($this->documentInsertions);
2964
    }
2965
2966
    /**
2967
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2968
     * number of documents in the identity map.
2969
     *
2970
     * @return integer
2971
     */
2972 2
    public function size()
2973
    {
2974 2
        $count = 0;
2975 2
        foreach ($this->identityMap as $documentSet) {
2976 2
            $count += count($documentSet);
2977 2
        }
2978 2
        return $count;
2979
    }
2980
2981
    /**
2982
     * INTERNAL:
2983
     * Registers a document as managed.
2984
     *
2985
     * TODO: This method assumes that $id is a valid PHP identifier for the
2986
     * document class. If the class expects its database identifier to be a
2987
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
2988
     * document identifiers map will become inconsistent with the identity map.
2989
     * In the future, we may want to round-trip $id through a PHP and database
2990
     * conversion and throw an exception if it's inconsistent.
2991
     *
2992
     * @param object $document The document.
2993
     * @param array $id The identifier values.
2994
     * @param array $data The original document data.
2995
     */
2996 368
    public function registerManaged($document, $id, array $data)
2997
    {
2998 368
        $oid = spl_object_hash($document);
2999 368
        $class = $this->dm->getClassMetadata(get_class($document));
3000
3001 368
        if ( ! $class->identifier || $id === null) {
3002 102
            $this->documentIdentifiers[$oid] = $oid;
3003 102
        } else {
3004 362
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
3005
        }
3006
3007 368
        $this->documentStates[$oid] = self::STATE_MANAGED;
3008 368
        $this->originalDocumentData[$oid] = $data;
3009 368
        $this->addToIdentityMap($document);
3010 368
    }
3011
3012
    /**
3013
     * INTERNAL:
3014
     * Clears the property changeset of the document with the given OID.
3015
     *
3016
     * @param string $oid The document's OID.
3017
     */
3018 1
    public function clearDocumentChangeSet($oid)
3019
    {
3020 1
        $this->documentChangeSets[$oid] = array();
3021 1
    }
3022
3023
    /* PropertyChangedListener implementation */
3024
3025
    /**
3026
     * Notifies this UnitOfWork of a property change in a document.
3027
     *
3028
     * @param object $document The document that owns the property.
3029
     * @param string $propertyName The name of the property that changed.
3030
     * @param mixed $oldValue The old value of the property.
3031
     * @param mixed $newValue The new value of the property.
3032
     */
3033 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
3034
    {
3035 2
        $oid = spl_object_hash($document);
3036 2
        $class = $this->dm->getClassMetadata(get_class($document));
3037
3038 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
3039 1
            return; // ignore non-persistent fields
3040
        }
3041
3042
        // Update changeset and mark document for synchronization
3043 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
3044 2
        if ( ! isset($this->scheduledForSynchronization[$class->name][$oid])) {
3045 2
            $this->scheduleForSynchronization($document);
3046 2
        }
3047 2
    }
3048
3049
    /**
3050
     * Gets the currently scheduled document insertions in this UnitOfWork.
3051
     *
3052
     * @return array
3053
     */
3054 5
    public function getScheduledDocumentInsertions()
3055
    {
3056 5
        return $this->documentInsertions;
3057
    }
3058
3059
    /**
3060
     * Gets the currently scheduled document upserts in this UnitOfWork.
3061
     *
3062
     * @return array
3063
     */
3064 3
    public function getScheduledDocumentUpserts()
3065
    {
3066 3
        return $this->documentUpserts;
3067
    }
3068
3069
    /**
3070
     * Gets the currently scheduled document updates in this UnitOfWork.
3071
     *
3072
     * @return array
3073
     */
3074 3
    public function getScheduledDocumentUpdates()
3075
    {
3076 3
        return $this->documentUpdates;
3077
    }
3078
3079
    /**
3080
     * Gets the currently scheduled document deletions in this UnitOfWork.
3081
     *
3082
     * @return array
3083
     */
3084
    public function getScheduledDocumentDeletions()
3085
    {
3086
        return $this->documentDeletions;
3087
    }
3088
3089
    /**
3090
     * Get the currently scheduled complete collection deletions
3091
     *
3092
     * @return array
3093
     */
3094
    public function getScheduledCollectionDeletions()
3095
    {
3096
        return $this->collectionDeletions;
3097
    }
3098
3099
    /**
3100
     * Gets the currently scheduled collection inserts, updates and deletes.
3101
     *
3102
     * @return array
3103
     */
3104
    public function getScheduledCollectionUpdates()
3105
    {
3106
        return $this->collectionUpdates;
3107
    }
3108
3109
    /**
3110
     * Helper method to initialize a lazy loading proxy or persistent collection.
3111
     *
3112
     * @param object
3113
     * @return void
3114
     */
3115
    public function initializeObject($obj)
3116
    {
3117
        if ($obj instanceof Proxy) {
3118
            $obj->__load();
3119
        } elseif ($obj instanceof PersistentCollection) {
3120
            $obj->initialize();
3121
        }
3122
    }
3123
3124 1
    private static function objToStr($obj)
3125
    {
3126 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
3127
    }
3128
}
3129