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

1577
            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...
1578
                // Check for a version field, if available, to avoid a db lookup.
1579 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...
1580 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

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

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

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

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

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

1988
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1989
        }
1990
1991
        // Mark the managed copy visited as well
1992 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

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

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

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

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

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