Failed Conditions
Pull Request — 2.7 (#7950)
by Mathieu
07:30
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 14.0643

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
357 1101
                $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...
358 27
            $this->dispatchOnFlushEvent();
359 27
            $this->dispatchPostFlushEvent();
360
361 27
            $this->postCommitCleanup($entity);
362
363 27
            return; // Nothing to do.
364
        }
365
366 1097
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
367
368 1095
        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...
369 16
            foreach ($this->orphanRemovals as $orphan) {
370 16
                $this->remove($orphan);
371
            }
372
        }
373
374 1095
        $this->dispatchOnFlushEvent();
375
376
        // Now we need a commit order to maintain referential integrity
377 1095
        $commitOrder = $this->getCommitOrder();
378
379 1095
        $conn = $this->em->getConnection();
380 1095
        $conn->beginTransaction();
381
382
        try {
383
            // Collection deletions (deletions of complete collections)
384 1095
            foreach ($this->collectionDeletions as $collectionToDelete) {
385 21
                if (! $collectionToDelete instanceof PersistentCollection) {
386
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
387
388
                    continue;
389
                }
390
391
                // Deferred explicit tracked collections can be removed only when owning relation was persisted
392 21
                $owner = $collectionToDelete->getOwner();
393
394 21
                if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
395 21
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
396
                }
397
            }
398
399 1095
            if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
400 1091
                foreach ($commitOrder as $class) {
401 1091
                    $this->executeInserts($class);
402
                }
403
            }
404
405 1094
            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...
406 123
                foreach ($commitOrder as $class) {
407 123
                    $this->executeUpdates($class);
408
                }
409
            }
410
411
            // Extra updates that were requested by persisters.
412 1090
            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...
413 44
                $this->executeExtraUpdates();
414
            }
415
416
            // Collection updates (deleteRows, updateRows, insertRows)
417 1090
            foreach ($this->collectionUpdates as $collectionToUpdate) {
418 552
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
419
            }
420
421
            // Entity deletions come last and need to be in reverse commit order
422 1090
            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...
423 65
                for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

1573
            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...
1574
                // Check for a version field, if available, to avoid a db lookup.
1575 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...
1576 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

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

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

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

Loading history...
1934 6
                $managedCopy = $this->newInstance($class);
1935
1936 6
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1937 6
                $this->persistNew($class, $managedCopy);
1938
            } else {
1939 37
                $flatId = ($class->containsForeignIdentifier)
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1940 3
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1941 37
                    : $id;
1942
1943 37
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1944
1945 37
                if ($managedCopy) {
1946
                    // We have the entity in-memory already, just make sure its not removed.
1947 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

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

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

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

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

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

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

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

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

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

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

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

2170
            /** @scrutinizer ignore-type */ array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
Loading history...
2171 17
            $entity
2172
        );
2173
2174 17
        $this->cascadeRefresh($entity, $visited);
2175 17
    }
2176
2177
    /**
2178
     * Cascades a refresh operation to associated entities.
2179
     *
2180
     * @param object $entity
2181
     * @param array  $visited
2182
     *
2183
     * @return void
2184
     */
2185 17
    private function cascadeRefresh($entity, array &$visited)
2186
    {
2187 17
        $class = $this->em->getClassMetadata(get_class($entity));
2188
2189 17
        $associationMappings = array_filter(
2190 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...
2191
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2192
        );
2193
2194 17
        foreach ($associationMappings as $assoc) {
2195 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...
2196
2197
            switch (true) {
2198 5
                case ($relatedEntities instanceof PersistentCollection):
2199
                    // Unwrap so that foreach() does not initialize
2200 5
                    $relatedEntities = $relatedEntities->unwrap();
2201
                    // break; is commented intentionally!
2202
2203
                case ($relatedEntities instanceof Collection):
2204
                case (is_array($relatedEntities)):
2205 5
                    foreach ($relatedEntities as $relatedEntity) {
2206
                        $this->doRefresh($relatedEntity, $visited);
2207
                    }
2208 5
                    break;
2209
2210
                case ($relatedEntities !== null):
2211
                    $this->doRefresh($relatedEntities, $visited);
2212
                    break;
2213
2214 5
                default:
2215
                    // Do nothing
2216
            }
2217
        }
2218 17
    }
2219
2220
    /**
2221
     * Cascades a detach operation to associated entities.
2222
     *
2223
     * @param object $entity
2224
     * @param array  $visited
2225
     *
2226
     * @return void
2227
     */
2228 16
    private function cascadeDetach($entity, array &$visited)
2229
    {
2230 16
        $class = $this->em->getClassMetadata(get_class($entity));
2231
2232 16
        $associationMappings = array_filter(
2233 16
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2234
            function ($assoc) { return $assoc['isCascadeDetach']; }
2235
        );
2236
2237 16
        foreach ($associationMappings as $assoc) {
2238 4
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2239
2240
            switch (true) {
2241 4
                case ($relatedEntities instanceof PersistentCollection):
2242
                    // Unwrap so that foreach() does not initialize
2243 2
                    $relatedEntities = $relatedEntities->unwrap();
2244
                    // break; is commented intentionally!
2245
2246 2
                case ($relatedEntities instanceof Collection):
2247 1
                case (is_array($relatedEntities)):
2248 3
                    foreach ($relatedEntities as $relatedEntity) {
2249 1
                        $this->doDetach($relatedEntity, $visited);
2250
                    }
2251 3
                    break;
2252
2253 1
                case ($relatedEntities !== null):
2254
                    $this->doDetach($relatedEntities, $visited);
2255
                    break;
2256
2257 4
                default:
2258
                    // Do nothing
2259
            }
2260
        }
2261 16
    }
2262
2263
    /**
2264
     * Cascades a merge operation to associated entities.
2265
     *
2266
     * @param object $entity
2267
     * @param object $managedCopy
2268
     * @param array  $visited
2269
     *
2270
     * @return void
2271
     */
2272 42
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2273
    {
2274 42
        $class = $this->em->getClassMetadata(get_class($entity));
2275
2276 42
        $associationMappings = array_filter(
2277 42
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2278
            function ($assoc) { return $assoc['isCascadeMerge']; }
2279
        );
2280
2281 42
        foreach ($associationMappings as $assoc) {
2282 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...
2283
2284 16
            if ($relatedEntities instanceof Collection) {
2285 10
                if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2286 1
                    continue;
2287
                }
2288
2289 9
                if ($relatedEntities instanceof PersistentCollection) {
2290
                    // Unwrap so that foreach() does not initialize
2291 5
                    $relatedEntities = $relatedEntities->unwrap();
2292
                }
2293
2294 9
                foreach ($relatedEntities as $relatedEntity) {
2295 9
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2296
                }
2297 7
            } else if ($relatedEntities !== null) {
2298 15
                $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2299
            }
2300
        }
2301 42
    }
2302
2303
    /**
2304
     * Cascades the save operation to associated entities.
2305
     *
2306
     * @param object $entity
2307
     * @param array  $visited
2308
     *
2309
     * @return void
2310
     */
2311 1124
    private function cascadePersist($entity, array &$visited)
2312
    {
2313 1124
        $class = $this->em->getClassMetadata(get_class($entity));
2314
2315 1124
        $associationMappings = array_filter(
2316 1124
            $class->getAssociationMappings(),
0 ignored issues
show
Bug introduced by
The method getAssociationMappings() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getAssociationNames()? ( Ignorable by Annotation )

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

2316
            $class->/** @scrutinizer ignore-call */ 
2317
                    getAssociationMappings(),

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...
2317
            function ($assoc) { return $assoc['isCascadePersist']; }
2318
        );
2319
2320 1124
        foreach ($associationMappings as $assoc) {
2321 694
            $relatedField    = $class->getReflectionProperty($assoc['fieldName']);
0 ignored issues
show
Bug introduced by
The method getReflectionProperty() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getReflectionClass()? ( Ignorable by Annotation )

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

2321
            /** @scrutinizer ignore-call */ 
2322
            $relatedField    = $class->getReflectionProperty($assoc['fieldName']);

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...
2322 694
            $relatedEntities = null;
2323
2324 694
            if (! method_exists($relatedField, 'isInitialized') || $relatedField->isInitialized($entity)) {
2325 694
                $relatedEntities = $relatedField->getValue($entity);
2326
            }
2327
2328
            switch (true) {
2329 694
                case ($relatedEntities instanceof PersistentCollection):
2330
                    // Unwrap so that foreach() does not initialize
2331 22
                    $relatedEntities = $relatedEntities->unwrap();
2332
                    // break; is commented intentionally!
2333
2334 694
                case ($relatedEntities instanceof Collection):
2335 628
                case (is_array($relatedEntities)):
2336 584
                    if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
2337 3
                        throw ORMInvalidArgumentException::invalidAssociation(
2338 3
                            $this->em->getClassMetadata($assoc['targetEntity']),
2339 3
                            $assoc,
2340 3
                            $relatedEntities
2341
                        );
2342
                    }
2343
2344 581
                    foreach ($relatedEntities as $relatedEntity) {
2345 301
                        $this->doPersist($relatedEntity, $visited);
2346
                    }
2347
2348 581
                    break;
2349
2350 612
                case ($relatedEntities !== null):
2351 256
                    if (! $relatedEntities instanceof $assoc['targetEntity']) {
2352 4
                        throw ORMInvalidArgumentException::invalidAssociation(
2353 4
                            $this->em->getClassMetadata($assoc['targetEntity']),
2354 4
                            $assoc,
2355 4
                            $relatedEntities
2356
                        );
2357
                    }
2358
2359 252
                    $this->doPersist($relatedEntities, $visited);
2360 252
                    break;
2361
2362 688
                default:
2363
                    // Do nothing
2364
            }
2365
        }
2366 1117
    }
2367
2368
    /**
2369
     * Cascades the delete operation to associated entities.
2370
     *
2371
     * @param object $entity
2372
     * @param array  $visited
2373
     *
2374
     * @return void
2375
     */
2376 67
    private function cascadeRemove($entity, array &$visited)
2377
    {
2378 67
        $class = $this->em->getClassMetadata(get_class($entity));
2379
2380 67
        $associationMappings = array_filter(
2381 67
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2382
            function ($assoc) { return $assoc['isCascadeRemove']; }
2383
        );
2384
2385 67
        $entitiesToCascade = [];
2386
2387 67
        foreach ($associationMappings as $assoc) {
2388 26
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2389 6
                $entity->__load();
2390
            }
2391
2392 26
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2393
2394
            switch (true) {
2395 26
                case ($relatedEntities instanceof Collection):
2396 19
                case (is_array($relatedEntities)):
2397
                    // If its a PersistentCollection initialization is intended! No unwrap!
2398 20
                    foreach ($relatedEntities as $relatedEntity) {
2399 10
                        $entitiesToCascade[] = $relatedEntity;
2400
                    }
2401 20
                    break;
2402
2403 19
                case ($relatedEntities !== null):
2404 7
                    $entitiesToCascade[] = $relatedEntities;
2405 7
                    break;
2406
2407 26
                default:
2408
                    // Do nothing
2409
            }
2410
        }
2411
2412 67
        foreach ($entitiesToCascade as $relatedEntity) {
2413 16
            $this->doRemove($relatedEntity, $visited);
2414
        }
2415 67
    }
2416
2417
    /**
2418
     * Acquire a lock on the given entity.
2419
     *
2420
     * @param object $entity
2421
     * @param int    $lockMode
2422
     * @param int    $lockVersion
2423
     *
2424
     * @return void
2425
     *
2426
     * @throws ORMInvalidArgumentException
2427
     * @throws TransactionRequiredException
2428
     * @throws OptimisticLockException
2429
     */
2430 10
    public function lock($entity, $lockMode, $lockVersion = null)
2431
    {
2432 10
        if ($entity === null) {
2433 1
            throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock().");
2434
        }
2435
2436 9
        if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2437 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2438
        }
2439
2440 8
        $class = $this->em->getClassMetadata(get_class($entity));
2441
2442
        switch (true) {
2443 8
            case LockMode::OPTIMISTIC === $lockMode:
2444 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...
2445 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...
2446
                }
2447
2448 5
                if ($lockVersion === null) {
2449 1
                    return;
2450
                }
2451
2452 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...
2453 1
                    $entity->__load();
2454
                }
2455
2456 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...
2457
2458 4
                if ($entityVersion != $lockVersion) {
2459 2
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2460
                }
2461
2462 2
                break;
2463
2464 2
            case LockMode::NONE === $lockMode:
2465 2
            case LockMode::PESSIMISTIC_READ === $lockMode:
2466 1
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2467 2
                if (!$this->em->getConnection()->isTransactionActive()) {
2468 2
                    throw TransactionRequiredException::transactionRequired();
2469
                }
2470
2471
                $oid = spl_object_hash($entity);
2472
2473
                $this->getEntityPersister($class->name)->lock(
2474
                    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

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

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

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