Failed Conditions
Pull Request — 2.6 (#7857)
by
unknown
07:44
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 method_exists;
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 2495
    public function __construct(EntityManagerInterface $em)
304
    {
305 2495
        $this->em                         = $em;
306 2495
        $this->evm                        = $em->getEventManager();
307 2495
        $this->listenersInvoker           = new ListenersInvoker($em);
308 2495
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
309 2495
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
310 2495
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
311 2495
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
312 2495
    }
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 1098
    public function commit($entity = null)
334
    {
335
        // Raise preFlush
336 1098
        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 1098
        if (null === $entity) {
342 1088
            $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 1095
        if ( ! ($this->entityInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
357 27
            $this->dispatchOnFlushEvent();
358 27
            $this->dispatchPostFlushEvent();
359
360 27
            $this->postCommitCleanup($entity);
361
362 27
            return; // Nothing to do.
363
        }
364
365 1091
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
366
367 1089
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
368 16
            foreach ($this->orphanRemovals as $orphan) {
369 16
                $this->remove($orphan);
370
            }
371
        }
372
373 1089
        $this->dispatchOnFlushEvent();
374
375
        // Now we need a commit order to maintain referential integrity
376 1089
        $commitOrder = $this->getCommitOrder();
377
378 1089
        $conn = $this->em->getConnection();
379 1089
        $conn->beginTransaction();
380
381
        try {
382
            // Collection deletions (deletions of complete collections)
383 1089
            foreach ($this->collectionDeletions as $collectionToDelete) {
384 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
385
            }
386
387 1089
            if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
388 1085
                foreach ($commitOrder as $class) {
389 1085
                    $this->executeInserts($class);
390
                }
391
            }
392
393 1088
            if ($this->entityUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
394 123
                foreach ($commitOrder as $class) {
395 123
                    $this->executeUpdates($class);
396
                }
397
            }
398
399
            // Extra updates that were requested by persisters.
400 1084
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
401 44
                $this->executeExtraUpdates();
402
            }
403
404
            // Collection updates (deleteRows, updateRows, insertRows)
405 1084
            foreach ($this->collectionUpdates as $collectionToUpdate) {
406 550
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
407
            }
408
409
            // Entity deletions come last and need to be in reverse commit order
410 1084
            if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
411 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...
412 65
                    $this->executeDeletions($commitOrder[$i]);
413
                }
414
            }
415
416 1084
            $conn->commit();
417 11
        } catch (Throwable $e) {
418 11
            $this->em->close();
419 11
            $conn->rollBack();
420
421 11
            $this->afterTransactionRolledBack();
422
423 11
            throw $e;
424
        }
425
426 1084
        $this->afterTransactionComplete();
427
428
        // Take new snapshots from visited collections
429 1084
        foreach ($this->visitedCollections as $coll) {
430 549
            $coll->takeSnapshot();
431
        }
432
433 1084
        $this->dispatchPostFlushEvent();
434
435 1083
        $this->postCommitCleanup($entity);
436 1083
    }
437
438
    /**
439
     * @param null|object|object[] $entity
440
     */
441 1087
    private function postCommitCleanup($entity) : void
442
    {
443 1087
        $this->entityInsertions =
444 1087
        $this->entityUpdates =
445 1087
        $this->entityDeletions =
446 1087
        $this->extraUpdates =
447 1087
        $this->collectionUpdates =
448 1087
        $this->nonCascadedNewDetectedEntities =
449 1087
        $this->collectionDeletions =
450 1087
        $this->visitedCollections =
451 1087
        $this->orphanRemovals = [];
452
453 1087
        if (null === $entity) {
454 1078
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
455
456 1078
            return;
457
        }
458
459 17
        $entities = \is_object($entity)
460 15
            ? [$entity]
461 17
            : $entity;
462
463 17
        foreach ($entities as $object) {
464 17
            $oid = \spl_object_hash($object);
465
466 17
            $this->clearEntityChangeSet($oid);
467
468 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...
469
        }
470 17
    }
471
472
    /**
473
     * Computes the changesets of all entities scheduled for insertion.
474
     *
475
     * @return void
476
     */
477 1097
    private function computeScheduleInsertsChangeSets()
478
    {
479 1097
        foreach ($this->entityInsertions as $entity) {
480 1089
            $class = $this->em->getClassMetadata(get_class($entity));
481
482 1089
            $this->computeChangeSet($class, $entity);
483
        }
484 1095
    }
485
486
    /**
487
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
488
     *
489
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
490
     * 2. Read Only entities are skipped.
491
     * 3. Proxies are skipped.
492
     * 4. Only if entity is properly managed.
493
     *
494
     * @param object $entity
495
     *
496
     * @return void
497
     *
498
     * @throws \InvalidArgumentException
499
     */
500 19
    private function computeSingleEntityChangeSet($entity)
501
    {
502 19
        $state = $this->getEntityState($entity);
503
504 19
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
505 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
506
        }
507
508 18
        $class = $this->em->getClassMetadata(get_class($entity));
509
510 18
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
511 17
            $this->persist($entity);
512
        }
513
514
        // Compute changes for INSERTed entities first. This must always happen even in this case.
515 18
        $this->computeScheduleInsertsChangeSets();
516
517 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...
518
            return;
519
        }
520
521
        // Ignore uninitialized proxy objects
522 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...
523 2
            return;
524
        }
525
526
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
527 16
        $oid = spl_object_hash($entity);
528
529 16
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
530 7
            $this->computeChangeSet($class, $entity);
531
        }
532 16
    }
533
534
    /**
535
     * Executes any extra updates that have been scheduled.
536
     */
537 44
    private function executeExtraUpdates()
538
    {
539 44
        foreach ($this->extraUpdates as $oid => $update) {
540 44
            list ($entity, $changeset) = $update;
541
542 44
            $this->entityChangeSets[$oid] = $changeset;
543 44
            $this->getEntityPersister(get_class($entity))->update($entity);
544
        }
545
546 44
        $this->extraUpdates = [];
547 44
    }
548
549
    /**
550
     * Gets the changeset for an entity.
551
     *
552
     * @param object $entity
553
     *
554
     * @return array
555
     */
556
    public function & getEntityChangeSet($entity)
557
    {
558 1084
        $oid  = spl_object_hash($entity);
559 1084
        $data = [];
560
561 1084
        if (!isset($this->entityChangeSets[$oid])) {
562 4
            return $data;
563
        }
564
565 1084
        return $this->entityChangeSets[$oid];
566
    }
567
568
    /**
569
     * Computes the changes that happened to a single entity.
570
     *
571
     * Modifies/populates the following properties:
572
     *
573
     * {@link _originalEntityData}
574
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
575
     * then it was not fetched from the database and therefore we have no original
576
     * entity data yet. All of the current entity data is stored as the original entity data.
577
     *
578
     * {@link _entityChangeSets}
579
     * The changes detected on all properties of the entity are stored there.
580
     * A change is a tuple array where the first entry is the old value and the second
581
     * entry is the new value of the property. Changesets are used by persisters
582
     * to INSERT/UPDATE the persistent entity state.
583
     *
584
     * {@link _entityUpdates}
585
     * If the entity is already fully MANAGED (has been fetched from the database before)
586
     * and any changes to its properties are detected, then a reference to the entity is stored
587
     * there to mark it for an update.
588
     *
589
     * {@link _collectionDeletions}
590
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
591
     * then this collection is marked for deletion.
592
     *
593
     * @ignore
594
     *
595
     * @internal Don't call from the outside.
596
     *
597
     * @param ClassMetadata $class  The class descriptor of the entity.
598
     * @param object        $entity The entity for which to compute the changes.
599
     *
600
     * @return void
601
     */
602 1099
    public function computeChangeSet(ClassMetadata $class, $entity)
603
    {
604 1099
        $oid = spl_object_hash($entity);
605
606 1099
        if (isset($this->readOnlyObjects[$oid])) {
607 2
            return;
608
        }
609
610 1099
        if ( ! $class->isInheritanceTypeNone()) {
611 338
            $class = $this->em->getClassMetadata(get_class($entity));
612
        }
613
614 1099
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
615
616 1099
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
617 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
618
        }
619
620 1099
        $actualData = [];
621
622 1099
        foreach ($class->reflFields as $name => $refProp) {
623 1099
            if (method_exists($refProp, 'isInitialized') && $refProp->getDeclaringClass()->isInstance($entity)) {
624
                $value = $refProp->isInitialized($entity) ? $refProp->getValue($entity) : null;
625
            } else {
626 1099
                $value = $refProp->getValue($entity);
627
            }
628
629 1099
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
630 822
                if ($value instanceof PersistentCollection) {
631 205
                    if ($value->getOwner() === $entity) {
632 205
                        continue;
633
                    }
634
635 5
                    $value = new ArrayCollection($value->getValues());
636
                }
637
638
                // If $value is not a Collection then use an ArrayCollection.
639 817
                if ( ! $value instanceof Collection) {
640 248
                    $value = new ArrayCollection($value);
641
                }
642
643 817
                $assoc = $class->associationMappings[$name];
644
645
                // Inject PersistentCollection
646 817
                $value = new PersistentCollection(
647 817
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
648
                );
649 817
                $value->setOwner($entity, $assoc);
650 817
                $value->setDirty( ! $value->isEmpty());
651
652 817
                $class->reflFields[$name]->setValue($entity, $value);
653
654 817
                $actualData[$name] = $value;
655
656 817
                continue;
657
            }
658
659 1099
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
660 1099
                $actualData[$name] = $value;
661
            }
662
        }
663
664 1099
        if ( ! isset($this->originalEntityData[$oid])) {
665
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
666
            // These result in an INSERT.
667 1095
            $this->originalEntityData[$oid] = $actualData;
668 1095
            $changeSet = [];
669
670 1095
            foreach ($actualData as $propName => $actualValue) {
671 1071
                if ( ! isset($class->associationMappings[$propName])) {
672 1013
                    $changeSet[$propName] = [null, $actualValue];
673
674 1013
                    continue;
675
                }
676
677 952
                $assoc = $class->associationMappings[$propName];
678
679 952
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
680 952
                    $changeSet[$propName] = [null, $actualValue];
681
                }
682
            }
683
684 1095
            $this->entityChangeSets[$oid] = $changeSet;
685
        } else {
686
            // Entity is "fully" MANAGED: it was already fully persisted before
687
            // and we have a copy of the original data
688 278
            $originalData           = $this->originalEntityData[$oid];
689 278
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
690 278
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
691
                ? $this->entityChangeSets[$oid]
692 278
                : [];
693
694 278
            foreach ($actualData as $propName => $actualValue) {
695
                // skip field, its a partially omitted one!
696 261
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
697 8
                    continue;
698
                }
699
700 261
                $orgValue = $originalData[$propName];
701
702
                // skip if value haven't changed
703 261
                if ($orgValue === $actualValue) {
704 244
                    continue;
705
                }
706
707
                // if regular field
708 119
                if ( ! isset($class->associationMappings[$propName])) {
709 64
                    if ($isChangeTrackingNotify) {
710
                        continue;
711
                    }
712
713 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
714
715 64
                    continue;
716
                }
717
718 59
                $assoc = $class->associationMappings[$propName];
719
720
                // Persistent collection was exchanged with the "originally"
721
                // created one. This can only mean it was cloned and replaced
722
                // on another entity.
723 59
                if ($actualValue instanceof PersistentCollection) {
724 8
                    $owner = $actualValue->getOwner();
725 8
                    if ($owner === null) { // cloned
726
                        $actualValue->setOwner($entity, $assoc);
727 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
728
                        if (!$actualValue->isInitialized()) {
729
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
730
                        }
731
                        $newValue = clone $actualValue;
732
                        $newValue->setOwner($entity, $assoc);
733
                        $class->reflFields[$propName]->setValue($entity, $newValue);
734
                    }
735
                }
736
737 59
                if ($orgValue instanceof PersistentCollection) {
738
                    // A PersistentCollection was de-referenced, so delete it.
739 8
                    $coid = spl_object_hash($orgValue);
740
741 8
                    if (isset($this->collectionDeletions[$coid])) {
742
                        continue;
743
                    }
744
745 8
                    $this->collectionDeletions[$coid] = $orgValue;
746 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
747
748 8
                    continue;
749
                }
750
751 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
752 50
                    if ($assoc['isOwningSide']) {
753 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
754
                    }
755
756 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
757 51
                        $this->scheduleOrphanRemoval($orgValue);
758
                    }
759
                }
760
            }
761
762 278
            if ($changeSet) {
763 92
                $this->entityChangeSets[$oid]   = $changeSet;
764 92
                $this->originalEntityData[$oid] = $actualData;
765 92
                $this->entityUpdates[$oid]      = $entity;
766
            }
767
        }
768
769
        // Look for changes in associations of the entity
770 1099
        foreach ($class->associationMappings as $field => $assoc) {
771 952
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
772 663
                continue;
773
            }
774
775 923
            $this->computeAssociationChanges($assoc, $val);
776
777 915
            if ( ! isset($this->entityChangeSets[$oid]) &&
778 915
                $assoc['isOwningSide'] &&
779 915
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
780 915
                $val instanceof PersistentCollection &&
781 915
                $val->isDirty()) {
782
783 35
                $this->entityChangeSets[$oid]   = [];
784 35
                $this->originalEntityData[$oid] = $actualData;
785 915
                $this->entityUpdates[$oid]      = $entity;
786
            }
787
        }
788 1091
    }
789
790
    /**
791
     * Computes all the changes that have been done to entities and collections
792
     * since the last commit and stores these changes in the _entityChangeSet map
793
     * temporarily for access by the persisters, until the UoW commit is finished.
794
     *
795
     * @return void
796
     */
797 1088
    public function computeChangeSets()
798
    {
799
        // Compute changes for INSERTed entities first. This must always happen.
800 1088
        $this->computeScheduleInsertsChangeSets();
801
802
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
803 1086
        foreach ($this->identityMap as $className => $entities) {
804 477
            $class = $this->em->getClassMetadata($className);
805
806
            // Skip class if instances are read-only
807 477
            if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
808 1
                continue;
809
            }
810
811
            // If change tracking is explicit or happens through notification, then only compute
812
            // changes on entities of that type that are explicitly marked for synchronization.
813
            switch (true) {
814 476
                case ($class->isChangeTrackingDeferredImplicit()):
815 472
                    $entitiesToProcess = $entities;
816 472
                    break;
817
818 5
                case (isset($this->scheduledForSynchronization[$className])):
819 4
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
820 4
                    break;
821
822
                default:
823 2
                    $entitiesToProcess = [];
824
825
            }
826
827 476
            foreach ($entitiesToProcess as $entity) {
828
                // Ignore uninitialized proxy objects
829 455
                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
830 37
                    continue;
831
                }
832
833
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
834 454
                $oid = spl_object_hash($entity);
835
836 454
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
837 476
                    $this->computeChangeSet($class, $entity);
838
                }
839
            }
840
        }
841 1086
    }
842
843
    /**
844
     * Computes the changes of an association.
845
     *
846
     * @param array $assoc The association mapping.
847
     * @param mixed $value The value of the association.
848
     *
849
     * @throws ORMInvalidArgumentException
850
     * @throws ORMException
851
     *
852
     * @return void
853
     */
854 923
    private function computeAssociationChanges($assoc, $value)
855
    {
856 923
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
857 30
            return;
858
        }
859
860 922
        if ($value instanceof PersistentCollection && $value->isDirty()) {
861 554
            $coid = spl_object_hash($value);
862
863 554
            $this->collectionUpdates[$coid] = $value;
864 554
            $this->visitedCollections[$coid] = $value;
865
        }
866
867
        // Look through the entities, and in any of their associations,
868
        // for transient (new) entities, recursively. ("Persistence by reachability")
869
        // Unwrap. Uninitialized collections will simply be empty.
870 922
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
871 922
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
872
873 922
        foreach ($unwrappedValue as $key => $entry) {
874 762
            if (! ($entry instanceof $targetClass->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
875 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
876
            }
877
878 754
            $state = $this->getEntityState($entry, self::STATE_NEW);
879
880 754
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
881
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
882
            }
883
884
            switch ($state) {
885 754
                case self::STATE_NEW:
886 42
                    if ( ! $assoc['isCascadePersist']) {
887
                        /*
888
                         * For now just record the details, because this may
889
                         * not be an issue if we later discover another pathway
890
                         * through the object-graph where cascade-persistence
891
                         * is enabled for this object.
892
                         */
893 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
894
895 6
                        break;
896
                    }
897
898 37
                    $this->persistNew($targetClass, $entry);
899 37
                    $this->computeChangeSet($targetClass, $entry);
900
901 37
                    break;
902
903 746
                case self::STATE_REMOVED:
904
                    // Consume the $value as array (it's either an array or an ArrayAccess)
905
                    // and remove the element from Collection.
906 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
907 3
                        unset($value[$key]);
908
                    }
909 4
                    break;
910
911 746
                case self::STATE_DETACHED:
912
                    // Can actually not happen right now as we assume STATE_NEW,
913
                    // so the exception will be raised from the DBAL layer (constraint violation).
914
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
915
                    break;
916
917 754
                default:
918
                    // MANAGED associated entities are already taken into account
919
                    // during changeset calculation anyway, since they are in the identity map.
920
            }
921
        }
922 914
    }
923
924
    /**
925
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
926
     * @param object                              $entity
927
     *
928
     * @return void
929
     */
930 1118
    private function persistNew($class, $entity)
931
    {
932 1118
        $oid    = spl_object_hash($entity);
933 1118
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
934
935 1118
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
936 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
937
        }
938
939 1118
        $idGen = $class->idGenerator;
940
941 1118
        if ( ! $idGen->isPostInsertGenerator()) {
942 293
            $idValue = $idGen->generate($this->em, $entity);
943
944 293
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
945 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
946
947 2
                $class->setIdentifierValues($entity, $idValue);
948
            }
949
950
            // Some identifiers may be foreign keys to new entities.
951
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
952 293
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
953 290
                $this->entityIdentifiers[$oid] = $idValue;
954
            }
955
        }
956
957 1118
        $this->entityStates[$oid] = self::STATE_MANAGED;
958
959 1118
        $this->scheduleForInsert($entity);
960 1118
    }
961
962
    /**
963
     * @param mixed[] $idValue
964
     */
965 293
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
966
    {
967 293
        foreach ($idValue as $idField => $idFieldValue) {
968 293
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
969 293
                return true;
970
            }
971
        }
972
973 290
        return false;
974
    }
975
976
    /**
977
     * INTERNAL:
978
     * Computes the changeset of an individual entity, independently of the
979
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
980
     *
981
     * The passed entity must be a managed entity. If the entity already has a change set
982
     * because this method is invoked during a commit cycle then the change sets are added.
983
     * whereby changes detected in this method prevail.
984
     *
985
     * @ignore
986
     *
987
     * @param ClassMetadata $class  The class descriptor of the entity.
988
     * @param object        $entity The entity for which to (re)calculate the change set.
989
     *
990
     * @return void
991
     *
992
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
993
     */
994 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
995
    {
996 16
        $oid = spl_object_hash($entity);
997
998 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
999
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1000
        }
1001
1002
        // skip if change tracking is "NOTIFY"
1003 16
        if ($class->isChangeTrackingNotify()) {
1004
            return;
1005
        }
1006
1007 16
        if ( ! $class->isInheritanceTypeNone()) {
1008 3
            $class = $this->em->getClassMetadata(get_class($entity));
1009
        }
1010
1011 16
        $actualData = [];
1012
1013 16
        foreach ($class->reflFields as $name => $refProp) {
1014 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1015 16
                && ($name !== $class->versionField)
1016 16
                && ! $class->isCollectionValuedAssociation($name)) {
1017 16
                $actualData[$name] = $refProp->getValue($entity);
1018
            }
1019
        }
1020
1021 16
        if ( ! isset($this->originalEntityData[$oid])) {
1022
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1023
        }
1024
1025 16
        $originalData = $this->originalEntityData[$oid];
1026 16
        $changeSet = [];
1027
1028 16
        foreach ($actualData as $propName => $actualValue) {
1029 16
            $orgValue = $originalData[$propName] ?? null;
1030
1031 16
            if ($orgValue !== $actualValue) {
1032 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1033
            }
1034
        }
1035
1036 16
        if ($changeSet) {
1037 7
            if (isset($this->entityChangeSets[$oid])) {
1038 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1039 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1040 1
                $this->entityChangeSets[$oid] = $changeSet;
1041 1
                $this->entityUpdates[$oid]    = $entity;
1042
            }
1043 7
            $this->originalEntityData[$oid] = $actualData;
1044
        }
1045 16
    }
1046
1047
    /**
1048
     * Executes all entity insertions for entities of the specified type.
1049
     *
1050
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1051
     *
1052
     * @return void
1053
     */
1054 1085
    private function executeInserts($class)
1055
    {
1056 1085
        $entities   = [];
1057 1085
        $className  = $class->name;
1058 1085
        $persister  = $this->getEntityPersister($className);
1059 1085
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1060
1061 1085
        $insertionsForClass = [];
1062
1063 1085
        foreach ($this->entityInsertions as $oid => $entity) {
1064
1065 1085
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1066 915
                continue;
1067
            }
1068
1069 1085
            $insertionsForClass[$oid] = $entity;
1070
1071 1085
            $persister->addInsert($entity);
1072
1073 1085
            unset($this->entityInsertions[$oid]);
1074
1075 1085
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1076 1085
                $entities[] = $entity;
1077
            }
1078
        }
1079
1080 1085
        $postInsertIds = $persister->executeInserts();
1081
1082 1085
        if ($postInsertIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postInsertIds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1083
            // Persister returned post-insert IDs
1084 981
            foreach ($postInsertIds as $postInsertId) {
1085 981
                $idField = $class->getSingleIdentifierFieldName();
1086 981
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1087
1088 981
                $entity  = $postInsertId['entity'];
1089 981
                $oid     = spl_object_hash($entity);
1090
1091 981
                $class->reflFields[$idField]->setValue($entity, $idValue);
1092
1093 981
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1094 981
                $this->entityStates[$oid] = self::STATE_MANAGED;
1095 981
                $this->originalEntityData[$oid][$idField] = $idValue;
1096
1097 981
                $this->addToIdentityMap($entity);
1098
            }
1099
        } else {
1100 818
            foreach ($insertionsForClass as $oid => $entity) {
1101 280
                if (! isset($this->entityIdentifiers[$oid])) {
1102
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1103
                    //add it now
1104 280
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1105
                }
1106
            }
1107
        }
1108
1109 1085
        foreach ($entities as $entity) {
1110 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1111
        }
1112 1085
    }
1113
1114
    /**
1115
     * @param object $entity
1116
     */
1117 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1118
    {
1119 3
        $identifier = [];
1120
1121 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1122 3
            $value = $class->getFieldValue($entity, $idField);
1123
1124 3
            if (isset($class->associationMappings[$idField])) {
1125
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1126 3
                $value = $this->getSingleIdentifierValue($value);
1127
            }
1128
1129 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1130
        }
1131
1132 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1133 3
        $this->entityIdentifiers[$oid] = $identifier;
1134
1135 3
        $this->addToIdentityMap($entity);
1136 3
    }
1137
1138
    /**
1139
     * Executes all entity updates for entities of the specified type.
1140
     *
1141
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1142
     *
1143
     * @return void
1144
     */
1145 123
    private function executeUpdates($class)
1146
    {
1147 123
        $className          = $class->name;
1148 123
        $persister          = $this->getEntityPersister($className);
1149 123
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1150 123
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1151
1152 123
        foreach ($this->entityUpdates as $oid => $entity) {
1153 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...
1154 79
                continue;
1155
            }
1156
1157 123
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1158 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1159
1160 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1161
            }
1162
1163 123
            if ( ! empty($this->entityChangeSets[$oid])) {
1164 89
                $persister->update($entity);
1165
            }
1166
1167 119
            unset($this->entityUpdates[$oid]);
1168
1169 119
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1170 119
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1171
            }
1172
        }
1173 119
    }
1174
1175
    /**
1176
     * Executes all entity deletions for entities of the specified type.
1177
     *
1178
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1179
     *
1180
     * @return void
1181
     */
1182 65
    private function executeDeletions($class)
1183
    {
1184 65
        $className  = $class->name;
1185 65
        $persister  = $this->getEntityPersister($className);
1186 65
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1187
1188 65
        foreach ($this->entityDeletions as $oid => $entity) {
1189 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...
1190 26
                continue;
1191
            }
1192
1193 65
            $persister->delete($entity);
1194
1195
            unset(
1196 65
                $this->entityDeletions[$oid],
1197 65
                $this->entityIdentifiers[$oid],
1198 65
                $this->originalEntityData[$oid],
1199 65
                $this->entityStates[$oid]
1200
            );
1201
1202
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1203
            // is obtained by a new entity because the old one went out of scope.
1204
            //$this->entityStates[$oid] = self::STATE_NEW;
1205 65
            if ( ! $class->isIdentifierNatural()) {
1206 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1207
            }
1208
1209 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1210 65
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1211
            }
1212
        }
1213 64
    }
1214
1215
    /**
1216
     * Gets the commit order.
1217
     *
1218
     * @param array|null $entityChangeSet
1219
     *
1220
     * @return array
1221
     */
1222 1089
    private function getCommitOrder(array $entityChangeSet = null)
1223
    {
1224 1089
        if ($entityChangeSet === null) {
1225 1089
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1226
        }
1227
1228 1089
        $calc = $this->getCommitOrderCalculator();
1229
1230
        // See if there are any new classes in the changeset, that are not in the
1231
        // commit order graph yet (don't have a node).
1232
        // We have to inspect changeSet to be able to correctly build dependencies.
1233
        // It is not possible to use IdentityMap here because post inserted ids
1234
        // are not yet available.
1235 1089
        $newNodes = [];
1236
1237 1089
        foreach ($entityChangeSet as $entity) {
1238 1089
            $class = $this->em->getClassMetadata(get_class($entity));
1239
1240 1089
            if ($calc->hasNode($class->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1241 665
                continue;
1242
            }
1243
1244 1089
            $calc->addNode($class->name, $class);
1245
1246 1089
            $newNodes[] = $class;
1247
        }
1248
1249
        // Calculate dependencies for new nodes
1250 1089
        while ($class = array_pop($newNodes)) {
1251 1089
            foreach ($class->associationMappings as $assoc) {
1252 941
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1253 893
                    continue;
1254
                }
1255
1256 889
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1257
1258 889
                if ( ! $calc->hasNode($targetClass->name)) {
1259 681
                    $calc->addNode($targetClass->name, $targetClass);
1260
1261 681
                    $newNodes[] = $targetClass;
1262
                }
1263
1264 889
                $joinColumns = reset($assoc['joinColumns']);
1265
1266 889
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1267
1268
                // If the target class has mapped subclasses, these share the same dependency.
1269 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...
1270 882
                    continue;
1271
                }
1272
1273 238
                foreach ($targetClass->subClasses as $subClassName) {
1274 238
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1275
1276 238
                    if ( ! $calc->hasNode($subClassName)) {
1277 208
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1278
1279 208
                        $newNodes[] = $targetSubClass;
1280
                    }
1281
1282 238
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1283
                }
1284
            }
1285
        }
1286
1287 1089
        return $calc->sort();
1288
    }
1289
1290
    /**
1291
     * Schedules an entity for insertion into the database.
1292
     * If the entity already has an identifier, it will be added to the identity map.
1293
     *
1294
     * @param object $entity The entity to schedule for insertion.
1295
     *
1296
     * @return void
1297
     *
1298
     * @throws ORMInvalidArgumentException
1299
     * @throws \InvalidArgumentException
1300
     */
1301 1119
    public function scheduleForInsert($entity)
1302
    {
1303 1119
        $oid = spl_object_hash($entity);
1304
1305 1119
        if (isset($this->entityUpdates[$oid])) {
1306
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1307
        }
1308
1309 1119
        if (isset($this->entityDeletions[$oid])) {
1310 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1311
        }
1312 1119
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1313 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1314
        }
1315
1316 1119
        if (isset($this->entityInsertions[$oid])) {
1317 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1318
        }
1319
1320 1119
        $this->entityInsertions[$oid] = $entity;
1321
1322 1119
        if (isset($this->entityIdentifiers[$oid])) {
1323 290
            $this->addToIdentityMap($entity);
1324
        }
1325
1326 1119
        if ($entity instanceof NotifyPropertyChanged) {
1327 8
            $entity->addPropertyChangedListener($this);
1328
        }
1329 1119
    }
1330
1331
    /**
1332
     * Checks whether an entity is scheduled for insertion.
1333
     *
1334
     * @param object $entity
1335
     *
1336
     * @return boolean
1337
     */
1338 661
    public function isScheduledForInsert($entity)
1339
    {
1340 661
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1341
    }
1342
1343
    /**
1344
     * Schedules an entity for being updated.
1345
     *
1346
     * @param object $entity The entity to schedule for being updated.
1347
     *
1348
     * @return void
1349
     *
1350
     * @throws ORMInvalidArgumentException
1351
     */
1352 1
    public function scheduleForUpdate($entity)
1353
    {
1354 1
        $oid = spl_object_hash($entity);
1355
1356 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1357
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1358
        }
1359
1360 1
        if (isset($this->entityDeletions[$oid])) {
1361
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1362
        }
1363
1364 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1365 1
            $this->entityUpdates[$oid] = $entity;
1366
        }
1367 1
    }
1368
1369
    /**
1370
     * INTERNAL:
1371
     * Schedules an extra update that will be executed immediately after the
1372
     * regular entity updates within the currently running commit cycle.
1373
     *
1374
     * Extra updates for entities are stored as (entity, changeset) tuples.
1375
     *
1376
     * @ignore
1377
     *
1378
     * @param object $entity    The entity for which to schedule an extra update.
1379
     * @param array  $changeset The changeset of the entity (what to update).
1380
     *
1381
     * @return void
1382
     */
1383 44
    public function scheduleExtraUpdate($entity, array $changeset)
1384
    {
1385 44
        $oid         = spl_object_hash($entity);
1386 44
        $extraUpdate = [$entity, $changeset];
1387
1388 44
        if (isset($this->extraUpdates[$oid])) {
1389 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1390
1391 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1392
        }
1393
1394 44
        $this->extraUpdates[$oid] = $extraUpdate;
1395 44
    }
1396
1397
    /**
1398
     * Checks whether an entity is registered as dirty in the unit of work.
1399
     * Note: Is not very useful currently as dirty entities are only registered
1400
     * at commit time.
1401
     *
1402
     * @param object $entity
1403
     *
1404
     * @return boolean
1405
     */
1406
    public function isScheduledForUpdate($entity)
1407
    {
1408
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1409
    }
1410
1411
    /**
1412
     * Checks whether an entity is registered to be checked in the unit of work.
1413
     *
1414
     * @param object $entity
1415
     *
1416
     * @return boolean
1417
     */
1418 3
    public function isScheduledForDirtyCheck($entity)
1419
    {
1420 3
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1421
1422 3
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1423
    }
1424
1425
    /**
1426
     * INTERNAL:
1427
     * Schedules an entity for deletion.
1428
     *
1429
     * @param object $entity
1430
     *
1431
     * @return void
1432
     */
1433 68
    public function scheduleForDelete($entity)
1434
    {
1435 68
        $oid = spl_object_hash($entity);
1436
1437 68
        if (isset($this->entityInsertions[$oid])) {
1438 1
            if ($this->isInIdentityMap($entity)) {
1439
                $this->removeFromIdentityMap($entity);
1440
            }
1441
1442 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1443
1444 1
            return; // entity has not been persisted yet, so nothing more to do.
1445
        }
1446
1447 68
        if ( ! $this->isInIdentityMap($entity)) {
1448 1
            return;
1449
        }
1450
1451 67
        $this->removeFromIdentityMap($entity);
1452
1453 67
        unset($this->entityUpdates[$oid]);
1454
1455 67
        if ( ! isset($this->entityDeletions[$oid])) {
1456 67
            $this->entityDeletions[$oid] = $entity;
1457 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1458
        }
1459 67
    }
1460
1461
    /**
1462
     * Checks whether an entity is registered as removed/deleted with the unit
1463
     * of work.
1464
     *
1465
     * @param object $entity
1466
     *
1467
     * @return boolean
1468
     */
1469 17
    public function isScheduledForDelete($entity)
1470
    {
1471 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1472
    }
1473
1474
    /**
1475
     * Checks whether an entity is scheduled for insertion, update or deletion.
1476
     *
1477
     * @param object $entity
1478
     *
1479
     * @return boolean
1480
     */
1481
    public function isEntityScheduled($entity)
1482
    {
1483
        $oid = spl_object_hash($entity);
1484
1485
        return isset($this->entityInsertions[$oid])
1486
            || isset($this->entityUpdates[$oid])
1487
            || isset($this->entityDeletions[$oid]);
1488
    }
1489
1490
    /**
1491
     * INTERNAL:
1492
     * Registers an entity in the identity map.
1493
     * Note that entities in a hierarchy are registered with the class name of
1494
     * the root entity.
1495
     *
1496
     * @ignore
1497
     *
1498
     * @param object $entity The entity to register.
1499
     *
1500
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1501
     *                 the entity in question is already managed.
1502
     *
1503
     * @throws ORMInvalidArgumentException
1504
     */
1505 1183
    public function addToIdentityMap($entity)
1506
    {
1507 1183
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1508 1183
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1509
1510 1183
        if (empty($identifier) || in_array(null, $identifier, true)) {
1511 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...
1512
        }
1513
1514 1177
        $idHash    = implode(' ', $identifier);
1515 1177
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1516
1517 1177
        if (isset($this->identityMap[$className][$idHash])) {
1518 87
            return false;
1519
        }
1520
1521 1177
        $this->identityMap[$className][$idHash] = $entity;
1522
1523 1177
        return true;
1524
    }
1525
1526
    /**
1527
     * Gets the state of an entity with regard to the current unit of work.
1528
     *
1529
     * @param object   $entity
1530
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1531
     *                         This parameter can be set to improve performance of entity state detection
1532
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1533
     *                         is either known or does not matter for the caller of the method.
1534
     *
1535
     * @return int The entity state.
1536
     */
1537 1133
    public function getEntityState($entity, $assume = null)
1538
    {
1539 1133
        $oid = spl_object_hash($entity);
1540
1541 1133
        if (isset($this->entityStates[$oid])) {
1542 827
            return $this->entityStates[$oid];
1543
        }
1544
1545 1127
        if ($assume !== null) {
1546 1123
            return $assume;
1547
        }
1548
1549
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1550
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1551
        // the UoW does not hold references to such objects and the object hash can be reused.
1552
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1553 13
        $class = $this->em->getClassMetadata(get_class($entity));
1554 13
        $id    = $class->getIdentifierValues($entity);
1555
1556 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...
1557 5
            return self::STATE_NEW;
1558
        }
1559
1560 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...
1561 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1562
        }
1563
1564
        switch (true) {
1565 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
2886 935
            return;
2887
        }
2888
2889
        // avoid infinite recursion
2890 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2891 7
        $this->eagerLoadingEntities = [];
2892
2893 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2894 7
            if ( ! $ids) {
2895
                continue;
2896
            }
2897
2898 7
            $class = $this->em->getClassMetadata($entityName);
2899
2900 7
            $this->getEntityPersister($entityName)->loadAll(
2901 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

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

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

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

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