Failed Conditions
Pull Request — 2.6 (#7857)
by
unknown
08:35
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
50
/**
51
 * The UnitOfWork is responsible for tracking changes to objects during an
52
 * "object-level" transaction and for writing out changes to the database
53
 * in the correct order.
54
 *
55
 * Internal note: This class contains highly performance-sensitive code.
56
 *
57
 * @since       2.0
58
 * @author      Benjamin Eberlei <[email protected]>
59
 * @author      Guilherme Blanco <[email protected]>
60
 * @author      Jonathan Wage <[email protected]>
61
 * @author      Roman Borschel <[email protected]>
62
 * @author      Rob Caiger <[email protected]>
63
 */
64
class UnitOfWork implements PropertyChangedListener
65
{
66
    /**
67
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
68
     */
69
    const STATE_MANAGED = 1;
70
71
    /**
72
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
73
     * and is not (yet) managed by an EntityManager.
74
     */
75
    const STATE_NEW = 2;
76
77
    /**
78
     * A detached entity is an instance with persistent state and identity that is not
79
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
80
     */
81
    const STATE_DETACHED = 3;
82
83
    /**
84
     * A removed entity instance is an instance with a persistent identity,
85
     * associated with an EntityManager, whose persistent state will be deleted
86
     * on commit.
87
     */
88
    const STATE_REMOVED = 4;
89
90
    /**
91
     * Hint used to collect all primary keys of associated entities during hydration
92
     * and execute it in a dedicated query afterwards
93
     * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
94
     */
95
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
96
97
    /**
98
     * The identity map that holds references to all managed entities that have
99
     * an identity. The entities are grouped by their class name.
100
     * Since all classes in a hierarchy must share the same identifier set,
101
     * we always take the root class name of the hierarchy.
102
     *
103
     * @var array
104
     */
105
    private $identityMap = [];
106
107
    /**
108
     * Map of all identifiers of managed entities.
109
     * Keys are object ids (spl_object_hash).
110
     *
111
     * @var array
112
     */
113
    private $entityIdentifiers = [];
114
115
    /**
116
     * Map of the original entity data of managed entities.
117
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
118
     * at commit time.
119
     *
120
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
121
     *                A value will only really be copied if the value in the entity is modified
122
     *                by the user.
123
     *
124
     * @var array
125
     */
126
    private $originalEntityData = [];
127
128
    /**
129
     * Map of entity changes. Keys are object ids (spl_object_hash).
130
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
131
     *
132
     * @var array
133
     */
134
    private $entityChangeSets = [];
135
136
    /**
137
     * The (cached) states of any known entities.
138
     * Keys are object ids (spl_object_hash).
139
     *
140
     * @var array
141
     */
142
    private $entityStates = [];
143
144
    /**
145
     * Map of entities that are scheduled for dirty checking at commit time.
146
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
147
     * Keys are object ids (spl_object_hash).
148
     *
149
     * @var array
150
     */
151
    private $scheduledForSynchronization = [];
152
153
    /**
154
     * A list of all pending entity insertions.
155
     *
156
     * @var array
157
     */
158
    private $entityInsertions = [];
159
160
    /**
161
     * A list of all pending entity updates.
162
     *
163
     * @var array
164
     */
165
    private $entityUpdates = [];
166
167
    /**
168
     * Any pending extra updates that have been scheduled by persisters.
169
     *
170
     * @var array
171
     */
172
    private $extraUpdates = [];
173
174
    /**
175
     * A list of all pending entity deletions.
176
     *
177
     * @var array
178
     */
179
    private $entityDeletions = [];
180
181
    /**
182
     * New entities that were discovered through relationships that were not
183
     * marked as cascade-persist. During flush, this array is populated and
184
     * then pruned of any entities that were discovered through a valid
185
     * cascade-persist path. (Leftovers cause an error.)
186
     *
187
     * Keys are OIDs, payload is a two-item array describing the association
188
     * and the entity.
189
     *
190
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
191
     */
192
    private $nonCascadedNewDetectedEntities = [];
193
194
    /**
195
     * All pending collection deletions.
196
     *
197
     * @var array
198
     */
199
    private $collectionDeletions = [];
200
201
    /**
202
     * All pending collection updates.
203
     *
204
     * @var array
205
     */
206
    private $collectionUpdates = [];
207
208
    /**
209
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
210
     * At the end of the UnitOfWork all these collections will make new snapshots
211
     * of their data.
212
     *
213
     * @var array
214
     */
215
    private $visitedCollections = [];
216
217
    /**
218
     * The EntityManager that "owns" this UnitOfWork instance.
219
     *
220
     * @var EntityManagerInterface
221
     */
222
    private $em;
223
224
    /**
225
     * The entity persister instances used to persist entity instances.
226
     *
227
     * @var array
228
     */
229
    private $persisters = [];
230
231
    /**
232
     * The collection persister instances used to persist collections.
233
     *
234
     * @var array
235
     */
236
    private $collectionPersisters = [];
237
238
    /**
239
     * The EventManager used for dispatching events.
240
     *
241
     * @var \Doctrine\Common\EventManager
242
     */
243
    private $evm;
244
245
    /**
246
     * The ListenersInvoker used for dispatching events.
247
     *
248
     * @var \Doctrine\ORM\Event\ListenersInvoker
249
     */
250
    private $listenersInvoker;
251
252
    /**
253
     * The IdentifierFlattener used for manipulating identifiers
254
     *
255
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
256
     */
257
    private $identifierFlattener;
258
259
    /**
260
     * Orphaned entities that are scheduled for removal.
261
     *
262
     * @var array
263
     */
264
    private $orphanRemovals = [];
265
266
    /**
267
     * Read-Only objects are never evaluated
268
     *
269
     * @var array
270
     */
271
    private $readOnlyObjects = [];
272
273
    /**
274
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
275
     *
276
     * @var array
277
     */
278
    private $eagerLoadingEntities = [];
279
280
    /**
281
     * @var boolean
282
     */
283
    protected $hasCache = false;
284
285
    /**
286
     * Helper for handling completion of hydration
287
     *
288
     * @var HydrationCompleteHandler
289
     */
290
    private $hydrationCompleteHandler;
291
292
    /**
293
     * @var ReflectionPropertiesGetter
294
     */
295
    private $reflectionPropertiesGetter;
296
297
    /**
298
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
299
     *
300
     * @param EntityManagerInterface $em
301
     */
302 2495
    public function __construct(EntityManagerInterface $em)
303
    {
304 2495
        $this->em                         = $em;
305 2495
        $this->evm                        = $em->getEventManager();
306 2495
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2495
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2495
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2495
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2495
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2495
    }
312
313
    /**
314
     * Commits the UnitOfWork, executing all operations that have been postponed
315
     * up to this point. The state of all managed entities will be synchronized with
316
     * the database.
317
     *
318
     * The operations are executed in the following order:
319
     *
320
     * 1) All entity insertions
321
     * 2) All entity updates
322
     * 3) All collection deletions
323
     * 4) All collection updates
324
     * 5) All entity deletions
325
     *
326
     * @param null|object|array $entity
327
     *
328
     * @return void
329
     *
330
     * @throws \Exception
331
     */
332 1098
    public function commit($entity = null)
333
    {
334
        // Raise preFlush
335 1098
        if ($this->evm->hasListeners(Events::preFlush)) {
336 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
337
        }
338
339
        // Compute changes done since last commit.
340 1098
        if (null === $entity) {
341 1088
            $this->computeChangeSets();
342 19
        } elseif (is_object($entity)) {
343 17
            $this->computeSingleEntityChangeSet($entity);
344 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
345 2
            foreach ($entity as $object) {
346 2
                $this->computeSingleEntityChangeSet($object);
347
            }
348
        }
349
350 1095
        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...
351 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...
352 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...
353 43
                $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...
354 39
                $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...
355 1095
                $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...
356 27
            $this->dispatchOnFlushEvent();
357 27
            $this->dispatchPostFlushEvent();
358
359 27
            $this->postCommitCleanup($entity);
360
361 27
            return; // Nothing to do.
362
        }
363
364 1091
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
365
366 1089
        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...
367 16
            foreach ($this->orphanRemovals as $orphan) {
368 16
                $this->remove($orphan);
369
            }
370
        }
371
372 1089
        $this->dispatchOnFlushEvent();
373
374
        // Now we need a commit order to maintain referential integrity
375 1089
        $commitOrder = $this->getCommitOrder();
376
377 1089
        $conn = $this->em->getConnection();
378 1089
        $conn->beginTransaction();
379
380
        try {
381
            // Collection deletions (deletions of complete collections)
382 1089
            foreach ($this->collectionDeletions as $collectionToDelete) {
383 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
384
            }
385
386 1089
            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...
387 1085
                foreach ($commitOrder as $class) {
388 1085
                    $this->executeInserts($class);
389
                }
390
            }
391
392 1088
            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...
393 123
                foreach ($commitOrder as $class) {
394 123
                    $this->executeUpdates($class);
395
                }
396
            }
397
398
            // Extra updates that were requested by persisters.
399 1084
            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...
400 44
                $this->executeExtraUpdates();
401
            }
402
403
            // Collection updates (deleteRows, updateRows, insertRows)
404 1084
            foreach ($this->collectionUpdates as $collectionToUpdate) {
405 550
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
406
            }
407
408
            // Entity deletions come last and need to be in reverse commit order
409 1084
            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...
410 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...
411 65
                    $this->executeDeletions($commitOrder[$i]);
412
                }
413
            }
414
415 1084
            $conn->commit();
416 11
        } catch (Throwable $e) {
417 11
            $this->em->close();
418 11
            $conn->rollBack();
419
420 11
            $this->afterTransactionRolledBack();
421
422 11
            throw $e;
423
        }
424
425 1084
        $this->afterTransactionComplete();
426
427
        // Take new snapshots from visited collections
428 1084
        foreach ($this->visitedCollections as $coll) {
429 549
            $coll->takeSnapshot();
430
        }
431
432 1084
        $this->dispatchPostFlushEvent();
433
434 1083
        $this->postCommitCleanup($entity);
435 1083
    }
436
437
    /**
438
     * @param null|object|object[] $entity
439
     */
440 1087
    private function postCommitCleanup($entity) : void
441
    {
442 1087
        $this->entityInsertions =
443 1087
        $this->entityUpdates =
444 1087
        $this->entityDeletions =
445 1087
        $this->extraUpdates =
446 1087
        $this->collectionUpdates =
447 1087
        $this->nonCascadedNewDetectedEntities =
448 1087
        $this->collectionDeletions =
449 1087
        $this->visitedCollections =
450 1087
        $this->orphanRemovals = [];
451
452 1087
        if (null === $entity) {
453 1078
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
454
455 1078
            return;
456
        }
457
458 17
        $entities = \is_object($entity)
459 15
            ? [$entity]
460 17
            : $entity;
461
462 17
        foreach ($entities as $object) {
463 17
            $oid = \spl_object_hash($object);
464
465 17
            $this->clearEntityChangeSet($oid);
466
467 17
            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...
468
        }
469 17
    }
470
471
    /**
472
     * Computes the changesets of all entities scheduled for insertion.
473
     *
474
     * @return void
475
     */
476 1097
    private function computeScheduleInsertsChangeSets()
477
    {
478 1097
        foreach ($this->entityInsertions as $entity) {
479 1089
            $class = $this->em->getClassMetadata(get_class($entity));
480
481 1089
            $this->computeChangeSet($class, $entity);
482
        }
483 1095
    }
484
485
    /**
486
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
487
     *
488
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
489
     * 2. Read Only entities are skipped.
490
     * 3. Proxies are skipped.
491
     * 4. Only if entity is properly managed.
492
     *
493
     * @param object $entity
494
     *
495
     * @return void
496
     *
497
     * @throws \InvalidArgumentException
498
     */
499 19
    private function computeSingleEntityChangeSet($entity)
500
    {
501 19
        $state = $this->getEntityState($entity);
502
503 19
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
504 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
505
        }
506
507 18
        $class = $this->em->getClassMetadata(get_class($entity));
508
509 18
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
510 17
            $this->persist($entity);
511
        }
512
513
        // Compute changes for INSERTed entities first. This must always happen even in this case.
514 18
        $this->computeScheduleInsertsChangeSets();
515
516 18
        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...
517
            return;
518
        }
519
520
        // Ignore uninitialized proxy objects
521 18
        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...
522 2
            return;
523
        }
524
525
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
526 16
        $oid = spl_object_hash($entity);
527
528 16
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
529 7
            $this->computeChangeSet($class, $entity);
530
        }
531 16
    }
532
533
    /**
534
     * Executes any extra updates that have been scheduled.
535
     */
536 44
    private function executeExtraUpdates()
537
    {
538 44
        foreach ($this->extraUpdates as $oid => $update) {
539 44
            list ($entity, $changeset) = $update;
540
541 44
            $this->entityChangeSets[$oid] = $changeset;
542 44
            $this->getEntityPersister(get_class($entity))->update($entity);
543
        }
544
545 44
        $this->extraUpdates = [];
546 44
    }
547
548
    /**
549
     * Gets the changeset for an entity.
550
     *
551
     * @param object $entity
552
     *
553
     * @return array
554
     */
555
    public function & getEntityChangeSet($entity)
556
    {
557 1084
        $oid  = spl_object_hash($entity);
558 1084
        $data = [];
559
560 1084
        if (!isset($this->entityChangeSets[$oid])) {
561 4
            return $data;
562
        }
563
564 1084
        return $this->entityChangeSets[$oid];
565
    }
566
567
    /**
568
     * Computes the changes that happened to a single entity.
569
     *
570
     * Modifies/populates the following properties:
571
     *
572
     * {@link _originalEntityData}
573
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
574
     * then it was not fetched from the database and therefore we have no original
575
     * entity data yet. All of the current entity data is stored as the original entity data.
576
     *
577
     * {@link _entityChangeSets}
578
     * The changes detected on all properties of the entity are stored there.
579
     * A change is a tuple array where the first entry is the old value and the second
580
     * entry is the new value of the property. Changesets are used by persisters
581
     * to INSERT/UPDATE the persistent entity state.
582
     *
583
     * {@link _entityUpdates}
584
     * If the entity is already fully MANAGED (has been fetched from the database before)
585
     * and any changes to its properties are detected, then a reference to the entity is stored
586
     * there to mark it for an update.
587
     *
588
     * {@link _collectionDeletions}
589
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
590
     * then this collection is marked for deletion.
591
     *
592
     * @ignore
593
     *
594
     * @internal Don't call from the outside.
595
     *
596
     * @param ClassMetadata $class  The class descriptor of the entity.
597
     * @param object        $entity The entity for which to compute the changes.
598
     *
599
     * @return void
600
     */
601 1099
    public function computeChangeSet(ClassMetadata $class, $entity)
602
    {
603 1099
        $oid = spl_object_hash($entity);
604
605 1099
        if (isset($this->readOnlyObjects[$oid])) {
606 2
            return;
607
        }
608
609 1099
        if ( ! $class->isInheritanceTypeNone()) {
610 338
            $class = $this->em->getClassMetadata(get_class($entity));
611
        }
612
613 1099
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
614
615 1099
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
616 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
617
        }
618
619 1099
        $actualData = [];
620
621 1099
        foreach ($class->reflFields as $name => $refProp) {
622 1099
            if (method_exists($refProp, 'isInitialized') && $refProp->getDeclaringClass()->isInstance($entity)) {
623
                $value = $refProp->isInitialized($entity) ? $refProp->getValue($entity) : null;
624
            } else {
625 1099
                $value = $refProp->getValue($entity);
626
            }
627
628 1099
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
629 822
                if ($value instanceof PersistentCollection) {
630 205
                    if ($value->getOwner() === $entity) {
631 205
                        continue;
632
                    }
633
634 5
                    $value = new ArrayCollection($value->getValues());
635
                }
636
637
                // If $value is not a Collection then use an ArrayCollection.
638 817
                if ( ! $value instanceof Collection) {
639 248
                    $value = new ArrayCollection($value);
640
                }
641
642 817
                $assoc = $class->associationMappings[$name];
643
644
                // Inject PersistentCollection
645 817
                $value = new PersistentCollection(
646 817
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
647
                );
648 817
                $value->setOwner($entity, $assoc);
649 817
                $value->setDirty( ! $value->isEmpty());
650
651 817
                $class->reflFields[$name]->setValue($entity, $value);
652
653 817
                $actualData[$name] = $value;
654
655 817
                continue;
656
            }
657
658 1099
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
659 1099
                $actualData[$name] = $value;
660
            }
661
        }
662
663 1099
        if ( ! isset($this->originalEntityData[$oid])) {
664
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
665
            // These result in an INSERT.
666 1095
            $this->originalEntityData[$oid] = $actualData;
667 1095
            $changeSet = [];
668
669 1095
            foreach ($actualData as $propName => $actualValue) {
670 1071
                if ( ! isset($class->associationMappings[$propName])) {
671 1013
                    $changeSet[$propName] = [null, $actualValue];
672
673 1013
                    continue;
674
                }
675
676 952
                $assoc = $class->associationMappings[$propName];
677
678 952
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
679 952
                    $changeSet[$propName] = [null, $actualValue];
680
                }
681
            }
682
683 1095
            $this->entityChangeSets[$oid] = $changeSet;
684
        } else {
685
            // Entity is "fully" MANAGED: it was already fully persisted before
686
            // and we have a copy of the original data
687 278
            $originalData           = $this->originalEntityData[$oid];
688 278
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
689 278
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
690
                ? $this->entityChangeSets[$oid]
691 278
                : [];
692
693 278
            foreach ($actualData as $propName => $actualValue) {
694
                // skip field, its a partially omitted one!
695 261
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
696 8
                    continue;
697
                }
698
699 261
                $orgValue = $originalData[$propName];
700
701
                // skip if value haven't changed
702 261
                if ($orgValue === $actualValue) {
703 244
                    continue;
704
                }
705
706
                // if regular field
707 119
                if ( ! isset($class->associationMappings[$propName])) {
708 64
                    if ($isChangeTrackingNotify) {
709
                        continue;
710
                    }
711
712 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
713
714 64
                    continue;
715
                }
716
717 59
                $assoc = $class->associationMappings[$propName];
718
719
                // Persistent collection was exchanged with the "originally"
720
                // created one. This can only mean it was cloned and replaced
721
                // on another entity.
722 59
                if ($actualValue instanceof PersistentCollection) {
723 8
                    $owner = $actualValue->getOwner();
724 8
                    if ($owner === null) { // cloned
725
                        $actualValue->setOwner($entity, $assoc);
726 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
727
                        if (!$actualValue->isInitialized()) {
728
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
729
                        }
730
                        $newValue = clone $actualValue;
731
                        $newValue->setOwner($entity, $assoc);
732
                        $class->reflFields[$propName]->setValue($entity, $newValue);
733
                    }
734
                }
735
736 59
                if ($orgValue instanceof PersistentCollection) {
737
                    // A PersistentCollection was de-referenced, so delete it.
738 8
                    $coid = spl_object_hash($orgValue);
739
740 8
                    if (isset($this->collectionDeletions[$coid])) {
741
                        continue;
742
                    }
743
744 8
                    $this->collectionDeletions[$coid] = $orgValue;
745 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
746
747 8
                    continue;
748
                }
749
750 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
751 50
                    if ($assoc['isOwningSide']) {
752 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
753
                    }
754
755 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
756 51
                        $this->scheduleOrphanRemoval($orgValue);
757
                    }
758
                }
759
            }
760
761 278
            if ($changeSet) {
762 92
                $this->entityChangeSets[$oid]   = $changeSet;
763 92
                $this->originalEntityData[$oid] = $actualData;
764 92
                $this->entityUpdates[$oid]      = $entity;
765
            }
766
        }
767
768
        // Look for changes in associations of the entity
769 1099
        foreach ($class->associationMappings as $field => $assoc) {
770 952
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
771 663
                continue;
772
            }
773
774 923
            $this->computeAssociationChanges($assoc, $val);
775
776 915
            if ( ! isset($this->entityChangeSets[$oid]) &&
777 915
                $assoc['isOwningSide'] &&
778 915
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
779 915
                $val instanceof PersistentCollection &&
780 915
                $val->isDirty()) {
781
782 35
                $this->entityChangeSets[$oid]   = [];
783 35
                $this->originalEntityData[$oid] = $actualData;
784 915
                $this->entityUpdates[$oid]      = $entity;
785
            }
786
        }
787 1091
    }
788
789
    /**
790
     * Computes all the changes that have been done to entities and collections
791
     * since the last commit and stores these changes in the _entityChangeSet map
792
     * temporarily for access by the persisters, until the UoW commit is finished.
793
     *
794
     * @return void
795
     */
796 1088
    public function computeChangeSets()
797
    {
798
        // Compute changes for INSERTed entities first. This must always happen.
799 1088
        $this->computeScheduleInsertsChangeSets();
800
801
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
802 1086
        foreach ($this->identityMap as $className => $entities) {
803 477
            $class = $this->em->getClassMetadata($className);
804
805
            // Skip class if instances are read-only
806 477
            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...
807 1
                continue;
808
            }
809
810
            // If change tracking is explicit or happens through notification, then only compute
811
            // changes on entities of that type that are explicitly marked for synchronization.
812
            switch (true) {
813 476
                case ($class->isChangeTrackingDeferredImplicit()):
814 472
                    $entitiesToProcess = $entities;
815 472
                    break;
816
817 5
                case (isset($this->scheduledForSynchronization[$className])):
818 4
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
819 4
                    break;
820
821
                default:
822 2
                    $entitiesToProcess = [];
823
824
            }
825
826 476
            foreach ($entitiesToProcess as $entity) {
827
                // Ignore uninitialized proxy objects
828 455
                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...
829 37
                    continue;
830
                }
831
832
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
833 454
                $oid = spl_object_hash($entity);
834
835 454
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
836 476
                    $this->computeChangeSet($class, $entity);
837
                }
838
            }
839
        }
840 1086
    }
841
842
    /**
843
     * Computes the changes of an association.
844
     *
845
     * @param array $assoc The association mapping.
846
     * @param mixed $value The value of the association.
847
     *
848
     * @throws ORMInvalidArgumentException
849
     * @throws ORMException
850
     *
851
     * @return void
852
     */
853 923
    private function computeAssociationChanges($assoc, $value)
854
    {
855 923
        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...
856 30
            return;
857
        }
858
859 922
        if ($value instanceof PersistentCollection && $value->isDirty()) {
860 554
            $coid = spl_object_hash($value);
861
862 554
            $this->collectionUpdates[$coid] = $value;
863 554
            $this->visitedCollections[$coid] = $value;
864
        }
865
866
        // Look through the entities, and in any of their associations,
867
        // for transient (new) entities, recursively. ("Persistence by reachability")
868
        // Unwrap. Uninitialized collections will simply be empty.
869 922
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
870 922
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
871
872 922
        foreach ($unwrappedValue as $key => $entry) {
873 762
            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...
874 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
875
            }
876
877 754
            $state = $this->getEntityState($entry, self::STATE_NEW);
878
879 754
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
880
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
881
            }
882
883
            switch ($state) {
884 754
                case self::STATE_NEW:
885 42
                    if ( ! $assoc['isCascadePersist']) {
886
                        /*
887
                         * For now just record the details, because this may
888
                         * not be an issue if we later discover another pathway
889
                         * through the object-graph where cascade-persistence
890
                         * is enabled for this object.
891
                         */
892 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
893
894 6
                        break;
895
                    }
896
897 37
                    $this->persistNew($targetClass, $entry);
898 37
                    $this->computeChangeSet($targetClass, $entry);
899
900 37
                    break;
901
902 746
                case self::STATE_REMOVED:
903
                    // Consume the $value as array (it's either an array or an ArrayAccess)
904
                    // and remove the element from Collection.
905 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
906 3
                        unset($value[$key]);
907
                    }
908 4
                    break;
909
910 746
                case self::STATE_DETACHED:
911
                    // Can actually not happen right now as we assume STATE_NEW,
912
                    // so the exception will be raised from the DBAL layer (constraint violation).
913
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
914
                    break;
915
916 754
                default:
917
                    // MANAGED associated entities are already taken into account
918
                    // during changeset calculation anyway, since they are in the identity map.
919
            }
920
        }
921 914
    }
922
923
    /**
924
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
925
     * @param object                              $entity
926
     *
927
     * @return void
928
     */
929 1118
    private function persistNew($class, $entity)
930
    {
931 1118
        $oid    = spl_object_hash($entity);
932 1118
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
933
934 1118
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
935 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
936
        }
937
938 1118
        $idGen = $class->idGenerator;
939
940 1118
        if ( ! $idGen->isPostInsertGenerator()) {
941 293
            $idValue = $idGen->generate($this->em, $entity);
942
943 293
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
944 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
945
946 2
                $class->setIdentifierValues($entity, $idValue);
947
            }
948
949
            // Some identifiers may be foreign keys to new entities.
950
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
951 293
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
952 290
                $this->entityIdentifiers[$oid] = $idValue;
953
            }
954
        }
955
956 1118
        $this->entityStates[$oid] = self::STATE_MANAGED;
957
958 1118
        $this->scheduleForInsert($entity);
959 1118
    }
960
961
    /**
962
     * @param mixed[] $idValue
963
     */
964 293
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
965
    {
966 293
        foreach ($idValue as $idField => $idFieldValue) {
967 293
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
968 293
                return true;
969
            }
970
        }
971
972 290
        return false;
973
    }
974
975
    /**
976
     * INTERNAL:
977
     * Computes the changeset of an individual entity, independently of the
978
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
979
     *
980
     * The passed entity must be a managed entity. If the entity already has a change set
981
     * because this method is invoked during a commit cycle then the change sets are added.
982
     * whereby changes detected in this method prevail.
983
     *
984
     * @ignore
985
     *
986
     * @param ClassMetadata $class  The class descriptor of the entity.
987
     * @param object        $entity The entity for which to (re)calculate the change set.
988
     *
989
     * @return void
990
     *
991
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
992
     */
993 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
994
    {
995 16
        $oid = spl_object_hash($entity);
996
997 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
998
            throw ORMInvalidArgumentException::entityNotManaged($entity);
999
        }
1000
1001
        // skip if change tracking is "NOTIFY"
1002 16
        if ($class->isChangeTrackingNotify()) {
1003
            return;
1004
        }
1005
1006 16
        if ( ! $class->isInheritanceTypeNone()) {
1007 3
            $class = $this->em->getClassMetadata(get_class($entity));
1008
        }
1009
1010 16
        $actualData = [];
1011
1012 16
        foreach ($class->reflFields as $name => $refProp) {
1013 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1014 16
                && ($name !== $class->versionField)
1015 16
                && ! $class->isCollectionValuedAssociation($name)) {
1016 16
                $actualData[$name] = $refProp->getValue($entity);
1017
            }
1018
        }
1019
1020 16
        if ( ! isset($this->originalEntityData[$oid])) {
1021
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1022
        }
1023
1024 16
        $originalData = $this->originalEntityData[$oid];
1025 16
        $changeSet = [];
1026
1027 16
        foreach ($actualData as $propName => $actualValue) {
1028 16
            $orgValue = $originalData[$propName] ?? null;
1029
1030 16
            if ($orgValue !== $actualValue) {
1031 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1032
            }
1033
        }
1034
1035 16
        if ($changeSet) {
1036 7
            if (isset($this->entityChangeSets[$oid])) {
1037 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1038 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1039 1
                $this->entityChangeSets[$oid] = $changeSet;
1040 1
                $this->entityUpdates[$oid]    = $entity;
1041
            }
1042 7
            $this->originalEntityData[$oid] = $actualData;
1043
        }
1044 16
    }
1045
1046
    /**
1047
     * Executes all entity insertions for entities of the specified type.
1048
     *
1049
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1050
     *
1051
     * @return void
1052
     */
1053 1085
    private function executeInserts($class)
1054
    {
1055 1085
        $entities   = [];
1056 1085
        $className  = $class->name;
1057 1085
        $persister  = $this->getEntityPersister($className);
1058 1085
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1059
1060 1085
        $insertionsForClass = [];
1061
1062 1085
        foreach ($this->entityInsertions as $oid => $entity) {
1063
1064 1085
            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...
1065 915
                continue;
1066
            }
1067
1068 1085
            $insertionsForClass[$oid] = $entity;
1069
1070 1085
            $persister->addInsert($entity);
1071
1072 1085
            unset($this->entityInsertions[$oid]);
1073
1074 1085
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1075 1085
                $entities[] = $entity;
1076
            }
1077
        }
1078
1079 1085
        $postInsertIds = $persister->executeInserts();
1080
1081 1085
        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...
1082
            // Persister returned post-insert IDs
1083 981
            foreach ($postInsertIds as $postInsertId) {
1084 981
                $idField = $class->getSingleIdentifierFieldName();
1085 981
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1086
1087 981
                $entity  = $postInsertId['entity'];
1088 981
                $oid     = spl_object_hash($entity);
1089
1090 981
                $class->reflFields[$idField]->setValue($entity, $idValue);
1091
1092 981
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1093 981
                $this->entityStates[$oid] = self::STATE_MANAGED;
1094 981
                $this->originalEntityData[$oid][$idField] = $idValue;
1095
1096 981
                $this->addToIdentityMap($entity);
1097
            }
1098
        } else {
1099 818
            foreach ($insertionsForClass as $oid => $entity) {
1100 280
                if (! isset($this->entityIdentifiers[$oid])) {
1101
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1102
                    //add it now
1103 280
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1104
                }
1105
            }
1106
        }
1107
1108 1085
        foreach ($entities as $entity) {
1109 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1110
        }
1111 1085
    }
1112
1113
    /**
1114
     * @param object $entity
1115
     */
1116 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1117
    {
1118 3
        $identifier = [];
1119
1120 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1121 3
            $value = $class->getFieldValue($entity, $idField);
1122
1123 3
            if (isset($class->associationMappings[$idField])) {
1124
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1125 3
                $value = $this->getSingleIdentifierValue($value);
1126
            }
1127
1128 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1129
        }
1130
1131 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1132 3
        $this->entityIdentifiers[$oid] = $identifier;
1133
1134 3
        $this->addToIdentityMap($entity);
1135 3
    }
1136
1137
    /**
1138
     * Executes all entity updates for entities of the specified type.
1139
     *
1140
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1141
     *
1142
     * @return void
1143
     */
1144 123
    private function executeUpdates($class)
1145
    {
1146 123
        $className          = $class->name;
1147 123
        $persister          = $this->getEntityPersister($className);
1148 123
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1149 123
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1150
1151 123
        foreach ($this->entityUpdates as $oid => $entity) {
1152 123
            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...
1153 79
                continue;
1154
            }
1155
1156 123
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1157 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1158
1159 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1160
            }
1161
1162 123
            if ( ! empty($this->entityChangeSets[$oid])) {
1163 89
                $persister->update($entity);
1164
            }
1165
1166 119
            unset($this->entityUpdates[$oid]);
1167
1168 119
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1169 119
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1170
            }
1171
        }
1172 119
    }
1173
1174
    /**
1175
     * Executes all entity deletions for entities of the specified type.
1176
     *
1177
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1178
     *
1179
     * @return void
1180
     */
1181 65
    private function executeDeletions($class)
1182
    {
1183 65
        $className  = $class->name;
1184 65
        $persister  = $this->getEntityPersister($className);
1185 65
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1186
1187 65
        foreach ($this->entityDeletions as $oid => $entity) {
1188 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...
1189 26
                continue;
1190
            }
1191
1192 65
            $persister->delete($entity);
1193
1194
            unset(
1195 65
                $this->entityDeletions[$oid],
1196 65
                $this->entityIdentifiers[$oid],
1197 65
                $this->originalEntityData[$oid],
1198 65
                $this->entityStates[$oid]
1199
            );
1200
1201
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1202
            // is obtained by a new entity because the old one went out of scope.
1203
            //$this->entityStates[$oid] = self::STATE_NEW;
1204 65
            if ( ! $class->isIdentifierNatural()) {
1205 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1206
            }
1207
1208 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1209 65
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1210
            }
1211
        }
1212 64
    }
1213
1214
    /**
1215
     * Gets the commit order.
1216
     *
1217
     * @param array|null $entityChangeSet
1218
     *
1219
     * @return array
1220
     */
1221 1089
    private function getCommitOrder(array $entityChangeSet = null)
1222
    {
1223 1089
        if ($entityChangeSet === null) {
1224 1089
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1225
        }
1226
1227 1089
        $calc = $this->getCommitOrderCalculator();
1228
1229
        // See if there are any new classes in the changeset, that are not in the
1230
        // commit order graph yet (don't have a node).
1231
        // We have to inspect changeSet to be able to correctly build dependencies.
1232
        // It is not possible to use IdentityMap here because post inserted ids
1233
        // are not yet available.
1234 1089
        $newNodes = [];
1235
1236 1089
        foreach ($entityChangeSet as $entity) {
1237 1089
            $class = $this->em->getClassMetadata(get_class($entity));
1238
1239 1089
            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...
1240 665
                continue;
1241
            }
1242
1243 1089
            $calc->addNode($class->name, $class);
1244
1245 1089
            $newNodes[] = $class;
1246
        }
1247
1248
        // Calculate dependencies for new nodes
1249 1089
        while ($class = array_pop($newNodes)) {
1250 1089
            foreach ($class->associationMappings as $assoc) {
1251 941
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1252 893
                    continue;
1253
                }
1254
1255 889
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1256
1257 889
                if ( ! $calc->hasNode($targetClass->name)) {
1258 681
                    $calc->addNode($targetClass->name, $targetClass);
1259
1260 681
                    $newNodes[] = $targetClass;
1261
                }
1262
1263 889
                $joinColumns = reset($assoc['joinColumns']);
1264
1265 889
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1266
1267
                // If the target class has mapped subclasses, these share the same dependency.
1268 889
                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...
1269 882
                    continue;
1270
                }
1271
1272 238
                foreach ($targetClass->subClasses as $subClassName) {
1273 238
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1274
1275 238
                    if ( ! $calc->hasNode($subClassName)) {
1276 208
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1277
1278 208
                        $newNodes[] = $targetSubClass;
1279
                    }
1280
1281 238
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1282
                }
1283
            }
1284
        }
1285
1286 1089
        return $calc->sort();
1287
    }
1288
1289
    /**
1290
     * Schedules an entity for insertion into the database.
1291
     * If the entity already has an identifier, it will be added to the identity map.
1292
     *
1293
     * @param object $entity The entity to schedule for insertion.
1294
     *
1295
     * @return void
1296
     *
1297
     * @throws ORMInvalidArgumentException
1298
     * @throws \InvalidArgumentException
1299
     */
1300 1119
    public function scheduleForInsert($entity)
1301
    {
1302 1119
        $oid = spl_object_hash($entity);
1303
1304 1119
        if (isset($this->entityUpdates[$oid])) {
1305
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1306
        }
1307
1308 1119
        if (isset($this->entityDeletions[$oid])) {
1309 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1310
        }
1311 1119
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1312 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1313
        }
1314
1315 1119
        if (isset($this->entityInsertions[$oid])) {
1316 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1317
        }
1318
1319 1119
        $this->entityInsertions[$oid] = $entity;
1320
1321 1119
        if (isset($this->entityIdentifiers[$oid])) {
1322 290
            $this->addToIdentityMap($entity);
1323
        }
1324
1325 1119
        if ($entity instanceof NotifyPropertyChanged) {
1326 8
            $entity->addPropertyChangedListener($this);
1327
        }
1328 1119
    }
1329
1330
    /**
1331
     * Checks whether an entity is scheduled for insertion.
1332
     *
1333
     * @param object $entity
1334
     *
1335
     * @return boolean
1336
     */
1337 661
    public function isScheduledForInsert($entity)
1338
    {
1339 661
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1340
    }
1341
1342
    /**
1343
     * Schedules an entity for being updated.
1344
     *
1345
     * @param object $entity The entity to schedule for being updated.
1346
     *
1347
     * @return void
1348
     *
1349
     * @throws ORMInvalidArgumentException
1350
     */
1351 1
    public function scheduleForUpdate($entity)
1352
    {
1353 1
        $oid = spl_object_hash($entity);
1354
1355 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1356
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1357
        }
1358
1359 1
        if (isset($this->entityDeletions[$oid])) {
1360
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1361
        }
1362
1363 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1364 1
            $this->entityUpdates[$oid] = $entity;
1365
        }
1366 1
    }
1367
1368
    /**
1369
     * INTERNAL:
1370
     * Schedules an extra update that will be executed immediately after the
1371
     * regular entity updates within the currently running commit cycle.
1372
     *
1373
     * Extra updates for entities are stored as (entity, changeset) tuples.
1374
     *
1375
     * @ignore
1376
     *
1377
     * @param object $entity    The entity for which to schedule an extra update.
1378
     * @param array  $changeset The changeset of the entity (what to update).
1379
     *
1380
     * @return void
1381
     */
1382 44
    public function scheduleExtraUpdate($entity, array $changeset)
1383
    {
1384 44
        $oid         = spl_object_hash($entity);
1385 44
        $extraUpdate = [$entity, $changeset];
1386
1387 44
        if (isset($this->extraUpdates[$oid])) {
1388 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1389
1390 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1391
        }
1392
1393 44
        $this->extraUpdates[$oid] = $extraUpdate;
1394 44
    }
1395
1396
    /**
1397
     * Checks whether an entity is registered as dirty in the unit of work.
1398
     * Note: Is not very useful currently as dirty entities are only registered
1399
     * at commit time.
1400
     *
1401
     * @param object $entity
1402
     *
1403
     * @return boolean
1404
     */
1405
    public function isScheduledForUpdate($entity)
1406
    {
1407
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1408
    }
1409
1410
    /**
1411
     * Checks whether an entity is registered to be checked in the unit of work.
1412
     *
1413
     * @param object $entity
1414
     *
1415
     * @return boolean
1416
     */
1417 3
    public function isScheduledForDirtyCheck($entity)
1418
    {
1419 3
        $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...
1420
1421 3
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1422
    }
1423
1424
    /**
1425
     * INTERNAL:
1426
     * Schedules an entity for deletion.
1427
     *
1428
     * @param object $entity
1429
     *
1430
     * @return void
1431
     */
1432 68
    public function scheduleForDelete($entity)
1433
    {
1434 68
        $oid = spl_object_hash($entity);
1435
1436 68
        if (isset($this->entityInsertions[$oid])) {
1437 1
            if ($this->isInIdentityMap($entity)) {
1438
                $this->removeFromIdentityMap($entity);
1439
            }
1440
1441 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1442
1443 1
            return; // entity has not been persisted yet, so nothing more to do.
1444
        }
1445
1446 68
        if ( ! $this->isInIdentityMap($entity)) {
1447 1
            return;
1448
        }
1449
1450 67
        $this->removeFromIdentityMap($entity);
1451
1452 67
        unset($this->entityUpdates[$oid]);
1453
1454 67
        if ( ! isset($this->entityDeletions[$oid])) {
1455 67
            $this->entityDeletions[$oid] = $entity;
1456 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1457
        }
1458 67
    }
1459
1460
    /**
1461
     * Checks whether an entity is registered as removed/deleted with the unit
1462
     * of work.
1463
     *
1464
     * @param object $entity
1465
     *
1466
     * @return boolean
1467
     */
1468 17
    public function isScheduledForDelete($entity)
1469
    {
1470 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1471
    }
1472
1473
    /**
1474
     * Checks whether an entity is scheduled for insertion, update or deletion.
1475
     *
1476
     * @param object $entity
1477
     *
1478
     * @return boolean
1479
     */
1480
    public function isEntityScheduled($entity)
1481
    {
1482
        $oid = spl_object_hash($entity);
1483
1484
        return isset($this->entityInsertions[$oid])
1485
            || isset($this->entityUpdates[$oid])
1486
            || isset($this->entityDeletions[$oid]);
1487
    }
1488
1489
    /**
1490
     * INTERNAL:
1491
     * Registers an entity in the identity map.
1492
     * Note that entities in a hierarchy are registered with the class name of
1493
     * the root entity.
1494
     *
1495
     * @ignore
1496
     *
1497
     * @param object $entity The entity to register.
1498
     *
1499
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1500
     *                 the entity in question is already managed.
1501
     *
1502
     * @throws ORMInvalidArgumentException
1503
     */
1504 1183
    public function addToIdentityMap($entity)
1505
    {
1506 1183
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1507 1183
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1508
1509 1183
        if (empty($identifier) || in_array(null, $identifier, true)) {
1510 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...
1511
        }
1512
1513 1177
        $idHash    = implode(' ', $identifier);
1514 1177
        $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...
1515
1516 1177
        if (isset($this->identityMap[$className][$idHash])) {
1517 87
            return false;
1518
        }
1519
1520 1177
        $this->identityMap[$className][$idHash] = $entity;
1521
1522 1177
        return true;
1523
    }
1524
1525
    /**
1526
     * Gets the state of an entity with regard to the current unit of work.
1527
     *
1528
     * @param object   $entity
1529
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1530
     *                         This parameter can be set to improve performance of entity state detection
1531
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1532
     *                         is either known or does not matter for the caller of the method.
1533
     *
1534
     * @return int The entity state.
1535
     */
1536 1133
    public function getEntityState($entity, $assume = null)
1537
    {
1538 1133
        $oid = spl_object_hash($entity);
1539
1540 1133
        if (isset($this->entityStates[$oid])) {
1541 827
            return $this->entityStates[$oid];
1542
        }
1543
1544 1127
        if ($assume !== null) {
1545 1123
            return $assume;
1546
        }
1547
1548
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1549
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1550
        // the UoW does not hold references to such objects and the object hash can be reused.
1551
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1552 13
        $class = $this->em->getClassMetadata(get_class($entity));
1553 13
        $id    = $class->getIdentifierValues($entity);
1554
1555 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...
1556 5
            return self::STATE_NEW;
1557
        }
1558
1559 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...
1560 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1561
        }
1562
1563
        switch (true) {
1564 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

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

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

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

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

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

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

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

1939
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, "merge");
Loading history...
1940
                    }
1941
                } else {
1942
                    // We need to fetch the managed copy in order to merge.
1943 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...
1944
                }
1945
1946 37
                if ($managedCopy === null) {
1947
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1948
                    // since the managed entity was not found.
1949 3
                    if ( ! $class->isIdentifierNatural()) {
1950 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1951 1
                            $class->getName(),
1952 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1953
                        );
1954
                    }
1955
1956 2
                    $managedCopy = $this->newInstance($class);
1957 2
                    $class->setIdentifierValues($managedCopy, $id);
1958
1959 2
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1960 2
                    $this->persistNew($class, $managedCopy);
1961
                } else {
1962 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

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

1963
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1964
                }
1965
            }
1966
1967 40
            $visited[$oid] = $managedCopy; // mark visited
1968
1969 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1970
                $this->scheduleForDirtyCheck($entity);
1971
            }
1972
        }
1973
1974 41
        if ($prevManagedCopy !== null) {
1975 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

1975
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1976
        }
1977
1978
        // Mark the managed copy visited as well
1979 41
        $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

1979
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
1980
1981 41
        $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

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

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

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

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

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