Failed Conditions
Pull Request — 2.7 (#8065)
by
unknown
07: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 27
CRAP Score 14.0643

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

1215
                $class->reflFields[$class->identifier[0]]->/** @scrutinizer ignore-call */ 
1216
                                                           getType()->allowsNull()) {

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...
1216
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1217
            }
1218
1219 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1220 65
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1221
            }
1222
        }
1223 64
    }
1224
1225
    /**
1226
     * Gets the commit order.
1227
     *
1228
     * @param array|null $entityChangeSet
1229
     *
1230
     * @return array
1231
     */
1232 1085
    private function getCommitOrder(array $entityChangeSet = null)
1233
    {
1234 1085
        if ($entityChangeSet === null) {
1235 1085
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1236
        }
1237
1238 1085
        $calc = $this->getCommitOrderCalculator();
1239
1240
        // See if there are any new classes in the changeset, that are not in the
1241
        // commit order graph yet (don't have a node).
1242
        // We have to inspect changeSet to be able to correctly build dependencies.
1243
        // It is not possible to use IdentityMap here because post inserted ids
1244
        // are not yet available.
1245 1085
        $newNodes = [];
1246
1247 1085
        foreach ($entityChangeSet as $entity) {
1248 1085
            $class = $this->em->getClassMetadata(get_class($entity));
1249
1250 1085
            if ($calc->hasNode($class->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1251 656
                continue;
1252
            }
1253
1254 1085
            $calc->addNode($class->name, $class);
1255
1256 1085
            $newNodes[] = $class;
1257
        }
1258
1259
        // Calculate dependencies for new nodes
1260 1085
        while ($class = array_pop($newNodes)) {
1261 1085
            foreach ($class->associationMappings as $assoc) {
1262 934
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1263 886
                    continue;
1264
                }
1265
1266 881
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1267
1268 881
                if ( ! $calc->hasNode($targetClass->name)) {
1269 672
                    $calc->addNode($targetClass->name, $targetClass);
1270
1271 672
                    $newNodes[] = $targetClass;
1272
                }
1273
1274 881
                $joinColumns = reset($assoc['joinColumns']);
1275
1276 881
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1277
1278
                // If the target class has mapped subclasses, these share the same dependency.
1279 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...
1280 874
                    continue;
1281
                }
1282
1283 239
                foreach ($targetClass->subClasses as $subClassName) {
1284 239
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1285
1286 239
                    if ( ! $calc->hasNode($subClassName)) {
1287 209
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1288
1289 209
                        $newNodes[] = $targetSubClass;
1290
                    }
1291
1292 239
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1293
                }
1294
            }
1295
        }
1296
1297 1085
        return $calc->sort();
1298
    }
1299
1300
    /**
1301
     * Schedules an entity for insertion into the database.
1302
     * If the entity already has an identifier, it will be added to the identity map.
1303
     *
1304
     * @param object $entity The entity to schedule for insertion.
1305
     *
1306
     * @return void
1307
     *
1308
     * @throws ORMInvalidArgumentException
1309
     * @throws \InvalidArgumentException
1310
     */
1311 1119
    public function scheduleForInsert($entity)
1312
    {
1313 1119
        $oid = spl_object_hash($entity);
1314
1315 1119
        if (isset($this->entityUpdates[$oid])) {
1316
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1317
        }
1318
1319 1119
        if (isset($this->entityDeletions[$oid])) {
1320 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1321
        }
1322 1119
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1323 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1324
        }
1325
1326 1119
        if (isset($this->entityInsertions[$oid])) {
1327 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1328
        }
1329
1330 1119
        $this->entityInsertions[$oid] = $entity;
1331
1332 1119
        if (isset($this->entityIdentifiers[$oid])) {
1333 284
            $this->addToIdentityMap($entity);
1334
        }
1335
1336 1119
        if ($entity instanceof NotifyPropertyChanged) {
1337 8
            $entity->addPropertyChangedListener($this);
1338
        }
1339 1119
    }
1340
1341
    /**
1342
     * Checks whether an entity is scheduled for insertion.
1343
     *
1344
     * @param object $entity
1345
     *
1346
     * @return boolean
1347
     */
1348 651
    public function isScheduledForInsert($entity)
1349
    {
1350 651
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1351
    }
1352
1353
    /**
1354
     * Schedules an entity for being updated.
1355
     *
1356
     * @param object $entity The entity to schedule for being updated.
1357
     *
1358
     * @return void
1359
     *
1360
     * @throws ORMInvalidArgumentException
1361
     */
1362 1
    public function scheduleForUpdate($entity)
1363
    {
1364 1
        $oid = spl_object_hash($entity);
1365
1366 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1367
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1368
        }
1369
1370 1
        if (isset($this->entityDeletions[$oid])) {
1371
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1372
        }
1373
1374 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1375 1
            $this->entityUpdates[$oid] = $entity;
1376
        }
1377 1
    }
1378
1379
    /**
1380
     * INTERNAL:
1381
     * Schedules an extra update that will be executed immediately after the
1382
     * regular entity updates within the currently running commit cycle.
1383
     *
1384
     * Extra updates for entities are stored as (entity, changeset) tuples.
1385
     *
1386
     * @ignore
1387
     *
1388
     * @param object $entity    The entity for which to schedule an extra update.
1389
     * @param array  $changeset The changeset of the entity (what to update).
1390
     *
1391
     * @return void
1392
     */
1393 44
    public function scheduleExtraUpdate($entity, array $changeset)
1394
    {
1395 44
        $oid         = spl_object_hash($entity);
1396 44
        $extraUpdate = [$entity, $changeset];
1397
1398 44
        if (isset($this->extraUpdates[$oid])) {
1399 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1400
1401 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1402
        }
1403
1404 44
        $this->extraUpdates[$oid] = $extraUpdate;
1405 44
    }
1406
1407
    /**
1408
     * Checks whether an entity is registered as dirty in the unit of work.
1409
     * Note: Is not very useful currently as dirty entities are only registered
1410
     * at commit time.
1411
     *
1412
     * @param object $entity
1413
     *
1414
     * @return boolean
1415
     */
1416
    public function isScheduledForUpdate($entity)
1417
    {
1418
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1419
    }
1420
1421
    /**
1422
     * Checks whether an entity is registered to be checked in the unit of work.
1423
     *
1424
     * @param object $entity
1425
     *
1426
     * @return boolean
1427
     */
1428 5
    public function isScheduledForDirtyCheck($entity)
1429
    {
1430 5
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1431
1432 5
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1433
    }
1434
1435
    /**
1436
     * INTERNAL:
1437
     * Schedules an entity for deletion.
1438
     *
1439
     * @param object $entity
1440
     *
1441
     * @return void
1442
     */
1443 68
    public function scheduleForDelete($entity)
1444
    {
1445 68
        $oid = spl_object_hash($entity);
1446
1447 68
        if (isset($this->entityInsertions[$oid])) {
1448 1
            if ($this->isInIdentityMap($entity)) {
1449
                $this->removeFromIdentityMap($entity);
1450
            }
1451
1452 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1453
1454 1
            return; // entity has not been persisted yet, so nothing more to do.
1455
        }
1456
1457 68
        if ( ! $this->isInIdentityMap($entity)) {
1458 1
            return;
1459
        }
1460
1461 67
        $this->removeFromIdentityMap($entity);
1462
1463 67
        unset($this->entityUpdates[$oid]);
1464
1465 67
        if ( ! isset($this->entityDeletions[$oid])) {
1466 67
            $this->entityDeletions[$oid] = $entity;
1467 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1468
        }
1469 67
    }
1470
1471
    /**
1472
     * Checks whether an entity is registered as removed/deleted with the unit
1473
     * of work.
1474
     *
1475
     * @param object $entity
1476
     *
1477
     * @return boolean
1478
     */
1479 17
    public function isScheduledForDelete($entity)
1480
    {
1481 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1482
    }
1483
1484
    /**
1485
     * Checks whether an entity is scheduled for insertion, update or deletion.
1486
     *
1487
     * @param object $entity
1488
     *
1489
     * @return boolean
1490
     */
1491
    public function isEntityScheduled($entity)
1492
    {
1493
        $oid = spl_object_hash($entity);
1494
1495
        return isset($this->entityInsertions[$oid])
1496
            || isset($this->entityUpdates[$oid])
1497
            || isset($this->entityDeletions[$oid]);
1498
    }
1499
1500
    /**
1501
     * INTERNAL:
1502
     * Registers an entity in the identity map.
1503
     * Note that entities in a hierarchy are registered with the class name of
1504
     * the root entity.
1505
     *
1506
     * @ignore
1507
     *
1508
     * @param object $entity The entity to register.
1509
     *
1510
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1511
     *                 the entity in question is already managed.
1512
     *
1513
     * @throws ORMInvalidArgumentException
1514
     */
1515 1180
    public function addToIdentityMap($entity)
1516
    {
1517 1180
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1518 1180
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1519
1520 1180
        if (empty($identifier) || in_array(null, $identifier, true)) {
1521 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1522
        }
1523
1524 1174
        $idHash    = implode(' ', $identifier);
1525 1174
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1526
1527 1174
        if (isset($this->identityMap[$className][$idHash])) {
1528 86
            return false;
1529
        }
1530
1531 1174
        $this->identityMap[$className][$idHash] = $entity;
1532
1533 1174
        return true;
1534
    }
1535
1536
    /**
1537
     * Gets the state of an entity with regard to the current unit of work.
1538
     *
1539
     * @param object   $entity
1540
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1541
     *                         This parameter can be set to improve performance of entity state detection
1542
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1543
     *                         is either known or does not matter for the caller of the method.
1544
     *
1545
     * @return int The entity state.
1546
     */
1547 1133
    public function getEntityState($entity, $assume = null)
1548
    {
1549 1133
        $oid = spl_object_hash($entity);
1550
1551 1133
        if (isset($this->entityStates[$oid])) {
1552 819
            return $this->entityStates[$oid];
1553
        }
1554
1555 1127
        if ($assume !== null) {
1556 1123
            return $assume;
1557
        }
1558
1559
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1560
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1561
        // the UoW does not hold references to such objects and the object hash can be reused.
1562
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1563 13
        $class = $this->em->getClassMetadata(get_class($entity));
1564 13
        $id    = $class->getIdentifierValues($entity);
1565
1566 13
        if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
2899 934
            return;
2900
        }
2901
2902
        // avoid infinite recursion
2903 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2904 7
        $this->eagerLoadingEntities = [];
2905
2906 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2907 7
            if ( ! $ids) {
2908
                continue;
2909
            }
2910
2911 7
            $class = $this->em->getClassMetadata($entityName);
2912
2913 7
            $this->getEntityPersister($entityName)->loadAll(
2914 7
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->id...ay(array_values($ids))) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...ityPersister::loadAll() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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