Completed
Pull Request — 2.7 (#8020)
by
unknown
12:25 queued 06:21
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 14.0643

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\NotifyPropertyChanged;
25
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
26
use Doctrine\Common\Persistence\ObjectManagerAware;
27
use Doctrine\Common\PropertyChangedListener;
28
use Doctrine\DBAL\LockMode;
29
use Doctrine\ORM\Cache\Persister\CachedPersister;
30
use Doctrine\ORM\Event\LifecycleEventArgs;
31
use Doctrine\ORM\Event\ListenersInvoker;
32
use Doctrine\ORM\Event\OnFlushEventArgs;
33
use Doctrine\ORM\Event\PostFlushEventArgs;
34
use Doctrine\ORM\Event\PreFlushEventArgs;
35
use Doctrine\ORM\Event\PreUpdateEventArgs;
36
use Doctrine\ORM\Internal\HydrationCompleteHandler;
37
use Doctrine\ORM\Mapping\ClassMetadata;
38
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
39
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
40
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
41
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
42
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
43
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
44
use Doctrine\ORM\Proxy\Proxy;
45
use Doctrine\ORM\Utility\IdentifierFlattener;
46
use InvalidArgumentException;
47
use Throwable;
48
use UnexpectedValueException;
49
use function get_class;
50
51
/**
52
 * The UnitOfWork is responsible for tracking changes to objects during an
53
 * "object-level" transaction and for writing out changes to the database
54
 * in the correct order.
55
 *
56
 * Internal note: This class contains highly performance-sensitive code.
57
 *
58
 * @since       2.0
59
 * @author      Benjamin Eberlei <[email protected]>
60
 * @author      Guilherme Blanco <[email protected]>
61
 * @author      Jonathan Wage <[email protected]>
62
 * @author      Roman Borschel <[email protected]>
63
 * @author      Rob Caiger <[email protected]>
64
 */
65
class UnitOfWork implements PropertyChangedListener
66
{
67
    /**
68
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
69
     */
70
    const STATE_MANAGED = 1;
71
72
    /**
73
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
74
     * and is not (yet) managed by an EntityManager.
75
     */
76
    const STATE_NEW = 2;
77
78
    /**
79
     * A detached entity is an instance with persistent state and identity that is not
80
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
81
     */
82
    const STATE_DETACHED = 3;
83
84
    /**
85
     * A removed entity instance is an instance with a persistent identity,
86
     * associated with an EntityManager, whose persistent state will be deleted
87
     * on commit.
88
     */
89
    const STATE_REMOVED = 4;
90
91
    /**
92
     * Hint used to collect all primary keys of associated entities during hydration
93
     * and execute it in a dedicated query afterwards
94
     * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
95
     */
96
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
97
98
    /**
99
     * The identity map that holds references to all managed entities that have
100
     * an identity. The entities are grouped by their class name.
101
     * Since all classes in a hierarchy must share the same identifier set,
102
     * we always take the root class name of the hierarchy.
103
     *
104
     * @var array
105
     */
106
    private $identityMap = [];
107
108
    /**
109
     * Map of all identifiers of managed entities.
110
     * Keys are object ids (spl_object_hash).
111
     *
112
     * @var array
113
     */
114
    private $entityIdentifiers = [];
115
116
    /**
117
     * Map of the original entity data of managed entities.
118
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
119
     * at commit time.
120
     *
121
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
122
     *                A value will only really be copied if the value in the entity is modified
123
     *                by the user.
124
     *
125
     * @var array
126
     */
127
    private $originalEntityData = [];
128
129
    /**
130
     * Map of entity changes. Keys are object ids (spl_object_hash).
131
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
132
     *
133
     * @var array
134
     */
135
    private $entityChangeSets = [];
136
137
    /**
138
     * The (cached) states of any known entities.
139
     * Keys are object ids (spl_object_hash).
140
     *
141
     * @var array
142
     */
143
    private $entityStates = [];
144
145
    /**
146
     * Map of entities that are scheduled for dirty checking at commit time.
147
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
148
     * Keys are object ids (spl_object_hash).
149
     *
150
     * @var array
151
     */
152
    private $scheduledForSynchronization = [];
153
154
    /**
155
     * A list of all pending entity insertions.
156
     *
157
     * @var array
158
     */
159
    private $entityInsertions = [];
160
161
    /**
162
     * A list of all pending entity updates.
163
     *
164
     * @var array
165
     */
166
    private $entityUpdates = [];
167
168
    /**
169
     * Any pending extra updates that have been scheduled by persisters.
170
     *
171
     * @var array
172
     */
173
    private $extraUpdates = [];
174
175
    /**
176
     * A list of all pending entity deletions.
177
     *
178
     * @var array
179
     */
180
    private $entityDeletions = [];
181
182
    /**
183
     * New entities that were discovered through relationships that were not
184
     * marked as cascade-persist. During flush, this array is populated and
185
     * then pruned of any entities that were discovered through a valid
186
     * cascade-persist path. (Leftovers cause an error.)
187
     *
188
     * Keys are OIDs, payload is a two-item array describing the association
189
     * and the entity.
190
     *
191
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
192
     */
193
    private $nonCascadedNewDetectedEntities = [];
194
195
    /**
196
     * All pending collection deletions.
197
     *
198
     * @var array
199
     */
200
    private $collectionDeletions = [];
201
202
    /**
203
     * All pending collection updates.
204
     *
205
     * @var array
206
     */
207
    private $collectionUpdates = [];
208
209
    /**
210
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
211
     * At the end of the UnitOfWork all these collections will make new snapshots
212
     * of their data.
213
     *
214
     * @var array
215
     */
216
    private $visitedCollections = [];
217
218
    /**
219
     * The EntityManager that "owns" this UnitOfWork instance.
220
     *
221
     * @var EntityManagerInterface
222
     */
223
    private $em;
224
225
    /**
226
     * The entity persister instances used to persist entity instances.
227
     *
228
     * @var array
229
     */
230
    private $persisters = [];
231
232
    /**
233
     * The collection persister instances used to persist collections.
234
     *
235
     * @var array
236
     */
237
    private $collectionPersisters = [];
238
239
    /**
240
     * The EventManager used for dispatching events.
241
     *
242
     * @var \Doctrine\Common\EventManager
243
     */
244
    private $evm;
245
246
    /**
247
     * The ListenersInvoker used for dispatching events.
248
     *
249
     * @var \Doctrine\ORM\Event\ListenersInvoker
250
     */
251
    private $listenersInvoker;
252
253
    /**
254
     * The IdentifierFlattener used for manipulating identifiers
255
     *
256
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
257
     */
258
    private $identifierFlattener;
259
260
    /**
261
     * Orphaned entities that are scheduled for removal.
262
     *
263
     * @var array
264
     */
265
    private $orphanRemovals = [];
266
267
    /**
268
     * Read-Only objects are never evaluated
269
     *
270
     * @var array
271
     */
272
    private $readOnlyObjects = [];
273
274
    /**
275
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
276
     *
277
     * @var array
278
     */
279
    private $eagerLoadingEntities = [];
280
281
    /**
282
     * @var boolean
283
     */
284
    protected $hasCache = false;
285
286
    /**
287
     * Helper for handling completion of hydration
288
     *
289
     * @var HydrationCompleteHandler
290
     */
291
    private $hydrationCompleteHandler;
292
293
    /**
294
     * @var ReflectionPropertiesGetter
295
     */
296
    private $reflectionPropertiesGetter;
297
298
    /**
299
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
300
     *
301
     * @param EntityManagerInterface $em
302
     */
303 2504
    public function __construct(EntityManagerInterface $em)
304
    {
305 2504
        $this->em                         = $em;
306 2504
        $this->evm                        = $em->getEventManager();
307 2504
        $this->listenersInvoker           = new ListenersInvoker($em);
308 2504
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
309 2504
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
310 2504
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
311 2504
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
312 2504
    }
313
314
    /**
315
     * @return IdentifierFlattener
316
     */
317 1189
    public function getIdentifierFlattener() : IdentifierFlattener
318
    {
319 1189
        return $this->identifierFlattener;
320
    }
321
322
    /**
323
     * Commits the UnitOfWork, executing all operations that have been postponed
324
     * up to this point. The state of all managed entities will be synchronized with
325
     * the database.
326
     *
327
     * The operations are executed in the following order:
328
     *
329
     * 1) All entity insertions
330
     * 2) All entity updates
331
     * 3) All collection deletions
332
     * 4) All collection updates
333
     * 5) All entity deletions
334
     *
335
     * @param null|object|array $entity
336
     *
337
     * @return void
338
     *
339
     * @throws \Exception
340
     */
341 1093
    public function commit($entity = null)
342
    {
343
        // Raise preFlush
344 1093
        if ($this->evm->hasListeners(Events::preFlush)) {
345 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
346
        }
347
348
        // Compute changes done since last commit.
349 1093
        if (null === $entity) {
350 1086
            $this->computeChangeSets();
351 14
        } elseif (is_object($entity)) {
352 12
            $this->computeSingleEntityChangeSet($entity);
353 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
354 2
            foreach ($entity as $object) {
355 2
                $this->computeSingleEntityChangeSet($object);
356
            }
357
        }
358
359 1090
        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...
360 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...
361 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...
362 44
                $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
363 40
                $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
364 1090
                $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...
365 27
            $this->dispatchOnFlushEvent();
366 27
            $this->dispatchPostFlushEvent();
367
368 27
            $this->postCommitCleanup($entity);
369
370 27
            return; // Nothing to do.
371
        }
372
373 1086
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
374
375 1084
        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...
376 16
            foreach ($this->orphanRemovals as $orphan) {
377 16
                $this->remove($orphan);
378
            }
379
        }
380
381 1084
        $this->dispatchOnFlushEvent();
382
383
        // Now we need a commit order to maintain referential integrity
384 1084
        $commitOrder = $this->getCommitOrder();
385
386 1084
        $conn = $this->em->getConnection();
387 1084
        $conn->beginTransaction();
388
389
        try {
390
            // Collection deletions (deletions of complete collections)
391 1084
            foreach ($this->collectionDeletions as $collectionToDelete) {
392 21
                if (! $collectionToDelete instanceof PersistentCollection) {
393
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
394
395
                    continue;
396
                }
397
398
                // Deferred explicit tracked collections can be removed only when owning relation was persisted
399 21
                $owner = $collectionToDelete->getOwner();
400
401 21
                if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
402 21
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
403
                }
404
            }
405
406 1084
            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...
407 1080
                foreach ($commitOrder as $class) {
408 1080
                    $this->executeInserts($class);
409
                }
410
            }
411
412 1083
            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...
413 122
                foreach ($commitOrder as $class) {
414 122
                    $this->executeUpdates($class);
415
                }
416
            }
417
418
            // Extra updates that were requested by persisters.
419 1079
            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...
420 44
                $this->executeExtraUpdates();
421
            }
422
423
            // Collection updates (deleteRows, updateRows, insertRows)
424 1079
            foreach ($this->collectionUpdates as $collectionToUpdate) {
425 540
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
426
            }
427
428
            // Entity deletions come last and need to be in reverse commit order
429 1079
            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...
430 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...
431 65
                    $this->executeDeletions($commitOrder[$i]);
432
                }
433
            }
434
435 1079
            $conn->commit();
436 11
        } catch (Throwable $e) {
437 11
            $this->em->close();
438 11
            $conn->rollBack();
439
440 11
            $this->afterTransactionRolledBack();
441
442 11
            throw $e;
443
        }
444
445 1079
        $this->afterTransactionComplete();
446
447
        // Take new snapshots from visited collections
448 1079
        foreach ($this->visitedCollections as $coll) {
449 539
            $coll->takeSnapshot();
450
        }
451
452 1079
        $this->dispatchPostFlushEvent();
453
454 1078
        $this->postCommitCleanup($entity);
455 1078
    }
456
457
    /**
458
     * @param null|object|object[] $entity
459
     */
460 1082
    private function postCommitCleanup($entity) : void
461
    {
462 1082
        $this->entityInsertions =
463 1082
        $this->entityUpdates =
464 1082
        $this->entityDeletions =
465 1082
        $this->extraUpdates =
466 1082
        $this->collectionUpdates =
467 1082
        $this->nonCascadedNewDetectedEntities =
468 1082
        $this->collectionDeletions =
469 1082
        $this->visitedCollections =
470 1082
        $this->orphanRemovals = [];
471
472 1082
        if (null === $entity) {
473 1076
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
474
475 1076
            return;
476
        }
477
478 12
        $entities = \is_object($entity)
479 10
            ? [$entity]
480 12
            : $entity;
481
482 12
        foreach ($entities as $object) {
483 12
            $oid = \spl_object_hash($object);
484
485 12
            $this->clearEntityChangeSet($oid);
486
487 12
            unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
488
        }
489 12
    }
490
491
    /**
492
     * Computes the changesets of all entities scheduled for insertion.
493
     *
494
     * @return void
495
     */
496 1092
    private function computeScheduleInsertsChangeSets()
497
    {
498 1092
        foreach ($this->entityInsertions as $entity) {
499 1084
            $class = $this->em->getClassMetadata(get_class($entity));
500
501 1084
            $this->computeChangeSet($class, $entity);
502
        }
503 1090
    }
504
505
    /**
506
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
507
     *
508
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
509
     * 2. Read Only entities are skipped.
510
     * 3. Proxies are skipped.
511
     * 4. Only if entity is properly managed.
512
     *
513
     * @param object $entity
514
     *
515
     * @return void
516
     *
517
     * @throws \InvalidArgumentException
518
     */
519 14
    private function computeSingleEntityChangeSet($entity)
520
    {
521 14
        $state = $this->getEntityState($entity);
522
523 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
524 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
525
        }
526
527 13
        $class = $this->em->getClassMetadata(get_class($entity));
528
529 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
530 13
            $this->persist($entity);
531
        }
532
533
        // Compute changes for INSERTed entities first. This must always happen even in this case.
534 13
        $this->computeScheduleInsertsChangeSets();
535
536 13
        if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
537
            return;
538
        }
539
540
        // Ignore uninitialized proxy objects
541 13
        if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
542 1
            return;
543
        }
544
545
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
546 12
        $oid = spl_object_hash($entity);
547
548 12
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
549 7
            $this->computeChangeSet($class, $entity);
550
        }
551 12
    }
552
553
    /**
554
     * Executes any extra updates that have been scheduled.
555
     */
556 44
    private function executeExtraUpdates()
557
    {
558 44
        foreach ($this->extraUpdates as $oid => $update) {
559 44
            list ($entity, $changeset) = $update;
560
561 44
            $this->entityChangeSets[$oid] = $changeset;
562 44
            $this->getEntityPersister(get_class($entity))->update($entity);
563
        }
564
565 44
        $this->extraUpdates = [];
566 44
    }
567
568
    /**
569
     * Gets the changeset for an entity.
570
     *
571
     * @param object $entity
572
     *
573
     * @return array
574
     */
575
    public function & getEntityChangeSet($entity)
576
    {
577 1079
        $oid  = spl_object_hash($entity);
578 1079
        $data = [];
579
580 1079
        if (!isset($this->entityChangeSets[$oid])) {
581 4
            return $data;
582
        }
583
584 1079
        return $this->entityChangeSets[$oid];
585
    }
586
587
    /**
588
     * Computes the changes that happened to a single entity.
589
     *
590
     * Modifies/populates the following properties:
591
     *
592
     * {@link _originalEntityData}
593
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
594
     * then it was not fetched from the database and therefore we have no original
595
     * entity data yet. All of the current entity data is stored as the original entity data.
596
     *
597
     * {@link _entityChangeSets}
598
     * The changes detected on all properties of the entity are stored there.
599
     * A change is a tuple array where the first entry is the old value and the second
600
     * entry is the new value of the property. Changesets are used by persisters
601
     * to INSERT/UPDATE the persistent entity state.
602
     *
603
     * {@link _entityUpdates}
604
     * If the entity is already fully MANAGED (has been fetched from the database before)
605
     * and any changes to its properties are detected, then a reference to the entity is stored
606
     * there to mark it for an update.
607
     *
608
     * {@link _collectionDeletions}
609
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
610
     * then this collection is marked for deletion.
611
     *
612
     * @ignore
613
     *
614
     * @internal Don't call from the outside.
615
     *
616
     * @param ClassMetadata $class  The class descriptor of the entity.
617
     * @param object        $entity The entity for which to compute the changes.
618
     *
619
     * @return void
620
     */
621 1094
    public function computeChangeSet(ClassMetadata $class, $entity)
622
    {
623 1094
        $oid = spl_object_hash($entity);
624
625 1094
        if (isset($this->readOnlyObjects[$oid])) {
626 2
            return;
627
        }
628
629 1094
        if ( ! $class->isInheritanceTypeNone()) {
630 327
            $class = $this->em->getClassMetadata(get_class($entity));
631
        }
632
633 1094
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
634
635 1094
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
636 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
637
        }
638
639 1094
        $actualData = [];
640
641 1094
        foreach ($class->reflFields as $name => $refProp) {
642 1094
            $value = $refProp->getValue($entity);
643
644 1094
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
645 812
                if ($value instanceof PersistentCollection) {
646 203
                    if ($value->getOwner() === $entity) {
647 203
                        continue;
648
                    }
649
650 5
                    $value = new ArrayCollection($value->getValues());
651
                }
652
653
                // If $value is not a Collection then use an ArrayCollection.
654 807
                if ( ! $value instanceof Collection) {
655 236
                    $value = new ArrayCollection($value);
656
                }
657
658 807
                $assoc = $class->associationMappings[$name];
659
660
                // Inject PersistentCollection
661 807
                $value = new PersistentCollection(
662 807
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
663
                );
664 807
                $value->setOwner($entity, $assoc);
665 807
                $value->setDirty( ! $value->isEmpty());
666
667 807
                $class->reflFields[$name]->setValue($entity, $value);
668
669 807
                $actualData[$name] = $value;
670
671 807
                continue;
672
            }
673
674 1094
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
675 1094
                $actualData[$name] = $value;
676
            }
677
        }
678
679 1094
        if ( ! isset($this->originalEntityData[$oid])) {
680
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
681
            // These result in an INSERT.
682 1090
            $this->originalEntityData[$oid] = $actualData;
683 1090
            $changeSet = [];
684
685 1090
            foreach ($actualData as $propName => $actualValue) {
686 1066
                if ( ! isset($class->associationMappings[$propName])) {
687 1007
                    $changeSet[$propName] = [null, $actualValue];
688
689 1007
                    continue;
690
                }
691
692 944
                $assoc = $class->associationMappings[$propName];
693
694 944
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
695 944
                    $changeSet[$propName] = [null, $actualValue];
696
                }
697
            }
698
699 1090
            $this->entityChangeSets[$oid] = $changeSet;
700
        } else {
701
            // Entity is "fully" MANAGED: it was already fully persisted before
702
            // and we have a copy of the original data
703 276
            $originalData           = $this->originalEntityData[$oid];
704 276
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
705 276
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
706
                ? $this->entityChangeSets[$oid]
707 276
                : [];
708
709 276
            foreach ($actualData as $propName => $actualValue) {
710
                // skip field, its a partially omitted one!
711 259
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
712 8
                    continue;
713
                }
714
715 259
                $orgValue = $originalData[$propName];
716
717
                // skip if value haven't changed
718 259
                if ($orgValue === $actualValue) {
719 243
                    continue;
720
                }
721
722
                // if regular field
723 118
                if ( ! isset($class->associationMappings[$propName])) {
724 64
                    if ($isChangeTrackingNotify) {
725
                        continue;
726
                    }
727
728 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
729
730 64
                    continue;
731
                }
732
733 58
                $assoc = $class->associationMappings[$propName];
734
735
                // Persistent collection was exchanged with the "originally"
736
                // created one. This can only mean it was cloned and replaced
737
                // on another entity.
738 58
                if ($actualValue instanceof PersistentCollection) {
739 8
                    $owner = $actualValue->getOwner();
740 8
                    if ($owner === null) { // cloned
741
                        $actualValue->setOwner($entity, $assoc);
742 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
743
                        if (!$actualValue->isInitialized()) {
744
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
745
                        }
746
                        $newValue = clone $actualValue;
747
                        $newValue->setOwner($entity, $assoc);
748
                        $class->reflFields[$propName]->setValue($entity, $newValue);
749
                    }
750
                }
751
752 58
                if ($orgValue instanceof PersistentCollection) {
753
                    // A PersistentCollection was de-referenced, so delete it.
754 8
                    $coid = spl_object_hash($orgValue);
755
756 8
                    if (isset($this->collectionDeletions[$coid])) {
757
                        continue;
758
                    }
759
760 8
                    $this->collectionDeletions[$coid] = $orgValue;
761 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
762
763 8
                    continue;
764
                }
765
766 50
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
767 49
                    if ($assoc['isOwningSide']) {
768 21
                        $changeSet[$propName] = [$orgValue, $actualValue];
769
                    }
770
771 49
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
772 50
                        $this->scheduleOrphanRemoval($orgValue);
773
                    }
774
                }
775
            }
776
777 276
            if ($changeSet) {
778 91
                $this->entityChangeSets[$oid]   = $changeSet;
779 91
                $this->originalEntityData[$oid] = $actualData;
780 91
                $this->entityUpdates[$oid]      = $entity;
781
            }
782
        }
783
784
        // Look for changes in associations of the entity
785 1094
        foreach ($class->associationMappings as $field => $assoc) {
786 944
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
787 653
                continue;
788
            }
789
790 913
            $this->computeAssociationChanges($assoc, $val);
791
792 905
            if ( ! isset($this->entityChangeSets[$oid]) &&
793 905
                $assoc['isOwningSide'] &&
794 905
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
795 905
                $val instanceof PersistentCollection &&
796 905
                $val->isDirty()) {
797
798 35
                $this->entityChangeSets[$oid]   = [];
799 35
                $this->originalEntityData[$oid] = $actualData;
800 905
                $this->entityUpdates[$oid]      = $entity;
801
            }
802
        }
803 1086
    }
804
805
    /**
806
     * Computes all the changes that have been done to entities and collections
807
     * since the last commit and stores these changes in the _entityChangeSet map
808
     * temporarily for access by the persisters, until the UoW commit is finished.
809
     *
810
     * @return void
811
     */
812 1086
    public function computeChangeSets()
813
    {
814
        // Compute changes for INSERTed entities first. This must always happen.
815 1086
        $this->computeScheduleInsertsChangeSets();
816
817
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
818 1084
        foreach ($this->identityMap as $className => $entities) {
819 469
            $class = $this->em->getClassMetadata($className);
820
821
            // Skip class if instances are read-only
822 469
            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...
823 1
                continue;
824
            }
825
826
            // If change tracking is explicit or happens through notification, then only compute
827
            // changes on entities of that type that are explicitly marked for synchronization.
828
            switch (true) {
829 468
                case ($class->isChangeTrackingDeferredImplicit()):
830 463
                    $entitiesToProcess = $entities;
831 463
                    break;
832
833 6
                case (isset($this->scheduledForSynchronization[$className])):
834 5
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
835 5
                    break;
836
837
                default:
838 2
                    $entitiesToProcess = [];
839
840
            }
841
842 468
            foreach ($entitiesToProcess as $entity) {
843
                // Ignore uninitialized proxy objects
844 447
                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...
845 38
                    continue;
846
                }
847
848
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
849 445
                $oid = spl_object_hash($entity);
850
851 445
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
852 468
                    $this->computeChangeSet($class, $entity);
853
                }
854
            }
855
        }
856 1084
    }
857
858
    /**
859
     * Computes the changes of an association.
860
     *
861
     * @param array $assoc The association mapping.
862
     * @param mixed $value The value of the association.
863
     *
864
     * @throws ORMInvalidArgumentException
865
     * @throws ORMException
866
     *
867
     * @return void
868
     */
869 913
    private function computeAssociationChanges($assoc, $value)
870
    {
871 913
        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...
872 30
            return;
873
        }
874
875 912
        if ($value instanceof PersistentCollection && $value->isDirty()) {
876 544
            $coid = spl_object_hash($value);
877
878 544
            $this->collectionUpdates[$coid] = $value;
879 544
            $this->visitedCollections[$coid] = $value;
880
        }
881
882
        // Look through the entities, and in any of their associations,
883
        // for transient (new) entities, recursively. ("Persistence by reachability")
884
        // Unwrap. Uninitialized collections will simply be empty.
885 912
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
886 912
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
887
888 912
        foreach ($unwrappedValue as $key => $entry) {
889 752
            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...
890 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
891
            }
892
893 744
            $state = $this->getEntityState($entry, self::STATE_NEW);
894
895 744
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
896
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
897
            }
898
899
            switch ($state) {
900 744
                case self::STATE_NEW:
901 42
                    if ( ! $assoc['isCascadePersist']) {
902
                        /*
903
                         * For now just record the details, because this may
904
                         * not be an issue if we later discover another pathway
905
                         * through the object-graph where cascade-persistence
906
                         * is enabled for this object.
907
                         */
908 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
909
910 6
                        break;
911
                    }
912
913 37
                    $this->persistNew($targetClass, $entry);
914 37
                    $this->computeChangeSet($targetClass, $entry);
915
916 37
                    break;
917
918 736
                case self::STATE_REMOVED:
919
                    // Consume the $value as array (it's either an array or an ArrayAccess)
920
                    // and remove the element from Collection.
921 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
922 3
                        unset($value[$key]);
923
                    }
924 4
                    break;
925
926 736
                case self::STATE_DETACHED:
927
                    // Can actually not happen right now as we assume STATE_NEW,
928
                    // so the exception will be raised from the DBAL layer (constraint violation).
929
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
930
                    break;
931
932 744
                default:
933
                    // MANAGED associated entities are already taken into account
934
                    // during changeset calculation anyway, since they are in the identity map.
935
            }
936
        }
937 904
    }
938
939
    /**
940
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
941
     * @param object                              $entity
942
     *
943
     * @return void
944
     */
945 1117
    private function persistNew($class, $entity)
946
    {
947 1117
        $oid    = spl_object_hash($entity);
948 1117
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
949
950 1117
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
951 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
952
        }
953
954 1117
        $idGen = $class->idGenerator;
955
956 1117
        if ( ! $idGen->isPostInsertGenerator()) {
957 287
            $idValue = $idGen->generate($this->em, $entity);
958
959 287
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
960 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
961
962 2
                $class->setIdentifierValues($entity, $idValue);
963
            }
964
965
            // Some identifiers may be foreign keys to new entities.
966
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
967 287
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
968 284
                $this->entityIdentifiers[$oid] = $idValue;
969
            }
970
        }
971
972 1117
        $this->entityStates[$oid] = self::STATE_MANAGED;
973
974 1117
        $this->scheduleForInsert($entity);
975 1117
    }
976
977
    /**
978
     * @param mixed[] $idValue
979
     */
980 287
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
981
    {
982 287
        foreach ($idValue as $idField => $idFieldValue) {
983 287
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
984 287
                return true;
985
            }
986
        }
987
988 284
        return false;
989
    }
990
991
    /**
992
     * INTERNAL:
993
     * Computes the changeset of an individual entity, independently of the
994
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
995
     *
996
     * The passed entity must be a managed entity. If the entity already has a change set
997
     * because this method is invoked during a commit cycle then the change sets are added.
998
     * whereby changes detected in this method prevail.
999
     *
1000
     * @ignore
1001
     *
1002
     * @param ClassMetadata $class  The class descriptor of the entity.
1003
     * @param object        $entity The entity for which to (re)calculate the change set.
1004
     *
1005
     * @return void
1006
     *
1007
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
1008
     */
1009 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
1010
    {
1011 16
        $oid = spl_object_hash($entity);
1012
1013 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
1014
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1015
        }
1016
1017
        // skip if change tracking is "NOTIFY"
1018 16
        if ($class->isChangeTrackingNotify()) {
1019
            return;
1020
        }
1021
1022 16
        if ( ! $class->isInheritanceTypeNone()) {
1023 3
            $class = $this->em->getClassMetadata(get_class($entity));
1024
        }
1025
1026 16
        $actualData = [];
1027
1028 16
        foreach ($class->reflFields as $name => $refProp) {
1029 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1030 16
                && ($name !== $class->versionField)
1031 16
                && ! $class->isCollectionValuedAssociation($name)) {
1032 16
                $actualData[$name] = $refProp->getValue($entity);
1033
            }
1034
        }
1035
1036 16
        if ( ! isset($this->originalEntityData[$oid])) {
1037
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1038
        }
1039
1040 16
        $originalData = $this->originalEntityData[$oid];
1041 16
        $changeSet = [];
1042
1043 16
        foreach ($actualData as $propName => $actualValue) {
1044 16
            $orgValue = $originalData[$propName] ?? null;
1045
1046 16
            if ($orgValue !== $actualValue) {
1047 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1048
            }
1049
        }
1050
1051 16
        if ($changeSet) {
1052 7
            if (isset($this->entityChangeSets[$oid])) {
1053 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1054 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1055 1
                $this->entityChangeSets[$oid] = $changeSet;
1056 1
                $this->entityUpdates[$oid]    = $entity;
1057
            }
1058 7
            $this->originalEntityData[$oid] = $actualData;
1059
        }
1060 16
    }
1061
1062
    /**
1063
     * Executes all entity insertions for entities of the specified type.
1064
     *
1065
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1066
     *
1067
     * @return void
1068
     */
1069 1080
    private function executeInserts($class)
1070
    {
1071 1080
        $entities   = [];
1072 1080
        $className  = $class->name;
1073 1080
        $persister  = $this->getEntityPersister($className);
1074 1080
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1075
1076 1080
        $insertionsForClass = [];
1077
1078 1080
        foreach ($this->entityInsertions as $oid => $entity) {
1079
1080 1080
            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...
1081 908
                continue;
1082
            }
1083
1084 1080
            $insertionsForClass[$oid] = $entity;
1085
1086 1080
            $persister->addInsert($entity);
1087
1088 1080
            unset($this->entityInsertions[$oid]);
1089
1090 1080
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1091 1080
                $entities[] = $entity;
1092
            }
1093
        }
1094
1095 1080
        $postInsertIds = $persister->executeInserts();
1096
1097 1080
        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...
1098
            // Persister returned post-insert IDs
1099 974
            foreach ($postInsertIds as $postInsertId) {
1100 974
                $idField = $class->getSingleIdentifierFieldName();
1101 974
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1102
1103 974
                $entity  = $postInsertId['entity'];
1104 974
                $oid     = spl_object_hash($entity);
1105
1106 974
                $class->reflFields[$idField]->setValue($entity, $idValue);
1107
1108 974
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1109 974
                $this->entityStates[$oid] = self::STATE_MANAGED;
1110 974
                $this->originalEntityData[$oid][$idField] = $idValue;
1111
1112 974
                $this->addToIdentityMap($entity);
1113
            }
1114
        } else {
1115 810
            foreach ($insertionsForClass as $oid => $entity) {
1116 270
                if (! isset($this->entityIdentifiers[$oid])) {
1117
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1118
                    //add it now
1119 270
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1120
                }
1121
            }
1122
        }
1123
1124 1080
        foreach ($entities as $entity) {
1125 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1126
        }
1127 1080
    }
1128
1129
    /**
1130
     * @param object $entity
1131
     */
1132 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1133
    {
1134 3
        $identifier = [];
1135
1136 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1137 3
            $value = $class->getFieldValue($entity, $idField);
1138
1139 3
            if (isset($class->associationMappings[$idField])) {
1140
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1141 3
                $value = $this->getSingleIdentifierValue($value);
1142
            }
1143
1144 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1145
        }
1146
1147 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1148 3
        $this->entityIdentifiers[$oid] = $identifier;
1149
1150 3
        $this->addToIdentityMap($entity);
1151 3
    }
1152
1153
    /**
1154
     * Executes all entity updates for entities of the specified type.
1155
     *
1156
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1157
     *
1158
     * @return void
1159
     */
1160 122
    private function executeUpdates($class)
1161
    {
1162 122
        $className          = $class->name;
1163 122
        $persister          = $this->getEntityPersister($className);
1164 122
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1165 122
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1166
1167 122
        foreach ($this->entityUpdates as $oid => $entity) {
1168 122
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1169 78
                continue;
1170
            }
1171
1172 122
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1173 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1174
1175 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1176
            }
1177
1178 122
            if ( ! empty($this->entityChangeSets[$oid])) {
1179 88
                $persister->update($entity);
1180
            }
1181
1182 118
            unset($this->entityUpdates[$oid]);
1183
1184 118
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1185 118
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1186
            }
1187
        }
1188 118
    }
1189
1190
    /**
1191
     * Executes all entity deletions for entities of the specified type.
1192
     *
1193
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1194
     *
1195
     * @return void
1196
     */
1197 65
    private function executeDeletions($class)
1198
    {
1199 65
        $className  = $class->name;
1200 65
        $persister  = $this->getEntityPersister($className);
1201 65
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1202
1203 65
        foreach ($this->entityDeletions as $oid => $entity) {
1204 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...
1205 26
                continue;
1206
            }
1207
1208 65
            $persister->delete($entity);
1209
1210
            unset(
1211 65
                $this->entityDeletions[$oid],
1212 65
                $this->entityIdentifiers[$oid],
1213 65
                $this->originalEntityData[$oid],
1214 65
                $this->entityStates[$oid]
1215
            );
1216
1217
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1218
            // is obtained by a new entity because the old one went out of scope.
1219
            //$this->entityStates[$oid] = self::STATE_NEW;
1220 65
            if ( ! $class->isIdentifierNatural()) {
1221 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1222
            }
1223
1224 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1225 65
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1226
            }
1227
        }
1228 64
    }
1229
1230
    /**
1231
     * Gets the commit order.
1232
     *
1233
     * @param array|null $entityChangeSet
1234
     *
1235
     * @return array
1236
     */
1237 1084
    private function getCommitOrder(array $entityChangeSet = null)
1238
    {
1239 1084
        if ($entityChangeSet === null) {
1240 1084
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1241
        }
1242
1243 1084
        $calc = $this->getCommitOrderCalculator();
1244
1245
        // See if there are any new classes in the changeset, that are not in the
1246
        // commit order graph yet (don't have a node).
1247
        // We have to inspect changeSet to be able to correctly build dependencies.
1248
        // It is not possible to use IdentityMap here because post inserted ids
1249
        // are not yet available.
1250 1084
        $newNodes = [];
1251
1252 1084
        foreach ($entityChangeSet as $entity) {
1253 1084
            $class = $this->em->getClassMetadata(get_class($entity));
1254
1255 1084
            if ($calc->hasNode($class->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1256 655
                continue;
1257
            }
1258
1259 1084
            $calc->addNode($class->name, $class);
1260
1261 1084
            $newNodes[] = $class;
1262
        }
1263
1264
        // Calculate dependencies for new nodes
1265 1084
        while ($class = array_pop($newNodes)) {
1266 1084
            foreach ($class->associationMappings as $assoc) {
1267 933
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1268 885
                    continue;
1269
                }
1270
1271 880
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1272
1273 880
                if ( ! $calc->hasNode($targetClass->name)) {
1274 671
                    $calc->addNode($targetClass->name, $targetClass);
1275
1276 671
                    $newNodes[] = $targetClass;
1277
                }
1278
1279 880
                $joinColumns = reset($assoc['joinColumns']);
1280
1281 880
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1282
1283
                // If the target class has mapped subclasses, these share the same dependency.
1284 880
                if ( ! $targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1285 873
                    continue;
1286
                }
1287
1288 239
                foreach ($targetClass->subClasses as $subClassName) {
1289 239
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1290
1291 239
                    if ( ! $calc->hasNode($subClassName)) {
1292 209
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1293
1294 209
                        $newNodes[] = $targetSubClass;
1295
                    }
1296
1297 239
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1298
                }
1299
            }
1300
        }
1301
1302 1084
        return $calc->sort();
1303
    }
1304
1305
    /**
1306
     * Schedules an entity for insertion into the database.
1307
     * If the entity already has an identifier, it will be added to the identity map.
1308
     *
1309
     * @param object $entity The entity to schedule for insertion.
1310
     *
1311
     * @return void
1312
     *
1313
     * @throws ORMInvalidArgumentException
1314
     * @throws \InvalidArgumentException
1315
     */
1316 1118
    public function scheduleForInsert($entity)
1317
    {
1318 1118
        $oid = spl_object_hash($entity);
1319
1320 1118
        if (isset($this->entityUpdates[$oid])) {
1321
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1322
        }
1323
1324 1118
        if (isset($this->entityDeletions[$oid])) {
1325 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1326
        }
1327 1118
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1328 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1329
        }
1330
1331 1118
        if (isset($this->entityInsertions[$oid])) {
1332 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1333
        }
1334
1335 1118
        $this->entityInsertions[$oid] = $entity;
1336
1337 1118
        if (isset($this->entityIdentifiers[$oid])) {
1338 284
            $this->addToIdentityMap($entity);
1339
        }
1340
1341 1118
        if ($entity instanceof NotifyPropertyChanged) {
1342 8
            $entity->addPropertyChangedListener($this);
1343
        }
1344 1118
    }
1345
1346
    /**
1347
     * Checks whether an entity is scheduled for insertion.
1348
     *
1349
     * @param object $entity
1350
     *
1351
     * @return boolean
1352
     */
1353 650
    public function isScheduledForInsert($entity)
1354
    {
1355 650
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1356
    }
1357
1358
    /**
1359
     * Schedules an entity for being updated.
1360
     *
1361
     * @param object $entity The entity to schedule for being updated.
1362
     *
1363
     * @return void
1364
     *
1365
     * @throws ORMInvalidArgumentException
1366
     */
1367 1
    public function scheduleForUpdate($entity)
1368
    {
1369 1
        $oid = spl_object_hash($entity);
1370
1371 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1372
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1373
        }
1374
1375 1
        if (isset($this->entityDeletions[$oid])) {
1376
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1377
        }
1378
1379 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1380 1
            $this->entityUpdates[$oid] = $entity;
1381
        }
1382 1
    }
1383
1384
    /**
1385
     * INTERNAL:
1386
     * Schedules an extra update that will be executed immediately after the
1387
     * regular entity updates within the currently running commit cycle.
1388
     *
1389
     * Extra updates for entities are stored as (entity, changeset) tuples.
1390
     *
1391
     * @ignore
1392
     *
1393
     * @param object $entity    The entity for which to schedule an extra update.
1394
     * @param array  $changeset The changeset of the entity (what to update).
1395
     *
1396
     * @return void
1397
     */
1398 44
    public function scheduleExtraUpdate($entity, array $changeset)
1399
    {
1400 44
        $oid         = spl_object_hash($entity);
1401 44
        $extraUpdate = [$entity, $changeset];
1402
1403 44
        if (isset($this->extraUpdates[$oid])) {
1404 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1405
1406 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1407
        }
1408
1409 44
        $this->extraUpdates[$oid] = $extraUpdate;
1410 44
    }
1411
1412
    /**
1413
     * Checks whether an entity is registered as dirty in the unit of work.
1414
     * Note: Is not very useful currently as dirty entities are only registered
1415
     * at commit time.
1416
     *
1417
     * @param object $entity
1418
     *
1419
     * @return boolean
1420
     */
1421
    public function isScheduledForUpdate($entity)
1422
    {
1423
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1424
    }
1425
1426
    /**
1427
     * Checks whether an entity is registered to be checked in the unit of work.
1428
     *
1429
     * @param object $entity
1430
     *
1431
     * @return boolean
1432
     */
1433 5
    public function isScheduledForDirtyCheck($entity)
1434
    {
1435 5
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1436
1437 5
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1438
    }
1439
1440
    /**
1441
     * INTERNAL:
1442
     * Schedules an entity for deletion.
1443
     *
1444
     * @param object $entity
1445
     *
1446
     * @return void
1447
     */
1448 68
    public function scheduleForDelete($entity)
1449
    {
1450 68
        $oid = spl_object_hash($entity);
1451
1452 68
        if (isset($this->entityInsertions[$oid])) {
1453 1
            if ($this->isInIdentityMap($entity)) {
1454
                $this->removeFromIdentityMap($entity);
1455
            }
1456
1457 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1458
1459 1
            return; // entity has not been persisted yet, so nothing more to do.
1460
        }
1461
1462 68
        if ( ! $this->isInIdentityMap($entity)) {
1463 1
            return;
1464
        }
1465
1466 67
        $this->removeFromIdentityMap($entity);
1467
1468 67
        unset($this->entityUpdates[$oid]);
1469
1470 67
        if ( ! isset($this->entityDeletions[$oid])) {
1471 67
            $this->entityDeletions[$oid] = $entity;
1472 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1473
        }
1474 67
    }
1475
1476
    /**
1477
     * Checks whether an entity is registered as removed/deleted with the unit
1478
     * of work.
1479
     *
1480
     * @param object $entity
1481
     *
1482
     * @return boolean
1483
     */
1484 17
    public function isScheduledForDelete($entity)
1485
    {
1486 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1487
    }
1488
1489
    /**
1490
     * Checks whether an entity is scheduled for insertion, update or deletion.
1491
     *
1492
     * @param object $entity
1493
     *
1494
     * @return boolean
1495
     */
1496
    public function isEntityScheduled($entity)
1497
    {
1498
        $oid = spl_object_hash($entity);
1499
1500
        return isset($this->entityInsertions[$oid])
1501
            || isset($this->entityUpdates[$oid])
1502
            || isset($this->entityDeletions[$oid]);
1503
    }
1504
1505
    /**
1506
     * INTERNAL:
1507
     * Registers an entity in the identity map.
1508
     * Note that entities in a hierarchy are registered with the class name of
1509
     * the root entity.
1510
     *
1511
     * @ignore
1512
     *
1513
     * @param object $entity The entity to register.
1514
     *
1515
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1516
     *                 the entity in question is already managed.
1517
     *
1518
     * @throws ORMInvalidArgumentException
1519
     */
1520 1179
    public function addToIdentityMap($entity)
1521
    {
1522 1179
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1523 1179
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1524
1525 1179
        if (empty($identifier) || in_array(null, $identifier, true)) {
1526 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1527
        }
1528
1529 1173
        $idHash    = implode(' ', $identifier);
1530 1173
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1531
1532 1173
        if (isset($this->identityMap[$className][$idHash])) {
1533 86
            return false;
1534
        }
1535
1536 1173
        $this->identityMap[$className][$idHash] = $entity;
1537
1538 1173
        return true;
1539
    }
1540
1541
    /**
1542
     * Gets the state of an entity with regard to the current unit of work.
1543
     *
1544
     * @param object   $entity
1545
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1546
     *                         This parameter can be set to improve performance of entity state detection
1547
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1548
     *                         is either known or does not matter for the caller of the method.
1549
     *
1550
     * @return int The entity state.
1551
     */
1552 1132
    public function getEntityState($entity, $assume = null)
1553
    {
1554 1132
        $oid = spl_object_hash($entity);
1555
1556 1132
        if (isset($this->entityStates[$oid])) {
1557 818
            return $this->entityStates[$oid];
1558
        }
1559
1560 1126
        if ($assume !== null) {
1561 1122
            return $assume;
1562
        }
1563
1564
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1565
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1566
        // the UoW does not hold references to such objects and the object hash can be reused.
1567
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1568 13
        $class = $this->em->getClassMetadata(get_class($entity));
1569 13
        $id    = $class->getIdentifierValues($entity);
1570
1571 13
        if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2919
                /** @scrutinizer ignore-type */ array_combine($class->identifier, [array_values($ids)])
Loading history...
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2920
            );
2921
        }
2922 7
    }
2923
2924
    /**
2925
     * Initializes (loads) an uninitialized persistent collection of an entity.
2926
     *
2927
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2928
     *
2929
     * @return void
2930
     *
2931
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2932
     */
2933 151
    public function loadCollection(PersistentCollection $collection)
2934
    {
2935 151
        $assoc     = $collection->getMapping();
2936 151
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2937
2938 151
        switch ($assoc['type']) {
2939 151
            case ClassMetadata::ONE_TO_MANY:
2940 79
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2941 79
                break;
2942
2943 86
            case ClassMetadata::MANY_TO_MANY:
2944 86
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2945 86
                break;
2946
        }
2947
2948 151
        $collection->setInitialized(true);
2949 151
    }
2950
2951
    /**
2952
     * Gets the identity map of the UnitOfWork.
2953
     *
2954
     * @return array
2955
     */
2956 2
    public function getIdentityMap()
2957
    {
2958 2
        return $this->identityMap;
2959
    }
2960
2961
    /**
2962
     * Gets the original data of an entity. The original data is the data that was
2963
     * present at the time the entity was reconstituted from the database.
2964
     *
2965
     * @param object $entity
2966
     *
2967
     * @return array
2968
     */
2969 123
    public function getOriginalEntityData($entity)
2970
    {
2971 123
        $oid = spl_object_hash($entity);
2972
2973 123
        return isset($this->originalEntityData[$oid])
2974 119
            ? $this->originalEntityData[$oid]
2975 123
            : [];
2976
    }
2977
2978
    /**
2979
     * @ignore
2980
     *
2981
     * @param object $entity
2982
     * @param array  $data
2983
     *
2984
     * @return void
2985
     */
2986
    public function setOriginalEntityData($entity, array $data)
2987
    {
2988
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2989
    }
2990
2991
    /**
2992
     * INTERNAL:
2993
     * Sets a property value of the original data array of an entity.
2994
     *
2995
     * @ignore
2996
     *
2997
     * @param string $oid
2998
     * @param string $property
2999
     * @param mixed  $value
3000
     *
3001
     * @return void
3002
     */
3003 313
    public function setOriginalEntityProperty($oid, $property, $value)
3004
    {
3005 313
        $this->originalEntityData[$oid][$property] = $value;
3006 313
    }
3007
3008
    /**
3009
     * Gets the identifier of an entity.
3010
     * The returned value is always an array of identifier values. If the entity
3011
     * has a composite identifier then the identifier values are in the same
3012
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
3013
     *
3014
     * @param object $entity
3015
     *
3016
     * @return array The identifier values.
3017
     */
3018 875
    public function getEntityIdentifier($entity)
3019
    {
3020 875
        return $this->entityIdentifiers[spl_object_hash($entity)];
3021
    }
3022
3023
    /**
3024
     * Processes an entity instance to extract their identifier values.
3025
     *
3026
     * @param object $entity The entity instance.
3027
     *
3028
     * @return mixed A scalar value.
3029
     *
3030
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
3031
     */
3032 137
    public function getSingleIdentifierValue($entity)
3033
    {
3034 137
        $class = $this->em->getClassMetadata(get_class($entity));
3035
3036 135
        if ($class->isIdentifierComposite) {
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3037
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
3038
        }
3039
3040 135
        $values = $this->isInIdentityMap($entity)
3041 121
            ? $this->getEntityIdentifier($entity)
3042 135
            : $class->getIdentifierValues($entity);
3043
3044 135
        return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null;
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3045
    }
3046
3047
    /**
3048
     * Tries to find an entity with the given identifier in the identity map of
3049
     * this UnitOfWork.
3050
     *
3051
     * @param mixed  $id            The entity identifier to look for.
3052
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
3053
     *
3054
     * @return object|bool Returns the entity with the specified identifier if it exists in
3055
     *                     this UnitOfWork, FALSE otherwise.
3056
     */
3057 558
    public function tryGetById($id, $rootClassName)
3058
    {
3059 558
        $idHash = implode(' ', (array) $id);
3060
3061 558
        return isset($this->identityMap[$rootClassName][$idHash])
3062 89
            ? $this->identityMap[$rootClassName][$idHash]
3063 558
            : false;
3064
    }
3065
3066
    /**
3067
     * Schedules an entity for dirty-checking at commit-time.
3068
     *
3069
     * @param object $entity The entity to schedule for dirty-checking.
3070
     *
3071
     * @return void
3072
     *
3073
     * @todo Rename: scheduleForSynchronization
3074
     */
3075 9
    public function scheduleForDirtyCheck($entity)
3076
    {
3077 9
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3078
3079 9
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
3080 9
    }
3081
3082
    /**
3083
     * Checks whether the UnitOfWork has any pending insertions.
3084
     *
3085
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
3086
     */
3087
    public function hasPendingInsertions()
3088
    {
3089
        return ! empty($this->entityInsertions);
3090
    }
3091
3092
    /**
3093
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
3094
     * number of entities in the identity map.
3095
     *
3096
     * @return integer
3097
     */
3098 1
    public function size()
3099
    {
3100 1
        $countArray = array_map('count', $this->identityMap);
3101
3102 1
        return array_sum($countArray);
3103
    }
3104
3105
    /**
3106
     * Gets the EntityPersister for an Entity.
3107
     *
3108
     * @param string $entityName The name of the Entity.
3109
     *
3110
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
3111
     */
3112 1147
    public function getEntityPersister($entityName)
3113
    {
3114 1147
        if (isset($this->persisters[$entityName])) {
3115 899
            return $this->persisters[$entityName];
3116
        }
3117
3118 1147
        $class = $this->em->getClassMetadata($entityName);
3119
3120
        switch (true) {
3121 1147
            case ($class->isInheritanceTypeNone()):
3122 1096
                $persister = new BasicEntityPersister($this->em, $class);
3123 1096
                break;
3124
3125 384
            case ($class->isInheritanceTypeSingleTable()):
3126 228
                $persister = new SingleTablePersister($this->em, $class);
3127 228
                break;
3128
3129 350
            case ($class->isInheritanceTypeJoined()):
3130 350
                $persister = new JoinedSubclassPersister($this->em, $class);
3131 350
                break;
3132
3133
            default:
3134
                throw new \RuntimeException('No persister found for entity.');
3135
        }
3136
3137 1147
        if ($this->hasCache && $class->cache !== null) {
0 ignored issues
show
Bug introduced by
Accessing cache on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3138 128
            $persister = $this->em->getConfiguration()
3139 128
                ->getSecondLevelCacheConfiguration()
3140 128
                ->getCacheFactory()
3141 128
                ->buildCachedEntityPersister($this->em, $persister, $class);
3142
        }
3143
3144 1147
        $this->persisters[$entityName] = $persister;
3145
3146 1147
        return $this->persisters[$entityName];
3147
    }
3148
3149
    /**
3150
     * Gets a collection persister for a collection-valued association.
3151
     *
3152
     * @param array $association
3153
     *
3154
     * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister
3155
     */
3156 579
    public function getCollectionPersister(array $association)
3157
    {
3158 579
        $role = isset($association['cache'])
3159 78
            ? $association['sourceEntity'] . '::' . $association['fieldName']
3160 579
            : $association['type'];
3161
3162 579
        if (isset($this->collectionPersisters[$role])) {
3163 446
            return $this->collectionPersisters[$role];
3164
        }
3165
3166 579
        $persister = ClassMetadata::ONE_TO_MANY === $association['type']
3167 405
            ? new OneToManyPersister($this->em)
3168 579
            : new ManyToManyPersister($this->em);
3169
3170 579
        if ($this->hasCache && isset($association['cache'])) {
3171 77
            $persister = $this->em->getConfiguration()
3172 77
                ->getSecondLevelCacheConfiguration()
3173 77
                ->getCacheFactory()
3174 77
                ->buildCachedCollectionPersister($this->em, $persister, $association);
3175
        }
3176
3177 579
        $this->collectionPersisters[$role] = $persister;
3178
3179 579
        return $this->collectionPersisters[$role];
3180
    }
3181
3182
    /**
3183
     * INTERNAL:
3184
     * Registers an entity as managed.
3185
     *
3186
     * @param object $entity The entity.
3187
     * @param array  $id     The identifier values.
3188
     * @param array  $data   The original entity data.
3189
     *
3190
     * @return void
3191
     */
3192 206
    public function registerManaged($entity, array $id, array $data)
3193
    {
3194 206
        $oid = spl_object_hash($entity);
3195
3196 206
        $this->entityIdentifiers[$oid]  = $id;
3197 206
        $this->entityStates[$oid]       = self::STATE_MANAGED;
3198 206
        $this->originalEntityData[$oid] = $data;
3199
3200 206
        $this->addToIdentityMap($entity);
3201
3202 200
        if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) {
3203 2
            $entity->addPropertyChangedListener($this);
3204
        }
3205 200
    }
3206
3207
    /**
3208
     * INTERNAL:
3209
     * Clears the property changeset of the entity with the given OID.
3210
     *
3211
     * @param string $oid The entity's OID.
3212
     *
3213
     * @return void
3214
     */
3215 12
    public function clearEntityChangeSet($oid)
3216
    {
3217 12
        unset($this->entityChangeSets[$oid]);
3218 12
    }
3219
3220
    /* PropertyChangedListener implementation */
3221
3222
    /**
3223
     * Notifies this UnitOfWork of a property change in an entity.
3224
     *
3225
     * @param object $entity       The entity that owns the property.
3226
     * @param string $propertyName The name of the property that changed.
3227
     * @param mixed  $oldValue     The old value of the property.
3228
     * @param mixed  $newValue     The new value of the property.
3229
     *
3230
     * @return void
3231
     */
3232 4
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
3233
    {
3234 4
        $oid   = spl_object_hash($entity);
3235 4
        $class = $this->em->getClassMetadata(get_class($entity));
3236
3237 4
        $isAssocField = isset($class->associationMappings[$propertyName]);
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3238
3239 4
        if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3240 1
            return; // ignore non-persistent fields
3241
        }
3242
3243
        // Update changeset and mark entity for synchronization
3244 4
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
3245
3246 4
        if ( ! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3247 4
            $this->scheduleForDirtyCheck($entity);
3248
        }
3249 4
    }
3250
3251
    /**
3252
     * Gets the currently scheduled entity insertions in this UnitOfWork.
3253
     *
3254
     * @return array
3255
     */
3256 2
    public function getScheduledEntityInsertions()
3257
    {
3258 2
        return $this->entityInsertions;
3259
    }
3260
3261
    /**
3262
     * Gets the currently scheduled entity updates in this UnitOfWork.
3263
     *
3264
     * @return array
3265
     */
3266 3
    public function getScheduledEntityUpdates()
3267
    {
3268 3
        return $this->entityUpdates;
3269
    }
3270
3271
    /**
3272
     * Gets the currently scheduled entity deletions in this UnitOfWork.
3273
     *
3274
     * @return array
3275
     */
3276 1
    public function getScheduledEntityDeletions()
3277
    {
3278 1
        return $this->entityDeletions;
3279
    }
3280
3281
    /**
3282
     * Gets the currently scheduled complete collection deletions
3283
     *
3284
     * @return array
3285
     */
3286 1
    public function getScheduledCollectionDeletions()
3287
    {
3288 1
        return $this->collectionDeletions;
3289
    }
3290
3291
    /**
3292
     * Gets the currently scheduled collection inserts, updates and deletes.
3293
     *
3294
     * @return array
3295
     */
3296
    public function getScheduledCollectionUpdates()
3297
    {
3298
        return $this->collectionUpdates;
3299
    }
3300
3301
    /**
3302
     * Helper method to initialize a lazy loading proxy or persistent collection.
3303
     *
3304
     * @param object $obj
3305
     *
3306
     * @return void
3307
     */
3308 2
    public function initializeObject($obj)
3309
    {
3310 2
        if ($obj instanceof Proxy) {
3311 1
            $obj->__load();
3312
3313 1
            return;
3314
        }
3315
3316 1
        if ($obj instanceof PersistentCollection) {
3317 1
            $obj->initialize();
3318
        }
3319 1
    }
3320
3321
    /**
3322
     * Helper method to show an object as string.
3323
     *
3324
     * @param object $obj
3325
     *
3326
     * @return string
3327
     */
3328 1
    private static function objToStr($obj)
3329
    {
3330 1
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj).'@'.spl_object_hash($obj);
3331
    }
3332
3333
    /**
3334
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3335
     *
3336
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3337
     * on this object that might be necessary to perform a correct update.
3338
     *
3339
     * @param object $object
3340
     *
3341
     * @return void
3342
     *
3343
     * @throws ORMInvalidArgumentException
3344
     */
3345 6
    public function markReadOnly($object)
3346
    {
3347 6
        if ( ! is_object($object) || ! $this->isInIdentityMap($object)) {
3348 1
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3349
        }
3350
3351 5
        $this->readOnlyObjects[spl_object_hash($object)] = true;
3352 5
    }
3353
3354
    /**
3355
     * Is this entity read only?
3356
     *
3357
     * @param object $object
3358
     *
3359
     * @return bool
3360
     *
3361
     * @throws ORMInvalidArgumentException
3362
     */
3363 3
    public function isReadOnly($object)
3364
    {
3365 3
        if ( ! is_object($object)) {
3366
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3367
        }
3368
3369 3
        return isset($this->readOnlyObjects[spl_object_hash($object)]);
3370
    }
3371
3372
    /**
3373
     * Perform whatever processing is encapsulated here after completion of the transaction.
3374
     */
3375 1079
    private function afterTransactionComplete()
3376
    {
3377
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3378 96
            $persister->afterTransactionComplete();
3379 1079
        });
3380 1079
    }
3381
3382
    /**
3383
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
3384
     */
3385 11
    private function afterTransactionRolledBack()
3386
    {
3387
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3388 3
            $persister->afterTransactionRolledBack();
3389 11
        });
3390 11
    }
3391
3392
    /**
3393
     * Performs an action after the transaction.
3394
     *
3395
     * @param callable $callback
3396
     */
3397 1084
    private function performCallbackOnCachedPersister(callable $callback)
3398
    {
3399 1084
        if ( ! $this->hasCache) {
3400 988
            return;
3401
        }
3402
3403 96
        foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) {
3404 96
            if ($persister instanceof CachedPersister) {
3405 96
                $callback($persister);
3406
            }
3407
        }
3408 96
    }
3409
3410 1088
    private function dispatchOnFlushEvent()
3411
    {
3412 1088
        if ($this->evm->hasListeners(Events::onFlush)) {
3413 4
            $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3414
        }
3415 1088
    }
3416
3417 1083
    private function dispatchPostFlushEvent()
3418
    {
3419 1083
        if ($this->evm->hasListeners(Events::postFlush)) {
3420 5
            $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3421
        }
3422 1082
    }
3423
3424
    /**
3425
     * Verifies if two given entities actually are the same based on identifier comparison
3426
     *
3427
     * @param object $entity1
3428
     * @param object $entity2
3429
     *
3430
     * @return bool
3431
     */
3432 14
    private function isIdentifierEquals($entity1, $entity2)
3433
    {
3434 14
        if ($entity1 === $entity2) {
3435
            return true;
3436
        }
3437
3438 14
        $class = $this->em->getClassMetadata(get_class($entity1));
3439
3440 14
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
3441 11
            return false;
3442
        }
3443
3444 3
        $oid1 = spl_object_hash($entity1);
3445 3
        $oid2 = spl_object_hash($entity2);
3446
3447 3
        $id1 = isset($this->entityIdentifiers[$oid1])
3448 3
            ? $this->entityIdentifiers[$oid1]
3449 3
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
3450 3
        $id2 = isset($this->entityIdentifiers[$oid2])
3451 3
            ? $this->entityIdentifiers[$oid2]
3452 3
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
3453
3454 3
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
3455
    }
3456
3457
    /**
3458
     * @throws ORMInvalidArgumentException
3459
     */
3460 1086
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
3461
    {
3462 1086
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
3463
3464 1086
        $this->nonCascadedNewDetectedEntities = [];
3465
3466 1086
        if ($entitiesNeedingCascadePersist) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entitiesNeedingCascadePersist of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
3467 5
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
3468 5
                \array_values($entitiesNeedingCascadePersist)
3469
            );
3470
        }
3471 1084
    }
3472
3473
    /**
3474
     * @param object $entity
3475
     * @param object $managedCopy
3476
     *
3477
     * @throws ORMException
3478
     * @throws OptimisticLockException
3479
     * @throws TransactionRequiredException
3480
     */
3481 40
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
3482
    {
3483 40
        if (! $this->isLoaded($entity)) {
3484 7
            return;
3485
        }
3486
3487 33
        if (! $this->isLoaded($managedCopy)) {
3488 4
            $managedCopy->__load();
3489
        }
3490
3491 33
        $class = $this->em->getClassMetadata(get_class($entity));
3492
3493 33
        foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
3494 33
            $name = $prop->name;
3495
3496 33
            $prop->setAccessible(true);
3497
3498 33
            if ( ! isset($class->associationMappings[$name])) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3499 33
                if ( ! $class->isIdentifier($name)) {
3500 33
                    $prop->setValue($managedCopy, $prop->getValue($entity));
3501
                }
3502
            } else {
3503 29
                $assoc2 = $class->associationMappings[$name];
3504
3505 29
                if ($assoc2['type'] & ClassMetadata::TO_ONE) {
3506 25
                    $other = $prop->getValue($entity);
3507 25
                    if ($other === null) {
3508 12
                        $prop->setValue($managedCopy, null);
3509
                    } else {
3510 16
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
3511
                            // do not merge fields marked lazy that have not been fetched.
3512 4
                            continue;
3513
                        }
3514
3515 12
                        if ( ! $assoc2['isCascadeMerge']) {
3516 6
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
3517 3
                                $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
3518 3
                                $relatedId   = $targetClass->getIdentifierValues($other);
3519
3520 3
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3521 2
                                    $other = $this->em->find($targetClass->name, $relatedId);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3522
                                } else {
3523 1
                                    $other = $this->em->getProxyFactory()->getProxy(
3524 1
                                        $assoc2['targetEntity'],
3525 1
                                        $relatedId
3526
                                    );
3527 1
                                    $this->registerManaged($other, $relatedId, []);
3528
                                }
3529
                            }
3530
3531 21
                            $prop->setValue($managedCopy, $other);
3532
                        }
3533
                    }
3534
                } else {
3535 17
                    $mergeCol = $prop->getValue($entity);
3536
3537 17
                    if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
3538
                        // do not merge fields marked lazy that have not been fetched.
3539
                        // keep the lazy persistent collection of the managed copy.
3540 5
                        continue;
3541
                    }
3542
3543 14
                    $managedCol = $prop->getValue($managedCopy);
3544
3545 14
                    if ( ! $managedCol) {
3546 4
                        $managedCol = new PersistentCollection(
3547 4
                            $this->em,
3548 4
                            $this->em->getClassMetadata($assoc2['targetEntity']),
3549 4
                            new ArrayCollection
3550
                        );
3551 4
                        $managedCol->setOwner($managedCopy, $assoc2);
3552 4
                        $prop->setValue($managedCopy, $managedCol);
3553
                    }
3554
3555 14
                    if ($assoc2['isCascadeMerge']) {
3556 9
                        $managedCol->initialize();
3557
3558
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
3559 9
                        if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
3560 1
                            $managedCol->unwrap()->clear();
3561 1
                            $managedCol->setDirty(true);
3562
3563 1
                            if ($assoc2['isOwningSide']
3564 1
                                && $assoc2['type'] == ClassMetadata::MANY_TO_MANY
3565 1
                                && $class->isChangeTrackingNotify()
3566
                            ) {
3567
                                $this->scheduleForDirtyCheck($managedCopy);
3568
                            }
3569
                        }
3570
                    }
3571
                }
3572
            }
3573
3574 33
            if ($class->isChangeTrackingNotify()) {
3575
                // Just treat all properties as changed, there is no other choice.
3576 33
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
3577
            }
3578
        }
3579 33
    }
3580
3581
    /**
3582
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
3583
     * Unit of work able to fire deferred events, related to loading events here.
3584
     *
3585
     * @internal should be called internally from object hydrators
3586
     */
3587 948
    public function hydrationComplete()
3588
    {
3589 948
        $this->hydrationCompleteHandler->hydrationComplete();
3590 948
    }
3591
3592
    /**
3593
     * @param string $entityName
3594
     */
3595 9
    private function clearIdentityMapForEntityName($entityName)
3596
    {
3597 9
        if (! isset($this->identityMap[$entityName])) {
3598
            return;
3599
        }
3600
3601 9
        $visited = [];
3602
3603 9
        foreach ($this->identityMap[$entityName] as $entity) {
3604 9
            $this->doDetach($entity, $visited, false);
3605
        }
3606 9
    }
3607
3608
    /**
3609
     * @param string $entityName
3610
     */
3611 9
    private function clearEntityInsertionsForEntityName($entityName)
3612
    {
3613 9
        foreach ($this->entityInsertions as $hash => $entity) {
3614
            // note: performance optimization - `instanceof` is much faster than a function call
3615 1
            if ($entity instanceof $entityName && get_class($entity) === $entityName) {
3616 1
                unset($this->entityInsertions[$hash]);
3617
            }
3618
        }
3619 9
    }
3620
3621
    /**
3622
     * @param ClassMetadata $class
3623
     * @param mixed         $identifierValue
3624
     *
3625
     * @return mixed the identifier after type conversion
3626
     *
3627
     * @throws \Doctrine\ORM\Mapping\MappingException if the entity has more than a single identifier
3628
     */
3629 976
    private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue)
3630
    {
3631 976
        return $this->em->getConnection()->convertToPHPValue(
3632 976
            $identifierValue,
3633 976
            $class->getTypeOfField($class->getSingleIdentifierFieldName())
3634
        );
3635
    }
3636
}
3637