Passed
Pull Request — 2.7 (#7950)
by Mathieu
07:48
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\ClassMetadataInfo;
39
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
40
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
41
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
42
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
43
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
44
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
45
use Doctrine\ORM\Proxy\Proxy;
46
use Doctrine\ORM\Utility\IdentifierFlattener;
47
use InvalidArgumentException;
48
use Throwable;
49
use UnexpectedValueException;
50
use function assert;
51
use function get_class;
52
use function method_exists;
53
54
/**
55
 * The UnitOfWork is responsible for tracking changes to objects during an
56
 * "object-level" transaction and for writing out changes to the database
57
 * in the correct order.
58
 *
59
 * Internal note: This class contains highly performance-sensitive code.
60
 *
61
 * @since       2.0
62
 * @author      Benjamin Eberlei <[email protected]>
63
 * @author      Guilherme Blanco <[email protected]>
64
 * @author      Jonathan Wage <[email protected]>
65
 * @author      Roman Borschel <[email protected]>
66
 * @author      Rob Caiger <[email protected]>
67
 */
68
class UnitOfWork implements PropertyChangedListener
69
{
70
    /**
71
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
72
     */
73
    const STATE_MANAGED = 1;
74
75
    /**
76
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
77
     * and is not (yet) managed by an EntityManager.
78
     */
79
    const STATE_NEW = 2;
80
81
    /**
82
     * A detached entity is an instance with persistent state and identity that is not
83
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
84
     */
85
    const STATE_DETACHED = 3;
86
87
    /**
88
     * A removed entity instance is an instance with a persistent identity,
89
     * associated with an EntityManager, whose persistent state will be deleted
90
     * on commit.
91
     */
92
    const STATE_REMOVED = 4;
93
94
    /**
95
     * Hint used to collect all primary keys of associated entities during hydration
96
     * and execute it in a dedicated query afterwards
97
     * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
98
     */
99
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
100
101
    /**
102
     * The identity map that holds references to all managed entities that have
103
     * an identity. The entities are grouped by their class name.
104
     * Since all classes in a hierarchy must share the same identifier set,
105
     * we always take the root class name of the hierarchy.
106
     *
107
     * @var array
108
     */
109
    private $identityMap = [];
110
111
    /**
112
     * Map of all identifiers of managed entities.
113
     * Keys are object ids (spl_object_hash).
114
     *
115
     * @var array
116
     */
117
    private $entityIdentifiers = [];
118
119
    /**
120
     * Map of the original entity data of managed entities.
121
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
122
     * at commit time.
123
     *
124
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
125
     *                A value will only really be copied if the value in the entity is modified
126
     *                by the user.
127
     *
128
     * @var array
129
     */
130
    private $originalEntityData = [];
131
132
    /**
133
     * Map of entity changes. Keys are object ids (spl_object_hash).
134
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
135
     *
136
     * @var array
137
     */
138
    private $entityChangeSets = [];
139
140
    /**
141
     * The (cached) states of any known entities.
142
     * Keys are object ids (spl_object_hash).
143
     *
144
     * @var array
145
     */
146
    private $entityStates = [];
147
148
    /**
149
     * Map of entities that are scheduled for dirty checking at commit time.
150
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
151
     * Keys are object ids (spl_object_hash).
152
     *
153
     * @var array
154
     */
155
    private $scheduledForSynchronization = [];
156
157
    /**
158
     * A list of all pending entity insertions.
159
     *
160
     * @var array
161
     */
162
    private $entityInsertions = [];
163
164
    /**
165
     * A list of all pending entity updates.
166
     *
167
     * @var array
168
     */
169
    private $entityUpdates = [];
170
171
    /**
172
     * Any pending extra updates that have been scheduled by persisters.
173
     *
174
     * @var array
175
     */
176
    private $extraUpdates = [];
177
178
    /**
179
     * A list of all pending entity deletions.
180
     *
181
     * @var array
182
     */
183
    private $entityDeletions = [];
184
185
    /**
186
     * New entities that were discovered through relationships that were not
187
     * marked as cascade-persist. During flush, this array is populated and
188
     * then pruned of any entities that were discovered through a valid
189
     * cascade-persist path. (Leftovers cause an error.)
190
     *
191
     * Keys are OIDs, payload is a two-item array describing the association
192
     * and the entity.
193
     *
194
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
195
     */
196
    private $nonCascadedNewDetectedEntities = [];
197
198
    /**
199
     * All pending collection deletions.
200
     *
201
     * @var array
202
     */
203
    private $collectionDeletions = [];
204
205
    /**
206
     * All pending collection updates.
207
     *
208
     * @var array
209
     */
210
    private $collectionUpdates = [];
211
212
    /**
213
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
214
     * At the end of the UnitOfWork all these collections will make new snapshots
215
     * of their data.
216
     *
217
     * @var array
218
     */
219
    private $visitedCollections = [];
220
221
    /**
222
     * The EntityManager that "owns" this UnitOfWork instance.
223
     *
224
     * @var EntityManagerInterface
225
     */
226
    private $em;
227
228
    /**
229
     * The entity persister instances used to persist entity instances.
230
     *
231
     * @var array
232
     */
233
    private $persisters = [];
234
235
    /**
236
     * The collection persister instances used to persist collections.
237
     *
238
     * @var array
239
     */
240
    private $collectionPersisters = [];
241
242
    /**
243
     * The EventManager used for dispatching events.
244
     *
245
     * @var \Doctrine\Common\EventManager
246
     */
247
    private $evm;
248
249
    /**
250
     * The ListenersInvoker used for dispatching events.
251
     *
252
     * @var \Doctrine\ORM\Event\ListenersInvoker
253
     */
254
    private $listenersInvoker;
255
256
    /**
257
     * The IdentifierFlattener used for manipulating identifiers
258
     *
259
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
260
     */
261
    private $identifierFlattener;
262
263
    /**
264
     * Orphaned entities that are scheduled for removal.
265
     *
266
     * @var array
267
     */
268
    private $orphanRemovals = [];
269
270
    /**
271
     * Read-Only objects are never evaluated
272
     *
273
     * @var array
274
     */
275
    private $readOnlyObjects = [];
276
277
    /**
278
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
279
     *
280
     * @var array
281
     */
282
    private $eagerLoadingEntities = [];
283
284
    /**
285
     * @var boolean
286
     */
287
    protected $hasCache = false;
288
289
    /**
290
     * Helper for handling completion of hydration
291
     *
292
     * @var HydrationCompleteHandler
293
     */
294
    private $hydrationCompleteHandler;
295
296
    /**
297
     * @var ReflectionPropertiesGetter
298
     */
299
    private $reflectionPropertiesGetter;
300
301
    /**
302
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
303
     *
304
     * @param EntityManagerInterface $em
305
     */
306 2512
    public function __construct(EntityManagerInterface $em)
307
    {
308 2512
        $this->em                         = $em;
309 2512
        $this->evm                        = $em->getEventManager();
310 2512
        $this->listenersInvoker           = new ListenersInvoker($em);
311 2512
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
312 2512
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
313 2512
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
314 2512
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
315 2512
    }
316
317
    /**
318
     * Commits the UnitOfWork, executing all operations that have been postponed
319
     * up to this point. The state of all managed entities will be synchronized with
320
     * the database.
321
     *
322
     * The operations are executed in the following order:
323
     *
324
     * 1) All entity insertions
325
     * 2) All entity updates
326
     * 3) All collection deletions
327
     * 4) All collection updates
328
     * 5) All entity deletions
329
     *
330
     * @param null|object|array $entity
331
     *
332
     * @return void
333
     *
334
     * @throws \Exception
335
     */
336 1104
    public function commit($entity = null)
337
    {
338
        // Raise preFlush
339 1104
        if ($this->evm->hasListeners(Events::preFlush)) {
340 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
341
        }
342
343
        // Compute changes done since last commit.
344 1104
        if (null === $entity) {
345 1097
            $this->computeChangeSets();
346 14
        } elseif (is_object($entity)) {
347 12
            $this->computeSingleEntityChangeSet($entity);
348 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
349 2
            foreach ($entity as $object) {
350 2
                $this->computeSingleEntityChangeSet($object);
351
            }
352
        }
353
354 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...
355 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...
356 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...
357 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...
358 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...
359 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...
360 27
            $this->dispatchOnFlushEvent();
361 27
            $this->dispatchPostFlushEvent();
362
363 27
            $this->postCommitCleanup($entity);
364
365 27
            return; // Nothing to do.
366
        }
367
368 1097
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
369
370 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...
371 16
            foreach ($this->orphanRemovals as $orphan) {
372 16
                $this->remove($orphan);
373
            }
374
        }
375
376 1095
        $this->dispatchOnFlushEvent();
377
378
        // Now we need a commit order to maintain referential integrity
379 1095
        $commitOrder = $this->getCommitOrder();
380
381 1095
        $conn = $this->em->getConnection();
382 1095
        $conn->beginTransaction();
383
384
        try {
385
            // Collection deletions (deletions of complete collections)
386 1095
            foreach ($this->collectionDeletions as $collectionToDelete) {
387 21
                if (! $collectionToDelete instanceof PersistentCollection) {
388
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
389
390
                    continue;
391
                }
392
393
                // Deferred explicit tracked collections can be removed only when owning relation was persisted
394 21
                $owner = $collectionToDelete->getOwner();
395
396 21
                if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
397 21
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
398
                }
399
            }
400
401 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...
402 1091
                foreach ($commitOrder as $class) {
403 1091
                    $this->executeInserts($class);
404
                }
405
            }
406
407 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...
408 123
                foreach ($commitOrder as $class) {
409 123
                    $this->executeUpdates($class);
410
                }
411
            }
412
413
            // Extra updates that were requested by persisters.
414 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...
415 44
                $this->executeExtraUpdates();
416
            }
417
418
            // Collection updates (deleteRows, updateRows, insertRows)
419 1090
            foreach ($this->collectionUpdates as $collectionToUpdate) {
420 552
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
421
            }
422
423
            // Entity deletions come last and need to be in reverse commit order
424 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...
425 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...
426 65
                    $this->executeDeletions($commitOrder[$i]);
427
                }
428
            }
429
430 1090
            $conn->commit();
431 11
        } catch (Throwable $e) {
432 11
            $this->em->close();
433 11
            $conn->rollBack();
434
435 11
            $this->afterTransactionRolledBack();
436
437 11
            throw $e;
438
        }
439
440 1090
        $this->afterTransactionComplete();
441
442
        // Take new snapshots from visited collections
443 1090
        foreach ($this->visitedCollections as $coll) {
444 551
            $coll->takeSnapshot();
445
        }
446
447 1090
        $this->dispatchPostFlushEvent();
448
449 1089
        $this->postCommitCleanup($entity);
450 1089
    }
451
452
    /**
453
     * @param null|object|object[] $entity
454
     */
455 1093
    private function postCommitCleanup($entity) : void
456
    {
457 1093
        $this->entityInsertions =
458 1093
        $this->entityUpdates =
459 1093
        $this->entityDeletions =
460 1093
        $this->extraUpdates =
461 1093
        $this->collectionUpdates =
462 1093
        $this->nonCascadedNewDetectedEntities =
463 1093
        $this->collectionDeletions =
464 1093
        $this->visitedCollections =
465 1093
        $this->orphanRemovals = [];
466
467 1093
        if (null === $entity) {
468 1087
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
469
470 1087
            return;
471
        }
472
473 12
        $entities = \is_object($entity)
474 10
            ? [$entity]
475 12
            : $entity;
476
477 12
        foreach ($entities as $object) {
478 12
            $oid = \spl_object_hash($object);
479
480 12
            $this->clearEntityChangeSet($oid);
481
482 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...
483
        }
484 12
    }
485
486
    /**
487
     * Computes the changesets of all entities scheduled for insertion.
488
     *
489
     * @return void
490
     */
491 1103
    private function computeScheduleInsertsChangeSets()
492
    {
493 1103
        foreach ($this->entityInsertions as $entity) {
494 1095
            $class = $this->em->getClassMetadata(get_class($entity));
495
496 1095
            $this->computeChangeSet($class, $entity);
497
        }
498 1101
    }
499
500
    /**
501
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
502
     *
503
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
504
     * 2. Read Only entities are skipped.
505
     * 3. Proxies are skipped.
506
     * 4. Only if entity is properly managed.
507
     *
508
     * @param object $entity
509
     *
510
     * @return void
511
     *
512
     * @throws \InvalidArgumentException
513
     */
514 14
    private function computeSingleEntityChangeSet($entity)
515
    {
516 14
        $state = $this->getEntityState($entity);
517
518 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
519 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
520
        }
521
522 13
        $class = $this->em->getClassMetadata(get_class($entity));
523
524 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
525 13
            $this->persist($entity);
526
        }
527
528
        // Compute changes for INSERTed entities first. This must always happen even in this case.
529 13
        $this->computeScheduleInsertsChangeSets();
530
531 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...
532
            return;
533
        }
534
535
        // Ignore uninitialized proxy objects
536 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...
537 1
            return;
538
        }
539
540
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
541 12
        $oid = spl_object_hash($entity);
542
543 12
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
544 7
            $this->computeChangeSet($class, $entity);
545
        }
546 12
    }
547
548
    /**
549
     * Executes any extra updates that have been scheduled.
550
     */
551 44
    private function executeExtraUpdates()
552
    {
553 44
        foreach ($this->extraUpdates as $oid => $update) {
554 44
            list ($entity, $changeset) = $update;
555
556 44
            $this->entityChangeSets[$oid] = $changeset;
557 44
            $this->getEntityPersister(get_class($entity))->update($entity);
558
        }
559
560 44
        $this->extraUpdates = [];
561 44
    }
562
563
    /**
564
     * Gets the changeset for an entity.
565
     *
566
     * @param object $entity
567
     *
568
     * @return array
569
     */
570
    public function & getEntityChangeSet($entity)
571
    {
572 1090
        $oid  = spl_object_hash($entity);
573 1090
        $data = [];
574
575 1090
        if (!isset($this->entityChangeSets[$oid])) {
576 4
            return $data;
577
        }
578
579 1090
        return $this->entityChangeSets[$oid];
580
    }
581
582
    /**
583
     * Computes the changes that happened to a single entity.
584
     *
585
     * Modifies/populates the following properties:
586
     *
587
     * {@link _originalEntityData}
588
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
589
     * then it was not fetched from the database and therefore we have no original
590
     * entity data yet. All of the current entity data is stored as the original entity data.
591
     *
592
     * {@link _entityChangeSets}
593
     * The changes detected on all properties of the entity are stored there.
594
     * A change is a tuple array where the first entry is the old value and the second
595
     * entry is the new value of the property. Changesets are used by persisters
596
     * to INSERT/UPDATE the persistent entity state.
597
     *
598
     * {@link _entityUpdates}
599
     * If the entity is already fully MANAGED (has been fetched from the database before)
600
     * and any changes to its properties are detected, then a reference to the entity is stored
601
     * there to mark it for an update.
602
     *
603
     * {@link _collectionDeletions}
604
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
605
     * then this collection is marked for deletion.
606
     *
607
     * @ignore
608
     *
609
     * @internal Don't call from the outside.
610
     *
611
     * @param ClassMetadata $class  The class descriptor of the entity.
612
     * @param object        $entity The entity for which to compute the changes.
613
     *
614
     * @return void
615
     */
616 1105
    public function computeChangeSet(ClassMetadata $class, $entity)
617
    {
618 1105
        $oid = spl_object_hash($entity);
619
620 1105
        if (isset($this->readOnlyObjects[$oid])) {
621 2
            return;
622
        }
623
624 1105
        if ( ! $class->isInheritanceTypeNone()) {
625 339
            $class = $this->em->getClassMetadata(get_class($entity));
626
        }
627
628 1105
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
629
630 1105
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
631 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
632
        }
633
634 1105
        $actualData = [];
635
636 1105
        foreach ($class->reflFields as $name => $refProp) {
637 1105
            $value = $refProp->getValue($entity);
638
639 1105
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
640 824
                if ($value instanceof PersistentCollection) {
641 207
                    if ($value->getOwner() === $entity) {
642 207
                        continue;
643
                    }
644
645 5
                    $value = new ArrayCollection($value->getValues());
646
                }
647
648
                // If $value is not a Collection then use an ArrayCollection.
649 819
                if ( ! $value instanceof Collection) {
650 249
                    $value = new ArrayCollection($value);
651
                }
652
653 819
                $assoc = $class->associationMappings[$name];
654
655
                // Inject PersistentCollection
656 819
                $value = new PersistentCollection(
657 819
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
658
                );
659 819
                $value->setOwner($entity, $assoc);
660 819
                $value->setDirty( ! $value->isEmpty());
661
662 819
                $class->reflFields[$name]->setValue($entity, $value);
663
664 819
                $actualData[$name] = $value;
665
666 819
                continue;
667
            }
668
669 1105
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
670 1105
                $actualData[$name] = $value;
671
            }
672
        }
673
674 1105
        if ( ! isset($this->originalEntityData[$oid])) {
675
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
676
            // These result in an INSERT.
677 1101
            $this->originalEntityData[$oid] = $actualData;
678 1101
            $changeSet = [];
679
680 1101
            foreach ($actualData as $propName => $actualValue) {
681 1077
                if ( ! isset($class->associationMappings[$propName])) {
682 1018
                    $changeSet[$propName] = [null, $actualValue];
683
684 1018
                    continue;
685
                }
686
687 956
                $assoc = $class->associationMappings[$propName];
688
689 956
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
690 956
                    $changeSet[$propName] = [null, $actualValue];
691
                }
692
            }
693
694 1101
            $this->entityChangeSets[$oid] = $changeSet;
695
        } else {
696
            // Entity is "fully" MANAGED: it was already fully persisted before
697
            // and we have a copy of the original data
698 280
            $originalData           = $this->originalEntityData[$oid];
699 280
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
700 280
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
701
                ? $this->entityChangeSets[$oid]
702 280
                : [];
703
704 280
            foreach ($actualData as $propName => $actualValue) {
705
                // skip field, its a partially omitted one!
706 262
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
707 8
                    continue;
708
                }
709
710 262
                $orgValue = $originalData[$propName];
711
712
                // skip if value haven't changed
713 262
                if ($orgValue === $actualValue) {
714 245
                    continue;
715
                }
716
717
                // if regular field
718 119
                if ( ! isset($class->associationMappings[$propName])) {
719 64
                    if ($isChangeTrackingNotify) {
720
                        continue;
721
                    }
722
723 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
724
725 64
                    continue;
726
                }
727
728 59
                $assoc = $class->associationMappings[$propName];
729
730
                // Persistent collection was exchanged with the "originally"
731
                // created one. This can only mean it was cloned and replaced
732
                // on another entity.
733 59
                if ($actualValue instanceof PersistentCollection) {
734 8
                    $owner = $actualValue->getOwner();
735 8
                    if ($owner === null) { // cloned
736
                        $actualValue->setOwner($entity, $assoc);
737 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
738
                        if (!$actualValue->isInitialized()) {
739
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
740
                        }
741
                        $newValue = clone $actualValue;
742
                        $newValue->setOwner($entity, $assoc);
743
                        $class->reflFields[$propName]->setValue($entity, $newValue);
744
                    }
745
                }
746
747 59
                if ($orgValue instanceof PersistentCollection) {
748
                    // A PersistentCollection was de-referenced, so delete it.
749 8
                    $coid = spl_object_hash($orgValue);
750
751 8
                    if (isset($this->collectionDeletions[$coid])) {
752
                        continue;
753
                    }
754
755 8
                    $this->collectionDeletions[$coid] = $orgValue;
756 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
757
758 8
                    continue;
759
                }
760
761 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
762 50
                    if ($assoc['isOwningSide']) {
763 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
764
                    }
765
766 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
767 51
                        $this->scheduleOrphanRemoval($orgValue);
768
                    }
769
                }
770
            }
771
772 280
            if ($changeSet) {
773 92
                $this->entityChangeSets[$oid]   = $changeSet;
774 92
                $this->originalEntityData[$oid] = $actualData;
775 92
                $this->entityUpdates[$oid]      = $entity;
776
            }
777
        }
778
779
        // Look for changes in associations of the entity
780 1105
        foreach ($class->associationMappings as $field => $assoc) {
781 956
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
782 666
                continue;
783
            }
784
785 925
            $this->computeAssociationChanges($assoc, $val);
786
787 917
            if ( ! isset($this->entityChangeSets[$oid]) &&
788 917
                $assoc['isOwningSide'] &&
789 917
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
790 917
                $val instanceof PersistentCollection &&
791 917
                $val->isDirty()) {
792
793 35
                $this->entityChangeSets[$oid]   = [];
794 35
                $this->originalEntityData[$oid] = $actualData;
795 917
                $this->entityUpdates[$oid]      = $entity;
796
            }
797
        }
798 1097
    }
799
800
    /**
801
     * Computes all the changes that have been done to entities and collections
802
     * since the last commit and stores these changes in the _entityChangeSet map
803
     * temporarily for access by the persisters, until the UoW commit is finished.
804
     *
805
     * @return void
806
     */
807 1097
    public function computeChangeSets()
808
    {
809
        // Compute changes for INSERTed entities first. This must always happen.
810 1097
        $this->computeScheduleInsertsChangeSets();
811
812
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
813 1095
        foreach ($this->identityMap as $className => $entities) {
814 482
            $class = $this->em->getClassMetadata($className);
815
816
            // Skip class if instances are read-only
817 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...
818 1
                continue;
819
            }
820
821
            // If change tracking is explicit or happens through notification, then only compute
822
            // changes on entities of that type that are explicitly marked for synchronization.
823
            switch (true) {
824 481
                case ($class->isChangeTrackingDeferredImplicit()):
825 476
                    $entitiesToProcess = $entities;
826 476
                    break;
827
828 6
                case (isset($this->scheduledForSynchronization[$className])):
829 5
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
830 5
                    break;
831
832
                default:
833 2
                    $entitiesToProcess = [];
834
835
            }
836
837 481
            foreach ($entitiesToProcess as $entity) {
838
                // Ignore uninitialized proxy objects
839 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...
840 38
                    continue;
841
                }
842
843
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
844 458
                $oid = spl_object_hash($entity);
845
846 458
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
847 481
                    $this->computeChangeSet($class, $entity);
848
                }
849
            }
850
        }
851 1095
    }
852
853
    /**
854
     * Computes the changes of an association.
855
     *
856
     * @param array $assoc The association mapping.
857
     * @param mixed $value The value of the association.
858
     *
859
     * @throws ORMInvalidArgumentException
860
     * @throws ORMException
861
     *
862
     * @return void
863
     */
864 925
    private function computeAssociationChanges($assoc, $value)
865
    {
866 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...
867 30
            return;
868
        }
869
870 924
        if ($value instanceof PersistentCollection && $value->isDirty()) {
871 556
            $coid = spl_object_hash($value);
872
873 556
            $this->collectionUpdates[$coid] = $value;
874 556
            $this->visitedCollections[$coid] = $value;
875
        }
876
877
        // Look through the entities, and in any of their associations,
878
        // for transient (new) entities, recursively. ("Persistence by reachability")
879
        // Unwrap. Uninitialized collections will simply be empty.
880 924
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
881 924
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
882
883 924
        foreach ($unwrappedValue as $key => $entry) {
884 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...
885 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
886
            }
887
888 756
            $state = $this->getEntityState($entry, self::STATE_NEW);
889
890 756
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
891
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
892
            }
893
894
            switch ($state) {
895 756
                case self::STATE_NEW:
896 42
                    if ( ! $assoc['isCascadePersist']) {
897
                        /*
898
                         * For now just record the details, because this may
899
                         * not be an issue if we later discover another pathway
900
                         * through the object-graph where cascade-persistence
901
                         * is enabled for this object.
902
                         */
903 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
904
905 6
                        break;
906
                    }
907
908 37
                    $this->persistNew($targetClass, $entry);
909 37
                    $this->computeChangeSet($targetClass, $entry);
910
911 37
                    break;
912
913 748
                case self::STATE_REMOVED:
914
                    // Consume the $value as array (it's either an array or an ArrayAccess)
915
                    // and remove the element from Collection.
916 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
917 3
                        unset($value[$key]);
918
                    }
919 4
                    break;
920
921 748
                case self::STATE_DETACHED:
922
                    // Can actually not happen right now as we assume STATE_NEW,
923
                    // so the exception will be raised from the DBAL layer (constraint violation).
924
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
925
                    break;
926
927 756
                default:
928
                    // MANAGED associated entities are already taken into account
929
                    // during changeset calculation anyway, since they are in the identity map.
930
            }
931
        }
932 916
    }
933
934
    /**
935
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
936
     * @param object                              $entity
937
     *
938
     * @return void
939
     */
940 1128
    private function persistNew($class, $entity)
941
    {
942 1128
        $oid    = spl_object_hash($entity);
943 1128
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
944
945 1128
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
946 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
947
        }
948
949 1128
        $idGen = $class->idGenerator;
950
951 1128
        if ( ! $idGen->isPostInsertGenerator()) {
952 300
            $idValue = $idGen->generate($this->em, $entity);
953
954 300
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
955 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
956
957 2
                $class->setIdentifierValues($entity, $idValue);
958
            }
959
960
            // Some identifiers may be foreign keys to new entities.
961
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
962 300
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
963 297
                $this->entityIdentifiers[$oid] = $idValue;
964
            }
965
        }
966
967 1128
        $this->entityStates[$oid] = self::STATE_MANAGED;
968
969 1128
        $this->scheduleForInsert($entity);
970 1128
    }
971
972
    /**
973
     * @param mixed[] $idValue
974
     */
975 300
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
976
    {
977 300
        foreach ($idValue as $idField => $idFieldValue) {
978 300
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
979 300
                return true;
980
            }
981
        }
982
983 297
        return false;
984
    }
985
986
    /**
987
     * INTERNAL:
988
     * Computes the changeset of an individual entity, independently of the
989
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
990
     *
991
     * The passed entity must be a managed entity. If the entity already has a change set
992
     * because this method is invoked during a commit cycle then the change sets are added.
993
     * whereby changes detected in this method prevail.
994
     *
995
     * @ignore
996
     *
997
     * @param ClassMetadata $class  The class descriptor of the entity.
998
     * @param object        $entity The entity for which to (re)calculate the change set.
999
     *
1000
     * @return void
1001
     *
1002
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
1003
     */
1004 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
1005
    {
1006 16
        $oid = spl_object_hash($entity);
1007
1008 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
1009
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1010
        }
1011
1012
        // skip if change tracking is "NOTIFY"
1013 16
        if ($class->isChangeTrackingNotify()) {
1014
            return;
1015
        }
1016
1017 16
        if ( ! $class->isInheritanceTypeNone()) {
1018 3
            $class = $this->em->getClassMetadata(get_class($entity));
1019
        }
1020
1021 16
        $actualData = [];
1022
1023 16
        foreach ($class->reflFields as $name => $refProp) {
1024 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1025 16
                && ($name !== $class->versionField)
1026 16
                && ! $class->isCollectionValuedAssociation($name)) {
1027 16
                $actualData[$name] = $refProp->getValue($entity);
1028
            }
1029
        }
1030
1031 16
        if ( ! isset($this->originalEntityData[$oid])) {
1032
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1033
        }
1034
1035 16
        $originalData = $this->originalEntityData[$oid];
1036 16
        $changeSet = [];
1037
1038 16
        foreach ($actualData as $propName => $actualValue) {
1039 16
            $orgValue = $originalData[$propName] ?? null;
1040
1041 16
            if ($orgValue !== $actualValue) {
1042 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1043
            }
1044
        }
1045
1046 16
        if ($changeSet) {
1047 7
            if (isset($this->entityChangeSets[$oid])) {
1048 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1049 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1050 1
                $this->entityChangeSets[$oid] = $changeSet;
1051 1
                $this->entityUpdates[$oid]    = $entity;
1052
            }
1053 7
            $this->originalEntityData[$oid] = $actualData;
1054
        }
1055 16
    }
1056
1057
    /**
1058
     * Executes all entity insertions for entities of the specified type.
1059
     *
1060
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1061
     *
1062
     * @return void
1063
     */
1064 1091
    private function executeInserts($class)
1065
    {
1066 1091
        $entities   = [];
1067 1091
        $className  = $class->name;
1068 1091
        $persister  = $this->getEntityPersister($className);
1069 1091
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1070
1071 1091
        $insertionsForClass = [];
1072
1073 1091
        foreach ($this->entityInsertions as $oid => $entity) {
1074
1075 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...
1076 919
                continue;
1077
            }
1078
1079 1091
            $insertionsForClass[$oid] = $entity;
1080
1081 1091
            $persister->addInsert($entity);
1082
1083 1091
            unset($this->entityInsertions[$oid]);
1084
1085 1091
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1086 1091
                $entities[] = $entity;
1087
            }
1088
        }
1089
1090 1091
        $postInsertIds = $persister->executeInserts();
1091
1092 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...
1093
            // Persister returned post-insert IDs
1094 985
            foreach ($postInsertIds as $postInsertId) {
1095 985
                $idField = $class->getSingleIdentifierFieldName();
1096 985
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1097
1098 985
                $entity  = $postInsertId['entity'];
1099 985
                $oid     = spl_object_hash($entity);
1100
1101 985
                $class->reflFields[$idField]->setValue($entity, $idValue);
1102
1103 985
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1104 985
                $this->entityStates[$oid] = self::STATE_MANAGED;
1105 985
                $this->originalEntityData[$oid][$idField] = $idValue;
1106
1107 985
                $this->addToIdentityMap($entity);
1108
            }
1109
        } else {
1110 823
            foreach ($insertionsForClass as $oid => $entity) {
1111 283
                if (! isset($this->entityIdentifiers[$oid])) {
1112
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1113
                    //add it now
1114 283
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1115
                }
1116
            }
1117
        }
1118
1119 1091
        foreach ($entities as $entity) {
1120 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1121
        }
1122 1091
    }
1123
1124
    /**
1125
     * @param object $entity
1126
     */
1127 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1128
    {
1129 3
        $identifier = [];
1130
1131 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1132 3
            $value = $class->getFieldValue($entity, $idField);
1133
1134 3
            if (isset($class->associationMappings[$idField])) {
1135
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1136 3
                $value = $this->getSingleIdentifierValue($value);
1137
            }
1138
1139 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1140
        }
1141
1142 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1143 3
        $this->entityIdentifiers[$oid] = $identifier;
1144
1145 3
        $this->addToIdentityMap($entity);
1146 3
    }
1147
1148
    /**
1149
     * Executes all entity updates for entities of the specified type.
1150
     *
1151
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1152
     *
1153
     * @return void
1154
     */
1155 123
    private function executeUpdates($class)
1156
    {
1157 123
        $className          = $class->name;
1158 123
        $persister          = $this->getEntityPersister($className);
1159 123
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1160 123
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1161
1162 123
        foreach ($this->entityUpdates as $oid => $entity) {
1163 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...
1164 79
                continue;
1165
            }
1166
1167 123
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1168 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1169
1170 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1171
            }
1172
1173 123
            if ( ! empty($this->entityChangeSets[$oid])) {
1174 89
                $persister->update($entity);
1175
            }
1176
1177 119
            unset($this->entityUpdates[$oid]);
1178
1179 119
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1180 119
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1181
            }
1182
        }
1183 119
    }
1184
1185
    /**
1186
     * Executes all entity deletions for entities of the specified type.
1187
     *
1188
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1189
     *
1190
     * @return void
1191
     */
1192 65
    private function executeDeletions($class)
1193
    {
1194 65
        $className  = $class->name;
1195 65
        $persister  = $this->getEntityPersister($className);
1196 65
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1197
1198 65
        foreach ($this->entityDeletions as $oid => $entity) {
1199 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...
1200 26
                continue;
1201
            }
1202
1203 65
            $persister->delete($entity);
1204
1205
            unset(
1206 65
                $this->entityDeletions[$oid],
1207 65
                $this->entityIdentifiers[$oid],
1208 65
                $this->originalEntityData[$oid],
1209 65
                $this->entityStates[$oid]
1210
            );
1211
1212
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1213
            // is obtained by a new entity because the old one went out of scope.
1214
            //$this->entityStates[$oid] = self::STATE_NEW;
1215 65
            if ( ! $class->isIdentifierNatural()) {
1216 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1217
            }
1218
1219 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1220 65
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1221
            }
1222
        }
1223 64
    }
1224
1225
    /**
1226
     * Gets the commit order.
1227
     *
1228
     * @param array|null $entityChangeSet
1229
     *
1230
     * @return array
1231
     */
1232 1095
    private function getCommitOrder(array $entityChangeSet = null)
1233
    {
1234 1095
        if ($entityChangeSet === null) {
1235 1095
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1236
        }
1237
1238 1095
        $calc = $this->getCommitOrderCalculator();
1239
1240
        // See if there are any new classes in the changeset, that are not in the
1241
        // commit order graph yet (don't have a node).
1242
        // We have to inspect changeSet to be able to correctly build dependencies.
1243
        // It is not possible to use IdentityMap here because post inserted ids
1244
        // are not yet available.
1245 1095
        $newNodes = [];
1246
1247 1095
        foreach ($entityChangeSet as $entity) {
1248 1095
            $class = $this->em->getClassMetadata(get_class($entity));
1249
1250 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...
1251 667
                continue;
1252
            }
1253
1254 1095
            $calc->addNode($class->name, $class);
1255
1256 1095
            $newNodes[] = $class;
1257
        }
1258
1259
        // Calculate dependencies for new nodes
1260 1095
        while ($class = array_pop($newNodes)) {
1261 1095
            foreach ($class->associationMappings as $assoc) {
1262 945
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1263 897
                    continue;
1264
                }
1265
1266 892
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1267
1268 892
                if ( ! $calc->hasNode($targetClass->name)) {
1269 684
                    $calc->addNode($targetClass->name, $targetClass);
1270
1271 684
                    $newNodes[] = $targetClass;
1272
                }
1273
1274 892
                $joinColumns = reset($assoc['joinColumns']);
1275
1276 892
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1277
1278
                // If the target class has mapped subclasses, these share the same dependency.
1279 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...
1280 885
                    continue;
1281
                }
1282
1283 239
                foreach ($targetClass->subClasses as $subClassName) {
1284 239
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1285
1286 239
                    if ( ! $calc->hasNode($subClassName)) {
1287 209
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1288
1289 209
                        $newNodes[] = $targetSubClass;
1290
                    }
1291
1292 239
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1293
                }
1294
            }
1295
        }
1296
1297 1095
        return $calc->sort();
1298
    }
1299
1300
    /**
1301
     * Schedules an entity for insertion into the database.
1302
     * If the entity already has an identifier, it will be added to the identity map.
1303
     *
1304
     * @param object $entity The entity to schedule for insertion.
1305
     *
1306
     * @return void
1307
     *
1308
     * @throws ORMInvalidArgumentException
1309
     * @throws \InvalidArgumentException
1310
     */
1311 1129
    public function scheduleForInsert($entity)
1312
    {
1313 1129
        $oid = spl_object_hash($entity);
1314
1315 1129
        if (isset($this->entityUpdates[$oid])) {
1316
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1317
        }
1318
1319 1129
        if (isset($this->entityDeletions[$oid])) {
1320 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1321
        }
1322 1129
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1323 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1324
        }
1325
1326 1129
        if (isset($this->entityInsertions[$oid])) {
1327 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1328
        }
1329
1330 1129
        $this->entityInsertions[$oid] = $entity;
1331
1332 1129
        if (isset($this->entityIdentifiers[$oid])) {
1333 297
            $this->addToIdentityMap($entity);
1334
        }
1335
1336 1129
        if ($entity instanceof NotifyPropertyChanged) {
1337 8
            $entity->addPropertyChangedListener($this);
1338
        }
1339 1129
    }
1340
1341
    /**
1342
     * Checks whether an entity is scheduled for insertion.
1343
     *
1344
     * @param object $entity
1345
     *
1346
     * @return boolean
1347
     */
1348 662
    public function isScheduledForInsert($entity)
1349
    {
1350 662
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1351
    }
1352
1353
    /**
1354
     * Schedules an entity for being updated.
1355
     *
1356
     * @param object $entity The entity to schedule for being updated.
1357
     *
1358
     * @return void
1359
     *
1360
     * @throws ORMInvalidArgumentException
1361
     */
1362 1
    public function scheduleForUpdate($entity)
1363
    {
1364 1
        $oid = spl_object_hash($entity);
1365
1366 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1367
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1368
        }
1369
1370 1
        if (isset($this->entityDeletions[$oid])) {
1371
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1372
        }
1373
1374 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1375 1
            $this->entityUpdates[$oid] = $entity;
1376
        }
1377 1
    }
1378
1379
    /**
1380
     * INTERNAL:
1381
     * Schedules an extra update that will be executed immediately after the
1382
     * regular entity updates within the currently running commit cycle.
1383
     *
1384
     * Extra updates for entities are stored as (entity, changeset) tuples.
1385
     *
1386
     * @ignore
1387
     *
1388
     * @param object $entity    The entity for which to schedule an extra update.
1389
     * @param array  $changeset The changeset of the entity (what to update).
1390
     *
1391
     * @return void
1392
     */
1393 44
    public function scheduleExtraUpdate($entity, array $changeset)
1394
    {
1395 44
        $oid         = spl_object_hash($entity);
1396 44
        $extraUpdate = [$entity, $changeset];
1397
1398 44
        if (isset($this->extraUpdates[$oid])) {
1399 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1400
1401 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1402
        }
1403
1404 44
        $this->extraUpdates[$oid] = $extraUpdate;
1405 44
    }
1406
1407
    /**
1408
     * Checks whether an entity is registered as dirty in the unit of work.
1409
     * Note: Is not very useful currently as dirty entities are only registered
1410
     * at commit time.
1411
     *
1412
     * @param object $entity
1413
     *
1414
     * @return boolean
1415
     */
1416
    public function isScheduledForUpdate($entity)
1417
    {
1418
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1419
    }
1420
1421
    /**
1422
     * Checks whether an entity is registered to be checked in the unit of work.
1423
     *
1424
     * @param object $entity
1425
     *
1426
     * @return boolean
1427
     */
1428 5
    public function isScheduledForDirtyCheck($entity)
1429
    {
1430 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...
1431
1432 5
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1433
    }
1434
1435
    /**
1436
     * INTERNAL:
1437
     * Schedules an entity for deletion.
1438
     *
1439
     * @param object $entity
1440
     *
1441
     * @return void
1442
     */
1443 68
    public function scheduleForDelete($entity)
1444
    {
1445 68
        $oid = spl_object_hash($entity);
1446
1447 68
        if (isset($this->entityInsertions[$oid])) {
1448 1
            if ($this->isInIdentityMap($entity)) {
1449
                $this->removeFromIdentityMap($entity);
1450
            }
1451
1452 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1453
1454 1
            return; // entity has not been persisted yet, so nothing more to do.
1455
        }
1456
1457 68
        if ( ! $this->isInIdentityMap($entity)) {
1458 1
            return;
1459
        }
1460
1461 67
        $this->removeFromIdentityMap($entity);
1462
1463 67
        unset($this->entityUpdates[$oid]);
1464
1465 67
        if ( ! isset($this->entityDeletions[$oid])) {
1466 67
            $this->entityDeletions[$oid] = $entity;
1467 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1468
        }
1469 67
    }
1470
1471
    /**
1472
     * Checks whether an entity is registered as removed/deleted with the unit
1473
     * of work.
1474
     *
1475
     * @param object $entity
1476
     *
1477
     * @return boolean
1478
     */
1479 17
    public function isScheduledForDelete($entity)
1480
    {
1481 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1482
    }
1483
1484
    /**
1485
     * Checks whether an entity is scheduled for insertion, update or deletion.
1486
     *
1487
     * @param object $entity
1488
     *
1489
     * @return boolean
1490
     */
1491
    public function isEntityScheduled($entity)
1492
    {
1493
        $oid = spl_object_hash($entity);
1494
1495
        return isset($this->entityInsertions[$oid])
1496
            || isset($this->entityUpdates[$oid])
1497
            || isset($this->entityDeletions[$oid]);
1498
    }
1499
1500
    /**
1501
     * INTERNAL:
1502
     * Registers an entity in the identity map.
1503
     * Note that entities in a hierarchy are registered with the class name of
1504
     * the root entity.
1505
     *
1506
     * @ignore
1507
     *
1508
     * @param object $entity The entity to register.
1509
     *
1510
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1511
     *                 the entity in question is already managed.
1512
     *
1513
     * @throws ORMInvalidArgumentException
1514
     */
1515 1193
    public function addToIdentityMap($entity)
1516
    {
1517 1193
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1518 1193
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1519
1520 1193
        if (empty($identifier) || in_array(null, $identifier, true)) {
1521 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...
1522
        }
1523
1524 1187
        $idHash    = implode(' ', $identifier);
1525 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...
1526
1527 1187
        if (isset($this->identityMap[$className][$idHash])) {
1528 87
            return false;
1529
        }
1530
1531 1187
        $this->identityMap[$className][$idHash] = $entity;
1532
1533 1187
        return true;
1534
    }
1535
1536
    /**
1537
     * Gets the state of an entity with regard to the current unit of work.
1538
     *
1539
     * @param object   $entity
1540
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1541
     *                         This parameter can be set to improve performance of entity state detection
1542
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1543
     *                         is either known or does not matter for the caller of the method.
1544
     *
1545
     * @return int The entity state.
1546
     */
1547 1143
    public function getEntityState($entity, $assume = null)
1548
    {
1549 1143
        $oid = spl_object_hash($entity);
1550
1551 1143
        if (isset($this->entityStates[$oid])) {
1552 830
            return $this->entityStates[$oid];
1553
        }
1554
1555 1137
        if ($assume !== null) {
1556 1133
            return $assume;
1557
        }
1558
1559
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1560
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1561
        // the UoW does not hold references to such objects and the object hash can be reused.
1562
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1563 13
        $class = $this->em->getClassMetadata(get_class($entity));
1564 13
        $id    = $class->getIdentifierValues($entity);
1565
1566 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...
1567 5
            return self::STATE_NEW;
1568
        }
1569
1570 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...
1571 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1572
        }
1573
1574
        switch (true) {
1575 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

1575
            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...
1576
                // Check for a version field, if available, to avoid a db lookup.
1577 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...
1578 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

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

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

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

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

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

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

1990
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
1991
1992 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

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

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

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

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