Passed
Pull Request — 2.8.x (#8083)
by karma
06:44
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 14.0713

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\NotifyPropertyChanged;
25
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
26
use Doctrine\Common\Persistence\ObjectManagerAware;
27
use Doctrine\Common\PropertyChangedListener;
28
use Doctrine\DBAL\LockMode;
29
use Doctrine\ORM\Cache\Persister\CachedPersister;
30
use Doctrine\ORM\Event\LifecycleEventArgs;
31
use Doctrine\ORM\Event\ListenersInvoker;
32
use Doctrine\ORM\Event\OnFlushEventArgs;
33
use Doctrine\ORM\Event\PostFlushEventArgs;
34
use Doctrine\ORM\Event\PreFlushEventArgs;
35
use Doctrine\ORM\Event\PreUpdateEventArgs;
36
use Doctrine\ORM\Internal\HydrationCompleteHandler;
37
use Doctrine\ORM\Mapping\ClassMetadata;
38
use Doctrine\ORM\Mapping\ClassMetadataInfo;
39
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
40
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
41
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
42
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
43
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
44
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
45
use Doctrine\ORM\Proxy\Proxy;
46
use Doctrine\ORM\Utility\IdentifierFlattener;
47
use InvalidArgumentException;
48
use Throwable;
49
use UnexpectedValueException;
50
use function array_key_exists;
51
use function get_class;
52
use function is_object;
53
54
/**
55
 * The UnitOfWork is responsible for tracking changes to objects during an
56
 * "object-level" transaction and for writing out changes to the database
57
 * in the correct order.
58
 *
59
 * Internal note: This class contains highly performance-sensitive code.
60
 *
61
 * @since       2.0
62
 * @author      Benjamin Eberlei <[email protected]>
63
 * @author      Guilherme Blanco <[email protected]>
64
 * @author      Jonathan Wage <[email protected]>
65
 * @author      Roman Borschel <[email protected]>
66
 * @author      Rob Caiger <[email protected]>
67
 */
68
class UnitOfWork implements PropertyChangedListener
69
{
70
    /**
71
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
72
     */
73
    const STATE_MANAGED = 1;
74
75
    /**
76
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
77
     * and is not (yet) managed by an EntityManager.
78
     */
79
    const STATE_NEW = 2;
80
81
    /**
82
     * A detached entity is an instance with persistent state and identity that is not
83
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
84
     */
85
    const STATE_DETACHED = 3;
86
87
    /**
88
     * A removed entity instance is an instance with a persistent identity,
89
     * associated with an EntityManager, whose persistent state will be deleted
90
     * on commit.
91
     */
92
    const STATE_REMOVED = 4;
93
94
    /**
95
     * Hint used to collect all primary keys of associated entities during hydration
96
     * and execute it in a dedicated query afterwards
97
     * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
98
     */
99
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
100
101
    /**
102
     * The identity map that holds references to all managed entities that have
103
     * an identity. The entities are grouped by their class name.
104
     * Since all classes in a hierarchy must share the same identifier set,
105
     * we always take the root class name of the hierarchy.
106
     *
107
     * @var array
108
     */
109
    private $identityMap = [];
110
111
    /**
112
     * Map of all identifiers of managed entities.
113
     * Keys are object ids (spl_object_hash).
114
     *
115
     * @var array
116
     */
117
    private $entityIdentifiers = [];
118
119
    /**
120
     * Map of the original entity data of managed entities.
121
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
122
     * at commit time.
123
     *
124
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
125
     *                A value will only really be copied if the value in the entity is modified
126
     *                by the user.
127
     *
128
     * @var array
129
     */
130
    private $originalEntityData = [];
131
132
    /**
133
     * Map of entity changes. Keys are object ids (spl_object_hash).
134
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
135
     *
136
     * @var array
137
     */
138
    private $entityChangeSets = [];
139
140
    /**
141
     * The (cached) states of any known entities.
142
     * Keys are object ids (spl_object_hash).
143
     *
144
     * @var array
145
     */
146
    private $entityStates = [];
147
148
    /**
149
     * Map of entities that are scheduled for dirty checking at commit time.
150
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
151
     * Keys are object ids (spl_object_hash).
152
     *
153
     * @var array
154
     */
155
    private $scheduledForSynchronization = [];
156
157
    /**
158
     * A list of all pending entity insertions.
159
     *
160
     * @var array
161
     */
162
    private $entityInsertions = [];
163
164
    /**
165
     * A list of all pending entity updates.
166
     *
167
     * @var array
168
     */
169
    private $entityUpdates = [];
170
171
    /**
172
     * Any pending extra updates that have been scheduled by persisters.
173
     *
174
     * @var array
175
     */
176
    private $extraUpdates = [];
177
178
    /**
179
     * A list of all pending entity deletions.
180
     *
181
     * @var array
182
     */
183
    private $entityDeletions = [];
184
185
    /**
186
     * New entities that were discovered through relationships that were not
187
     * marked as cascade-persist. During flush, this array is populated and
188
     * then pruned of any entities that were discovered through a valid
189
     * cascade-persist path. (Leftovers cause an error.)
190
     *
191
     * Keys are OIDs, payload is a two-item array describing the association
192
     * and the entity.
193
     *
194
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
195
     */
196
    private $nonCascadedNewDetectedEntities = [];
197
198
    /**
199
     * All pending collection deletions.
200
     *
201
     * @var array
202
     */
203
    private $collectionDeletions = [];
204
205
    /**
206
     * All pending collection updates.
207
     *
208
     * @var array
209
     */
210
    private $collectionUpdates = [];
211
212
    /**
213
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
214
     * At the end of the UnitOfWork all these collections will make new snapshots
215
     * of their data.
216
     *
217
     * @var array
218
     */
219
    private $visitedCollections = [];
220
221
    /**
222
     * The EntityManager that "owns" this UnitOfWork instance.
223
     *
224
     * @var EntityManagerInterface
225
     */
226
    private $em;
227
228
    /**
229
     * The entity persister instances used to persist entity instances.
230
     *
231
     * @var array
232
     */
233
    private $persisters = [];
234
235
    /**
236
     * The collection persister instances used to persist collections.
237
     *
238
     * @var array
239
     */
240
    private $collectionPersisters = [];
241
242
    /**
243
     * The EventManager used for dispatching events.
244
     *
245
     * @var \Doctrine\Common\EventManager
246
     */
247
    private $evm;
248
249
    /**
250
     * The ListenersInvoker used for dispatching events.
251
     *
252
     * @var \Doctrine\ORM\Event\ListenersInvoker
253
     */
254
    private $listenersInvoker;
255
256
    /**
257
     * The IdentifierFlattener used for manipulating identifiers
258
     *
259
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
260
     */
261
    private $identifierFlattener;
262
263
    /**
264
     * Orphaned entities that are scheduled for removal.
265
     *
266
     * @var array
267
     */
268
    private $orphanRemovals = [];
269
270
    /**
271
     * Read-Only objects are never evaluated
272
     *
273
     * @var array
274
     */
275
    private $readOnlyObjects = [];
276
277
    /**
278
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
279
     *
280
     * @var array
281
     */
282
    private $eagerLoadingEntities = [];
283
284
    /**
285
     * @var boolean
286
     */
287
    protected $hasCache = false;
288
289
    /**
290
     * Helper for handling completion of hydration
291
     *
292
     * @var HydrationCompleteHandler
293
     */
294
    private $hydrationCompleteHandler;
295
296
    /**
297
     * @var ReflectionPropertiesGetter
298
     */
299
    private $reflectionPropertiesGetter;
300
301
    /**
302
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
303
     *
304
     * @param EntityManagerInterface $em
305
     */
306 2507
    public function __construct(EntityManagerInterface $em)
307
    {
308 2507
        $this->em                         = $em;
309 2507
        $this->evm                        = $em->getEventManager();
310 2507
        $this->listenersInvoker           = new ListenersInvoker($em);
311 2507
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
312 2507
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
313 2507
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
314 2507
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
315 2507
    }
316
317
    /**
318
     * Commits the UnitOfWork, executing all operations that have been postponed
319
     * up to this point. The state of all managed entities will be synchronized with
320
     * the database.
321
     *
322
     * The operations are executed in the following order:
323
     *
324
     * 1) All entity insertions
325
     * 2) All entity updates
326
     * 3) All collection deletions
327
     * 4) All collection updates
328
     * 5) All entity deletions
329
     *
330
     * @param null|object|array $entity
331
     *
332
     * @return void
333
     *
334
     * @throws \Exception
335
     */
336 1095
    public function commit($entity = null)
337
    {
338
        // Raise preFlush
339 1095
        if ($this->evm->hasListeners(Events::preFlush)) {
340 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
341
        }
342
343
        // Compute changes done since last commit.
344 1095
        if (null === $entity) {
345 1088
            $this->computeChangeSets();
346 14
        } elseif (is_object($entity)) {
347 12
            $this->computeSingleEntityChangeSet($entity);
348 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
349 2
            foreach ($entity as $object) {
350 2
                $this->computeSingleEntityChangeSet($object);
351
            }
352
        }
353
354 1092
        if ( ! ($this->entityInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
426 65
                    $this->executeDeletions($commitOrder[$i]);
427
                }
428
            }
429
430
            // Commit failed silently
431 1081
            if ($conn->commit() === false) {
432 1
                $object = is_object($entity) ? $entity : null;
433
434 1081
                throw new OptimisticLockException('Commit failed', $object);
435
            }
436 12
        } catch (Throwable $e) {
437 12
            $this->em->close();
438
439 12
            if ($conn->isTransactionActive()) {
440 12
                $conn->rollBack();
441
            }
442
443 12
            $this->afterTransactionRolledBack();
444
445 12
            throw $e;
446
        }
447
448 1080
        $this->afterTransactionComplete();
449
450
        // Take new snapshots from visited collections
451 1080
        foreach ($this->visitedCollections as $coll) {
452 539
            $coll->takeSnapshot();
453
        }
454
455 1080
        $this->dispatchPostFlushEvent();
456
457 1079
        $this->postCommitCleanup($entity);
458 1079
    }
459
460
    /**
461
     * @param null|object|object[] $entity
462
     */
463 1083
    private function postCommitCleanup($entity) : void
464
    {
465 1083
        $this->entityInsertions =
466 1083
        $this->entityUpdates =
467 1083
        $this->entityDeletions =
468 1083
        $this->extraUpdates =
469 1083
        $this->collectionUpdates =
470 1083
        $this->nonCascadedNewDetectedEntities =
471 1083
        $this->collectionDeletions =
472 1083
        $this->visitedCollections =
473 1083
        $this->orphanRemovals = [];
474
475 1083
        if (null === $entity) {
476 1077
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
477
478 1077
            return;
479
        }
480
481 12
        $entities = \is_object($entity)
482 10
            ? [$entity]
483 12
            : $entity;
484
485 12
        foreach ($entities as $object) {
486 12
            $oid = \spl_object_hash($object);
487
488 12
            $this->clearEntityChangeSet($oid);
489
490 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...
491
        }
492 12
    }
493
494
    /**
495
     * Computes the changesets of all entities scheduled for insertion.
496
     *
497
     * @return void
498
     */
499 1094
    private function computeScheduleInsertsChangeSets()
500
    {
501 1094
        foreach ($this->entityInsertions as $entity) {
502 1086
            $class = $this->em->getClassMetadata(get_class($entity));
503
504 1086
            $this->computeChangeSet($class, $entity);
505
        }
506 1092
    }
507
508
    /**
509
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
510
     *
511
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
512
     * 2. Read Only entities are skipped.
513
     * 3. Proxies are skipped.
514
     * 4. Only if entity is properly managed.
515
     *
516
     * @param object $entity
517
     *
518
     * @return void
519
     *
520
     * @throws \InvalidArgumentException
521
     */
522 14
    private function computeSingleEntityChangeSet($entity)
523
    {
524 14
        $state = $this->getEntityState($entity);
525
526 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
527 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
528
        }
529
530 13
        $class = $this->em->getClassMetadata(get_class($entity));
531
532 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
533 13
            $this->persist($entity);
534
        }
535
536
        // Compute changes for INSERTed entities first. This must always happen even in this case.
537 13
        $this->computeScheduleInsertsChangeSets();
538
539 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...
540
            return;
541
        }
542
543
        // Ignore uninitialized proxy objects
544 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...
545 1
            return;
546
        }
547
548
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
549 12
        $oid = spl_object_hash($entity);
550
551 12
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
552 7
            $this->computeChangeSet($class, $entity);
553
        }
554 12
    }
555
556
    /**
557
     * Executes any extra updates that have been scheduled.
558
     */
559 44
    private function executeExtraUpdates()
560
    {
561 44
        foreach ($this->extraUpdates as $oid => $update) {
562 44
            list ($entity, $changeset) = $update;
563
564 44
            $this->entityChangeSets[$oid] = $changeset;
565 44
            $this->getEntityPersister(get_class($entity))->update($entity);
566
        }
567
568 44
        $this->extraUpdates = [];
569 44
    }
570
571
    /**
572
     * Gets the changeset for an entity.
573
     *
574
     * @param object $entity
575
     *
576
     * @return array
577
     */
578
    public function & getEntityChangeSet($entity)
579
    {
580 1080
        $oid  = spl_object_hash($entity);
581 1080
        $data = [];
582
583 1080
        if (!isset($this->entityChangeSets[$oid])) {
584 4
            return $data;
585
        }
586
587 1080
        return $this->entityChangeSets[$oid];
588
    }
589
590
    /**
591
     * Computes the changes that happened to a single entity.
592
     *
593
     * Modifies/populates the following properties:
594
     *
595
     * {@link _originalEntityData}
596
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
597
     * then it was not fetched from the database and therefore we have no original
598
     * entity data yet. All of the current entity data is stored as the original entity data.
599
     *
600
     * {@link _entityChangeSets}
601
     * The changes detected on all properties of the entity are stored there.
602
     * A change is a tuple array where the first entry is the old value and the second
603
     * entry is the new value of the property. Changesets are used by persisters
604
     * to INSERT/UPDATE the persistent entity state.
605
     *
606
     * {@link _entityUpdates}
607
     * If the entity is already fully MANAGED (has been fetched from the database before)
608
     * and any changes to its properties are detected, then a reference to the entity is stored
609
     * there to mark it for an update.
610
     *
611
     * {@link _collectionDeletions}
612
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
613
     * then this collection is marked for deletion.
614
     *
615
     * @ignore
616
     *
617
     * @internal Don't call from the outside.
618
     *
619
     * @param ClassMetadata $class  The class descriptor of the entity.
620
     * @param object        $entity The entity for which to compute the changes.
621
     *
622
     * @return void
623
     */
624 1096
    public function computeChangeSet(ClassMetadata $class, $entity)
625
    {
626 1096
        $oid = spl_object_hash($entity);
627
628 1096
        if (isset($this->readOnlyObjects[$oid])) {
629 2
            return;
630
        }
631
632 1096
        if ( ! $class->isInheritanceTypeNone()) {
633 327
            $class = $this->em->getClassMetadata(get_class($entity));
634
        }
635
636 1096
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
637
638 1096
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
639 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
640
        }
641
642 1096
        $actualData = [];
643
644 1096
        foreach ($class->reflFields as $name => $refProp) {
645 1096
            $value = $refProp->getValue($entity);
646
647 1096
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
648 812
                if ($value instanceof PersistentCollection) {
649 203
                    if ($value->getOwner() === $entity) {
650 203
                        continue;
651
                    }
652
653 5
                    $value = new ArrayCollection($value->getValues());
654
                }
655
656
                // If $value is not a Collection then use an ArrayCollection.
657 807
                if ( ! $value instanceof Collection) {
658 236
                    $value = new ArrayCollection($value);
659
                }
660
661 807
                $assoc = $class->associationMappings[$name];
662
663
                // Inject PersistentCollection
664 807
                $value = new PersistentCollection(
665 807
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
666
                );
667 807
                $value->setOwner($entity, $assoc);
668 807
                $value->setDirty( ! $value->isEmpty());
669
670 807
                $class->reflFields[$name]->setValue($entity, $value);
671
672 807
                $actualData[$name] = $value;
673
674 807
                continue;
675
            }
676
677 1096
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
678 1055
                $actualData[$name] = $value;
679
            }
680
        }
681
682 1096
        if ( ! isset($this->originalEntityData[$oid])) {
683
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
684
            // These result in an INSERT.
685 1092
            $this->originalEntityData[$oid] = $actualData;
686 1092
            $changeSet = [];
687
688 1092
            foreach ($actualData as $propName => $actualValue) {
689 1068
                if ( ! isset($class->associationMappings[$propName])) {
690 1009
                    $changeSet[$propName] = [null, $actualValue];
691
692 1009
                    continue;
693
                }
694
695 945
                $assoc = $class->associationMappings[$propName];
696
697 945
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
698 890
                    $changeSet[$propName] = [null, $actualValue];
699
                }
700
            }
701
702 1092
            $this->entityChangeSets[$oid] = $changeSet;
703
        } else {
704
            // Entity is "fully" MANAGED: it was already fully persisted before
705
            // and we have a copy of the original data
706 276
            $originalData           = $this->originalEntityData[$oid];
707 276
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
708 276
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
709
                ? $this->entityChangeSets[$oid]
710 276
                : [];
711
712 276
            foreach ($actualData as $propName => $actualValue) {
713
                // skip field, its a partially omitted one!
714 259
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
715 8
                    continue;
716
                }
717
718 259
                $orgValue = $originalData[$propName];
719
720
                // skip if value haven't changed
721 259
                if ($orgValue === $actualValue) {
722 243
                    continue;
723
                }
724
725
                // if regular field
726 118
                if ( ! isset($class->associationMappings[$propName])) {
727 64
                    if ($isChangeTrackingNotify) {
728
                        continue;
729
                    }
730
731 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
732
733 64
                    continue;
734
                }
735
736 58
                $assoc = $class->associationMappings[$propName];
737
738
                // Persistent collection was exchanged with the "originally"
739
                // created one. This can only mean it was cloned and replaced
740
                // on another entity.
741 58
                if ($actualValue instanceof PersistentCollection) {
742 8
                    $owner = $actualValue->getOwner();
743 8
                    if ($owner === null) { // cloned
744
                        $actualValue->setOwner($entity, $assoc);
745 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
746
                        if (!$actualValue->isInitialized()) {
747
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
748
                        }
749
                        $newValue = clone $actualValue;
750
                        $newValue->setOwner($entity, $assoc);
751
                        $class->reflFields[$propName]->setValue($entity, $newValue);
752
                    }
753
                }
754
755 58
                if ($orgValue instanceof PersistentCollection) {
756
                    // A PersistentCollection was de-referenced, so delete it.
757 8
                    $coid = spl_object_hash($orgValue);
758
759 8
                    if (isset($this->collectionDeletions[$coid])) {
760
                        continue;
761
                    }
762
763 8
                    $this->collectionDeletions[$coid] = $orgValue;
764 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
765
766 8
                    continue;
767
                }
768
769 50
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
770 49
                    if ($assoc['isOwningSide']) {
771 21
                        $changeSet[$propName] = [$orgValue, $actualValue];
772
                    }
773
774 49
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
775 4
                        $this->scheduleOrphanRemoval($orgValue);
776
                    }
777
                }
778
            }
779
780 276
            if ($changeSet) {
781 91
                $this->entityChangeSets[$oid]   = $changeSet;
782 91
                $this->originalEntityData[$oid] = $actualData;
783 91
                $this->entityUpdates[$oid]      = $entity;
784
            }
785
        }
786
787
        // Look for changes in associations of the entity
788 1096
        foreach ($class->associationMappings as $field => $assoc) {
789 945
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
790 654
                continue;
791
            }
792
793 913
            $this->computeAssociationChanges($assoc, $val);
794
795 905
            if ( ! isset($this->entityChangeSets[$oid]) &&
796 905
                $assoc['isOwningSide'] &&
797 905
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
798 905
                $val instanceof PersistentCollection &&
799 905
                $val->isDirty()) {
800
801 35
                $this->entityChangeSets[$oid]   = [];
802 35
                $this->originalEntityData[$oid] = $actualData;
803 35
                $this->entityUpdates[$oid]      = $entity;
804
            }
805
        }
806 1088
    }
807
808
    /**
809
     * Computes all the changes that have been done to entities and collections
810
     * since the last commit and stores these changes in the _entityChangeSet map
811
     * temporarily for access by the persisters, until the UoW commit is finished.
812
     *
813
     * @return void
814
     */
815 1088
    public function computeChangeSets()
816
    {
817
        // Compute changes for INSERTed entities first. This must always happen.
818 1088
        $this->computeScheduleInsertsChangeSets();
819
820
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
821 1086
        foreach ($this->identityMap as $className => $entities) {
822 469
            $class = $this->em->getClassMetadata($className);
823
824
            // Skip class if instances are read-only
825 469
            if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
826 1
                continue;
827
            }
828
829
            // If change tracking is explicit or happens through notification, then only compute
830
            // changes on entities of that type that are explicitly marked for synchronization.
831
            switch (true) {
832 468
                case ($class->isChangeTrackingDeferredImplicit()):
833 463
                    $entitiesToProcess = $entities;
834 463
                    break;
835
836 6
                case (isset($this->scheduledForSynchronization[$className])):
837 5
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
838 5
                    break;
839
840
                default:
841 2
                    $entitiesToProcess = [];
842
843
            }
844
845 468
            foreach ($entitiesToProcess as $entity) {
846
                // Ignore uninitialized proxy objects
847 447
                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
848 38
                    continue;
849
                }
850
851
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
852 445
                $oid = spl_object_hash($entity);
853
854 445
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
855 269
                    $this->computeChangeSet($class, $entity);
856
                }
857
            }
858
        }
859 1086
    }
860
861
    /**
862
     * Computes the changes of an association.
863
     *
864
     * @param array $assoc The association mapping.
865
     * @param mixed $value The value of the association.
866
     *
867
     * @throws ORMInvalidArgumentException
868
     * @throws ORMException
869
     *
870
     * @return void
871
     */
872 913
    private function computeAssociationChanges($assoc, $value)
873
    {
874 913
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
875 30
            return;
876
        }
877
878 912
        if ($value instanceof PersistentCollection && $value->isDirty()) {
879 544
            $coid = spl_object_hash($value);
880
881 544
            $this->collectionUpdates[$coid] = $value;
882 544
            $this->visitedCollections[$coid] = $value;
883
        }
884
885
        // Look through the entities, and in any of their associations,
886
        // for transient (new) entities, recursively. ("Persistence by reachability")
887
        // Unwrap. Uninitialized collections will simply be empty.
888 912
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
889 912
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
890
891 912
        foreach ($unwrappedValue as $key => $entry) {
892 752
            if (! ($entry instanceof $targetClass->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
893 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
894
            }
895
896 744
            $state = $this->getEntityState($entry, self::STATE_NEW);
897
898 744
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
899
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
900
            }
901
902
            switch ($state) {
903 744
                case self::STATE_NEW:
904 42
                    if ( ! $assoc['isCascadePersist']) {
905
                        /*
906
                         * For now just record the details, because this may
907
                         * not be an issue if we later discover another pathway
908
                         * through the object-graph where cascade-persistence
909
                         * is enabled for this object.
910
                         */
911 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
912
913 6
                        break;
914
                    }
915
916 37
                    $this->persistNew($targetClass, $entry);
917 37
                    $this->computeChangeSet($targetClass, $entry);
918
919 37
                    break;
920
921 736
                case self::STATE_REMOVED:
922
                    // Consume the $value as array (it's either an array or an ArrayAccess)
923
                    // and remove the element from Collection.
924 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
925 3
                        unset($value[$key]);
926
                    }
927 4
                    break;
928
929 736
                case self::STATE_DETACHED:
930
                    // Can actually not happen right now as we assume STATE_NEW,
931
                    // so the exception will be raised from the DBAL layer (constraint violation).
932
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
933
                    break;
934
935
                default:
936
                    // MANAGED associated entities are already taken into account
937
                    // during changeset calculation anyway, since they are in the identity map.
938
            }
939
        }
940 904
    }
941
942
    /**
943
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
944
     * @param object                              $entity
945
     *
946
     * @return void
947
     */
948 1119
    private function persistNew($class, $entity)
949
    {
950 1119
        $oid    = spl_object_hash($entity);
951 1119
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
952
953 1119
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
954 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
955
        }
956
957 1119
        $idGen = $class->idGenerator;
958
959 1119
        if ( ! $idGen->isPostInsertGenerator()) {
960 287
            $idValue = $idGen->generate($this->em, $entity);
961
962 287
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
963 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
964
965 2
                $class->setIdentifierValues($entity, $idValue);
966
            }
967
968
            // Some identifiers may be foreign keys to new entities.
969
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
970 287
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
971 284
                $this->entityIdentifiers[$oid] = $idValue;
972
            }
973
        }
974
975 1119
        $this->entityStates[$oid] = self::STATE_MANAGED;
976
977 1119
        $this->scheduleForInsert($entity);
978 1119
    }
979
980
    /**
981
     * @param mixed[] $idValue
982
     */
983 287
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
984
    {
985 287
        foreach ($idValue as $idField => $idFieldValue) {
986 287
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
987 3
                return true;
988
            }
989
        }
990
991 284
        return false;
992
    }
993
994
    /**
995
     * INTERNAL:
996
     * Computes the changeset of an individual entity, independently of the
997
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
998
     *
999
     * The passed entity must be a managed entity. If the entity already has a change set
1000
     * because this method is invoked during a commit cycle then the change sets are added.
1001
     * whereby changes detected in this method prevail.
1002
     *
1003
     * @ignore
1004
     *
1005
     * @param ClassMetadata $class  The class descriptor of the entity.
1006
     * @param object        $entity The entity for which to (re)calculate the change set.
1007
     *
1008
     * @return void
1009
     *
1010
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
1011
     */
1012 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
1013
    {
1014 16
        $oid = spl_object_hash($entity);
1015
1016 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
1017
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1018
        }
1019
1020
        // skip if change tracking is "NOTIFY"
1021 16
        if ($class->isChangeTrackingNotify()) {
1022
            return;
1023
        }
1024
1025 16
        if ( ! $class->isInheritanceTypeNone()) {
1026 3
            $class = $this->em->getClassMetadata(get_class($entity));
1027
        }
1028
1029 16
        $actualData = [];
1030
1031 16
        foreach ($class->reflFields as $name => $refProp) {
1032 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1033 16
                && ($name !== $class->versionField)
1034 16
                && ! $class->isCollectionValuedAssociation($name)) {
1035 16
                $actualData[$name] = $refProp->getValue($entity);
1036
            }
1037
        }
1038
1039 16
        if ( ! isset($this->originalEntityData[$oid])) {
1040
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1041
        }
1042
1043 16
        $originalData = $this->originalEntityData[$oid];
1044 16
        $changeSet = [];
1045
1046 16
        foreach ($actualData as $propName => $actualValue) {
1047 16
            $orgValue = $originalData[$propName] ?? null;
1048
1049 16
            if ($orgValue !== $actualValue) {
1050 7
                $changeSet[$propName] = [$orgValue, $actualValue];
1051
            }
1052
        }
1053
1054 16
        if ($changeSet) {
1055 7
            if (isset($this->entityChangeSets[$oid])) {
1056 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1057 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1058 1
                $this->entityChangeSets[$oid] = $changeSet;
1059 1
                $this->entityUpdates[$oid]    = $entity;
1060
            }
1061 7
            $this->originalEntityData[$oid] = $actualData;
1062
        }
1063 16
    }
1064
1065
    /**
1066
     * Executes all entity insertions for entities of the specified type.
1067
     *
1068
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1069
     *
1070
     * @return void
1071
     */
1072 1082
    private function executeInserts($class)
1073
    {
1074 1082
        $entities   = [];
1075 1082
        $className  = $class->name;
1076 1082
        $persister  = $this->getEntityPersister($className);
1077 1082
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1078
1079 1082
        $insertionsForClass = [];
1080
1081 1082
        foreach ($this->entityInsertions as $oid => $entity) {
1082
1083 1082
            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...
1084 909
                continue;
1085
            }
1086
1087 1082
            $insertionsForClass[$oid] = $entity;
1088
1089 1082
            $persister->addInsert($entity);
1090
1091 1082
            unset($this->entityInsertions[$oid]);
1092
1093 1082
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1094 136
                $entities[] = $entity;
1095
            }
1096
        }
1097
1098 1082
        $postInsertIds = $persister->executeInserts();
1099
1100 1082
        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...
1101
            // Persister returned post-insert IDs
1102 976
            foreach ($postInsertIds as $postInsertId) {
1103 976
                $idField = $class->getSingleIdentifierFieldName();
1104 976
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1105
1106 976
                $entity  = $postInsertId['entity'];
1107 976
                $oid     = spl_object_hash($entity);
1108
1109 976
                $class->reflFields[$idField]->setValue($entity, $idValue);
1110
1111 976
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1112 976
                $this->entityStates[$oid] = self::STATE_MANAGED;
1113 976
                $this->originalEntityData[$oid][$idField] = $idValue;
1114
1115 976
                $this->addToIdentityMap($entity);
1116
            }
1117
        } else {
1118 811
            foreach ($insertionsForClass as $oid => $entity) {
1119 270
                if (! isset($this->entityIdentifiers[$oid])) {
1120
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1121
                    //add it now
1122 3
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1123
                }
1124
            }
1125
        }
1126
1127 1082
        foreach ($entities as $entity) {
1128 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1129
        }
1130 1082
    }
1131
1132
    /**
1133
     * @param object $entity
1134
     */
1135 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1136
    {
1137 3
        $identifier = [];
1138
1139 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1140 3
            $value = $class->getFieldValue($entity, $idField);
1141
1142 3
            if (isset($class->associationMappings[$idField])) {
1143
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1144 3
                $value = $this->getSingleIdentifierValue($value);
1145
            }
1146
1147 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1148
        }
1149
1150 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1151 3
        $this->entityIdentifiers[$oid] = $identifier;
1152
1153 3
        $this->addToIdentityMap($entity);
1154 3
    }
1155
1156
    /**
1157
     * Executes all entity updates for entities of the specified type.
1158
     *
1159
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1160
     *
1161
     * @return void
1162
     */
1163 122
    private function executeUpdates($class)
1164
    {
1165 122
        $className          = $class->name;
1166 122
        $persister          = $this->getEntityPersister($className);
1167 122
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1168 122
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1169
1170 122
        foreach ($this->entityUpdates as $oid => $entity) {
1171 122
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1172 78
                continue;
1173
            }
1174
1175 122
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1176 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1177
1178 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1179
            }
1180
1181 122
            if ( ! empty($this->entityChangeSets[$oid])) {
1182 88
                $persister->update($entity);
1183
            }
1184
1185 118
            unset($this->entityUpdates[$oid]);
1186
1187 118
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1188 11
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1189
            }
1190
        }
1191 118
    }
1192
1193
    /**
1194
     * Executes all entity deletions for entities of the specified type.
1195
     *
1196
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1197
     *
1198
     * @return void
1199
     */
1200 65
    private function executeDeletions($class)
1201
    {
1202 65
        $className  = $class->name;
1203 65
        $persister  = $this->getEntityPersister($className);
1204 65
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1205
1206 65
        foreach ($this->entityDeletions as $oid => $entity) {
1207 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...
1208 26
                continue;
1209
            }
1210
1211 65
            $persister->delete($entity);
1212
1213
            unset(
1214 65
                $this->entityDeletions[$oid],
1215 65
                $this->entityIdentifiers[$oid],
1216 65
                $this->originalEntityData[$oid],
1217 65
                $this->entityStates[$oid]
1218
            );
1219
1220
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1221
            // is obtained by a new entity because the old one went out of scope.
1222
            //$this->entityStates[$oid] = self::STATE_NEW;
1223 65
            if ( ! $class->isIdentifierNatural()) {
1224 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1225
            }
1226
1227 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1228 9
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1229
            }
1230
        }
1231 64
    }
1232
1233
    /**
1234
     * Gets the commit order.
1235
     *
1236
     * @param array|null $entityChangeSet
1237
     *
1238
     * @return array
1239
     */
1240 1086
    private function getCommitOrder(array $entityChangeSet = null)
1241
    {
1242 1086
        if ($entityChangeSet === null) {
1243 1086
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1244
        }
1245
1246 1086
        $calc = $this->getCommitOrderCalculator();
1247
1248
        // See if there are any new classes in the changeset, that are not in the
1249
        // commit order graph yet (don't have a node).
1250
        // We have to inspect changeSet to be able to correctly build dependencies.
1251
        // It is not possible to use IdentityMap here because post inserted ids
1252
        // are not yet available.
1253 1086
        $newNodes = [];
1254
1255 1086
        foreach ($entityChangeSet as $entity) {
1256 1086
            $class = $this->em->getClassMetadata(get_class($entity));
1257
1258 1086
            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...
1259 656
                continue;
1260
            }
1261
1262 1086
            $calc->addNode($class->name, $class);
1263
1264 1086
            $newNodes[] = $class;
1265
        }
1266
1267
        // Calculate dependencies for new nodes
1268 1086
        while ($class = array_pop($newNodes)) {
1269 1086
            foreach ($class->associationMappings as $assoc) {
1270 934
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1271 885
                    continue;
1272
                }
1273
1274 881
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1275
1276 881
                if ( ! $calc->hasNode($targetClass->name)) {
1277 672
                    $calc->addNode($targetClass->name, $targetClass);
1278
1279 672
                    $newNodes[] = $targetClass;
1280
                }
1281
1282 881
                $joinColumns = reset($assoc['joinColumns']);
1283
1284 881
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1285
1286
                // If the target class has mapped subclasses, these share the same dependency.
1287 881
                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...
1288 874
                    continue;
1289
                }
1290
1291 239
                foreach ($targetClass->subClasses as $subClassName) {
1292 239
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1293
1294 239
                    if ( ! $calc->hasNode($subClassName)) {
1295 209
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1296
1297 209
                        $newNodes[] = $targetSubClass;
1298
                    }
1299
1300 239
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1301
                }
1302
            }
1303
        }
1304
1305 1086
        return $calc->sort();
1306
    }
1307
1308
    /**
1309
     * Schedules an entity for insertion into the database.
1310
     * If the entity already has an identifier, it will be added to the identity map.
1311
     *
1312
     * @param object $entity The entity to schedule for insertion.
1313
     *
1314
     * @return void
1315
     *
1316
     * @throws ORMInvalidArgumentException
1317
     * @throws \InvalidArgumentException
1318
     */
1319 1120
    public function scheduleForInsert($entity)
1320
    {
1321 1120
        $oid = spl_object_hash($entity);
1322
1323 1120
        if (isset($this->entityUpdates[$oid])) {
1324
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1325
        }
1326
1327 1120
        if (isset($this->entityDeletions[$oid])) {
1328 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1329
        }
1330 1120
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1331 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1332
        }
1333
1334 1120
        if (isset($this->entityInsertions[$oid])) {
1335 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1336
        }
1337
1338 1120
        $this->entityInsertions[$oid] = $entity;
1339
1340 1120
        if (isset($this->entityIdentifiers[$oid])) {
1341 284
            $this->addToIdentityMap($entity);
1342
        }
1343
1344 1120
        if ($entity instanceof NotifyPropertyChanged) {
1345 8
            $entity->addPropertyChangedListener($this);
1346
        }
1347 1120
    }
1348
1349
    /**
1350
     * Checks whether an entity is scheduled for insertion.
1351
     *
1352
     * @param object $entity
1353
     *
1354
     * @return boolean
1355
     */
1356 650
    public function isScheduledForInsert($entity)
1357
    {
1358 650
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1359
    }
1360
1361
    /**
1362
     * Schedules an entity for being updated.
1363
     *
1364
     * @param object $entity The entity to schedule for being updated.
1365
     *
1366
     * @return void
1367
     *
1368
     * @throws ORMInvalidArgumentException
1369
     */
1370 1
    public function scheduleForUpdate($entity)
1371
    {
1372 1
        $oid = spl_object_hash($entity);
1373
1374 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1375
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1376
        }
1377
1378 1
        if (isset($this->entityDeletions[$oid])) {
1379
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1380
        }
1381
1382 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1383 1
            $this->entityUpdates[$oid] = $entity;
1384
        }
1385 1
    }
1386
1387
    /**
1388
     * INTERNAL:
1389
     * Schedules an extra update that will be executed immediately after the
1390
     * regular entity updates within the currently running commit cycle.
1391
     *
1392
     * Extra updates for entities are stored as (entity, changeset) tuples.
1393
     *
1394
     * @ignore
1395
     *
1396
     * @param object $entity    The entity for which to schedule an extra update.
1397
     * @param array  $changeset The changeset of the entity (what to update).
1398
     *
1399
     * @return void
1400
     */
1401 44
    public function scheduleExtraUpdate($entity, array $changeset)
1402
    {
1403 44
        $oid         = spl_object_hash($entity);
1404 44
        $extraUpdate = [$entity, $changeset];
1405
1406 44
        if (isset($this->extraUpdates[$oid])) {
1407 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1408
1409 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1410
        }
1411
1412 44
        $this->extraUpdates[$oid] = $extraUpdate;
1413 44
    }
1414
1415
    /**
1416
     * Checks whether an entity is registered as dirty in the unit of work.
1417
     * Note: Is not very useful currently as dirty entities are only registered
1418
     * at commit time.
1419
     *
1420
     * @param object $entity
1421
     *
1422
     * @return boolean
1423
     */
1424
    public function isScheduledForUpdate($entity)
1425
    {
1426
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1427
    }
1428
1429
    /**
1430
     * Checks whether an entity is registered to be checked in the unit of work.
1431
     *
1432
     * @param object $entity
1433
     *
1434
     * @return boolean
1435
     */
1436 5
    public function isScheduledForDirtyCheck($entity)
1437
    {
1438 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...
1439
1440 5
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1441
    }
1442
1443
    /**
1444
     * INTERNAL:
1445
     * Schedules an entity for deletion.
1446
     *
1447
     * @param object $entity
1448
     *
1449
     * @return void
1450
     */
1451 68
    public function scheduleForDelete($entity)
1452
    {
1453 68
        $oid = spl_object_hash($entity);
1454
1455 68
        if (isset($this->entityInsertions[$oid])) {
1456 1
            if ($this->isInIdentityMap($entity)) {
1457
                $this->removeFromIdentityMap($entity);
1458
            }
1459
1460 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1461
1462 1
            return; // entity has not been persisted yet, so nothing more to do.
1463
        }
1464
1465 68
        if ( ! $this->isInIdentityMap($entity)) {
1466 1
            return;
1467
        }
1468
1469 67
        $this->removeFromIdentityMap($entity);
1470
1471 67
        unset($this->entityUpdates[$oid]);
1472
1473 67
        if ( ! isset($this->entityDeletions[$oid])) {
1474 67
            $this->entityDeletions[$oid] = $entity;
1475 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1476
        }
1477 67
    }
1478
1479
    /**
1480
     * Checks whether an entity is registered as removed/deleted with the unit
1481
     * of work.
1482
     *
1483
     * @param object $entity
1484
     *
1485
     * @return boolean
1486
     */
1487 17
    public function isScheduledForDelete($entity)
1488
    {
1489 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1490
    }
1491
1492
    /**
1493
     * Checks whether an entity is scheduled for insertion, update or deletion.
1494
     *
1495
     * @param object $entity
1496
     *
1497
     * @return boolean
1498
     */
1499
    public function isEntityScheduled($entity)
1500
    {
1501
        $oid = spl_object_hash($entity);
1502
1503
        return isset($this->entityInsertions[$oid])
1504
            || isset($this->entityUpdates[$oid])
1505
            || isset($this->entityDeletions[$oid]);
1506
    }
1507
1508
    /**
1509
     * INTERNAL:
1510
     * Registers an entity in the identity map.
1511
     * Note that entities in a hierarchy are registered with the class name of
1512
     * the root entity.
1513
     *
1514
     * @ignore
1515
     *
1516
     * @param object $entity The entity to register.
1517
     *
1518
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1519
     *                 the entity in question is already managed.
1520
     *
1521
     * @throws ORMInvalidArgumentException
1522
     */
1523 1181
    public function addToIdentityMap($entity)
1524
    {
1525 1181
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1526 1181
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1527
1528 1181
        if (empty($identifier) || in_array(null, $identifier, true)) {
1529 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...
1530
        }
1531
1532 1175
        $idHash    = implode(' ', $identifier);
1533 1175
        $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...
1534
1535 1175
        if (isset($this->identityMap[$className][$idHash])) {
1536 86
            return false;
1537
        }
1538
1539 1175
        $this->identityMap[$className][$idHash] = $entity;
1540
1541 1175
        return true;
1542
    }
1543
1544
    /**
1545
     * Gets the state of an entity with regard to the current unit of work.
1546
     *
1547
     * @param object   $entity
1548
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1549
     *                         This parameter can be set to improve performance of entity state detection
1550
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1551
     *                         is either known or does not matter for the caller of the method.
1552
     *
1553
     * @return int The entity state.
1554
     */
1555 1134
    public function getEntityState($entity, $assume = null)
1556
    {
1557 1134
        $oid = spl_object_hash($entity);
1558
1559 1134
        if (isset($this->entityStates[$oid])) {
1560 818
            return $this->entityStates[$oid];
1561
        }
1562
1563 1128
        if ($assume !== null) {
1564 1124
            return $assume;
1565
        }
1566
1567
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1568
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1569
        // the UoW does not hold references to such objects and the object hash can be reused.
1570
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1571 13
        $class = $this->em->getClassMetadata(get_class($entity));
1572 13
        $id    = $class->getIdentifierValues($entity);
1573
1574 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...
1575 5
            return self::STATE_NEW;
1576
        }
1577
1578 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...
1579 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1580
        }
1581
1582
        switch (true) {
1583 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

1583
            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...
1584
                // Check for a version field, if available, to avoid a db lookup.
1585 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...
1586 1
                    return ($class->getFieldValue($entity, $class->versionField))
0 ignored issues
show
Bug introduced by
The method getFieldValue() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getFieldNames()? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

1994
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1995
        }
1996
1997
        // Mark the managed copy visited as well
1998 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

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

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

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

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

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

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