Passed
Pull Request — 2.8.x (#7928)
by Oliver
08:12
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 14.0643

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

1572
            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...
1573
                // Check for a version field, if available, to avoid a db lookup.
1574 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...
1575 1
                    return ($class->getFieldValue($entity, $class->versionField))
0 ignored issues
show
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...
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

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

1946
                    if ($this->getEntityState(/** @scrutinizer ignore-type */ $managedCopy) == self::STATE_REMOVED) {
Loading history...
1947 15
                        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

1947
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, "merge");
Loading history...
1948
                    }
1949
                } else {
1950
                    // We need to fetch the managed copy in order to merge.
1951 25
                    $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...
1952
                }
1953
1954 37
                if ($managedCopy === null) {
1955
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1956
                    // since the managed entity was not found.
1957 3
                    if ( ! $class->isIdentifierNatural()) {
1958 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1959 1
                            $class->getName(),
1960 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1961
                        );
1962
                    }
1963
1964 2
                    $managedCopy = $this->newInstance($class);
1965 2
                    $class->setIdentifierValues($managedCopy, $id);
1966
1967 2
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1968 2
                    $this->persistNew($class, $managedCopy);
1969
                } else {
1970 34
                    $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

1970
                    $this->ensureVersionMatch($class, $entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1971 33
                    $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

1971
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1972
                }
1973
            }
1974
1975 40
            $visited[$oid] = $managedCopy; // mark visited
1976
1977 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1978
                $this->scheduleForDirtyCheck($entity);
1979
            }
1980
        }
1981
1982 42
        if ($prevManagedCopy !== null) {
1983 6
            $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

1983
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1984
        }
1985
1986
        // Mark the managed copy visited as well
1987 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

1987
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
1988
1989 42
        $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

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

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

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

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