Passed
Pull Request — 2.7 (#8020)
by
unknown
07:13
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 14.0643

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 14
eloc 30
c 2
b 1
f 0
nop 2
dl 0
loc 64
ccs 27
cts 29
cp 0.931
crap 14.0643
rs 6.2666
nc 37

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

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

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