Failed Conditions
Pull Request — 2.7 (#8065)
by
unknown
12:21
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 53
                if (method_exists($class->reflFields[$class->identifier[0]], 'getType') &&
1215 53
                    $class->reflFields[$class->identifier[0]]->getType() &&
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() &&

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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