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

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

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

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

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

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

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

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

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

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

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

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

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

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

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