Passed
Pull Request — 2.8.x (#7936)
by Benjamin
05:38
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 2513
    public function __construct(EntityManagerInterface $em)
304
    {
305 2513
        $this->em                         = $em;
306 2513
        $this->evm                        = $em->getEventManager();
307 2513
        $this->listenersInvoker           = new ListenersInvoker($em);
308 2513
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
309 2513
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
310 2513
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
311 2513
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
312 2513
    }
313
314
    /**
315
     * Commits the UnitOfWork, executing all operations that have been postponed
316
     * up to this point. The state of all managed entities will be synchronized with
317
     * the database.
318
     *
319
     * The operations are executed in the following order:
320
     *
321
     * 1) All entity insertions
322
     * 2) All entity updates
323
     * 3) All collection deletions
324
     * 4) All collection updates
325
     * 5) All entity deletions
326
     *
327
     * @param null|object|array $entity
328
     *
329
     * @return void
330
     *
331
     * @throws \Exception
332
     */
333 1106
    public function commit($entity = null)
334
    {
335
        // Raise preFlush
336 1106
        if ($this->evm->hasListeners(Events::preFlush)) {
337 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
338
        }
339
340
        // Compute changes done since last commit.
341 1106
        if (null === $entity) {
342 1099
            $this->computeChangeSets();
343 14
        } elseif (is_object($entity)) {
344 12
            $this->computeSingleEntityChangeSet($entity);
345 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
346 2
            foreach ($entity as $object) {
347 2
                $this->computeSingleEntityChangeSet($object);
348
            }
349
        }
350
351 1103
        if ( ! ($this->entityInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
352 178
                $this->entityDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
353 140
                $this->entityUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
354 44
                $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
355 40
                $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
356 1103
                $this->orphanRemovals)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
357 27
            $this->dispatchOnFlushEvent();
358 27
            $this->dispatchPostFlushEvent();
359
360 27
            $this->postCommitCleanup($entity);
361
362 27
            return; // Nothing to do.
363
        }
364
365 1099
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
366
367 1097
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
368 16
            foreach ($this->orphanRemovals as $orphan) {
369 16
                $this->remove($orphan);
370
            }
371
        }
372
373 1097
        $this->dispatchOnFlushEvent();
374
375
        // Now we need a commit order to maintain referential integrity
376 1097
        $commitOrder = $this->getCommitOrder();
377
378 1097
        $conn = $this->em->getConnection();
379 1097
        $conn->beginTransaction();
380
381
        try {
382
            // Collection deletions (deletions of complete collections)
383 1097
            foreach ($this->collectionDeletions as $collectionToDelete) {
384 21
                if (! $collectionToDelete instanceof PersistentCollection) {
385
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
386
387
                    continue;
388
                }
389
390
                // Deferred explicit tracked collections can be removed only when owning relation was persisted
391 21
                $owner = $collectionToDelete->getOwner();
392
393 21
                if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
394 21
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
395
                }
396
            }
397
398 1097
            if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
399 1093
                foreach ($commitOrder as $class) {
400 1093
                    $this->executeInserts($class);
401
                }
402
            }
403
404 1096
            if ($this->entityUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
405 123
                foreach ($commitOrder as $class) {
406 123
                    $this->executeUpdates($class);
407
                }
408
            }
409
410
            // Extra updates that were requested by persisters.
411 1092
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
412 44
                $this->executeExtraUpdates();
413
            }
414
415
            // Collection updates (deleteRows, updateRows, insertRows)
416 1092
            foreach ($this->collectionUpdates as $collectionToUpdate) {
417 552
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
418
            }
419
420
            // Entity deletions come last and need to be in reverse commit order
421 1092
            if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
422 65
                for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
423 65
                    $this->executeDeletions($commitOrder[$i]);
424
                }
425
            }
426
427 1092
            $conn->commit();
428 11
        } catch (Throwable $e) {
429 11
            $this->em->close();
430 11
            $conn->rollBack();
431
432 11
            $this->afterTransactionRolledBack();
433
434 11
            throw $e;
435
        }
436
437 1092
        $this->afterTransactionComplete();
438
439
        // Take new snapshots from visited collections
440 1092
        foreach ($this->visitedCollections as $coll) {
441 551
            $coll->takeSnapshot();
442
        }
443
444 1092
        $this->dispatchPostFlushEvent();
445
446 1091
        $this->postCommitCleanup($entity);
447 1091
    }
448
449
    /**
450
     * @param null|object|object[] $entity
451
     */
452 1095
    private function postCommitCleanup($entity) : void
453
    {
454 1095
        $this->entityInsertions =
455 1095
        $this->entityUpdates =
456 1095
        $this->entityDeletions =
457 1095
        $this->extraUpdates =
458 1095
        $this->collectionUpdates =
459 1095
        $this->nonCascadedNewDetectedEntities =
460 1095
        $this->collectionDeletions =
461 1095
        $this->visitedCollections =
462 1095
        $this->orphanRemovals = [];
463
464 1095
        if (null === $entity) {
465 1089
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
466
467 1089
            return;
468
        }
469
470 12
        $entities = \is_object($entity)
471 10
            ? [$entity]
472 12
            : $entity;
473
474 12
        foreach ($entities as $object) {
475 12
            $oid = \spl_object_hash($object);
476
477 12
            $this->clearEntityChangeSet($oid);
478
479 12
            unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
480
        }
481 12
    }
482
483
    /**
484
     * Computes the changesets of all entities scheduled for insertion.
485
     *
486
     * @return void
487
     */
488 1105
    private function computeScheduleInsertsChangeSets()
489
    {
490 1105
        foreach ($this->entityInsertions as $entity) {
491 1097
            $class = $this->em->getClassMetadata(get_class($entity));
492
493 1097
            $this->computeChangeSet($class, $entity);
494
        }
495 1103
    }
496
497
    /**
498
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
499
     *
500
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
501
     * 2. Read Only entities are skipped.
502
     * 3. Proxies are skipped.
503
     * 4. Only if entity is properly managed.
504
     *
505
     * @param object $entity
506
     *
507
     * @return void
508
     *
509
     * @throws \InvalidArgumentException
510
     */
511 14
    private function computeSingleEntityChangeSet($entity)
512
    {
513 14
        $state = $this->getEntityState($entity);
514
515 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
516 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
517
        }
518
519 13
        $class = $this->em->getClassMetadata(get_class($entity));
520
521 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
522 13
            $this->persist($entity);
523
        }
524
525
        // Compute changes for INSERTed entities first. This must always happen even in this case.
526 13
        $this->computeScheduleInsertsChangeSets();
527
528 13
        if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
529
            return;
530
        }
531
532
        // Ignore uninitialized proxy objects
533 13
        if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
534 1
            return;
535
        }
536
537
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
538 12
        $oid = spl_object_hash($entity);
539
540 12
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
541 7
            $this->computeChangeSet($class, $entity);
542
        }
543 12
    }
544
545
    /**
546
     * Executes any extra updates that have been scheduled.
547
     */
548 44
    private function executeExtraUpdates()
549
    {
550 44
        foreach ($this->extraUpdates as $oid => $update) {
551 44
            list ($entity, $changeset) = $update;
552
553 44
            $this->entityChangeSets[$oid] = $changeset;
554 44
            $this->getEntityPersister(get_class($entity))->update($entity);
555
        }
556
557 44
        $this->extraUpdates = [];
558 44
    }
559
560
    /**
561
     * Gets the changeset for an entity.
562
     *
563
     * @param object $entity
564
     *
565
     * @return array
566
     */
567
    public function & getEntityChangeSet($entity)
568
    {
569 1092
        $oid  = spl_object_hash($entity);
570 1092
        $data = [];
571
572 1092
        if (!isset($this->entityChangeSets[$oid])) {
573 4
            return $data;
574
        }
575
576 1092
        return $this->entityChangeSets[$oid];
577
    }
578
579
    /**
580
     * Computes the changes that happened to a single entity.
581
     *
582
     * Modifies/populates the following properties:
583
     *
584
     * {@link _originalEntityData}
585
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
586
     * then it was not fetched from the database and therefore we have no original
587
     * entity data yet. All of the current entity data is stored as the original entity data.
588
     *
589
     * {@link _entityChangeSets}
590
     * The changes detected on all properties of the entity are stored there.
591
     * A change is a tuple array where the first entry is the old value and the second
592
     * entry is the new value of the property. Changesets are used by persisters
593
     * to INSERT/UPDATE the persistent entity state.
594
     *
595
     * {@link _entityUpdates}
596
     * If the entity is already fully MANAGED (has been fetched from the database before)
597
     * and any changes to its properties are detected, then a reference to the entity is stored
598
     * there to mark it for an update.
599
     *
600
     * {@link _collectionDeletions}
601
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
602
     * then this collection is marked for deletion.
603
     *
604
     * @ignore
605
     *
606
     * @internal Don't call from the outside.
607
     *
608
     * @param ClassMetadata $class  The class descriptor of the entity.
609
     * @param object        $entity The entity for which to compute the changes.
610
     *
611
     * @return void
612
     */
613 1107
    public function computeChangeSet(ClassMetadata $class, $entity)
614
    {
615 1107
        $oid = spl_object_hash($entity);
616
617 1107
        if (isset($this->readOnlyObjects[$oid])) {
618 2
            return;
619
        }
620
621 1107
        if ( ! $class->isInheritanceTypeNone()) {
622 339
            $class = $this->em->getClassMetadata(get_class($entity));
623
        }
624
625 1107
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
626
627 1107
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
628 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
629
        }
630
631 1107
        $actualData = [];
632
633 1107
        foreach ($class->reflFields as $name => $refProp) {
634 1107
            $value = $refProp->getValue($entity);
635
636 1107
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
637 826
                if ($value instanceof PersistentCollection) {
638 207
                    if ($value->getOwner() === $entity) {
639 207
                        continue;
640
                    }
641
642 5
                    $value = new ArrayCollection($value->getValues());
643
                }
644
645
                // If $value is not a Collection then use an ArrayCollection.
646 821
                if ( ! $value instanceof Collection) {
647 249
                    $value = new ArrayCollection($value);
648
                }
649
650 821
                $assoc = $class->associationMappings[$name];
651
652
                // Inject PersistentCollection
653 821
                $value = new PersistentCollection(
654 821
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
655
                );
656 821
                $value->setOwner($entity, $assoc);
657 821
                $value->setDirty( ! $value->isEmpty());
658
659 821
                $class->reflFields[$name]->setValue($entity, $value);
660
661 821
                $actualData[$name] = $value;
662
663 821
                continue;
664
            }
665
666 1107
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
667 1107
                $actualData[$name] = $value;
668
            }
669
        }
670
671 1107
        if ( ! isset($this->originalEntityData[$oid])) {
672
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
673
            // These result in an INSERT.
674 1103
            $this->originalEntityData[$oid] = $actualData;
675 1103
            $changeSet = [];
676
677 1103
            foreach ($actualData as $propName => $actualValue) {
678 1079
                if ( ! isset($class->associationMappings[$propName])) {
679 1020
                    $changeSet[$propName] = [null, $actualValue];
680
681 1020
                    continue;
682
                }
683
684 958
                $assoc = $class->associationMappings[$propName];
685
686 958
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
687 958
                    $changeSet[$propName] = [null, $actualValue];
688
                }
689
            }
690
691 1103
            $this->entityChangeSets[$oid] = $changeSet;
692
        } else {
693
            // Entity is "fully" MANAGED: it was already fully persisted before
694
            // and we have a copy of the original data
695 280
            $originalData           = $this->originalEntityData[$oid];
696 280
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
697 280
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
698
                ? $this->entityChangeSets[$oid]
699 280
                : [];
700
701 280
            foreach ($actualData as $propName => $actualValue) {
702
                // skip field, its a partially omitted one!
703 262
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
704 8
                    continue;
705
                }
706
707 262
                $orgValue = $originalData[$propName];
708
709
                // skip if value haven't changed
710 262
                if ($orgValue === $actualValue) {
711 245
                    continue;
712
                }
713
714
                // if regular field
715 119
                if ( ! isset($class->associationMappings[$propName])) {
716 64
                    if ($isChangeTrackingNotify) {
717
                        continue;
718
                    }
719
720 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
721
722 64
                    continue;
723
                }
724
725 59
                $assoc = $class->associationMappings[$propName];
726
727
                // Persistent collection was exchanged with the "originally"
728
                // created one. This can only mean it was cloned and replaced
729
                // on another entity.
730 59
                if ($actualValue instanceof PersistentCollection) {
731 8
                    $owner = $actualValue->getOwner();
732 8
                    if ($owner === null) { // cloned
733
                        $actualValue->setOwner($entity, $assoc);
734 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
735
                        if (!$actualValue->isInitialized()) {
736
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
737
                        }
738
                        $newValue = clone $actualValue;
739
                        $newValue->setOwner($entity, $assoc);
740
                        $class->reflFields[$propName]->setValue($entity, $newValue);
741
                    }
742
                }
743
744 59
                if ($orgValue instanceof PersistentCollection) {
745
                    // A PersistentCollection was de-referenced, so delete it.
746 8
                    $coid = spl_object_hash($orgValue);
747
748 8
                    if (isset($this->collectionDeletions[$coid])) {
749
                        continue;
750
                    }
751
752 8
                    $this->collectionDeletions[$coid] = $orgValue;
753 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
754
755 8
                    continue;
756
                }
757
758 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
759 50
                    if ($assoc['isOwningSide']) {
760 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
761
                    }
762
763 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
764 51
                        $this->scheduleOrphanRemoval($orgValue);
765
                    }
766
                }
767
            }
768
769 280
            if ($changeSet) {
770 92
                $this->entityChangeSets[$oid]   = $changeSet;
771 92
                $this->originalEntityData[$oid] = $actualData;
772 92
                $this->entityUpdates[$oid]      = $entity;
773
            }
774
        }
775
776
        // Look for changes in associations of the entity
777 1107
        foreach ($class->associationMappings as $field => $assoc) {
778 958
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
779 668
                continue;
780
            }
781
782 927
            $this->computeAssociationChanges($assoc, $val);
783
784 919
            if ( ! isset($this->entityChangeSets[$oid]) &&
785 919
                $assoc['isOwningSide'] &&
786 919
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
787 919
                $val instanceof PersistentCollection &&
788 919
                $val->isDirty()) {
789
790 35
                $this->entityChangeSets[$oid]   = [];
791 35
                $this->originalEntityData[$oid] = $actualData;
792 919
                $this->entityUpdates[$oid]      = $entity;
793
            }
794
        }
795 1099
    }
796
797
    /**
798
     * Computes all the changes that have been done to entities and collections
799
     * since the last commit and stores these changes in the _entityChangeSet map
800
     * temporarily for access by the persisters, until the UoW commit is finished.
801
     *
802
     * @return void
803
     */
804 1099
    public function computeChangeSets()
805
    {
806
        // Compute changes for INSERTed entities first. This must always happen.
807 1099
        $this->computeScheduleInsertsChangeSets();
808
809
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
810 1097
        foreach ($this->identityMap as $className => $entities) {
811 482
            $class = $this->em->getClassMetadata($className);
812
813
            // Skip class if instances are read-only
814 482
            if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
815 1
                continue;
816
            }
817
818
            // If change tracking is explicit or happens through notification, then only compute
819
            // changes on entities of that type that are explicitly marked for synchronization.
820
            switch (true) {
821 481
                case ($class->isChangeTrackingDeferredImplicit()):
822 476
                    $entitiesToProcess = $entities;
823 476
                    break;
824
825 6
                case (isset($this->scheduledForSynchronization[$className])):
826 5
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
827 5
                    break;
828
829
                default:
830 2
                    $entitiesToProcess = [];
831
832
            }
833
834 481
            foreach ($entitiesToProcess as $entity) {
835
                // Ignore uninitialized proxy objects
836 460
                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
837 38
                    continue;
838
                }
839
840
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
841 458
                $oid = spl_object_hash($entity);
842
843 458
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
844 481
                    $this->computeChangeSet($class, $entity);
845
                }
846
            }
847
        }
848 1097
    }
849
850
    /**
851
     * Computes the changes of an association.
852
     *
853
     * @param array $assoc The association mapping.
854
     * @param mixed $value The value of the association.
855
     *
856
     * @throws ORMInvalidArgumentException
857
     * @throws ORMException
858
     *
859
     * @return void
860
     */
861 927
    private function computeAssociationChanges($assoc, $value)
862
    {
863 927
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
864 30
            return;
865
        }
866
867 926
        if ($value instanceof PersistentCollection && $value->isDirty()) {
868 556
            $coid = spl_object_hash($value);
869
870 556
            $this->collectionUpdates[$coid] = $value;
871 556
            $this->visitedCollections[$coid] = $value;
872
        }
873
874
        // Look through the entities, and in any of their associations,
875
        // for transient (new) entities, recursively. ("Persistence by reachability")
876
        // Unwrap. Uninitialized collections will simply be empty.
877 926
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
878 926
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
879
880 926
        foreach ($unwrappedValue as $key => $entry) {
881 764
            if (! ($entry instanceof $targetClass->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
882 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
883
            }
884
885 756
            $state = $this->getEntityState($entry, self::STATE_NEW);
886
887 756
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
888
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
889
            }
890
891
            switch ($state) {
892 756
                case self::STATE_NEW:
893 42
                    if ( ! $assoc['isCascadePersist']) {
894
                        /*
895
                         * For now just record the details, because this may
896
                         * not be an issue if we later discover another pathway
897
                         * through the object-graph where cascade-persistence
898
                         * is enabled for this object.
899
                         */
900 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
901
902 6
                        break;
903
                    }
904
905 37
                    $this->persistNew($targetClass, $entry);
906 37
                    $this->computeChangeSet($targetClass, $entry);
907
908 37
                    break;
909
910 748
                case self::STATE_REMOVED:
911
                    // Consume the $value as array (it's either an array or an ArrayAccess)
912
                    // and remove the element from Collection.
913 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
914 3
                        unset($value[$key]);
915
                    }
916 4
                    break;
917
918 748
                case self::STATE_DETACHED:
919
                    // Can actually not happen right now as we assume STATE_NEW,
920
                    // so the exception will be raised from the DBAL layer (constraint violation).
921
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
922
                    break;
923
924 756
                default:
925
                    // MANAGED associated entities are already taken into account
926
                    // during changeset calculation anyway, since they are in the identity map.
927
            }
928
        }
929 918
    }
930
931
    /**
932
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
933
     * @param object                              $entity
934
     *
935
     * @return void
936
     */
937 1130
    private function persistNew($class, $entity)
938
    {
939 1130
        $oid    = spl_object_hash($entity);
940 1130
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
941
942 1130
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
943 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
944
        }
945
946 1130
        $idGen = $class->idGenerator;
947
948 1130
        if ( ! $idGen->isPostInsertGenerator()) {
949 300
            $idValue = $idGen->generate($this->em, $entity);
950
951 300
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
952 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
953
954 2
                $class->setIdentifierValues($entity, $idValue);
955
            }
956
957
            // Some identifiers may be foreign keys to new entities.
958
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
959 300
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
960 297
                $this->entityIdentifiers[$oid] = $idValue;
961
            }
962
        }
963
964 1130
        $this->entityStates[$oid] = self::STATE_MANAGED;
965
966 1130
        $this->scheduleForInsert($entity);
967 1130
    }
968
969
    /**
970
     * @param mixed[] $idValue
971
     */
972 300
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
973
    {
974 300
        foreach ($idValue as $idField => $idFieldValue) {
975 300
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
976 300
                return true;
977
            }
978
        }
979
980 297
        return false;
981
    }
982
983
    /**
984
     * INTERNAL:
985
     * Computes the changeset of an individual entity, independently of the
986
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
987
     *
988
     * The passed entity must be a managed entity. If the entity already has a change set
989
     * because this method is invoked during a commit cycle then the change sets are added.
990
     * whereby changes detected in this method prevail.
991
     *
992
     * @ignore
993
     *
994
     * @param ClassMetadata $class  The class descriptor of the entity.
995
     * @param object        $entity The entity for which to (re)calculate the change set.
996
     *
997
     * @return void
998
     *
999
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
1000
     */
1001 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
1002
    {
1003 16
        $oid = spl_object_hash($entity);
1004
1005 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
1006
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1007
        }
1008
1009
        // skip if change tracking is "NOTIFY"
1010 16
        if ($class->isChangeTrackingNotify()) {
1011
            return;
1012
        }
1013
1014 16
        if ( ! $class->isInheritanceTypeNone()) {
1015 3
            $class = $this->em->getClassMetadata(get_class($entity));
1016
        }
1017
1018 16
        $actualData = [];
1019
1020 16
        foreach ($class->reflFields as $name => $refProp) {
1021 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1022 16
                && ($name !== $class->versionField)
1023 16
                && ! $class->isCollectionValuedAssociation($name)) {
1024 16
                $actualData[$name] = $refProp->getValue($entity);
1025
            }
1026
        }
1027
1028 16
        if ( ! isset($this->originalEntityData[$oid])) {
1029
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1030
        }
1031
1032 16
        $originalData = $this->originalEntityData[$oid];
1033 16
        $changeSet = [];
1034
1035 16
        foreach ($actualData as $propName => $actualValue) {
1036 16
            $orgValue = $originalData[$propName] ?? null;
1037
1038 16
            if ($orgValue !== $actualValue) {
1039 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1040
            }
1041
        }
1042
1043 16
        if ($changeSet) {
1044 7
            if (isset($this->entityChangeSets[$oid])) {
1045 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1046 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1047 1
                $this->entityChangeSets[$oid] = $changeSet;
1048 1
                $this->entityUpdates[$oid]    = $entity;
1049
            }
1050 7
            $this->originalEntityData[$oid] = $actualData;
1051
        }
1052 16
    }
1053
1054
    /**
1055
     * Executes all entity insertions for entities of the specified type.
1056
     *
1057
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1058
     *
1059
     * @return void
1060
     */
1061 1093
    private function executeInserts($class)
1062
    {
1063 1093
        $entities   = [];
1064 1093
        $className  = $class->name;
1065 1093
        $persister  = $this->getEntityPersister($className);
1066 1093
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1067
1068 1093
        $insertionsForClass = [];
1069
1070 1093
        foreach ($this->entityInsertions as $oid => $entity) {
1071
1072 1093
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1073 921
                continue;
1074
            }
1075
1076 1093
            $insertionsForClass[$oid] = $entity;
1077
1078 1093
            $persister->addInsert($entity);
1079
1080 1093
            unset($this->entityInsertions[$oid]);
1081
1082 1093
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1083 1093
                $entities[] = $entity;
1084
            }
1085
        }
1086
1087 1093
        $postInsertIds = $persister->executeInserts();
1088
1089 1093
        if ($postInsertIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postInsertIds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1090
            // Persister returned post-insert IDs
1091 987
            foreach ($postInsertIds as $postInsertId) {
1092 987
                $idField = $class->getSingleIdentifierFieldName();
1093 987
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1094
1095 987
                $entity  = $postInsertId['entity'];
1096 987
                $oid     = spl_object_hash($entity);
1097
1098 987
                $class->reflFields[$idField]->setValue($entity, $idValue);
1099
1100 987
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1101 987
                $this->entityStates[$oid] = self::STATE_MANAGED;
1102 987
                $this->originalEntityData[$oid][$idField] = $idValue;
1103
1104 987
                $this->addToIdentityMap($entity);
1105
            }
1106
        } else {
1107 825
            foreach ($insertionsForClass as $oid => $entity) {
1108 283
                if (! isset($this->entityIdentifiers[$oid])) {
1109
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1110
                    //add it now
1111 283
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1112
                }
1113
            }
1114
        }
1115
1116 1093
        foreach ($entities as $entity) {
1117 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1118
        }
1119 1093
    }
1120
1121
    /**
1122
     * @param object $entity
1123
     */
1124 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1125
    {
1126 3
        $identifier = [];
1127
1128 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1129 3
            $value = $class->getFieldValue($entity, $idField);
1130
1131 3
            if (isset($class->associationMappings[$idField])) {
1132
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1133 3
                $value = $this->getSingleIdentifierValue($value);
1134
            }
1135
1136 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1137
        }
1138
1139 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1140 3
        $this->entityIdentifiers[$oid] = $identifier;
1141
1142 3
        $this->addToIdentityMap($entity);
1143 3
    }
1144
1145
    /**
1146
     * Executes all entity updates for entities of the specified type.
1147
     *
1148
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1149
     *
1150
     * @return void
1151
     */
1152 123
    private function executeUpdates($class)
1153
    {
1154 123
        $className          = $class->name;
1155 123
        $persister          = $this->getEntityPersister($className);
1156 123
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1157 123
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1158
1159 123
        foreach ($this->entityUpdates as $oid => $entity) {
1160 123
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1161 79
                continue;
1162
            }
1163
1164 123
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1165 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1166
1167 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1168
            }
1169
1170 123
            if ( ! empty($this->entityChangeSets[$oid])) {
1171 89
                $persister->update($entity);
1172
            }
1173
1174 119
            unset($this->entityUpdates[$oid]);
1175
1176 119
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1177 119
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1178
            }
1179
        }
1180 119
    }
1181
1182
    /**
1183
     * Executes all entity deletions for entities of the specified type.
1184
     *
1185
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1186
     *
1187
     * @return void
1188
     */
1189 65
    private function executeDeletions($class)
1190
    {
1191 65
        $className  = $class->name;
1192 65
        $persister  = $this->getEntityPersister($className);
1193 65
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1194
1195 65
        foreach ($this->entityDeletions as $oid => $entity) {
1196 65
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1197 26
                continue;
1198
            }
1199
1200 65
            $persister->delete($entity);
1201
1202
            unset(
1203 65
                $this->entityDeletions[$oid],
1204 65
                $this->entityIdentifiers[$oid],
1205 65
                $this->originalEntityData[$oid],
1206 65
                $this->entityStates[$oid]
1207
            );
1208
1209
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1210
            // is obtained by a new entity because the old one went out of scope.
1211
            //$this->entityStates[$oid] = self::STATE_NEW;
1212 65
            if ( ! $class->isIdentifierNatural()) {
1213 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1214
            }
1215
1216 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1217 65
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1218
            }
1219
        }
1220 64
    }
1221
1222
    /**
1223
     * Gets the commit order.
1224
     *
1225
     * @param array|null $entityChangeSet
1226
     *
1227
     * @return array
1228
     */
1229 1097
    private function getCommitOrder(array $entityChangeSet = null)
1230
    {
1231 1097
        if ($entityChangeSet === null) {
1232 1097
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1233
        }
1234
1235 1097
        $calc = $this->getCommitOrderCalculator();
1236
1237
        // See if there are any new classes in the changeset, that are not in the
1238
        // commit order graph yet (don't have a node).
1239
        // We have to inspect changeSet to be able to correctly build dependencies.
1240
        // It is not possible to use IdentityMap here because post inserted ids
1241
        // are not yet available.
1242 1097
        $newNodes = [];
1243
1244 1097
        foreach ($entityChangeSet as $entity) {
1245 1097
            $class = $this->em->getClassMetadata(get_class($entity));
1246
1247 1097
            if ($calc->hasNode($class->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1248 667
                continue;
1249
            }
1250
1251 1097
            $calc->addNode($class->name, $class);
1252
1253 1097
            $newNodes[] = $class;
1254
        }
1255
1256
        // Calculate dependencies for new nodes
1257 1097
        while ($class = array_pop($newNodes)) {
1258 1097
            foreach ($class->associationMappings as $assoc) {
1259 947
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1260 899
                    continue;
1261
                }
1262
1263 894
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1264
1265 894
                if ( ! $calc->hasNode($targetClass->name)) {
1266 686
                    $calc->addNode($targetClass->name, $targetClass);
1267
1268 686
                    $newNodes[] = $targetClass;
1269
                }
1270
1271 894
                $joinColumns = reset($assoc['joinColumns']);
1272
1273 894
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1274
1275
                // If the target class has mapped subclasses, these share the same dependency.
1276 894
                if ( ! $targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1277 887
                    continue;
1278
                }
1279
1280 239
                foreach ($targetClass->subClasses as $subClassName) {
1281 239
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1282
1283 239
                    if ( ! $calc->hasNode($subClassName)) {
1284 209
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1285
1286 209
                        $newNodes[] = $targetSubClass;
1287
                    }
1288
1289 239
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1290
                }
1291
            }
1292
        }
1293
1294 1097
        return $calc->sort();
1295
    }
1296
1297
    /**
1298
     * Schedules an entity for insertion into the database.
1299
     * If the entity already has an identifier, it will be added to the identity map.
1300
     *
1301
     * @param object $entity The entity to schedule for insertion.
1302
     *
1303
     * @return void
1304
     *
1305
     * @throws ORMInvalidArgumentException
1306
     * @throws \InvalidArgumentException
1307
     */
1308 1131
    public function scheduleForInsert($entity)
1309
    {
1310 1131
        $oid = spl_object_hash($entity);
1311
1312 1131
        if (isset($this->entityUpdates[$oid])) {
1313
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1314
        }
1315
1316 1131
        if (isset($this->entityDeletions[$oid])) {
1317 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1318
        }
1319 1131
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1320 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1321
        }
1322
1323 1131
        if (isset($this->entityInsertions[$oid])) {
1324 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1325
        }
1326
1327 1131
        $this->entityInsertions[$oid] = $entity;
1328
1329 1131
        if (isset($this->entityIdentifiers[$oid])) {
1330 297
            $this->addToIdentityMap($entity);
1331
        }
1332
1333 1131
        if ($entity instanceof NotifyPropertyChanged) {
1334 8
            $entity->addPropertyChangedListener($this);
1335
        }
1336 1131
    }
1337
1338
    /**
1339
     * Checks whether an entity is scheduled for insertion.
1340
     *
1341
     * @param object $entity
1342
     *
1343
     * @return boolean
1344
     */
1345 662
    public function isScheduledForInsert($entity)
1346
    {
1347 662
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1348
    }
1349
1350
    /**
1351
     * Schedules an entity for being updated.
1352
     *
1353
     * @param object $entity The entity to schedule for being updated.
1354
     *
1355
     * @return void
1356
     *
1357
     * @throws ORMInvalidArgumentException
1358
     */
1359 1
    public function scheduleForUpdate($entity)
1360
    {
1361 1
        $oid = spl_object_hash($entity);
1362
1363 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1364
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1365
        }
1366
1367 1
        if (isset($this->entityDeletions[$oid])) {
1368
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1369
        }
1370
1371 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1372 1
            $this->entityUpdates[$oid] = $entity;
1373
        }
1374 1
    }
1375
1376
    /**
1377
     * INTERNAL:
1378
     * Schedules an extra update that will be executed immediately after the
1379
     * regular entity updates within the currently running commit cycle.
1380
     *
1381
     * Extra updates for entities are stored as (entity, changeset) tuples.
1382
     *
1383
     * @ignore
1384
     *
1385
     * @param object $entity    The entity for which to schedule an extra update.
1386
     * @param array  $changeset The changeset of the entity (what to update).
1387
     *
1388
     * @return void
1389
     */
1390 44
    public function scheduleExtraUpdate($entity, array $changeset)
1391
    {
1392 44
        $oid         = spl_object_hash($entity);
1393 44
        $extraUpdate = [$entity, $changeset];
1394
1395 44
        if (isset($this->extraUpdates[$oid])) {
1396 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1397
1398 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1399
        }
1400
1401 44
        $this->extraUpdates[$oid] = $extraUpdate;
1402 44
    }
1403
1404
    /**
1405
     * Checks whether an entity is registered as dirty in the unit of work.
1406
     * Note: Is not very useful currently as dirty entities are only registered
1407
     * at commit time.
1408
     *
1409
     * @param object $entity
1410
     *
1411
     * @return boolean
1412
     */
1413
    public function isScheduledForUpdate($entity)
1414
    {
1415
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1416
    }
1417
1418
    /**
1419
     * Checks whether an entity is registered to be checked in the unit of work.
1420
     *
1421
     * @param object $entity
1422
     *
1423
     * @return boolean
1424
     */
1425 5
    public function isScheduledForDirtyCheck($entity)
1426
    {
1427 5
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1428
1429 5
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1430
    }
1431
1432
    /**
1433
     * INTERNAL:
1434
     * Schedules an entity for deletion.
1435
     *
1436
     * @param object $entity
1437
     *
1438
     * @return void
1439
     */
1440 68
    public function scheduleForDelete($entity)
1441
    {
1442 68
        $oid = spl_object_hash($entity);
1443
1444 68
        if (isset($this->entityInsertions[$oid])) {
1445 1
            if ($this->isInIdentityMap($entity)) {
1446
                $this->removeFromIdentityMap($entity);
1447
            }
1448
1449 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1450
1451 1
            return; // entity has not been persisted yet, so nothing more to do.
1452
        }
1453
1454 68
        if ( ! $this->isInIdentityMap($entity)) {
1455 1
            return;
1456
        }
1457
1458 67
        $this->removeFromIdentityMap($entity);
1459
1460 67
        unset($this->entityUpdates[$oid]);
1461
1462 67
        if ( ! isset($this->entityDeletions[$oid])) {
1463 67
            $this->entityDeletions[$oid] = $entity;
1464 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1465
        }
1466 67
    }
1467
1468
    /**
1469
     * Checks whether an entity is registered as removed/deleted with the unit
1470
     * of work.
1471
     *
1472
     * @param object $entity
1473
     *
1474
     * @return boolean
1475
     */
1476 17
    public function isScheduledForDelete($entity)
1477
    {
1478 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1479
    }
1480
1481
    /**
1482
     * Checks whether an entity is scheduled for insertion, update or deletion.
1483
     *
1484
     * @param object $entity
1485
     *
1486
     * @return boolean
1487
     */
1488
    public function isEntityScheduled($entity)
1489
    {
1490
        $oid = spl_object_hash($entity);
1491
1492
        return isset($this->entityInsertions[$oid])
1493
            || isset($this->entityUpdates[$oid])
1494
            || isset($this->entityDeletions[$oid]);
1495
    }
1496
1497
    /**
1498
     * INTERNAL:
1499
     * Registers an entity in the identity map.
1500
     * Note that entities in a hierarchy are registered with the class name of
1501
     * the root entity.
1502
     *
1503
     * @ignore
1504
     *
1505
     * @param object $entity The entity to register.
1506
     *
1507
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1508
     *                 the entity in question is already managed.
1509
     *
1510
     * @throws ORMInvalidArgumentException
1511
     */
1512 1195
    public function addToIdentityMap($entity)
1513
    {
1514 1195
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1515 1195
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1516
1517 1195
        if (empty($identifier) || in_array(null, $identifier, true)) {
1518 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1519
        }
1520
1521 1189
        $idHash    = implode(' ', $identifier);
1522 1189
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1523
1524 1189
        if (isset($this->identityMap[$className][$idHash])) {
1525 87
            return false;
1526
        }
1527
1528 1189
        $this->identityMap[$className][$idHash] = $entity;
1529
1530 1189
        return true;
1531
    }
1532
1533
    /**
1534
     * Gets the state of an entity with regard to the current unit of work.
1535
     *
1536
     * @param object   $entity
1537
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1538
     *                         This parameter can be set to improve performance of entity state detection
1539
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1540
     *                         is either known or does not matter for the caller of the method.
1541
     *
1542
     * @return int The entity state.
1543
     */
1544 1145
    public function getEntityState($entity, $assume = null)
1545
    {
1546 1145
        $oid = spl_object_hash($entity);
1547
1548 1145
        if (isset($this->entityStates[$oid])) {
1549 830
            return $this->entityStates[$oid];
1550
        }
1551
1552 1139
        if ($assume !== null) {
1553 1135
            return $assume;
1554
        }
1555
1556
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1557
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1558
        // the UoW does not hold references to such objects and the object hash can be reused.
1559
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1560 13
        $class = $this->em->getClassMetadata(get_class($entity));
1561 13
        $id    = $class->getIdentifierValues($entity);
1562
1563 13
        if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1564 5
            return self::STATE_NEW;
1565
        }
1566
1567 10
        if ($class->containsForeignIdentifier) {
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1568 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1569
        }
1570
1571
        switch (true) {
1572 10
            case ($class->isIdentifierNatural()):
0 ignored issues
show
Bug introduced by
The method isIdentifierNatural() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
1933 6
                $managedCopy = $this->newInstance($class);
1934
1935 6
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1936 6
                $this->persistNew($class, $managedCopy);
1937
            } else {
1938 37
                $flatId = ($class->containsForeignIdentifier)
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1939 3
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1940 37
                    : $id;
1941
1942 37
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1943
1944 37
                if ($managedCopy) {
1945
                    // We have the entity in-memory already, just make sure its not removed.
1946 15
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $entity of Doctrine\ORM\UnitOfWork::getEntityState() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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