Failed Conditions
Pull Request — 2.8.x (#7935)
by Michael
62:22
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 14.0713

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 14
eloc 30
c 2
b 1
f 0
nop 2
dl 0
loc 64
ccs 26
cts 28
cp 0.9286
crap 14.0713
rs 6.2666
nc 37

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\ORM;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\NotifyPropertyChanged;
25
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
26
use Doctrine\Common\Persistence\ObjectManagerAware;
27
use Doctrine\Common\PropertyChangedListener;
28
use Doctrine\DBAL\LockMode;
29
use Doctrine\ORM\Cache\Persister\CachedPersister;
30
use Doctrine\ORM\Event\LifecycleEventArgs;
31
use Doctrine\ORM\Event\ListenersInvoker;
32
use Doctrine\ORM\Event\OnFlushEventArgs;
33
use Doctrine\ORM\Event\PostFlushEventArgs;
34
use Doctrine\ORM\Event\PreFlushEventArgs;
35
use Doctrine\ORM\Event\PreUpdateEventArgs;
36
use Doctrine\ORM\Internal\HydrationCompleteHandler;
37
use Doctrine\ORM\Mapping\ClassMetadata;
38
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
39
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
40
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
41
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
42
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
43
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
44
use Doctrine\ORM\Proxy\Proxy;
45
use Doctrine\ORM\Utility\IdentifierFlattener;
46
use InvalidArgumentException;
47
use Throwable;
48
use UnexpectedValueException;
49
use function get_class;
50
51
/**
52
 * The UnitOfWork is responsible for tracking changes to objects during an
53
 * "object-level" transaction and for writing out changes to the database
54
 * in the correct order.
55
 *
56
 * Internal note: This class contains highly performance-sensitive code.
57
 *
58
 * @since       2.0
59
 * @author      Benjamin Eberlei <[email protected]>
60
 * @author      Guilherme Blanco <[email protected]>
61
 * @author      Jonathan Wage <[email protected]>
62
 * @author      Roman Borschel <[email protected]>
63
 * @author      Rob Caiger <[email protected]>
64
 */
65
class UnitOfWork implements PropertyChangedListener
66
{
67
    /**
68
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
69
     */
70
    const STATE_MANAGED = 1;
71
72
    /**
73
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
74
     * and is not (yet) managed by an EntityManager.
75
     */
76
    const STATE_NEW = 2;
77
78
    /**
79
     * A detached entity is an instance with persistent state and identity that is not
80
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
81
     */
82
    const STATE_DETACHED = 3;
83
84
    /**
85
     * A removed entity instance is an instance with a persistent identity,
86
     * associated with an EntityManager, whose persistent state will be deleted
87
     * on commit.
88
     */
89
    const STATE_REMOVED = 4;
90
91
    /**
92
     * Hint used to collect all primary keys of associated entities during hydration
93
     * and execute it in a dedicated query afterwards
94
     * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
95
     */
96
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
97
98
    /**
99
     * The identity map that holds references to all managed entities that have
100
     * an identity. The entities are grouped by their class name.
101
     * Since all classes in a hierarchy must share the same identifier set,
102
     * we always take the root class name of the hierarchy.
103
     *
104
     * @var array
105
     */
106
    private $identityMap = [];
107
108
    /**
109
     * Map of all identifiers of managed entities.
110
     * Keys are object ids (spl_object_hash).
111
     *
112
     * @var array
113
     */
114
    private $entityIdentifiers = [];
115
116
    /**
117
     * Map of the original entity data of managed entities.
118
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
119
     * at commit time.
120
     *
121
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
122
     *                A value will only really be copied if the value in the entity is modified
123
     *                by the user.
124
     *
125
     * @var array
126
     */
127
    private $originalEntityData = [];
128
129
    /**
130
     * Map of entity changes. Keys are object ids (spl_object_hash).
131
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
132
     *
133
     * @var array
134
     */
135
    private $entityChangeSets = [];
136
137
    /**
138
     * The (cached) states of any known entities.
139
     * Keys are object ids (spl_object_hash).
140
     *
141
     * @var array
142
     */
143
    private $entityStates = [];
144
145
    /**
146
     * Map of entities that are scheduled for dirty checking at commit time.
147
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
148
     * Keys are object ids (spl_object_hash).
149
     *
150
     * @var array
151
     */
152
    private $scheduledForSynchronization = [];
153
154
    /**
155
     * A list of all pending entity insertions.
156
     *
157
     * @var array
158
     */
159
    private $entityInsertions = [];
160
161
    /**
162
     * A list of all pending entity updates.
163
     *
164
     * @var array
165
     */
166
    private $entityUpdates = [];
167
168
    /**
169
     * Any pending extra updates that have been scheduled by persisters.
170
     *
171
     * @var array
172
     */
173
    private $extraUpdates = [];
174
175
    /**
176
     * A list of all pending entity deletions.
177
     *
178
     * @var array
179
     */
180
    private $entityDeletions = [];
181
182
    /**
183
     * New entities that were discovered through relationships that were not
184
     * marked as cascade-persist. During flush, this array is populated and
185
     * then pruned of any entities that were discovered through a valid
186
     * cascade-persist path. (Leftovers cause an error.)
187
     *
188
     * Keys are OIDs, payload is a two-item array describing the association
189
     * and the entity.
190
     *
191
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
192
     */
193
    private $nonCascadedNewDetectedEntities = [];
194
195
    /**
196
     * All pending collection deletions.
197
     *
198
     * @var array
199
     */
200
    private $collectionDeletions = [];
201
202
    /**
203
     * All pending collection updates.
204
     *
205
     * @var array
206
     */
207
    private $collectionUpdates = [];
208
209
    /**
210
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
211
     * At the end of the UnitOfWork all these collections will make new snapshots
212
     * of their data.
213
     *
214
     * @var array
215
     */
216
    private $visitedCollections = [];
217
218
    /**
219
     * The EntityManager that "owns" this UnitOfWork instance.
220
     *
221
     * @var EntityManagerInterface
222
     */
223
    private $em;
224
225
    /**
226
     * The entity persister instances used to persist entity instances.
227
     *
228
     * @var array
229
     */
230
    private $persisters = [];
231
232
    /**
233
     * The collection persister instances used to persist collections.
234
     *
235
     * @var array
236
     */
237
    private $collectionPersisters = [];
238
239
    /**
240
     * The EventManager used for dispatching events.
241
     *
242
     * @var \Doctrine\Common\EventManager
243
     */
244
    private $evm;
245
246
    /**
247
     * The ListenersInvoker used for dispatching events.
248
     *
249
     * @var \Doctrine\ORM\Event\ListenersInvoker
250
     */
251
    private $listenersInvoker;
252
253
    /**
254
     * The IdentifierFlattener used for manipulating identifiers
255
     *
256
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
257
     */
258
    private $identifierFlattener;
259
260
    /**
261
     * Orphaned entities that are scheduled for removal.
262
     *
263
     * @var array
264
     */
265
    private $orphanRemovals = [];
266
267
    /**
268
     * Read-Only objects are never evaluated
269
     *
270
     * @var array
271
     */
272
    private $readOnlyObjects = [];
273
274
    /**
275
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
276
     *
277
     * @var array
278
     */
279
    private $eagerLoadingEntities = [];
280
281
    /**
282
     * @var boolean
283
     */
284
    protected $hasCache = false;
285
286
    /**
287
     * Helper for handling completion of hydration
288
     *
289
     * @var HydrationCompleteHandler
290
     */
291
    private $hydrationCompleteHandler;
292
293
    /**
294
     * @var ReflectionPropertiesGetter
295
     */
296
    private $reflectionPropertiesGetter;
297
298
    /**
299
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
300
     *
301
     * @param EntityManagerInterface $em
302
     */
303 2511
    public function __construct(EntityManagerInterface $em)
304
    {
305 2511
        $this->em                         = $em;
306 2511
        $this->evm                        = $em->getEventManager();
307 2511
        $this->listenersInvoker           = new ListenersInvoker($em);
308 2511
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
309 2511
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
310 2511
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
311 2511
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
312 2511
    }
313
314
    /**
315
     * Commits the UnitOfWork, executing all operations that have been postponed
316
     * up to this point. The state of all managed entities will be synchronized with
317
     * the database.
318
     *
319
     * The operations are executed in the following order:
320
     *
321
     * 1) All entity insertions
322
     * 2) All entity updates
323
     * 3) All collection deletions
324
     * 4) All collection updates
325
     * 5) All entity deletions
326
     *
327
     * @param null|object|array $entity
328
     *
329
     * @return void
330
     *
331
     * @throws \Exception
332
     */
333 1104
    public function commit($entity = null)
334
    {
335
        // Raise preFlush
336 1104
        if ($this->evm->hasListeners(Events::preFlush)) {
337 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
338
        }
339
340
        // Compute changes done since last commit.
341 1104
        if (null === $entity) {
342 1097
            $this->computeChangeSets();
343 14
        } elseif (is_object($entity)) {
344 12
            $this->computeSingleEntityChangeSet($entity);
345 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
346 2
            foreach ($entity as $object) {
347 2
                $this->computeSingleEntityChangeSet($object);
348
            }
349
        }
350
351 1101
        if ( ! ($this->entityInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions 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...
352 178
                $this->entityDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions 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...
353 140
                $this->entityUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates 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...
354 44
                $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...
355 40
                $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...
356 1101
                $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...
357 27
            $this->dispatchOnFlushEvent();
358 27
            $this->dispatchPostFlushEvent();
359
360 27
            $this->postCommitCleanup($entity);
361
362 27
            return; // Nothing to do.
363
        }
364
365 1097
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
366
367 1095
        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...
368 16
            foreach ($this->orphanRemovals as $orphan) {
369 16
                $this->remove($orphan);
370
            }
371
        }
372
373 1095
        $this->dispatchOnFlushEvent();
374
375
        // Now we need a commit order to maintain referential integrity
376 1095
        $commitOrder = $this->getCommitOrder();
377
378 1095
        $conn = $this->em->getConnection();
379 1095
        $conn->beginTransaction();
380
381
        try {
382
            // Collection deletions (deletions of complete collections)
383 1095
            foreach ($this->collectionDeletions as $collectionToDelete) {
384 21
                if (! $collectionToDelete instanceof PersistentCollection) {
385
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
386
387
                    continue;
388
                }
389
390
                // Deferred explicit tracked collections can be removed only when owning relation was persisted
391 21
                $owner = $collectionToDelete->getOwner();
392
393 21
                if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
394 21
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
395
                }
396
            }
397
398 1095
            if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
399 1091
                foreach ($commitOrder as $class) {
400 1091
                    $this->executeInserts($class);
401
                }
402
            }
403
404 1094
            if ($this->entityUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates 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...
405 123
                foreach ($commitOrder as $class) {
406 123
                    $this->executeUpdates($class);
407
                }
408
            }
409
410
            // Extra updates that were requested by persisters.
411 1090
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates 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...
412 44
                $this->executeExtraUpdates();
413
            }
414
415
            // Collection updates (deleteRows, updateRows, insertRows)
416 1090
            foreach ($this->collectionUpdates as $collectionToUpdate) {
417 552
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
418
            }
419
420
            // Entity deletions come last and need to be in reverse commit order
421 1090
            if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions 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...
422 65
                for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions 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...
423 65
                    $this->executeDeletions($commitOrder[$i]);
424
                }
425
            }
426
427 1090
            $conn->commit();
428 11
        } catch (Throwable $e) {
429 11
            $this->em->close();
430 11
            $conn->rollBack();
431
432 11
            $this->afterTransactionRolledBack();
433
434 11
            throw $e;
435
        }
436
437 1090
        $this->afterTransactionComplete();
438
439
        // Take new snapshots from visited collections
440 1090
        foreach ($this->visitedCollections as $coll) {
441 551
            $coll->takeSnapshot();
442
        }
443
444 1090
        $this->dispatchPostFlushEvent();
445
446 1089
        $this->postCommitCleanup($entity);
447 1089
    }
448
449
    /**
450
     * @param null|object|object[] $entity
451
     */
452 1093
    private function postCommitCleanup($entity) : void
453
    {
454 1093
        $this->entityInsertions =
455 1093
        $this->entityUpdates =
456 1093
        $this->entityDeletions =
457 1093
        $this->extraUpdates =
458 1093
        $this->collectionUpdates =
459 1093
        $this->nonCascadedNewDetectedEntities =
460 1093
        $this->collectionDeletions =
461 1093
        $this->visitedCollections =
462 1093
        $this->orphanRemovals = [];
463
464 1093
        if (null === $entity) {
465 1087
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
466
467 1087
            return;
468
        }
469
470 12
        $entities = \is_object($entity)
471 10
            ? [$entity]
472 12
            : $entity;
473
474 12
        foreach ($entities as $object) {
475 12
            $oid = \spl_object_hash($object);
476
477 12
            $this->clearEntityChangeSet($oid);
478
479 12
            unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
480
        }
481 12
    }
482
483
    /**
484
     * Computes the changesets of all entities scheduled for insertion.
485
     *
486
     * @return void
487
     */
488 1103
    private function computeScheduleInsertsChangeSets()
489
    {
490 1103
        foreach ($this->entityInsertions as $entity) {
491 1095
            $class = $this->em->getClassMetadata(get_class($entity));
492
493 1095
            $this->computeChangeSet($class, $entity);
494
        }
495 1101
    }
496
497
    /**
498
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
499
     *
500
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
501
     * 2. Read Only entities are skipped.
502
     * 3. Proxies are skipped.
503
     * 4. Only if entity is properly managed.
504
     *
505
     * @param object $entity
506
     *
507
     * @return void
508
     *
509
     * @throws \InvalidArgumentException
510
     */
511 14
    private function computeSingleEntityChangeSet($entity)
512
    {
513 14
        $state = $this->getEntityState($entity);
514
515 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
516 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
517
        }
518
519 13
        $class = $this->em->getClassMetadata(get_class($entity));
520
521 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
522 13
            $this->persist($entity);
523
        }
524
525
        // Compute changes for INSERTed entities first. This must always happen even in this case.
526 13
        $this->computeScheduleInsertsChangeSets();
527
528 13
        if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
529
            return;
530
        }
531
532
        // Ignore uninitialized proxy objects
533 13
        if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
534 1
            return;
535
        }
536
537
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
538 12
        $oid = spl_object_hash($entity);
539
540 12
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
541 7
            $this->computeChangeSet($class, $entity);
542
        }
543 12
    }
544
545
    /**
546
     * Executes any extra updates that have been scheduled.
547
     */
548 44
    private function executeExtraUpdates()
549
    {
550 44
        foreach ($this->extraUpdates as $oid => $update) {
551 44
            list ($entity, $changeset) = $update;
552
553 44
            $this->entityChangeSets[$oid] = $changeset;
554 44
            $this->getEntityPersister(get_class($entity))->update($entity);
555
        }
556
557 44
        $this->extraUpdates = [];
558 44
    }
559
560
    /**
561
     * Gets the changeset for an entity.
562
     *
563
     * @param object $entity
564
     *
565
     * @return array
566
     */
567
    public function & getEntityChangeSet($entity)
568
    {
569 1090
        $oid  = spl_object_hash($entity);
570 1090
        $data = [];
571
572 1090
        if (!isset($this->entityChangeSets[$oid])) {
573 4
            return $data;
574
        }
575
576 1090
        return $this->entityChangeSets[$oid];
577
    }
578
579
    /**
580
     * Computes the changes that happened to a single entity.
581
     *
582
     * Modifies/populates the following properties:
583
     *
584
     * {@link _originalEntityData}
585
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
586
     * then it was not fetched from the database and therefore we have no original
587
     * entity data yet. All of the current entity data is stored as the original entity data.
588
     *
589
     * {@link _entityChangeSets}
590
     * The changes detected on all properties of the entity are stored there.
591
     * A change is a tuple array where the first entry is the old value and the second
592
     * entry is the new value of the property. Changesets are used by persisters
593
     * to INSERT/UPDATE the persistent entity state.
594
     *
595
     * {@link _entityUpdates}
596
     * If the entity is already fully MANAGED (has been fetched from the database before)
597
     * and any changes to its properties are detected, then a reference to the entity is stored
598
     * there to mark it for an update.
599
     *
600
     * {@link _collectionDeletions}
601
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
602
     * then this collection is marked for deletion.
603
     *
604
     * @ignore
605
     *
606
     * @internal Don't call from the outside.
607
     *
608
     * @param ClassMetadata $class  The class descriptor of the entity.
609
     * @param object        $entity The entity for which to compute the changes.
610
     *
611
     * @return void
612
     */
613 1105
    public function computeChangeSet(ClassMetadata $class, $entity)
614
    {
615 1105
        $oid = spl_object_hash($entity);
616
617 1105
        if (isset($this->readOnlyObjects[$oid])) {
618 2
            return;
619
        }
620
621 1105
        if ( ! $class->isInheritanceTypeNone()) {
622 339
            $class = $this->em->getClassMetadata(get_class($entity));
623
        }
624
625 1105
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
626
627 1105
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
628 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
629
        }
630
631 1105
        $actualData = [];
632
633 1105
        foreach ($class->reflFields as $name => $refProp) {
634 1105
            $value = $refProp->getValue($entity);
635
636 1105
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
637 824
                if ($value instanceof PersistentCollection) {
638 207
                    if ($value->getOwner() === $entity) {
639 207
                        continue;
640
                    }
641
642 5
                    $value = new ArrayCollection($value->getValues());
643
                }
644
645
                // If $value is not a Collection then use an ArrayCollection.
646 819
                if ( ! $value instanceof Collection) {
647 249
                    $value = new ArrayCollection($value);
648
                }
649
650 819
                $assoc = $class->associationMappings[$name];
651
652
                // Inject PersistentCollection
653 819
                $value = new PersistentCollection(
654 819
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
655
                );
656 819
                $value->setOwner($entity, $assoc);
657 819
                $value->setDirty( ! $value->isEmpty());
658
659 819
                $class->reflFields[$name]->setValue($entity, $value);
660
661 819
                $actualData[$name] = $value;
662
663 819
                continue;
664
            }
665
666 1105
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
667 1105
                $actualData[$name] = $value;
668
            }
669
        }
670
671 1105
        if ( ! isset($this->originalEntityData[$oid])) {
672
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
673
            // These result in an INSERT.
674 1101
            $this->originalEntityData[$oid] = $actualData;
675 1101
            $changeSet = [];
676
677 1101
            foreach ($actualData as $propName => $actualValue) {
678 1077
                if ( ! isset($class->associationMappings[$propName])) {
679 1018
                    $changeSet[$propName] = [null, $actualValue];
680
681 1018
                    continue;
682
                }
683
684 956
                $assoc = $class->associationMappings[$propName];
685
686 956
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
687 956
                    $changeSet[$propName] = [null, $actualValue];
688
                }
689
            }
690
691 1101
            $this->entityChangeSets[$oid] = $changeSet;
692
        } else {
693
            // Entity is "fully" MANAGED: it was already fully persisted before
694
            // and we have a copy of the original data
695 280
            $originalData           = $this->originalEntityData[$oid];
696 280
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
697 280
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
698
                ? $this->entityChangeSets[$oid]
699 280
                : [];
700
701 280
            foreach ($actualData as $propName => $actualValue) {
702
                // skip field, its a partially omitted one!
703 262
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
704 8
                    continue;
705
                }
706
707 262
                $orgValue = $originalData[$propName];
708
709
                // skip if value haven't changed
710 262
                if ($orgValue === $actualValue) {
711 245
                    continue;
712
                }
713
714
                // if regular field
715 119
                if ( ! isset($class->associationMappings[$propName])) {
716 64
                    if ($isChangeTrackingNotify) {
717
                        continue;
718
                    }
719
720 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
721
722 64
                    continue;
723
                }
724
725 59
                $assoc = $class->associationMappings[$propName];
726
727
                // Persistent collection was exchanged with the "originally"
728
                // created one. This can only mean it was cloned and replaced
729
                // on another entity.
730 59
                if ($actualValue instanceof PersistentCollection) {
731 8
                    $owner = $actualValue->getOwner();
732 8
                    if ($owner === null) { // cloned
733
                        $actualValue->setOwner($entity, $assoc);
734 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
735
                        if (!$actualValue->isInitialized()) {
736
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
737
                        }
738
                        $newValue = clone $actualValue;
739
                        $newValue->setOwner($entity, $assoc);
740
                        $class->reflFields[$propName]->setValue($entity, $newValue);
741
                    }
742
                }
743
744 59
                if ($orgValue instanceof PersistentCollection) {
745
                    // A PersistentCollection was de-referenced, so delete it.
746 8
                    $coid = spl_object_hash($orgValue);
747
748 8
                    if (isset($this->collectionDeletions[$coid])) {
749
                        continue;
750
                    }
751
752 8
                    $this->collectionDeletions[$coid] = $orgValue;
753 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
754
755 8
                    continue;
756
                }
757
758 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
759 50
                    if ($assoc['isOwningSide']) {
760 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
761
                    }
762
763 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
764 51
                        $this->scheduleOrphanRemoval($orgValue);
765
                    }
766
                }
767
            }
768
769 280
            if ($changeSet) {
770 92
                $this->entityChangeSets[$oid]   = $changeSet;
771 92
                $this->originalEntityData[$oid] = $actualData;
772 92
                $this->entityUpdates[$oid]      = $entity;
773
            }
774
        }
775
776
        // Look for changes in associations of the entity
777 1105
        foreach ($class->associationMappings as $field => $assoc) {
778 956
            if ((method_exists($class->reflFields[$field], 'isInitialized') && ! $class->reflFields[$field]->isInitialized($entity))
0 ignored issues
show
Bug introduced by
The method isInitialized() does not exist on ReflectionProperty. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

778
            if ((method_exists($class->reflFields[$field], 'isInitialized') && ! $class->reflFields[$field]->/** @scrutinizer ignore-call */ isInitialized($entity))

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
779 666
                || $class->reflFields[$field]->getValue($entity) === null
780
            ) {
781
                continue;
782 925
            }
783
784 917
            $this->computeAssociationChanges($assoc, $val);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $val does not exist. Did you maybe mean $value?
Loading history...
785 917
786 917
            if ( ! isset($this->entityChangeSets[$oid]) &&
787 917
                $assoc['isOwningSide'] &&
788 917
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
789
                $val instanceof PersistentCollection &&
790 35
                $val->isDirty()) {
791 35
792 917
                $this->entityChangeSets[$oid]   = [];
793
                $this->originalEntityData[$oid] = $actualData;
794
                $this->entityUpdates[$oid]      = $entity;
795 1097
            }
796
        }
797
    }
798
799
    /**
800
     * Computes all the changes that have been done to entities and collections
801
     * since the last commit and stores these changes in the _entityChangeSet map
802
     * temporarily for access by the persisters, until the UoW commit is finished.
803
     *
804 1097
     * @return void
805
     */
806
    public function computeChangeSets()
807 1097
    {
808
        // Compute changes for INSERTed entities first. This must always happen.
809
        $this->computeScheduleInsertsChangeSets();
810 1095
811 482
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
812
        foreach ($this->identityMap as $className => $entities) {
813
            $class = $this->em->getClassMetadata($className);
814 482
815 1
            // Skip class if instances are read-only
816
            if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
817
                continue;
818
            }
819
820
            // If change tracking is explicit or happens through notification, then only compute
821 481
            // changes on entities of that type that are explicitly marked for synchronization.
822 476
            switch (true) {
823 476
                case ($class->isChangeTrackingDeferredImplicit()):
824
                    $entitiesToProcess = $entities;
825 6
                    break;
826 5
827 5
                case (isset($this->scheduledForSynchronization[$className])):
828
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
829
                    break;
830 2
831
                default:
832
                    $entitiesToProcess = [];
833
834 481
            }
835
836 460
            foreach ($entitiesToProcess as $entity) {
837 38
                // Ignore uninitialized proxy objects
838
                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
839
                    continue;
840
                }
841 458
842
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
843 458
                $oid = spl_object_hash($entity);
844 481
845
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
846
                    $this->computeChangeSet($class, $entity);
847
                }
848 1095
            }
849
        }
850
    }
851
852
    /**
853
     * Computes the changes of an association.
854
     *
855
     * @param array $assoc The association mapping.
856
     * @param mixed $value The value of the association.
857
     *
858
     * @throws ORMInvalidArgumentException
859
     * @throws ORMException
860
     *
861 925
     * @return void
862
     */
863 925
    private function computeAssociationChanges($assoc, $value)
864 30
    {
865
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
866
            return;
867 924
        }
868 556
869
        if ($value instanceof PersistentCollection && $value->isDirty()) {
870 556
            $coid = spl_object_hash($value);
871 556
872
            $this->collectionUpdates[$coid] = $value;
873
            $this->visitedCollections[$coid] = $value;
874
        }
875
876
        // Look through the entities, and in any of their associations,
877 924
        // for transient (new) entities, recursively. ("Persistence by reachability")
878 924
        // Unwrap. Uninitialized collections will simply be empty.
879
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
880 924
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
881 764
882 8
        foreach ($unwrappedValue as $key => $entry) {
883
            if (! ($entry instanceof $targetClass->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
884
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
885 756
            }
886
887 756
            $state = $this->getEntityState($entry, self::STATE_NEW);
888
889
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
890
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
891
            }
892 756
893 42
            switch ($state) {
894
                case self::STATE_NEW:
895
                    if ( ! $assoc['isCascadePersist']) {
896
                        /*
897
                         * For now just record the details, because this may
898
                         * not be an issue if we later discover another pathway
899
                         * through the object-graph where cascade-persistence
900 6
                         * is enabled for this object.
901
                         */
902 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
903
904
                        break;
905 37
                    }
906 37
907
                    $this->persistNew($targetClass, $entry);
908 37
                    $this->computeChangeSet($targetClass, $entry);
909
910 748
                    break;
911
912
                case self::STATE_REMOVED:
913 4
                    // Consume the $value as array (it's either an array or an ArrayAccess)
914 3
                    // and remove the element from Collection.
915
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
916 4
                        unset($value[$key]);
917
                    }
918 748
                    break;
919
920
                case self::STATE_DETACHED:
921
                    // Can actually not happen right now as we assume STATE_NEW,
922
                    // so the exception will be raised from the DBAL layer (constraint violation).
923
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
924 756
                    break;
925
926
                default:
927
                    // MANAGED associated entities are already taken into account
928
                    // during changeset calculation anyway, since they are in the identity map.
929 916
            }
930
        }
931
    }
932
933
    /**
934
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
935
     * @param object                              $entity
936
     *
937 1128
     * @return void
938
     */
939 1128
    private function persistNew($class, $entity)
940 1128
    {
941
        $oid    = spl_object_hash($entity);
942 1128
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
943 141
944
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
945
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
946 1128
        }
947
948 1128
        $idGen = $class->idGenerator;
949 300
950
        if ( ! $idGen->isPostInsertGenerator()) {
951 300
            $idValue = $idGen->generate($this->em, $entity);
952 2
953
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
954 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
955
956
                $class->setIdentifierValues($entity, $idValue);
957
            }
958
959 300
            // Some identifiers may be foreign keys to new entities.
960 297
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
961
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
962
                $this->entityIdentifiers[$oid] = $idValue;
963
            }
964 1128
        }
965
966 1128
        $this->entityStates[$oid] = self::STATE_MANAGED;
967 1128
968
        $this->scheduleForInsert($entity);
969
    }
970
971
    /**
972 300
     * @param mixed[] $idValue
973
     */
974 300
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
975 300
    {
976 300
        foreach ($idValue as $idField => $idFieldValue) {
977
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
978
                return true;
979
            }
980 297
        }
981
982
        return false;
983
    }
984
985
    /**
986
     * INTERNAL:
987
     * Computes the changeset of an individual entity, independently of the
988
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
989
     *
990
     * The passed entity must be a managed entity. If the entity already has a change set
991
     * because this method is invoked during a commit cycle then the change sets are added.
992
     * whereby changes detected in this method prevail.
993
     *
994
     * @ignore
995
     *
996
     * @param ClassMetadata $class  The class descriptor of the entity.
997
     * @param object        $entity The entity for which to (re)calculate the change set.
998
     *
999
     * @return void
1000
     *
1001 16
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
1002
     */
1003 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
1004
    {
1005 16
        $oid = spl_object_hash($entity);
1006
1007
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
1008
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1009
        }
1010 16
1011
        // skip if change tracking is "NOTIFY"
1012
        if ($class->isChangeTrackingNotify()) {
1013
            return;
1014 16
        }
1015 3
1016
        if ( ! $class->isInheritanceTypeNone()) {
1017
            $class = $this->em->getClassMetadata(get_class($entity));
1018 16
        }
1019
1020 16
        $actualData = [];
1021 16
1022 16
        foreach ($class->reflFields as $name => $refProp) {
1023 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1024 16
                && ($name !== $class->versionField)
1025
                && ! $class->isCollectionValuedAssociation($name)
1026
                && (method_exists($refProp, 'isInitialized') && $refProp->isInitialized($entity))
1027
            ) {
1028 16
                $actualData[$name] = $refProp->getValue($entity);
1029
            }
1030
        }
1031
1032 16
        if ( ! isset($this->originalEntityData[$oid])) {
1033 16
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1034
        }
1035 16
1036 16
        $originalData = $this->originalEntityData[$oid];
1037
        $changeSet = [];
1038 16
1039 16
        foreach ($actualData as $propName => $actualValue) {
1040
            $orgValue = $originalData[$propName] ?? null;
1041
1042
            if ($orgValue !== $actualValue) {
1043 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1044 7
            }
1045 6
        }
1046 1
1047 1
        if ($changeSet) {
1048 1
            if (isset($this->entityChangeSets[$oid])) {
1049
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1050 7
            } else if ( ! isset($this->entityInsertions[$oid])) {
1051
                $this->entityChangeSets[$oid] = $changeSet;
1052 16
                $this->entityUpdates[$oid]    = $entity;
1053
            }
1054
            $this->originalEntityData[$oid] = $actualData;
1055
        }
1056
    }
1057
1058
    /**
1059
     * Executes all entity insertions for entities of the specified type.
1060
     *
1061 1091
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1062
     *
1063 1091
     * @return void
1064 1091
     */
1065 1091
    private function executeInserts($class)
1066 1091
    {
1067
        $entities   = [];
1068 1091
        $className  = $class->name;
1069
        $persister  = $this->getEntityPersister($className);
1070 1091
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1071
1072 1091
        $insertionsForClass = [];
1073 919
1074
        foreach ($this->entityInsertions as $oid => $entity) {
1075
1076 1091
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1077
                continue;
1078 1091
            }
1079
1080 1091
            $insertionsForClass[$oid] = $entity;
1081
1082 1091
            $persister->addInsert($entity);
1083 1091
1084
            unset($this->entityInsertions[$oid]);
1085
1086
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1087 1091
                $entities[] = $entity;
1088
            }
1089 1091
        }
1090
1091 985
        $postInsertIds = $persister->executeInserts();
1092 985
1093 985
        if ($postInsertIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postInsertIds 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...
1094
            // Persister returned post-insert IDs
1095 985
            foreach ($postInsertIds as $postInsertId) {
1096 985
                $idField = $class->getSingleIdentifierFieldName();
1097
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1098 985
1099
                $entity  = $postInsertId['entity'];
1100 985
                $oid     = spl_object_hash($entity);
1101 985
1102 985
                $class->reflFields[$idField]->setValue($entity, $idValue);
1103
1104 985
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1105
                $this->entityStates[$oid] = self::STATE_MANAGED;
1106
                $this->originalEntityData[$oid][$idField] = $idValue;
1107 823
1108 283
                $this->addToIdentityMap($entity);
1109
            }
1110
        } else {
1111 283
            foreach ($insertionsForClass as $oid => $entity) {
1112
                if (! isset($this->entityIdentifiers[$oid])) {
1113
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1114
                    //add it now
1115
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1116 1091
                }
1117 136
            }
1118
        }
1119 1091
1120
        foreach ($entities as $entity) {
1121
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1122
        }
1123
    }
1124 3
1125
    /**
1126 3
     * @param object $entity
1127
     */
1128 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1129 3
    {
1130
        $identifier = [];
1131 3
1132
        foreach ($class->getIdentifierFieldNames() as $idField) {
1133 3
            $value = $class->getFieldValue($entity, $idField);
1134
1135
            if (isset($class->associationMappings[$idField])) {
1136 3
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1137
                $value = $this->getSingleIdentifierValue($value);
1138
            }
1139 3
1140 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1141
        }
1142 3
1143 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1144
        $this->entityIdentifiers[$oid] = $identifier;
1145
1146
        $this->addToIdentityMap($entity);
1147
    }
1148
1149
    /**
1150
     * Executes all entity updates for entities of the specified type.
1151
     *
1152 123
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1153
     *
1154 123
     * @return void
1155 123
     */
1156 123
    private function executeUpdates($class)
1157 123
    {
1158
        $className          = $class->name;
1159 123
        $persister          = $this->getEntityPersister($className);
1160 123
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1161 79
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1162
1163
        foreach ($this->entityUpdates as $oid => $entity) {
1164 123
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1165 13
                continue;
1166
            }
1167 13
1168
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1169
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1170 123
1171 89
                $this->recomputeSingleEntityChangeSet($class, $entity);
1172
            }
1173
1174 119
            if ( ! empty($this->entityChangeSets[$oid])) {
1175
                $persister->update($entity);
1176 119
            }
1177 119
1178
            unset($this->entityUpdates[$oid]);
1179
1180 119
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1181
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1182
            }
1183
        }
1184
    }
1185
1186
    /**
1187
     * Executes all entity deletions for entities of the specified type.
1188
     *
1189 65
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1190
     *
1191 65
     * @return void
1192 65
     */
1193 65
    private function executeDeletions($class)
1194
    {
1195 65
        $className  = $class->name;
1196 65
        $persister  = $this->getEntityPersister($className);
1197 26
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1198
1199
        foreach ($this->entityDeletions as $oid => $entity) {
1200 65
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1201
                continue;
1202
            }
1203 65
1204 65
            $persister->delete($entity);
1205 65
1206 65
            unset(
1207
                $this->entityDeletions[$oid],
1208
                $this->entityIdentifiers[$oid],
1209
                $this->originalEntityData[$oid],
1210
                $this->entityStates[$oid]
1211
            );
1212 65
1213 53
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1214
            // is obtained by a new entity because the old one went out of scope.
1215
            //$this->entityStates[$oid] = self::STATE_NEW;
1216 65
            if ( ! $class->isIdentifierNatural()) {
1217 65
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1218
            }
1219
1220 64
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1221
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1222
            }
1223
        }
1224
    }
1225
1226
    /**
1227
     * Gets the commit order.
1228
     *
1229 1095
     * @param array|null $entityChangeSet
1230
     *
1231 1095
     * @return array
1232 1095
     */
1233
    private function getCommitOrder(array $entityChangeSet = null)
1234
    {
1235 1095
        if ($entityChangeSet === null) {
1236
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1237
        }
1238
1239
        $calc = $this->getCommitOrderCalculator();
1240
1241
        // See if there are any new classes in the changeset, that are not in the
1242 1095
        // commit order graph yet (don't have a node).
1243
        // We have to inspect changeSet to be able to correctly build dependencies.
1244 1095
        // It is not possible to use IdentityMap here because post inserted ids
1245 1095
        // are not yet available.
1246
        $newNodes = [];
1247 1095
1248 667
        foreach ($entityChangeSet as $entity) {
1249
            $class = $this->em->getClassMetadata(get_class($entity));
1250
1251 1095
            if ($calc->hasNode($class->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1252
                continue;
1253 1095
            }
1254
1255
            $calc->addNode($class->name, $class);
1256
1257 1095
            $newNodes[] = $class;
1258 1095
        }
1259 945
1260 897
        // Calculate dependencies for new nodes
1261
        while ($class = array_pop($newNodes)) {
1262
            foreach ($class->associationMappings as $assoc) {
1263 892
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1264
                    continue;
1265 892
                }
1266 684
1267
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1268 684
1269
                if ( ! $calc->hasNode($targetClass->name)) {
1270
                    $calc->addNode($targetClass->name, $targetClass);
1271 892
1272
                    $newNodes[] = $targetClass;
1273 892
                }
1274
1275
                $joinColumns = reset($assoc['joinColumns']);
1276 892
1277 885
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1278
1279
                // If the target class has mapped subclasses, these share the same dependency.
1280 239
                if ( ! $targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1281 239
                    continue;
1282
                }
1283 239
1284 209
                foreach ($targetClass->subClasses as $subClassName) {
1285
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1286 209
1287
                    if ( ! $calc->hasNode($subClassName)) {
1288
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1289 239
1290
                        $newNodes[] = $targetSubClass;
1291
                    }
1292
1293
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1294 1095
                }
1295
            }
1296
        }
1297
1298
        return $calc->sort();
1299
    }
1300
1301
    /**
1302
     * Schedules an entity for insertion into the database.
1303
     * If the entity already has an identifier, it will be added to the identity map.
1304
     *
1305
     * @param object $entity The entity to schedule for insertion.
1306
     *
1307
     * @return void
1308 1129
     *
1309
     * @throws ORMInvalidArgumentException
1310 1129
     * @throws \InvalidArgumentException
1311
     */
1312 1129
    public function scheduleForInsert($entity)
1313
    {
1314
        $oid = spl_object_hash($entity);
1315
1316 1129
        if (isset($this->entityUpdates[$oid])) {
1317 1
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1318
        }
1319 1129
1320 1
        if (isset($this->entityDeletions[$oid])) {
1321
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1322
        }
1323 1129
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1324 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1325
        }
1326
1327 1129
        if (isset($this->entityInsertions[$oid])) {
1328
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1329 1129
        }
1330 297
1331
        $this->entityInsertions[$oid] = $entity;
1332
1333 1129
        if (isset($this->entityIdentifiers[$oid])) {
1334 8
            $this->addToIdentityMap($entity);
1335
        }
1336 1129
1337
        if ($entity instanceof NotifyPropertyChanged) {
1338
            $entity->addPropertyChangedListener($this);
1339
        }
1340
    }
1341
1342
    /**
1343
     * Checks whether an entity is scheduled for insertion.
1344
     *
1345 662
     * @param object $entity
1346
     *
1347 662
     * @return boolean
1348
     */
1349
    public function isScheduledForInsert($entity)
1350
    {
1351
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1352
    }
1353
1354
    /**
1355
     * Schedules an entity for being updated.
1356
     *
1357
     * @param object $entity The entity to schedule for being updated.
1358
     *
1359 1
     * @return void
1360
     *
1361 1
     * @throws ORMInvalidArgumentException
1362
     */
1363 1
    public function scheduleForUpdate($entity)
1364
    {
1365
        $oid = spl_object_hash($entity);
1366
1367 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1368
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1369
        }
1370
1371 1
        if (isset($this->entityDeletions[$oid])) {
1372 1
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1373
        }
1374 1
1375
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1376
            $this->entityUpdates[$oid] = $entity;
1377
        }
1378
    }
1379
1380
    /**
1381
     * INTERNAL:
1382
     * Schedules an extra update that will be executed immediately after the
1383
     * regular entity updates within the currently running commit cycle.
1384
     *
1385
     * Extra updates for entities are stored as (entity, changeset) tuples.
1386
     *
1387
     * @ignore
1388
     *
1389
     * @param object $entity    The entity for which to schedule an extra update.
1390 44
     * @param array  $changeset The changeset of the entity (what to update).
1391
     *
1392 44
     * @return void
1393 44
     */
1394
    public function scheduleExtraUpdate($entity, array $changeset)
1395 44
    {
1396 1
        $oid         = spl_object_hash($entity);
1397
        $extraUpdate = [$entity, $changeset];
1398 1
1399
        if (isset($this->extraUpdates[$oid])) {
1400
            list(, $changeset2) = $this->extraUpdates[$oid];
1401 44
1402 44
            $extraUpdate = [$entity, $changeset + $changeset2];
1403
        }
1404
1405
        $this->extraUpdates[$oid] = $extraUpdate;
1406
    }
1407
1408
    /**
1409
     * Checks whether an entity is registered as dirty in the unit of work.
1410
     * Note: Is not very useful currently as dirty entities are only registered
1411
     * at commit time.
1412
     *
1413
     * @param object $entity
1414
     *
1415
     * @return boolean
1416
     */
1417
    public function isScheduledForUpdate($entity)
1418
    {
1419
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1420
    }
1421
1422
    /**
1423
     * Checks whether an entity is registered to be checked in the unit of work.
1424
     *
1425 5
     * @param object $entity
1426
     *
1427 5
     * @return boolean
1428
     */
1429 5
    public function isScheduledForDirtyCheck($entity)
1430
    {
1431
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1432
1433
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1434
    }
1435
1436
    /**
1437
     * INTERNAL:
1438
     * Schedules an entity for deletion.
1439
     *
1440 68
     * @param object $entity
1441
     *
1442 68
     * @return void
1443
     */
1444 68
    public function scheduleForDelete($entity)
1445 1
    {
1446
        $oid = spl_object_hash($entity);
1447
1448
        if (isset($this->entityInsertions[$oid])) {
1449 1
            if ($this->isInIdentityMap($entity)) {
1450
                $this->removeFromIdentityMap($entity);
1451 1
            }
1452
1453
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1454 68
1455 1
            return; // entity has not been persisted yet, so nothing more to do.
1456
        }
1457
1458 67
        if ( ! $this->isInIdentityMap($entity)) {
1459
            return;
1460 67
        }
1461
1462 67
        $this->removeFromIdentityMap($entity);
1463 67
1464 67
        unset($this->entityUpdates[$oid]);
1465
1466 67
        if ( ! isset($this->entityDeletions[$oid])) {
1467
            $this->entityDeletions[$oid] = $entity;
1468
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1469
        }
1470
    }
1471
1472
    /**
1473
     * Checks whether an entity is registered as removed/deleted with the unit
1474
     * of work.
1475
     *
1476 17
     * @param object $entity
1477
     *
1478 17
     * @return boolean
1479
     */
1480
    public function isScheduledForDelete($entity)
1481
    {
1482
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1483
    }
1484
1485
    /**
1486
     * Checks whether an entity is scheduled for insertion, update or deletion.
1487
     *
1488
     * @param object $entity
1489
     *
1490
     * @return boolean
1491
     */
1492
    public function isEntityScheduled($entity)
1493
    {
1494
        $oid = spl_object_hash($entity);
1495
1496
        return isset($this->entityInsertions[$oid])
1497
            || isset($this->entityUpdates[$oid])
1498
            || isset($this->entityDeletions[$oid]);
1499
    }
1500
1501
    /**
1502
     * INTERNAL:
1503
     * Registers an entity in the identity map.
1504
     * Note that entities in a hierarchy are registered with the class name of
1505
     * the root entity.
1506
     *
1507
     * @ignore
1508
     *
1509
     * @param object $entity The entity to register.
1510
     *
1511
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1512 1193
     *                 the entity in question is already managed.
1513
     *
1514 1193
     * @throws ORMInvalidArgumentException
1515 1193
     */
1516
    public function addToIdentityMap($entity)
1517 1193
    {
1518 6
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1519
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1520
1521 1187
        if (empty($identifier) || in_array(null, $identifier, true)) {
1522 1187
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1523
        }
1524 1187
1525 87
        $idHash    = implode(' ', $identifier);
1526
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1527
1528 1187
        if (isset($this->identityMap[$className][$idHash])) {
1529
            return false;
1530 1187
        }
1531
1532
        $this->identityMap[$className][$idHash] = $entity;
1533
1534
        return true;
1535
    }
1536
1537
    /**
1538
     * Gets the state of an entity with regard to the current unit of work.
1539
     *
1540
     * @param object   $entity
1541
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1542
     *                         This parameter can be set to improve performance of entity state detection
1543
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1544 1143
     *                         is either known or does not matter for the caller of the method.
1545
     *
1546 1143
     * @return int The entity state.
1547
     */
1548 1143
    public function getEntityState($entity, $assume = null)
1549 830
    {
1550
        $oid = spl_object_hash($entity);
1551
1552 1137
        if (isset($this->entityStates[$oid])) {
1553 1133
            return $this->entityStates[$oid];
1554
        }
1555
1556
        if ($assume !== null) {
1557
            return $assume;
1558
        }
1559
1560 13
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1561 13
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1562
        // the UoW does not hold references to such objects and the object hash can be reused.
1563 13
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1564 5
        $class = $this->em->getClassMetadata(get_class($entity));
1565
        $id    = $class->getIdentifierValues($entity);
1566
1567 10
        if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array<mixed,mixed> 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...
1568 1
            return self::STATE_NEW;
1569
        }
1570
1571
        if ($class->containsForeignIdentifier) {
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1572 10
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1573
        }
1574 5
1575 1
        switch (true) {
1576
            case ($class->isIdentifierNatural()):
0 ignored issues
show
Bug introduced by
The method isIdentifierNatural() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1576
            case ($class->/** @scrutinizer ignore-call */ isIdentifierNatural()):

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1577 1
                // Check for a version field, if available, to avoid a db lookup.
1578
                if ($class->isVersioned) {
0 ignored issues
show
Bug introduced by
Accessing isVersioned on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1579
                    return ($class->getFieldValue($entity, $class->versionField))
0 ignored issues
show
Bug introduced by
The method getFieldValue() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getFieldNames()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1579
                    return ($class->/** @scrutinizer ignore-call */ getFieldValue($entity, $class->versionField))

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
Accessing versionField on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1580
                        ? self::STATE_DETACHED
1581 4
                        : self::STATE_NEW;
1582 1
                }
1583
1584
                // Last try before db lookup: check the identity map.
1585
                if ($this->tryGetById($id, $class->rootEntityName)) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1586 4
                    return self::STATE_DETACHED;
1587
                }
1588
1589
                // db lookup
1590 4
                if ($this->getEntityPersister($class->name)->exists($entity)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1591
                    return self::STATE_DETACHED;
1592 5
                }
1593
1594
                return self::STATE_NEW;
1595
1596
            case ( ! $class->idGenerator->isPostInsertGenerator()):
0 ignored issues
show
Bug introduced by
Accessing idGenerator on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1597
                // if we have a pre insert generator we can't be sure that having an id
1598
                // really means that the entity exists. We have to verify this through
1599
                // the last resort: a db lookup
1600
1601
                // Last try before db lookup: check the identity map.
1602
                if ($this->tryGetById($id, $class->rootEntityName)) {
1603
                    return self::STATE_DETACHED;
1604
                }
1605
1606
                // db lookup
1607
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1608
                    return self::STATE_DETACHED;
1609
                }
1610 5
1611
                return self::STATE_NEW;
1612
1613
            default:
1614
                return self::STATE_DETACHED;
1615
        }
1616
    }
1617
1618
    /**
1619
     * INTERNAL:
1620
     * Removes an entity from the identity map. This effectively detaches the
1621
     * entity from the persistence management of Doctrine.
1622
     *
1623
     * @ignore
1624
     *
1625
     * @param object $entity
1626
     *
1627 82
     * @return boolean
1628
     *
1629 82
     * @throws ORMInvalidArgumentException
1630 82
     */
1631 82
    public function removeFromIdentityMap($entity)
1632
    {
1633 82
        $oid           = spl_object_hash($entity);
1634
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1635
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1636
1637 82
        if ($idHash === '') {
1638
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1639 82
        }
1640 82
1641 82
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1642
1643
        if (isset($this->identityMap[$className][$idHash])) {
1644
            unset($this->identityMap[$className][$idHash]);
1645 82
            unset($this->readOnlyObjects[$oid]);
1646
1647
            //$this->entityStates[$oid] = self::STATE_DETACHED;
1648
1649
            return true;
1650
        }
1651
1652
        return false;
1653
    }
1654
1655
    /**
1656
     * INTERNAL:
1657
     * Gets an entity in the identity map by its identifier hash.
1658
     *
1659
     * @ignore
1660
     *
1661
     * @param string $idHash
1662 6
     * @param string $rootClassName
1663
     *
1664 6
     * @return object
1665
     */
1666
    public function getByIdHash($idHash, $rootClassName)
1667
    {
1668
        return $this->identityMap[$rootClassName][$idHash];
1669
    }
1670
1671
    /**
1672
     * INTERNAL:
1673
     * Tries to get an entity by its identifier hash. If no entity is found for
1674
     * the given hash, FALSE is returned.
1675
     *
1676
     * @ignore
1677
     *
1678
     * @param mixed  $idHash        (must be possible to cast it to string)
1679 35
     * @param string $rootClassName
1680
     *
1681 35
     * @return object|bool The found entity or FALSE.
1682
     */
1683 35
    public function tryGetByIdHash($idHash, $rootClassName)
1684 35
    {
1685 35
        $stringIdHash = (string) $idHash;
1686
1687
        return isset($this->identityMap[$rootClassName][$stringIdHash])
1688
            ? $this->identityMap[$rootClassName][$stringIdHash]
1689
            : false;
1690
    }
1691
1692
    /**
1693
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1694
     *
1695 234
     * @param object $entity
1696
     *
1697 234
     * @return boolean
1698
     */
1699 234
    public function isInIdentityMap($entity)
1700 37
    {
1701
        $oid = spl_object_hash($entity);
1702
1703 217
        if (empty($this->entityIdentifiers[$oid])) {
1704 217
            return false;
1705
        }
1706 217
1707
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1708
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1709
1710
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1711
    }
1712
1713
    /**
1714
     * INTERNAL:
1715
     * Checks whether an identifier hash exists in the identity map.
1716
     *
1717
     * @ignore
1718
     *
1719
     * @param string $idHash
1720
     * @param string $rootClassName
1721
     *
1722
     * @return boolean
1723
     */
1724
    public function containsIdHash($idHash, $rootClassName)
1725
    {
1726
        return isset($this->identityMap[$rootClassName][$idHash]);
1727
    }
1728
1729
    /**
1730
     * Persists an entity as part of the current unit of work.
1731
     *
1732 1124
     * @param object $entity The entity to persist.
1733
     *
1734 1124
     * @return void
1735
     */
1736 1124
    public function persist($entity)
1737 1117
    {
1738
        $visited = [];
1739
1740
        $this->doPersist($entity, $visited);
1741
    }
1742
1743
    /**
1744
     * Persists an entity as part of the current unit of work.
1745
     *
1746
     * This method is internally called during persist() cascades as it tracks
1747
     * the already visited entities to prevent infinite recursions.
1748
     *
1749
     * @param object $entity  The entity to persist.
1750
     * @param array  $visited The already visited entities.
1751
     *
1752
     * @return void
1753 1124
     *
1754
     * @throws ORMInvalidArgumentException
1755 1124
     * @throws UnexpectedValueException
1756
     */
1757 1124
    private function doPersist($entity, array &$visited)
1758 111
    {
1759
        $oid = spl_object_hash($entity);
1760
1761 1124
        if (isset($visited[$oid])) {
1762
            return; // Prevent infinite recursion
1763 1124
        }
1764
1765
        $visited[$oid] = $entity; // Mark visited
1766
1767
        $class = $this->em->getClassMetadata(get_class($entity));
1768
1769 1124
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1770
        // If we would detect DETACHED here we would throw an exception anyway with the same
1771
        // consequences (not recoverable/programming error), so just assuming NEW here
1772 1124
        // lets us avoid some database lookups for entities with natural identifiers.
1773
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1774 240
1775 5
        switch ($entityState) {
1776
            case self::STATE_MANAGED:
1777 240
                // Nothing to do, except if policy is "deferred explicit"
1778
                if ($class->isChangeTrackingDeferredExplicit()) {
1779 1124
                    $this->scheduleForDirtyCheck($entity);
1780 1123
                }
1781 1123
                break;
1782
1783 1
            case self::STATE_NEW:
1784
                $this->persistNew($class, $entity);
1785 1
                break;
1786 1
1787
            case self::STATE_REMOVED:
1788 1
                // Entity becomes managed again
1789 1
                unset($this->entityDeletions[$oid]);
1790
                $this->addToIdentityMap($entity);
1791
1792
                $this->entityStates[$oid] = self::STATE_MANAGED;
1793
                break;
1794
1795
            case self::STATE_DETACHED:
1796
                // Can actually not happen right now since we assume STATE_NEW.
1797
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1798
1799 1124
            default:
1800 1117
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1801
        }
1802
1803
        $this->cascadePersist($entity, $visited);
1804
    }
1805
1806
    /**
1807
     * Deletes an entity as part of the current unit of work.
1808
     *
1809 67
     * @param object $entity The entity to remove.
1810
     *
1811 67
     * @return void
1812
     */
1813 67
    public function remove($entity)
1814 67
    {
1815
        $visited = [];
1816
1817
        $this->doRemove($entity, $visited);
1818
    }
1819
1820
    /**
1821
     * Deletes an entity as part of the current unit of work.
1822
     *
1823
     * This method is internally called during delete() cascades as it tracks
1824
     * the already visited entities to prevent infinite recursions.
1825
     *
1826
     * @param object $entity  The entity to delete.
1827
     * @param array  $visited The map of the already visited entities.
1828
     *
1829
     * @return void
1830 67
     *
1831
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1832 67
     * @throws UnexpectedValueException
1833
     */
1834 67
    private function doRemove($entity, array &$visited)
1835 1
    {
1836
        $oid = spl_object_hash($entity);
1837
1838 67
        if (isset($visited[$oid])) {
1839
            return; // Prevent infinite recursion
1840
        }
1841
1842 67
        $visited[$oid] = $entity; // mark visited
1843
1844 67
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1845 67
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1846
        $this->cascadeRemove($entity, $visited);
1847
1848 67
        $class       = $this->em->getClassMetadata(get_class($entity));
1849 67
        $entityState = $this->getEntityState($entity);
1850
1851 2
        switch ($entityState) {
1852
            case self::STATE_NEW:
1853 67
            case self::STATE_REMOVED:
1854 67
                // nothing to do
1855
                break;
1856 67
1857 8
            case self::STATE_MANAGED:
1858
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1859
1860 67
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1861 67
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1862
                }
1863
1864
                $this->scheduleForDelete($entity);
1865
                break;
1866
1867
            case self::STATE_DETACHED:
1868
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1869 67
            default:
1870
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1871
        }
1872
1873
    }
1874
1875
    /**
1876
     * Merges the state of the given detached entity into this UnitOfWork.
1877
     *
1878
     * @param object $entity
1879
     *
1880
     * @return object The managed copy of the entity.
1881
     *
1882
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1883 44
     *         attribute and the version check against the managed copy fails.
1884
     *
1885 44
     * @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
1886
     */
1887 44
    public function merge($entity)
1888
    {
1889
        $visited = [];
1890
1891
        return $this->doMerge($entity, $visited);
1892
    }
1893
1894
    /**
1895
     * Executes a merge operation on an entity.
1896
     *
1897
     * @param object      $entity
1898
     * @param array       $visited
1899
     * @param object|null $prevManagedCopy
1900
     * @param array|null  $assoc
1901
     *
1902
     * @return object The managed copy of the entity.
1903
     *
1904
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1905 44
     *         attribute and the version check against the managed copy fails.
1906
     * @throws ORMInvalidArgumentException If the entity instance is NEW.
1907 44
     * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided
1908
     */
1909 44
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
1910 4
    {
1911
        $oid = spl_object_hash($entity);
1912 4
1913 4
        if (isset($visited[$oid])) {
1914
            $managedCopy = $visited[$oid];
1915
1916 4
            if ($prevManagedCopy !== null) {
1917
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1918
            }
1919 44
1920
            return $managedCopy;
1921
        }
1922
1923
        $class = $this->em->getClassMetadata(get_class($entity));
1924
1925 44
        // First we assume DETACHED, although it can still be NEW but we can avoid
1926
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1927 44
        // we need to fetch it from the db anyway in order to merge.
1928
        // MANAGED entities are ignored by the merge operation.
1929 42
        $managedCopy = $entity;
1930
1931
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1932 42
            // Try to look the entity up in the identity map.
1933 6
            $id = $class->getIdentifierValues($entity);
1934
1935 6
            // If there is no ID, it is actually NEW.
1936 6
            if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array<mixed,mixed> 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...
1937
                $managedCopy = $this->newInstance($class);
1938 37
1939 3
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1940 37
                $this->persistNew($class, $managedCopy);
1941
            } else {
1942 37
                $flatId = ($class->containsForeignIdentifier)
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1943
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1944 37
                    : $id;
1945
1946 15
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1947 15
1948
                if ($managedCopy) {
1949
                    // We have the entity in-memory already, just make sure its not removed.
1950
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $entity of Doctrine\ORM\UnitOfWork::getEntityState() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1950
                    if ($this->getEntityState(/** @scrutinizer ignore-type */ $managedCopy) == self::STATE_REMOVED) {
Loading history...
1951 25
                        throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge");
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $entity of Doctrine\ORM\ORMInvalidA...tion::entityIsRemoved() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1951
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, "merge");
Loading history...
1952
                    }
1953
                } else {
1954 37
                    // We need to fetch the managed copy in order to merge.
1955
                    $managedCopy = $this->em->find($class->name, $flatId);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1956
                }
1957 3
1958 1
                if ($managedCopy === null) {
1959 1
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1960 1
                    // since the managed entity was not found.
1961
                    if ( ! $class->isIdentifierNatural()) {
1962
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1963
                            $class->getName(),
1964 2
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1965 2
                        );
1966
                    }
1967 2
1968 2
                    $managedCopy = $this->newInstance($class);
1969
                    $class->setIdentifierValues($managedCopy, $id);
1970 34
1971 33
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1972
                    $this->persistNew($class, $managedCopy);
1973
                } else {
1974
                    $this->ensureVersionMatch($class, $entity, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork::ensureVersionMatch() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1974
                    $this->ensureVersionMatch($class, $entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1975 40
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork:...yStateIntoManagedCopy() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1975
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1976
                }
1977 40
            }
1978
1979
            $visited[$oid] = $managedCopy; // mark visited
1980
1981
            if ($class->isChangeTrackingDeferredExplicit()) {
1982 42
                $this->scheduleForDirtyCheck($entity);
1983 6
            }
1984
        }
1985
1986
        if ($prevManagedCopy !== null) {
1987 42
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1987
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1988
        }
1989 42
1990
        // Mark the managed copy visited as well
1991 42
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $obj of spl_object_hash() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1991
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
1992
1993
        $this->cascadeMerge($entity, $managedCopy, $visited);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork::cascadeMerge() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1993
        $this->cascadeMerge($entity, /** @scrutinizer ignore-type */ $managedCopy, $visited);
Loading history...
1994
1995
        return $managedCopy;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $managedCopy also could return the type true which is incompatible with the documented return type object.
Loading history...
1996
    }
1997
1998
    /**
1999
     * @param ClassMetadata $class
2000
     * @param object        $entity
2001
     * @param object        $managedCopy
2002
     *
2003 34
     * @return void
2004
     *
2005 34
     * @throws OptimisticLockException
2006 31
     */
2007
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
2008
    {
2009 4
        if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
2010 4
            return;
2011 4
        }
2012
2013
        $reflField          = $class->reflFields[$class->versionField];
2014 4
        $managedCopyVersion = $reflField->getValue($managedCopy);
2015 3
        $entityVersion      = $reflField->getValue($entity);
2016
2017
        // Throw exception if versions don't match.
2018 1
        if ($managedCopyVersion == $entityVersion) {
2019
            return;
2020
        }
2021
2022
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
2023
    }
2024
2025
    /**
2026
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2027
     *
2028 41
     * @param object $entity
2029
     *
2030 41
     * @return bool
2031
     */
2032
    private function isLoaded($entity)
2033
    {
2034
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2035
    }
2036
2037
    /**
2038
     * Sets/adds associated managed copies into the previous entity's association field
2039
     *
2040
     * @param object $entity
2041
     * @param array  $association
2042
     * @param object $previousManagedCopy
2043 6
     * @param object $managedCopy
2044
     *
2045 6
     * @return void
2046 6
     */
2047
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2048 6
    {
2049 6
        $assocField = $association['fieldName'];
2050
        $prevClass  = $this->em->getClassMetadata(get_class($previousManagedCopy));
2051 6
2052
        if ($association['type'] & ClassMetadata::TO_ONE) {
2053
            $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2054 1
2055 1
            return;
2056
        }
2057 1
2058 1
        $value   = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
2059
        $value[] = $managedCopy;
2060 1
2061
        if ($association['type'] == ClassMetadata::ONE_TO_MANY) {
2062 1
            $class = $this->em->getClassMetadata(get_class($entity));
2063
2064
            $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
2065
        }
2066
    }
2067
2068
    /**
2069
     * Detaches an entity from the persistence management. It's persistence will
2070
     * no longer be managed by Doctrine.
2071
     *
2072
     * @param object $entity The entity to detach.
2073
     *
2074 9
     * @return void
2075
     *
2076 9
     * @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
2077
     */
2078 9
    public function detach($entity)
2079 9
    {
2080
        $visited = [];
2081
2082
        $this->doDetach($entity, $visited);
2083
    }
2084
2085
    /**
2086
     * Executes a detach operation on the given entity.
2087
     *
2088
     * @param object  $entity
2089
     * @param array   $visited
2090 18
     * @param boolean $noCascade if true, don't cascade detach operation.
2091
     *
2092 18
     * @return void
2093
     */
2094 18
    private function doDetach($entity, array &$visited, $noCascade = false)
2095
    {
2096
        $oid = spl_object_hash($entity);
2097
2098 18
        if (isset($visited[$oid])) {
2099
            return; // Prevent infinite recursion
2100 18
        }
2101 18
2102 16
        $visited[$oid] = $entity; // mark visited
2103 15
2104
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2105
            case self::STATE_MANAGED:
2106
                if ($this->isInIdentityMap($entity)) {
2107 16
                    $this->removeFromIdentityMap($entity);
2108 16
                }
2109 16
2110 16
                unset(
2111 16
                    $this->entityInsertions[$oid],
2112 16
                    $this->entityUpdates[$oid],
2113
                    $this->entityDeletions[$oid],
2114 16
                    $this->entityIdentifiers[$oid],
2115 2
                    $this->entityStates[$oid],
2116 2
                    $this->originalEntityData[$oid]
2117 2
                );
2118
                break;
2119
            case self::STATE_NEW:
2120 16
            case self::STATE_DETACHED:
2121 16
                return;
2122
        }
2123 16
2124
        if ( ! $noCascade) {
2125
            $this->cascadeDetach($entity, $visited);
2126
        }
2127
    }
2128
2129
    /**
2130
     * Refreshes the state of the given entity from the database, overwriting
2131
     * any local, unpersisted changes.
2132
     *
2133
     * @param object $entity The entity to refresh.
2134
     *
2135 17
     * @return void
2136
     *
2137 17
     * @throws InvalidArgumentException If the entity is not MANAGED.
2138
     */
2139 17
    public function refresh($entity)
2140 17
    {
2141
        $visited = [];
2142
2143
        $this->doRefresh($entity, $visited);
2144
    }
2145
2146
    /**
2147
     * Executes a refresh operation on an entity.
2148
     *
2149
     * @param object $entity  The entity to refresh.
2150
     * @param array  $visited The already visited entities during cascades.
2151
     *
2152 17
     * @return void
2153
     *
2154 17
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
2155
     */
2156 17
    private function doRefresh($entity, array &$visited)
2157
    {
2158
        $oid = spl_object_hash($entity);
2159
2160 17
        if (isset($visited[$oid])) {
2161
            return; // Prevent infinite recursion
2162 17
        }
2163
2164 17
        $visited[$oid] = $entity; // mark visited
2165
2166
        $class = $this->em->getClassMetadata(get_class($entity));
2167
2168 17
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
2169 17
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2170 17
        }
2171
2172
        $this->getEntityPersister($class->name)->refresh(
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2173 17
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->ge...ntityIdentifiers[$oid]) can also be of type false; however, parameter $id of Doctrine\ORM\Persisters\...ityPersister::refresh() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2173
            /** @scrutinizer ignore-type */ array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
Loading history...
2174 17
            $entity
2175
        );
2176
2177
        $this->cascadeRefresh($entity, $visited);
2178
    }
2179
2180
    /**
2181
     * Cascades a refresh operation to associated entities.
2182
     *
2183
     * @param object $entity
2184 17
     * @param array  $visited
2185
     *
2186 17
     * @return void
2187
     */
2188 17
    private function cascadeRefresh($entity, array &$visited)
2189 17
    {
2190
        $class = $this->em->getClassMetadata(get_class($entity));
2191
2192
        $associationMappings = array_filter(
2193 17
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2194 5
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2195
        );
2196
2197 5
        foreach ($associationMappings as $assoc) {
2198
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2199 5
2200
            switch (true) {
2201
                case ($relatedEntities instanceof PersistentCollection):
2202
                    // Unwrap so that foreach() does not initialize
2203
                    $relatedEntities = $relatedEntities->unwrap();
2204 5
                    // break; is commented intentionally!
2205
2206
                case ($relatedEntities instanceof Collection):
2207 5
                case (is_array($relatedEntities)):
2208
                    foreach ($relatedEntities as $relatedEntity) {
2209
                        $this->doRefresh($relatedEntity, $visited);
2210
                    }
2211
                    break;
2212
2213 5
                case ($relatedEntities !== null):
2214
                    $this->doRefresh($relatedEntities, $visited);
2215
                    break;
2216
2217 17
                default:
2218
                    // Do nothing
2219
            }
2220
        }
2221
    }
2222
2223
    /**
2224
     * Cascades a detach operation to associated entities.
2225
     *
2226
     * @param object $entity
2227 16
     * @param array  $visited
2228
     *
2229 16
     * @return void
2230
     */
2231 16
    private function cascadeDetach($entity, array &$visited)
2232 16
    {
2233
        $class = $this->em->getClassMetadata(get_class($entity));
2234
2235
        $associationMappings = array_filter(
2236 16
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2237 4
            function ($assoc) { return $assoc['isCascadeDetach']; }
2238
        );
2239
2240 4
        foreach ($associationMappings as $assoc) {
2241
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2242 2
2243
            switch (true) {
2244
                case ($relatedEntities instanceof PersistentCollection):
2245 2
                    // Unwrap so that foreach() does not initialize
2246 1
                    $relatedEntities = $relatedEntities->unwrap();
2247 3
                    // break; is commented intentionally!
2248 1
2249
                case ($relatedEntities instanceof Collection):
2250 3
                case (is_array($relatedEntities)):
2251
                    foreach ($relatedEntities as $relatedEntity) {
2252 1
                        $this->doDetach($relatedEntity, $visited);
2253
                    }
2254
                    break;
2255
2256 4
                case ($relatedEntities !== null):
2257
                    $this->doDetach($relatedEntities, $visited);
2258
                    break;
2259
2260 16
                default:
2261
                    // Do nothing
2262
            }
2263
        }
2264
    }
2265
2266
    /**
2267
     * Cascades a merge operation to associated entities.
2268
     *
2269
     * @param object $entity
2270
     * @param object $managedCopy
2271 42
     * @param array  $visited
2272
     *
2273 42
     * @return void
2274
     */
2275 42
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2276 42
    {
2277
        $class = $this->em->getClassMetadata(get_class($entity));
2278
2279
        $associationMappings = array_filter(
2280 42
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2281 16
            function ($assoc) { return $assoc['isCascadeMerge']; }
2282
        );
2283 16
2284 10
        foreach ($associationMappings as $assoc) {
2285 1
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2286
2287
            if ($relatedEntities instanceof Collection) {
2288 9
                if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2289
                    continue;
2290 5
                }
2291
2292
                if ($relatedEntities instanceof PersistentCollection) {
2293 9
                    // Unwrap so that foreach() does not initialize
2294 9
                    $relatedEntities = $relatedEntities->unwrap();
2295
                }
2296 7
2297 15
                foreach ($relatedEntities as $relatedEntity) {
2298
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2299
                }
2300 42
            } else if ($relatedEntities !== null) {
2301
                $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2302
            }
2303
        }
2304
    }
2305
2306
    /**
2307
     * Cascades the save operation to associated entities.
2308
     *
2309
     * @param object $entity
2310 1124
     * @param array  $visited
2311
     *
2312 1124
     * @return void
2313
     */
2314 1124
    private function cascadePersist($entity, array &$visited)
2315 1124
    {
2316
        $class = $this->em->getClassMetadata(get_class($entity));
2317
2318
        $associationMappings = array_filter(
2319 1124
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2320 694
            function ($assoc) { return $assoc['isCascadePersist']; }
2321
        );
2322
2323 694
        foreach ($associationMappings as $assoc) {
2324
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2325 22
2326
            switch (true) {
2327
                case ($relatedEntities instanceof PersistentCollection):
2328 694
                    // Unwrap so that foreach() does not initialize
2329 628
                    $relatedEntities = $relatedEntities->unwrap();
2330 584
                    // break; is commented intentionally!
2331 3
2332 3
                case ($relatedEntities instanceof Collection):
2333 3
                case (is_array($relatedEntities)):
2334 3
                    if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
2335
                        throw ORMInvalidArgumentException::invalidAssociation(
2336
                            $this->em->getClassMetadata($assoc['targetEntity']),
2337
                            $assoc,
2338 581
                            $relatedEntities
2339 301
                        );
2340
                    }
2341
2342 581
                    foreach ($relatedEntities as $relatedEntity) {
2343
                        $this->doPersist($relatedEntity, $visited);
2344 612
                    }
2345 256
2346 4
                    break;
2347 4
2348 4
                case ($relatedEntities !== null):
2349 4
                    if (! $relatedEntities instanceof $assoc['targetEntity']) {
2350
                        throw ORMInvalidArgumentException::invalidAssociation(
2351
                            $this->em->getClassMetadata($assoc['targetEntity']),
2352
                            $assoc,
2353 252
                            $relatedEntities
2354 252
                        );
2355
                    }
2356 688
2357
                    $this->doPersist($relatedEntities, $visited);
2358
                    break;
2359
2360 1117
                default:
2361
                    // Do nothing
2362
            }
2363
        }
2364
    }
2365
2366
    /**
2367
     * Cascades the delete operation to associated entities.
2368
     *
2369
     * @param object $entity
2370 67
     * @param array  $visited
2371
     *
2372 67
     * @return void
2373
     */
2374 67
    private function cascadeRemove($entity, array &$visited)
2375 67
    {
2376
        $class = $this->em->getClassMetadata(get_class($entity));
2377
2378
        $associationMappings = array_filter(
2379 67
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2380
            function ($assoc) { return $assoc['isCascadeRemove']; }
2381 67
        );
2382 26
2383 6
        $entitiesToCascade = [];
2384
2385
        foreach ($associationMappings as $assoc) {
2386 26
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2387
                $entity->__load();
2388
            }
2389 26
2390 19
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2391
2392 20
            switch (true) {
2393 10
                case ($relatedEntities instanceof Collection):
2394
                case (is_array($relatedEntities)):
2395 20
                    // If its a PersistentCollection initialization is intended! No unwrap!
2396
                    foreach ($relatedEntities as $relatedEntity) {
2397 19
                        $entitiesToCascade[] = $relatedEntity;
2398 7
                    }
2399 7
                    break;
2400
2401 26
                case ($relatedEntities !== null):
2402
                    $entitiesToCascade[] = $relatedEntities;
2403
                    break;
2404
2405
                default:
2406 67
                    // Do nothing
2407 16
            }
2408
        }
2409 67
2410
        foreach ($entitiesToCascade as $relatedEntity) {
2411
            $this->doRemove($relatedEntity, $visited);
2412
        }
2413
    }
2414
2415
    /**
2416
     * Acquire a lock on the given entity.
2417
     *
2418
     * @param object $entity
2419
     * @param int    $lockMode
2420
     * @param int    $lockVersion
2421
     *
2422
     * @return void
2423
     *
2424 10
     * @throws ORMInvalidArgumentException
2425
     * @throws TransactionRequiredException
2426 10
     * @throws OptimisticLockException
2427 1
     */
2428
    public function lock($entity, $lockMode, $lockVersion = null)
2429
    {
2430 9
        if ($entity === null) {
2431 1
            throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock().");
2432
        }
2433
2434 8
        if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2435
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2436
        }
2437 8
2438 6
        $class = $this->em->getClassMetadata(get_class($entity));
2439 1
2440
        switch (true) {
2441
            case LockMode::OPTIMISTIC === $lockMode:
2442 5
                if ( ! $class->isVersioned) {
0 ignored issues
show
Bug introduced by
Accessing isVersioned on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2443 1
                    throw OptimisticLockException::notVersioned($class->name);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2444
                }
2445
2446 4
                if ($lockVersion === null) {
2447 1
                    return;
2448
                }
2449
2450 4
                if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2451
                    $entity->__load();
2452 4
                }
2453 2
2454
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
Accessing versionField on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2455
2456 2
                if ($entityVersion != $lockVersion) {
2457
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2458 2
                }
2459 2
2460 1
                break;
2461 2
2462 2
            case LockMode::NONE === $lockMode:
2463
            case LockMode::PESSIMISTIC_READ === $lockMode:
2464
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2465
                if (!$this->em->getConnection()->isTransactionActive()) {
2466
                    throw TransactionRequiredException::transactionRequired();
2467
                }
2468
2469
                $oid = spl_object_hash($entity);
2470
2471
                $this->getEntityPersister($class->name)->lock(
2472
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->ge...ntityIdentifiers[$oid]) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...EntityPersister::lock() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2472
                    /** @scrutinizer ignore-type */ array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
Loading history...
2473
                    $lockMode
2474
                );
2475
                break;
2476 2
2477
            default:
2478
                // Do nothing
2479
        }
2480
    }
2481
2482
    /**
2483 1095
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
2484
     *
2485 1095
     * @return \Doctrine\ORM\Internal\CommitOrderCalculator
2486
     */
2487
    public function getCommitOrderCalculator()
2488
    {
2489
        return new Internal\CommitOrderCalculator();
2490
    }
2491
2492
    /**
2493
     * Clears the UnitOfWork.
2494
     *
2495
     * @param string|null $entityName if given, only entities of this type will get detached.
2496
     *
2497 1332
     * @return void
2498
     *
2499 1332
     * @throws ORMInvalidArgumentException if an invalid entity name is given
2500 1329
     */
2501 1329
    public function clear($entityName = null)
2502 1329
    {
2503 1329
        if ($entityName === null) {
2504 1329
            $this->identityMap                    =
2505 1329
            $this->entityIdentifiers              =
2506 1329
            $this->originalEntityData             =
2507 1329
            $this->entityChangeSets               =
2508 1329
            $this->entityStates                   =
2509 1329
            $this->scheduledForSynchronization    =
2510 1329
            $this->entityInsertions               =
2511 1329
            $this->entityUpdates                  =
2512 1329
            $this->entityDeletions                =
2513 1329
            $this->nonCascadedNewDetectedEntities =
2514 1329
            $this->collectionDeletions            =
2515 1329
            $this->collectionUpdates              =
2516 1329
            $this->extraUpdates                   =
2517
            $this->readOnlyObjects                =
2518 9
            $this->visitedCollections             =
2519 9
            $this->eagerLoadingEntities           =
2520
            $this->orphanRemovals                 = [];
2521
        } else {
2522 1332
            $this->clearIdentityMapForEntityName($entityName);
2523 9
            $this->clearEntityInsertionsForEntityName($entityName);
2524
        }
2525 1332
2526
        if ($this->evm->hasListeners(Events::onClear)) {
2527
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
2528
        }
2529
    }
2530
2531
    /**
2532
     * INTERNAL:
2533
     * Schedules an orphaned entity for removal. The remove() operation will be
2534
     * invoked on that entity at the beginning of the next commit of this
2535
     * UnitOfWork.
2536
     *
2537
     * @ignore
2538
     *
2539 17
     * @param object $entity
2540
     *
2541 17
     * @return void
2542 17
     */
2543
    public function scheduleOrphanRemoval($entity)
2544
    {
2545
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2546
    }
2547
2548
    /**
2549
     * INTERNAL:
2550
     * Cancels a previously scheduled orphan removal.
2551
     *
2552
     * @ignore
2553
     *
2554 117
     * @param object $entity
2555
     *
2556 117
     * @return void
2557 117
     */
2558
    public function cancelOrphanRemoval($entity)
2559
    {
2560
        unset($this->orphanRemovals[spl_object_hash($entity)]);
2561
    }
2562
2563
    /**
2564
     * INTERNAL:
2565
     * Schedules a complete collection for removal when this UnitOfWork commits.
2566
     *
2567 16
     * @param PersistentCollection $coll
2568
     *
2569 16
     * @return void
2570
     */
2571
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2572
    {
2573 16
        $coid = spl_object_hash($coll);
2574
2575 16
        // TODO: if $coll is already scheduled for recreation ... what to do?
2576 16
        // Just remove $coll from the scheduled recreations?
2577
        unset($this->collectionUpdates[$coid]);
2578
2579
        $this->collectionDeletions[$coid] = $coll;
2580
    }
2581
2582
    /**
2583
     * @param PersistentCollection $coll
2584
     *
2585
     * @return bool
2586
     */
2587
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2588
    {
2589
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2590
    }
2591
2592
    /**
2593 728
     * @param ClassMetadata $class
2594
     *
2595 728
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
2596
     */
2597 728
    private function newInstance($class)
2598 4
    {
2599
        $entity = $class->newInstance();
2600
2601 728
        if ($entity instanceof \Doctrine\Common\Persistence\ObjectManagerAware) {
2602
            $entity->injectObjectManager($this->em, $class);
2603
        }
2604
2605
        return $entity;
2606
    }
2607
2608
    /**
2609
     * INTERNAL:
2610
     * Creates an entity. Used for reconstitution of persistent entities.
2611
     *
2612
     * Internal note: Highly performance-sensitive method.
2613
     *
2614
     * @ignore
2615
     *
2616
     * @param string $className The name of the entity class.
2617
     * @param array  $data      The data for the entity.
2618
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the entity.
2619
     *
2620 873
     * @return object The managed entity instance.
2621
     *
2622 873
     * @todo Rename: getOrCreateEntity
2623
     */
2624 873
    public function createEntity($className, array $data, &$hints = [])
2625 873
    {
2626
        $class = $this->em->getClassMetadata($className);
2627 873
2628 329
        $id = $this->identifierFlattener->flattenIdentifier($class, $data);
2629 329
        $idHash = implode(' ', $id);
2630
2631
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2632 329
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
2633 329
            $oid = spl_object_hash($entity);
2634 329
2635 329
            if (
2636 329
                isset($hints[Query::HINT_REFRESH])
2637
                && isset($hints[Query::HINT_REFRESH_ENTITY])
2638
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
2639
                && $unmanagedProxy instanceof Proxy
2640
                && $this->isIdentifierEquals($unmanagedProxy, $entity)
2641
            ) {
2642 2
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
2643 2
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
2644
                // refreshed object may be anything
2645
2646 2
                foreach ($class->identifier as $fieldName) {
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2647
                    $class->reflFields[$fieldName]->setValue($unmanagedProxy, null);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2648
                }
2649 327
2650 23
                return $unmanagedProxy;
2651
            }
2652 23
2653 23
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2654
                $entity->__setInitialized(true);
2655
2656 306
                if ($entity instanceof NotifyPropertyChanged) {
2657 306
                    $entity->addPropertyChangedListener($this);
2658 234
                }
2659
            } else {
2660
                if ( ! isset($hints[Query::HINT_REFRESH])
2661
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2662
                    return $entity;
2663 116
                }
2664 3
            }
2665
2666
            // inject ObjectManager upon refresh.
2667 116
            if ($entity instanceof ObjectManagerAware) {
2668
                $entity->injectObjectManager($this->em, $class);
2669 723
            }
2670 723
2671
            $this->originalEntityData[$oid] = $data;
2672 723
        } else {
2673 723
            $entity = $this->newInstance($class);
2674 723
            $oid    = spl_object_hash($entity);
2675
2676 723
            $this->entityIdentifiers[$oid]  = $id;
2677
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2678 723
            $this->originalEntityData[$oid] = $data;
2679 2
2680
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
2681
2682
            if ($entity instanceof NotifyPropertyChanged) {
2683 761
                $entity->addPropertyChangedListener($this);
2684 761
            }
2685 761
        }
2686
2687
        foreach ($data as $field => $value) {
2688
            if (isset($class->fieldMappings[$field])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2689
                $class->reflFields[$field]->setValue($entity, $value);
2690 761
            }
2691
        }
2692 761
2693
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2694
        unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2695
2696
        if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) {
2697 761
            unset($this->eagerLoadingEntities[$class->rootEntityName]);
2698 34
        }
2699
2700
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2701 727
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2702
            return $entity;
2703 623
        }
2704 261
2705
        foreach ($class->associationMappings as $field => $assoc) {
2706
            // Check if the association is not among the fetch-joined associations already.
2707 599
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2708
                continue;
2709
            }
2710 599
2711 516
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2712
2713
            switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $assoc['type'] & Doctrin...g\ClassMetadata::TO_ONE of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
2714 67
                case ($assoc['type'] & ClassMetadata::TO_ONE):
2715
                    if ( ! $assoc['isOwningSide']) {
2716 3
2717
                        // use the given entity association
2718 3
                        if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2719 3
2720
                            $this->originalEntityData[$oid][$field] = $data[$field];
2721 3
2722
                            $class->reflFields[$field]->setValue($entity, $data[$field]);
2723
                            $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
2724
2725 64
                            continue 2;
2726
                        }
2727 64
2728
                        // Inverse side of x-to-one can never be lazy
2729
                        $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
2730
2731 516
                        continue 2;
2732 39
                    }
2733 39
2734
                    // use the entity association
2735 39
                    if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2736
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2737
                        $this->originalEntityData[$oid][$field] = $data[$field];
2738 508
2739
                        break;
2740
                    }
2741 508
2742 508
                    $associatedId = [];
2743
2744 508
                    // TODO: Is this even computed right in all cases of composite keys?
2745 306
                    foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
2746 12
                        $joinColumnValue = $data[$srcColumn] ?? null;
2747
2748 306
                        if ($joinColumnValue !== null) {
2749
                            if ($targetClass->containsForeignIdentifier) {
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2750 296
                                $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
2751 296
                            } else {
2752
                                $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2753
                            }
2754 7
                        } elseif ($targetClass->containsForeignIdentifier
2755 508
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2756
                        ) {
2757
                            // the missing key is part of target's entity primary key
2758
                            $associatedId = [];
2759 508
                            break;
2760
                        }
2761 296
                    }
2762 296
2763
                    if ( ! $associatedId) {
2764 296
                        // Foreign key is NULL
2765
                        $class->reflFields[$field]->setValue($entity, null);
2766
                        $this->originalEntityData[$oid][$field] = null;
2767 306
2768 302
                        break;
2769
                    }
2770
2771
                    if ( ! isset($hints['fetchMode'][$class->name][$field])) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2772
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2773
                    }
2774
2775 306
                    // Foreign key is set
2776
                    // Check identity map first
2777
                    // FIXME: Can break easily with composite keys if join column values are in
2778 306
                    //        wrong order. The correct order is the one in ClassMetadata#identifier.
2779 178
                    $relatedIdHash = implode(' ', $associatedId);
2780
2781
                    switch (true) {
2782
                        case (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])):
2783
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2784 178
2785 178
                            // If this is an uninitialized proxy, we are deferring eager loads,
2786 178
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2787 178
                            // then we can append this entity for eager loading!
2788 178
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2789
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2790
                                !$targetClass->isIdentifierComposite &&
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2791
                                $newValue instanceof Proxy &&
2792
                                $newValue->__isInitialized__ === false) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2793 178
2794
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2795 205
                            }
2796
2797
                            break;
2798
2799 32
                        case ($targetClass->subClasses):
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2800 32
                            // If it might be a subtype, it can not be lazy. There isn't even
2801
                            // a way to solve this with deferred eager loading, which means putting
2802
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2803
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2804
                            break;
2805 175
2806 167
                        default:
2807 167
                            switch (true) {
2808
                                // We are negating the condition here. Other cases will assume it is valid!
2809
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2810 8
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2811
                                    break;
2812 8
2813
                                // Deferred eager load only works for single identifier classes
2814 8
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
2815 8
                                    // TODO: Is there a faster approach?
2816
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2817
2818
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2819
                                    break;
2820
2821
                                default:
2822
                                    // TODO: This is very imperformant, ignore it?
2823
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2824 175
                                    break;
2825 175
                            }
2826 175
2827
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2828
                            $newValueOid = spl_object_hash($newValue);
2829 175
                            $this->entityIdentifiers[$newValueOid] = $associatedId;
2830 175
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2831
2832
                            if (
2833
                                $newValue instanceof NotifyPropertyChanged &&
2834 175
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
2835
                            ) {
2836 175
                                $newValue->addPropertyChangedListener($this);
2837
                            }
2838
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2839 306
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2840 306
                            break;
2841
                    }
2842 306
2843 60
                    $this->originalEntityData[$oid][$field] = $newValue;
2844 60
                    $class->reflFields[$field]->setValue($entity, $newValue);
2845
2846
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2847 306
                        $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2848
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2849
                    }
2850
2851 507
                    break;
2852
2853
                default:
2854
                    // Ignore if its a cached collection
2855
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2856 507
                        break;
2857
                    }
2858 3
2859
                    // use the given collection
2860 3
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2861 3
2862
                        $data[$field]->setOwner($entity, $assoc);
2863 3
2864
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2865
                        $this->originalEntityData[$oid][$field] = $data[$field];
2866
2867 507
                        break;
2868 507
                    }
2869 507
2870
                    // Inject collection
2871 507
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2872 507
                    $pColl->setOwner($entity, $assoc);
2873
                    $pColl->setInitialized(false);
2874 507
2875 4
                    $reflField = $class->reflFields[$field];
2876 4
                    $reflField->setValue($entity, $pColl);
2877
2878
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2879 507
                        $this->loadCollection($pColl);
2880 599
                        $pColl->takeSnapshot();
2881
                    }
2882
2883
                    $this->originalEntityData[$oid][$field] = $pColl;
2884
                    break;
2885 727
            }
2886
        }
2887 727
2888
        // defer invoking of postLoad event to hydration complete step
2889
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2890
2891
        return $entity;
2892
    }
2893 945
2894
    /**
2895 945
     * @return void
2896 945
     */
2897
    public function triggerEagerLoads()
2898
    {
2899
        if ( ! $this->eagerLoadingEntities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->eagerLoadingEntities 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...
2900 7
            return;
2901 7
        }
2902
2903 7
        // avoid infinite recursion
2904 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2905
        $this->eagerLoadingEntities = [];
2906
2907
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2908 7
            if ( ! $ids) {
2909
                continue;
2910 7
            }
2911 7
2912
            $class = $this->em->getClassMetadata($entityName);
2913
2914 7
            $this->getEntityPersister($entityName)->loadAll(
2915
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
It seems like array_combine($class->id...ay(array_values($ids))) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...ityPersister::loadAll() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2915
                /** @scrutinizer ignore-type */ array_combine($class->identifier, [array_values($ids)])
Loading history...
2916
            );
2917
        }
2918
    }
2919
2920
    /**
2921
     * Initializes (loads) an uninitialized persistent collection of an entity.
2922
     *
2923
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2924
     *
2925 150
     * @return void
2926
     *
2927 150
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2928 150
     */
2929
    public function loadCollection(PersistentCollection $collection)
2930 150
    {
2931 150
        $assoc     = $collection->getMapping();
2932 78
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2933 78
2934
        switch ($assoc['type']) {
2935 86
            case ClassMetadata::ONE_TO_MANY:
2936 86
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2937 86
                break;
2938
2939
            case ClassMetadata::MANY_TO_MANY:
2940 150
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2941 150
                break;
2942
        }
2943
2944
        $collection->setInitialized(true);
2945
    }
2946
2947
    /**
2948 2
     * Gets the identity map of the UnitOfWork.
2949
     *
2950 2
     * @return array
2951
     */
2952
    public function getIdentityMap()
2953
    {
2954
        return $this->identityMap;
2955
    }
2956
2957
    /**
2958
     * Gets the original data of an entity. The original data is the data that was
2959
     * present at the time the entity was reconstituted from the database.
2960
     *
2961 123
     * @param object $entity
2962
     *
2963 123
     * @return array
2964
     */
2965 123
    public function getOriginalEntityData($entity)
2966 119
    {
2967 123
        $oid = spl_object_hash($entity);
2968
2969
        return isset($this->originalEntityData[$oid])
2970
            ? $this->originalEntityData[$oid]
2971
            : [];
2972
    }
2973
2974
    /**
2975
     * @ignore
2976
     *
2977
     * @param object $entity
2978
     * @param array  $data
2979
     *
2980
     * @return void
2981
     */
2982
    public function setOriginalEntityData($entity, array $data)
2983
    {
2984
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2985
    }
2986
2987
    /**
2988
     * INTERNAL:
2989
     * Sets a property value of the original data array of an entity.
2990
     *
2991
     * @ignore
2992
     *
2993
     * @param string $oid
2994
     * @param string $property
2995 315
     * @param mixed  $value
2996
     *
2997 315
     * @return void
2998 315
     */
2999
    public function setOriginalEntityProperty($oid, $property, $value)
3000
    {
3001
        $this->originalEntityData[$oid][$property] = $value;
3002
    }
3003
3004
    /**
3005
     * Gets the identifier of an entity.
3006
     * The returned value is always an array of identifier values. If the entity
3007
     * has a composite identifier then the identifier values are in the same
3008
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
3009
     *
3010 887
     * @param object $entity
3011
     *
3012 887
     * @return array The identifier values.
3013
     */
3014
    public function getEntityIdentifier($entity)
3015
    {
3016
        return $this->entityIdentifiers[spl_object_hash($entity)];
3017
    }
3018
3019
    /**
3020
     * Processes an entity instance to extract their identifier values.
3021
     *
3022
     * @param object $entity The entity instance.
3023
     *
3024 142
     * @return mixed A scalar value.
3025
     *
3026 142
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
3027
     */
3028 140
    public function getSingleIdentifierValue($entity)
3029
    {
3030
        $class = $this->em->getClassMetadata(get_class($entity));
3031
3032 140
        if ($class->isIdentifierComposite) {
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3033 126
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
3034 140
        }
3035
3036 140
        $values = $this->isInIdentityMap($entity)
3037
            ? $this->getEntityIdentifier($entity)
3038
            : $class->getIdentifierValues($entity);
3039
3040
        return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null;
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3041
    }
3042
3043
    /**
3044
     * Tries to find an entity with the given identifier in the identity map of
3045
     * this UnitOfWork.
3046
     *
3047
     * @param mixed  $id            The entity identifier to look for.
3048
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
3049 569
     *
3050
     * @return object|bool Returns the entity with the specified identifier if it exists in
3051 569
     *                     this UnitOfWork, FALSE otherwise.
3052
     */
3053 569
    public function tryGetById($id, $rootClassName)
3054 90
    {
3055 569
        $idHash = implode(' ', (array) $id);
3056
3057
        return isset($this->identityMap[$rootClassName][$idHash])
3058
            ? $this->identityMap[$rootClassName][$idHash]
3059
            : false;
3060
    }
3061
3062
    /**
3063
     * Schedules an entity for dirty-checking at commit-time.
3064
     *
3065
     * @param object $entity The entity to schedule for dirty-checking.
3066
     *
3067 9
     * @return void
3068
     *
3069 9
     * @todo Rename: scheduleForSynchronization
3070
     */
3071 9
    public function scheduleForDirtyCheck($entity)
3072 9
    {
3073
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3074
3075
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
3076
    }
3077
3078
    /**
3079
     * Checks whether the UnitOfWork has any pending insertions.
3080
     *
3081
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
3082
     */
3083
    public function hasPendingInsertions()
3084
    {
3085
        return ! empty($this->entityInsertions);
3086
    }
3087
3088
    /**
3089
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
3090 1
     * number of entities in the identity map.
3091
     *
3092 1
     * @return integer
3093
     */
3094 1
    public function size()
3095
    {
3096
        $countArray = array_map('count', $this->identityMap);
3097
3098
        return array_sum($countArray);
3099
    }
3100
3101
    /**
3102
     * Gets the EntityPersister for an Entity.
3103
     *
3104 1158
     * @param string $entityName The name of the Entity.
3105
     *
3106 1158
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
3107 910
     */
3108
    public function getEntityPersister($entityName)
3109
    {
3110 1158
        if (isset($this->persisters[$entityName])) {
3111
            return $this->persisters[$entityName];
3112
        }
3113 1158
3114 1108
        $class = $this->em->getClassMetadata($entityName);
3115 1108
3116
        switch (true) {
3117 396
            case ($class->isInheritanceTypeNone()):
3118 227
                $persister = new BasicEntityPersister($this->em, $class);
3119 227
                break;
3120
3121 363
            case ($class->isInheritanceTypeSingleTable()):
3122 363
                $persister = new SingleTablePersister($this->em, $class);
3123 363
                break;
3124
3125
            case ($class->isInheritanceTypeJoined()):
3126
                $persister = new JoinedSubclassPersister($this->em, $class);
3127
                break;
3128
3129 1158
            default:
3130 128
                throw new \RuntimeException('No persister found for entity.');
3131 128
        }
3132 128
3133 128
        if ($this->hasCache && $class->cache !== null) {
0 ignored issues
show
Bug introduced by
Accessing cache on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3134
            $persister = $this->em->getConfiguration()
3135
                ->getSecondLevelCacheConfiguration()
3136 1158
                ->getCacheFactory()
3137
                ->buildCachedEntityPersister($this->em, $persister, $class);
3138 1158
        }
3139
3140
        $this->persisters[$entityName] = $persister;
3141
3142
        return $this->persisters[$entityName];
3143
    }
3144
3145
    /**
3146
     * Gets a collection persister for a collection-valued association.
3147
     *
3148 591
     * @param array $association
3149
     *
3150 591
     * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister
3151 78
     */
3152 591
    public function getCollectionPersister(array $association)
3153
    {
3154 591
        $role = isset($association['cache'])
3155 459
            ? $association['sourceEntity'] . '::' . $association['fieldName']
3156
            : $association['type'];
3157
3158 591
        if (isset($this->collectionPersisters[$role])) {
3159 417
            return $this->collectionPersisters[$role];
3160 591
        }
3161
3162 591
        $persister = ClassMetadata::ONE_TO_MANY === $association['type']
3163 77
            ? new OneToManyPersister($this->em)
3164 77
            : new ManyToManyPersister($this->em);
3165 77
3166 77
        if ($this->hasCache && isset($association['cache'])) {
3167
            $persister = $this->em->getConfiguration()
3168
                ->getSecondLevelCacheConfiguration()
3169 591
                ->getCacheFactory()
3170
                ->buildCachedCollectionPersister($this->em, $persister, $association);
3171 591
        }
3172
3173
        $this->collectionPersisters[$role] = $persister;
3174
3175
        return $this->collectionPersisters[$role];
3176
    }
3177
3178
    /**
3179
     * INTERNAL:
3180
     * Registers an entity as managed.
3181
     *
3182
     * @param object $entity The entity.
3183
     * @param array  $id     The identifier values.
3184 211
     * @param array  $data   The original entity data.
3185
     *
3186 211
     * @return void
3187
     */
3188 211
    public function registerManaged($entity, array $id, array $data)
3189 211
    {
3190 211
        $oid = spl_object_hash($entity);
3191
3192 211
        $this->entityIdentifiers[$oid]  = $id;
3193
        $this->entityStates[$oid]       = self::STATE_MANAGED;
3194 205
        $this->originalEntityData[$oid] = $data;
3195 2
3196
        $this->addToIdentityMap($entity);
3197 205
3198
        if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) {
3199
            $entity->addPropertyChangedListener($this);
3200
        }
3201
    }
3202
3203
    /**
3204
     * INTERNAL:
3205
     * Clears the property changeset of the entity with the given OID.
3206
     *
3207 12
     * @param string $oid The entity's OID.
3208
     *
3209 12
     * @return void
3210 12
     */
3211
    public function clearEntityChangeSet($oid)
3212
    {
3213
        unset($this->entityChangeSets[$oid]);
3214
    }
3215
3216
    /* PropertyChangedListener implementation */
3217
3218
    /**
3219
     * Notifies this UnitOfWork of a property change in an entity.
3220
     *
3221
     * @param object $entity       The entity that owns the property.
3222
     * @param string $propertyName The name of the property that changed.
3223
     * @param mixed  $oldValue     The old value of the property.
3224 4
     * @param mixed  $newValue     The new value of the property.
3225
     *
3226 4
     * @return void
3227 4
     */
3228
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
3229 4
    {
3230
        $oid   = spl_object_hash($entity);
3231 4
        $class = $this->em->getClassMetadata(get_class($entity));
3232 1
3233
        $isAssocField = isset($class->associationMappings[$propertyName]);
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3234
3235
        if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3236 4
            return; // ignore non-persistent fields
3237
        }
3238 4
3239 4
        // Update changeset and mark entity for synchronization
3240
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
3241 4
3242
        if ( ! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3243
            $this->scheduleForDirtyCheck($entity);
3244
        }
3245
    }
3246
3247
    /**
3248 2
     * Gets the currently scheduled entity insertions in this UnitOfWork.
3249
     *
3250 2
     * @return array
3251
     */
3252
    public function getScheduledEntityInsertions()
3253
    {
3254
        return $this->entityInsertions;
3255
    }
3256
3257
    /**
3258 3
     * Gets the currently scheduled entity updates in this UnitOfWork.
3259
     *
3260 3
     * @return array
3261
     */
3262
    public function getScheduledEntityUpdates()
3263
    {
3264
        return $this->entityUpdates;
3265
    }
3266
3267
    /**
3268 1
     * Gets the currently scheduled entity deletions in this UnitOfWork.
3269
     *
3270 1
     * @return array
3271
     */
3272
    public function getScheduledEntityDeletions()
3273
    {
3274
        return $this->entityDeletions;
3275
    }
3276
3277
    /**
3278 1
     * Gets the currently scheduled complete collection deletions
3279
     *
3280 1
     * @return array
3281
     */
3282
    public function getScheduledCollectionDeletions()
3283
    {
3284
        return $this->collectionDeletions;
3285
    }
3286
3287
    /**
3288
     * Gets the currently scheduled collection inserts, updates and deletes.
3289
     *
3290
     * @return array
3291
     */
3292
    public function getScheduledCollectionUpdates()
3293
    {
3294
        return $this->collectionUpdates;
3295
    }
3296
3297
    /**
3298
     * Helper method to initialize a lazy loading proxy or persistent collection.
3299
     *
3300 2
     * @param object $obj
3301
     *
3302 2
     * @return void
3303 1
     */
3304
    public function initializeObject($obj)
3305 1
    {
3306
        if ($obj instanceof Proxy) {
3307
            $obj->__load();
3308 1
3309 1
            return;
3310
        }
3311 1
3312
        if ($obj instanceof PersistentCollection) {
3313
            $obj->initialize();
3314
        }
3315
    }
3316
3317
    /**
3318
     * Helper method to show an object as string.
3319
     *
3320 1
     * @param object $obj
3321
     *
3322 1
     * @return string
3323
     */
3324
    private static function objToStr($obj)
3325
    {
3326
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj).'@'.spl_object_hash($obj);
3327
    }
3328
3329
    /**
3330
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3331
     *
3332
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3333
     * on this object that might be necessary to perform a correct update.
3334
     *
3335
     * @param object $object
3336
     *
3337 6
     * @return void
3338
     *
3339 6
     * @throws ORMInvalidArgumentException
3340 1
     */
3341
    public function markReadOnly($object)
3342
    {
3343 5
        if ( ! is_object($object) || ! $this->isInIdentityMap($object)) {
3344 5
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3345
        }
3346
3347
        $this->readOnlyObjects[spl_object_hash($object)] = true;
3348
    }
3349
3350
    /**
3351
     * Is this entity read only?
3352
     *
3353
     * @param object $object
3354
     *
3355 3
     * @return bool
3356
     *
3357 3
     * @throws ORMInvalidArgumentException
3358
     */
3359
    public function isReadOnly($object)
3360
    {
3361 3
        if ( ! is_object($object)) {
3362
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3363
        }
3364
3365
        return isset($this->readOnlyObjects[spl_object_hash($object)]);
3366
    }
3367 1090
3368
    /**
3369
     * Perform whatever processing is encapsulated here after completion of the transaction.
3370 96
     */
3371 1090
    private function afterTransactionComplete()
3372 1090
    {
3373
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3374
            $persister->afterTransactionComplete();
3375
        });
3376
    }
3377 11
3378
    /**
3379
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
3380 3
     */
3381 11
    private function afterTransactionRolledBack()
3382 11
    {
3383
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3384
            $persister->afterTransactionRolledBack();
3385
        });
3386
    }
3387
3388
    /**
3389 1095
     * Performs an action after the transaction.
3390
     *
3391 1095
     * @param callable $callback
3392 999
     */
3393
    private function performCallbackOnCachedPersister(callable $callback)
3394
    {
3395 96
        if ( ! $this->hasCache) {
3396 96
            return;
3397 96
        }
3398
3399
        foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) {
3400 96
            if ($persister instanceof CachedPersister) {
3401
                $callback($persister);
3402 1099
            }
3403
        }
3404 1099
    }
3405 4
3406
    private function dispatchOnFlushEvent()
3407 1099
    {
3408
        if ($this->evm->hasListeners(Events::onFlush)) {
3409 1094
            $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3410
        }
3411 1094
    }
3412 5
3413
    private function dispatchPostFlushEvent()
3414 1093
    {
3415
        if ($this->evm->hasListeners(Events::postFlush)) {
3416
            $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3417
        }
3418
    }
3419
3420
    /**
3421
     * Verifies if two given entities actually are the same based on identifier comparison
3422
     *
3423
     * @param object $entity1
3424 14
     * @param object $entity2
3425
     *
3426 14
     * @return bool
3427
     */
3428
    private function isIdentifierEquals($entity1, $entity2)
3429
    {
3430 14
        if ($entity1 === $entity2) {
3431
            return true;
3432 14
        }
3433 11
3434
        $class = $this->em->getClassMetadata(get_class($entity1));
3435
3436 3
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
3437 3
            return false;
3438
        }
3439 3
3440 3
        $oid1 = spl_object_hash($entity1);
3441 3
        $oid2 = spl_object_hash($entity2);
3442 3
3443 3
        $id1 = isset($this->entityIdentifiers[$oid1])
3444 3
            ? $this->entityIdentifiers[$oid1]
3445
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
3446 3
        $id2 = isset($this->entityIdentifiers[$oid2])
3447
            ? $this->entityIdentifiers[$oid2]
3448
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
3449
3450
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
3451
    }
3452 1097
3453
    /**
3454 1097
     * @throws ORMInvalidArgumentException
3455
     */
3456 1097
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
3457
    {
3458 1097
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
3459 5
3460 5
        $this->nonCascadedNewDetectedEntities = [];
3461
3462
        if ($entitiesNeedingCascadePersist) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entitiesNeedingCascadePersist 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...
3463 1095
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
3464
                \array_values($entitiesNeedingCascadePersist)
3465
            );
3466
        }
3467
    }
3468
3469
    /**
3470
     * @param object $entity
3471
     * @param object $managedCopy
3472
     *
3473 40
     * @throws ORMException
3474
     * @throws OptimisticLockException
3475 40
     * @throws TransactionRequiredException
3476 7
     */
3477
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
3478
    {
3479 33
        if (! $this->isLoaded($entity)) {
3480 4
            return;
3481
        }
3482
3483 33
        if (! $this->isLoaded($managedCopy)) {
3484
            $managedCopy->__load();
3485 33
        }
3486 33
3487
        $class = $this->em->getClassMetadata(get_class($entity));
3488 33
3489
        foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
3490 33
            $name = $prop->name;
3491 33
3492 33
            $prop->setAccessible(true);
3493
3494
            if ( ! isset($class->associationMappings[$name])) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3495 29
                if ( ! $class->isIdentifier($name)) {
3496
                    $prop->setValue($managedCopy, $prop->getValue($entity));
3497 29
                }
3498 25
            } else {
3499 25
                $assoc2 = $class->associationMappings[$name];
3500 12
3501
                if ($assoc2['type'] & ClassMetadata::TO_ONE) {
3502 16
                    $other = $prop->getValue($entity);
3503
                    if ($other === null) {
3504 4
                        $prop->setValue($managedCopy, null);
3505
                    } else {
3506
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
3507 12
                            // do not merge fields marked lazy that have not been fetched.
3508 6
                            continue;
3509 3
                        }
3510 3
3511
                        if ( ! $assoc2['isCascadeMerge']) {
3512 3
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
3513 2
                                $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
3514
                                $relatedId   = $targetClass->getIdentifierValues($other);
3515 1
3516 1
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3517 1
                                    $other = $this->em->find($targetClass->name, $relatedId);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3518
                                } else {
3519 1
                                    $other = $this->em->getProxyFactory()->getProxy(
3520
                                        $assoc2['targetEntity'],
3521
                                        $relatedId
3522
                                    );
3523 21
                                    $this->registerManaged($other, $relatedId, []);
3524
                                }
3525
                            }
3526
3527 17
                            $prop->setValue($managedCopy, $other);
3528
                        }
3529 17
                    }
3530
                } else {
3531
                    $mergeCol = $prop->getValue($entity);
3532 5
3533
                    if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
3534
                        // do not merge fields marked lazy that have not been fetched.
3535 14
                        // keep the lazy persistent collection of the managed copy.
3536
                        continue;
3537 14
                    }
3538 4
3539 4
                    $managedCol = $prop->getValue($managedCopy);
3540 4
3541 4
                    if ( ! $managedCol) {
3542
                        $managedCol = new PersistentCollection(
3543 4
                            $this->em,
3544 4
                            $this->em->getClassMetadata($assoc2['targetEntity']),
3545
                            new ArrayCollection
3546
                        );
3547 14
                        $managedCol->setOwner($managedCopy, $assoc2);
3548 9
                        $prop->setValue($managedCopy, $managedCol);
3549
                    }
3550
3551 9
                    if ($assoc2['isCascadeMerge']) {
3552 1
                        $managedCol->initialize();
3553 1
3554
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
3555 1
                        if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
3556 1
                            $managedCol->unwrap()->clear();
3557 1
                            $managedCol->setDirty(true);
3558
3559
                            if ($assoc2['isOwningSide']
3560
                                && $assoc2['type'] == ClassMetadata::MANY_TO_MANY
3561
                                && $class->isChangeTrackingNotify()
3562
                            ) {
3563
                                $this->scheduleForDirtyCheck($managedCopy);
3564
                            }
3565
                        }
3566 33
                    }
3567
                }
3568 33
            }
3569
3570
            if ($class->isChangeTrackingNotify()) {
3571 33
                // Just treat all properties as changed, there is no other choice.
3572
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
3573
            }
3574
        }
3575
    }
3576
3577
    /**
3578
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
3579 959
     * Unit of work able to fire deferred events, related to loading events here.
3580
     *
3581 959
     * @internal should be called internally from object hydrators
3582 959
     */
3583
    public function hydrationComplete()
3584
    {
3585
        $this->hydrationCompleteHandler->hydrationComplete();
3586
    }
3587 9
3588
    /**
3589 9
     * @param string $entityName
3590
     */
3591
    private function clearIdentityMapForEntityName($entityName)
3592
    {
3593 9
        if (! isset($this->identityMap[$entityName])) {
3594
            return;
3595 9
        }
3596 9
3597
        $visited = [];
3598 9
3599
        foreach ($this->identityMap[$entityName] as $entity) {
3600
            $this->doDetach($entity, $visited, false);
3601
        }
3602
    }
3603 9
3604
    /**
3605 9
     * @param string $entityName
3606
     */
3607 1
    private function clearEntityInsertionsForEntityName($entityName)
3608 1
    {
3609
        foreach ($this->entityInsertions as $hash => $entity) {
3610
            // note: performance optimization - `instanceof` is much faster than a function call
3611 9
            if ($entity instanceof $entityName && get_class($entity) === $entityName) {
3612
                unset($this->entityInsertions[$hash]);
3613
            }
3614
        }
3615
    }
3616
3617
    /**
3618
     * @param ClassMetadata $class
3619
     * @param mixed         $identifierValue
3620
     *
3621 987
     * @return mixed the identifier after type conversion
3622
     *
3623 987
     * @throws \Doctrine\ORM\Mapping\MappingException if the entity has more than a single identifier
3624 987
     */
3625 987
    private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue)
3626
    {
3627
        return $this->em->getConnection()->convertToPHPValue(
3628
            $identifierValue,
3629
            $class->getTypeOfField($class->getSingleIdentifierFieldName())
3630
        );
3631
    }
3632
}
3633