Failed Conditions
Push — 2.6 ( c83094...6a827d )
by Luís
63:56 queued 58:10
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 2497
    public function __construct(EntityManagerInterface $em)
304
    {
305 2497
        $this->em                         = $em;
306 2497
        $this->evm                        = $em->getEventManager();
307 2497
        $this->listenersInvoker           = new ListenersInvoker($em);
308 2497
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
309 2497
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
310 2497
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
311 2497
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
312 2497
    }
313
314
    /**
315
     * Commits the UnitOfWork, executing all operations that have been postponed
316
     * up to this point. The state of all managed entities will be synchronized with
317
     * the database.
318
     *
319
     * The operations are executed in the following order:
320
     *
321
     * 1) All entity insertions
322
     * 2) All entity updates
323
     * 3) All collection deletions
324
     * 4) All collection updates
325
     * 5) All entity deletions
326
     *
327
     * @param null|object|array $entity
328
     *
329
     * @return void
330
     *
331
     * @throws \Exception
332
     */
333 1100
    public function commit($entity = null)
334
    {
335
        // Raise preFlush
336 1100
        if ($this->evm->hasListeners(Events::preFlush)) {
337 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
338
        }
339
340
        // Compute changes done since last commit.
341 1100
        if (null === $entity) {
342 1090
            $this->computeChangeSets();
343 19
        } elseif (is_object($entity)) {
344 17
            $this->computeSingleEntityChangeSet($entity);
345 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
346 2
            foreach ($entity as $object) {
347 2
                $this->computeSingleEntityChangeSet($object);
348
            }
349
        }
350
351 1097
        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...
352 178
                $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...
353 140
                $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...
354 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...
355 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...
356 1097
                $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...
357 27
            $this->dispatchOnFlushEvent();
358 27
            $this->dispatchPostFlushEvent();
359
360 27
            $this->postCommitCleanup($entity);
361
362 27
            return; // Nothing to do.
363
        }
364
365 1093
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
366
367 1091
        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...
368 16
            foreach ($this->orphanRemovals as $orphan) {
369 16
                $this->remove($orphan);
370
            }
371
        }
372
373 1091
        $this->dispatchOnFlushEvent();
374
375
        // Now we need a commit order to maintain referential integrity
376 1091
        $commitOrder = $this->getCommitOrder();
377
378 1091
        $conn = $this->em->getConnection();
379 1091
        $conn->beginTransaction();
380
381
        try {
382
            // Collection deletions (deletions of complete collections)
383 1091
            foreach ($this->collectionDeletions as $collectionToDelete) {
384 21
                if (! $collectionToDelete instanceof PersistentCollection) {
385
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
386
387
                    continue;
388
                }
389
390
                // Deferred explicit tracked collections can be removed only when owning relation was persisted
391 21
                $owner = $collectionToDelete->getOwner();
392
393 21
                if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
394 21
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
395
                }
396
            }
397
398 1091
            if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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