Failed Conditions
Pull Request — 2.7 (#7901)
by Luís
06:54
created

UnitOfWork::computeAssociationChanges()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 64
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 14.0643

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1560
            case ($class->/** @scrutinizer ignore-call */ isIdentifierNatural()):

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

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

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

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

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

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

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

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

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

1934
                    if ($this->getEntityState(/** @scrutinizer ignore-type */ $managedCopy) == self::STATE_REMOVED) {
Loading history...
1935 15
                        throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge");
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $entity of Doctrine\ORM\ORMInvalidA...tion::entityIsRemoved() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

1959
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1960
                }
1961
            }
1962
1963 40
            $visited[$oid] = $managedCopy; // mark visited
1964
1965 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1966
                $this->scheduleForDirtyCheck($entity);
1967
            }
1968
        }
1969
1970 42
        if ($prevManagedCopy !== null) {
1971 6
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

Loading history...
2883 935
            return;
2884
        }
2885
2886
        // avoid infinite recursion
2887 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2888 7
        $this->eagerLoadingEntities = [];
2889
2890 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2891 7
            if ( ! $ids) {
2892
                continue;
2893
            }
2894
2895 7
            $class = $this->em->getClassMetadata($entityName);
2896
2897 7
            $this->getEntityPersister($entityName)->loadAll(
2898 7
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
It seems like array_combine($class->id...ay(array_values($ids))) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...ityPersister::loadAll() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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