Failed Conditions
Pull Request — 2.6 (#7882)
by
unknown
08:05
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 14.0643

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1971
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1972
        }
1973
1974
        // Mark the managed copy visited as well
1975 40
        $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

1975
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
1976
1977 40
        $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

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

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

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

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