Failed Conditions
Pull Request — 2.7 (#8065)
by
unknown
06:50
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
                (
1215 53
                    ! method_exists($class->reflFields[$class->identifier[0]], 'getType') ||
1216 65
                    $class->reflFields[$class->identifier[0]]->getType()->allowsNull()
0 ignored issues
show
Bug introduced by
The method getType() does not exist on ReflectionProperty. ( Ignorable by Annotation )

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

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

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

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

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

1577
            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...
1578
                // Check for a version field, if available, to avoid a db lookup.
1579 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...
1580 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

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

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

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

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

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

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

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

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

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

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

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