Issues (3627)

build/patches/UnitOfWork.php (98 issues)

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\AssociationCacheEntry;
30
use Doctrine\ORM\Cache\Persister\CachedPersister;
31
use Doctrine\ORM\Event\LifecycleEventArgs;
32
use Doctrine\ORM\Event\ListenersInvoker;
33
use Doctrine\ORM\Event\OnFlushEventArgs;
34
use Doctrine\ORM\Event\PostFlushEventArgs;
35
use Doctrine\ORM\Event\PreFlushEventArgs;
36
use Doctrine\ORM\Event\PreUpdateEventArgs;
37
use Doctrine\ORM\Internal\HydrationCompleteHandler;
38
use Doctrine\ORM\Mapping\ClassMetadata;
39
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
40
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
41
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
42
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
43
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
44
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
45
use Doctrine\ORM\Proxy\Proxy;
46
use Doctrine\ORM\Utility\IdentifierFlattener;
47
use Exception;
48
use InvalidArgumentException;
49
use UnexpectedValueException;
50
51
/**
52
 * The UnitOfWork is responsible for tracking changes to objects during an
53
 * "object-level" transaction and for writing out changes to the database
54
 * in the correct order.
55
 *
56
 * Internal note: This class contains highly performance-sensitive code.
57
 *
58
 * @since       2.0
59
 *
60
 * @author      Benjamin Eberlei <[email protected]>
61
 * @author      Guilherme Blanco <[email protected]>
62
 * @author      Jonathan Wage <[email protected]>
63
 * @author      Roman Borschel <[email protected]>
64
 * @author      Rob Caiger <[email protected]>
65
 */
66
class UnitOfWork implements PropertyChangedListener
67
{
68
    /**
69
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
70
     */
71
    const STATE_MANAGED = 1;
72
73
    /**
74
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
75
     * and is not (yet) managed by an EntityManager.
76
     */
77
    const STATE_NEW = 2;
78
79
    /**
80
     * A detached entity is an instance with persistent state and identity that is not
81
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
82
     */
83
    const STATE_DETACHED = 3;
84
85
    /**
86
     * A removed entity instance is an instance with a persistent identity,
87
     * associated with an EntityManager, whose persistent state will be deleted
88
     * on commit.
89
     */
90
    const STATE_REMOVED = 4;
91
92
    /**
93
     * Hint used to collect all primary keys of associated entities during hydration
94
     * and execute it in a dedicated query afterwards.
95
     *
96
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
97
     */
98
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
99
100
    /**
101
     * The identity map that holds references to all managed entities that have
102
     * an identity. The entities are grouped by their class name.
103
     * Since all classes in a hierarchy must share the same identifier set,
104
     * we always take the root class name of the hierarchy.
105
     *
106
     * @var array
107
     */
108
    private $identityMap = [];
109
110
    /**
111
     * Map of all identifiers of managed entities.
112
     * Keys are object ids (spl_object_hash).
113
     *
114
     * @var array
115
     */
116
    private $entityIdentifiers = [];
117
118
    /**
119
     * Map of the original entity data of managed entities.
120
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
121
     * at commit time.
122
     *
123
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
124
     *                A value will only really be copied if the value in the entity is modified
125
     *                by the user.
126
     *
127
     * @var array
128
     */
129
    private $originalEntityData = [];
130
131
    /**
132
     * Map of entity changes. Keys are object ids (spl_object_hash).
133
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
134
     *
135
     * @var array
136
     */
137
    private $entityChangeSets = [];
138
139
    /**
140
     * The (cached) states of any known entities.
141
     * Keys are object ids (spl_object_hash).
142
     *
143
     * @var array
144
     */
145
    private $entityStates = [];
146
147
    /**
148
     * Map of entities that are scheduled for dirty checking at commit time.
149
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
150
     * Keys are object ids (spl_object_hash).
151
     *
152
     * @var array
153
     */
154
    private $scheduledForSynchronization = [];
155
156
    /**
157
     * A list of all pending entity insertions.
158
     *
159
     * @var array
160
     */
161
    private $entityInsertions = [];
162
163
    /**
164
     * A list of all pending entity updates.
165
     *
166
     * @var array
167
     */
168
    private $entityUpdates = [];
169
170
    /**
171
     * Any pending extra updates that have been scheduled by persisters.
172
     *
173
     * @var array
174
     */
175
    private $extraUpdates = [];
176
177
    /**
178
     * A list of all pending entity deletions.
179
     *
180
     * @var array
181
     */
182
    private $entityDeletions = [];
183
184
    /**
185
     * All pending collection deletions.
186
     *
187
     * @var array
188
     */
189
    private $collectionDeletions = [];
190
191
    /**
192
     * All pending collection updates.
193
     *
194
     * @var array
195
     */
196
    private $collectionUpdates = [];
197
198
    /**
199
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
200
     * At the end of the UnitOfWork all these collections will make new snapshots
201
     * of their data.
202
     *
203
     * @var array
204
     */
205
    private $visitedCollections = [];
206
207
    /**
208
     * The EntityManager that "owns" this UnitOfWork instance.
209
     *
210
     * @var EntityManagerInterface
211
     */
212
    private $em;
213
214
    /**
215
     * The calculator used to calculate the order in which changes to
216
     * entities need to be written to the database.
217
     *
218
     * @var \Doctrine\ORM\Internal\CommitOrderCalculator
219
     */
220
    private $commitOrderCalculator;
221
222
    /**
223
     * The entity persister instances used to persist entity instances.
224
     *
225
     * @var array
226
     */
227
    private $persisters = [];
228
229
    /**
230
     * The collection persister instances used to persist collections.
231
     *
232
     * @var array
233
     */
234
    private $collectionPersisters = [];
235
236
    /**
237
     * The EventManager used for dispatching events.
238
     *
239
     * @var \Doctrine\Common\EventManager
240
     */
241
    private $evm;
242
243
    /**
244
     * The ListenersInvoker used for dispatching events.
245
     *
246
     * @var \Doctrine\ORM\Event\ListenersInvoker
247
     */
248
    private $listenersInvoker;
249
250
    /**
251
     * The IdentifierFlattener used for manipulating identifiers.
252
     *
253
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
254
     */
255
    private $identifierFlattener;
256
257
    /**
258
     * Orphaned entities that are scheduled for removal.
259
     *
260
     * @var array
261
     */
262
    private $orphanRemovals = [];
263
264
    /**
265
     * Read-Only objects are never evaluated.
266
     *
267
     * @var array
268
     */
269
    private $readOnlyObjects = [];
270
271
    /**
272
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
273
     *
274
     * @var array
275
     */
276
    private $eagerLoadingEntities = [];
277
278
    /**
279
     * @var bool
280
     */
281
    protected $hasCache = false;
282
283
    /**
284
     * Helper for handling completion of hydration.
285
     *
286
     * @var HydrationCompleteHandler
287
     */
288
    private $hydrationCompleteHandler;
289
290
    /**
291
     * @var ReflectionPropertiesGetter
292
     */
293
    private $reflectionPropertiesGetter;
294
295
    /**
296
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
297
     */
298
    public function __construct(EntityManagerInterface $em)
299
    {
300
        $this->em                         = $em;
301
        $this->evm                        = $em->getEventManager();
302
        $this->listenersInvoker           = new ListenersInvoker($em);
303
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
304
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
305
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
306
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
307
    }
308
309
    /**
310
     * Commits the UnitOfWork, executing all operations that have been postponed
311
     * up to this point. The state of all managed entities will be synchronized with
312
     * the database.
313
     *
314
     * The operations are executed in the following order:
315
     *
316
     * 1) All entity insertions
317
     * 2) All entity updates
318
     * 3) All collection deletions
319
     * 4) All collection updates
320
     * 5) All entity deletions
321
     *
322
     * @param object|array|null $entity
323
     *
324
     * @throws \Exception
325
     */
326
    public function commit($entity = null)
327
    {
328
        // Raise preFlush
329
        if ($this->evm->hasListeners(Events::preFlush)) {
330
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
331
        }
332
333
        // Compute changes done since last commit.
334
        if (null === $entity) {
335
            $this->computeChangeSets();
336
        } elseif (is_object($entity)) {
337
            $this->computeSingleEntityChangeSet($entity);
338
        } elseif (is_array($entity)) {
0 ignored issues
show
The condition is_array($entity) is always true.
Loading history...
339
            foreach ($entity as $object) {
340
                $this->computeSingleEntityChangeSet($object);
341
            }
342
        }
343
344
        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...
345
                $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...
346
                $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...
347
                $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...
348
                $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...
349
                $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...
350
            $this->dispatchOnFlushEvent();
351
            $this->dispatchPostFlushEvent();
352
353
            return; // Nothing to do.
354
        }
355
356
        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...
357
            foreach ($this->orphanRemovals as $orphan) {
358
                $this->remove($orphan);
359
            }
360
        }
361
362
        $this->dispatchOnFlushEvent();
363
364
        // Now we need a commit order to maintain referential integrity
365
        $commitOrder = $this->getCommitOrder();
366
367
        $conn = $this->em->getConnection();
368
        $conn->beginTransaction();
369
370
        try {
371
            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...
372
                foreach ($commitOrder as $class) {
373
                    $this->executeInserts($class);
374
                }
375
            }
376
377
            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...
378
                foreach ($commitOrder as $class) {
379
                    $this->executeUpdates($class);
380
                }
381
            }
382
383
            // Extra updates that were requested by persisters.
384
            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...
385
                $this->executeExtraUpdates();
386
            }
387
388
            // Collection deletions (deletions of complete collections)
389
            foreach ($this->collectionDeletions as $collectionToDelete) {
390
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
391
            }
392
393
            // Collection updates (deleteRows, updateRows, insertRows)
394
            foreach ($this->collectionUpdates as $collectionToUpdate) {
395
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
396
            }
397
398
            // Entity deletions come last and need to be in reverse commit order
399
            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...
400
                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...
401
                    $this->executeDeletions($commitOrder[$i]);
402
                }
403
            }
404
405
            $conn->commit();
406
        } catch (Exception $e) {
407
            $this->em->close();
408
            $conn->rollback();
409
410
            $this->afterTransactionRolledBack();
411
412
            throw $e;
413
        }
414
415
        $this->afterTransactionComplete();
416
417
        // Take new snapshots from visited collections
418
        foreach ($this->visitedCollections as $coll) {
419
            $coll->takeSnapshot();
420
        }
421
422
        $this->dispatchPostFlushEvent();
423
424
        // Clear up
425
        $this->entityInsertions            =
426
        $this->entityUpdates               =
427
        $this->entityDeletions             =
428
        $this->extraUpdates                =
429
        $this->entityChangeSets            =
430
        $this->collectionUpdates           =
431
        $this->collectionDeletions         =
432
        $this->visitedCollections          =
433
        $this->scheduledForSynchronization =
434
        $this->orphanRemovals              = [];
435
    }
436
437
    /**
438
     * Computes the changesets of all entities scheduled for insertion.
439
     */
440
    private function computeScheduleInsertsChangeSets()
441
    {
442
        foreach ($this->entityInsertions as $entity) {
443
            $class = $this->em->getClassMetadata(get_class($entity));
444
445
            $this->computeChangeSet($class, $entity);
446
        }
447
    }
448
449
    /**
450
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
451
     *
452
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
453
     * 2. Read Only entities are skipped.
454
     * 3. Proxies are skipped.
455
     * 4. Only if entity is properly managed.
456
     *
457
     * @param object $entity
458
     *
459
     * @throws \InvalidArgumentException
460
     */
461
    private function computeSingleEntityChangeSet($entity)
462
    {
463
        $state = $this->getEntityState($entity);
464
465
        if (self::STATE_MANAGED !== $state && self::STATE_REMOVED !== $state) {
466
            throw new \InvalidArgumentException('Entity has to be managed or scheduled for removal for single computation '.self::objToStr($entity));
467
        }
468
469
        $class = $this->em->getClassMetadata(get_class($entity));
470
471
        if (self::STATE_MANAGED === $state && $class->isChangeTrackingDeferredImplicit()) {
472
            $this->persist($entity);
473
        }
474
475
        // Compute changes for INSERTed entities first. This must always happen even in this case.
476
        $this->computeScheduleInsertsChangeSets();
477
478
        if ($class->isReadOnly) {
0 ignored issues
show
Accessing isReadOnly on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
479
            return;
480
        }
481
482
        // Ignore uninitialized proxy objects
483
        if ($entity instanceof Proxy && !$entity->__isInitialized__) {
484
            return;
485
        }
486
487
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
488
        $oid = spl_object_hash($entity);
489
490
        if (!isset($this->entityInsertions[$oid]) && !isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
491
            $this->computeChangeSet($class, $entity);
492
        }
493
    }
494
495
    /**
496
     * Executes any extra updates that have been scheduled.
497
     */
498
    private function executeExtraUpdates()
499
    {
500
        foreach ($this->extraUpdates as $oid => $update) {
501
            list($entity, $changeset) = $update;
502
503
            $this->entityChangeSets[$oid] = $changeset;
504
            $this->getEntityPersister(get_class($entity))->update($entity);
505
        }
506
507
        $this->extraUpdates = [];
508
    }
509
510
    /**
511
     * Gets the changeset for an entity.
512
     *
513
     * @param object $entity
514
     *
515
     * @return array
516
     */
517
    public function getEntityChangeSet($entity)
518
    {
519
        $oid = spl_object_hash($entity);
520
521
        if (isset($this->entityChangeSets[$oid])) {
522
            return $this->entityChangeSets[$oid];
523
        }
524
525
        return [];
526
    }
527
528
    /**
529
     * Computes the changes that happened to a single entity.
530
     *
531
     * Modifies/populates the following properties:
532
     *
533
     * {@link _originalEntityData}
534
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
535
     * then it was not fetched from the database and therefore we have no original
536
     * entity data yet. All of the current entity data is stored as the original entity data.
537
     *
538
     * {@link _entityChangeSets}
539
     * The changes detected on all properties of the entity are stored there.
540
     * A change is a tuple array where the first entry is the old value and the second
541
     * entry is the new value of the property. Changesets are used by persisters
542
     * to INSERT/UPDATE the persistent entity state.
543
     *
544
     * {@link _entityUpdates}
545
     * If the entity is already fully MANAGED (has been fetched from the database before)
546
     * and any changes to its properties are detected, then a reference to the entity is stored
547
     * there to mark it for an update.
548
     *
549
     * {@link _collectionDeletions}
550
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
551
     * then this collection is marked for deletion.
552
     *
553
     * @ignore
554
     *
555
     * @internal don't call from the outside
556
     *
557
     * @param ClassMetadata $class  the class descriptor of the entity
558
     * @param object        $entity the entity for which to compute the changes
559
     */
560
    public function computeChangeSet(ClassMetadata $class, $entity)
561
    {
562
        $oid = spl_object_hash($entity);
563
564
        if (isset($this->readOnlyObjects[$oid])) {
565
            return;
566
        }
567
568
        if (!$class->isInheritanceTypeNone()) {
569
            $class = $this->em->getClassMetadata(get_class($entity));
570
        }
571
572
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
573
574
        if (ListenersInvoker::INVOKE_NONE !== $invoke) {
575
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
576
        }
577
578
        $actualData = [];
579
580
        foreach ($class->reflFields as $name => $refProp) {
581
            $value = $refProp->getValue($entity);
582
583
            if ($class->isCollectionValuedAssociation($name) && null !== $value) {
584
                if ($value instanceof PersistentCollection) {
585
                    if ($value->getOwner() === $entity) {
586
                        continue;
587
                    }
588
589
                    $value = new ArrayCollection($value->getValues());
590
                }
591
592
                // If $value is not a Collection then use an ArrayCollection.
593
                if (!$value instanceof Collection) {
594
                    $value = new ArrayCollection($value);
595
                }
596
597
                $assoc = $class->associationMappings[$name];
598
599
                // Inject PersistentCollection
600
                $value = new PersistentCollection(
601
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
602
                );
603
                $value->setOwner($entity, $assoc);
604
                $value->setDirty(!$value->isEmpty());
605
606
                $class->reflFields[$name]->setValue($entity, $value);
607
608
                $actualData[$name] = $value;
609
610
                continue;
611
            }
612
613
            if ((!$class->isIdentifier($name) || !$class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
0 ignored issues
show
The method isIdGeneratorIdentity() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

613
            if ((!$class->isIdentifier($name) || !$class->/** @scrutinizer ignore-call */ isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
Loading history...
614
                $actualData[$name] = $value;
615
            }
616
        }
617
618
        if (!isset($this->originalEntityData[$oid])) {
619
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
620
            // These result in an INSERT.
621
            $this->originalEntityData[$oid] = $actualData;
622
            $changeSet                      = [];
623
624
            foreach ($actualData as $propName => $actualValue) {
625
                if (!isset($class->associationMappings[$propName])) {
626
                    $changeSet[$propName] = [null, $actualValue];
627
628
                    continue;
629
                }
630
631
                $assoc = $class->associationMappings[$propName];
632
633
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
634
                    $changeSet[$propName] = [null, $actualValue];
635
                }
636
            }
637
638
            $this->entityChangeSets[$oid] = $changeSet;
639
        } else {
640
            // Entity is "fully" MANAGED: it was already fully persisted before
641
            // and we have a copy of the original data
642
            $originalData           = $this->originalEntityData[$oid];
643
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
0 ignored issues
show
The method isChangeTrackingNotify() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

643
            /** @scrutinizer ignore-call */ 
644
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
Loading history...
644
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
645
                ? $this->entityChangeSets[$oid]
646
                : [];
647
648
            foreach ($actualData as $propName => $actualValue) {
649
                // skip field, its a partially omitted one!
650
                if (!(isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
651
                    continue;
652
                }
653
654
                $orgValue = $originalData[$propName];
655
656
                // skip if value haven't changed
657
                if ($orgValue === $actualValue) {
658
                    continue;
659
                }
660
661
                // if regular field
662
                if (!isset($class->associationMappings[$propName])) {
663
                    if ($isChangeTrackingNotify) {
664
                        continue;
665
                    }
666
667
                    $changeSet[$propName] = [$orgValue, $actualValue];
668
669
                    continue;
670
                }
671
672
                $assoc = $class->associationMappings[$propName];
673
674
                // Persistent collection was exchanged with the "originally"
675
                // created one. This can only mean it was cloned and replaced
676
                // on another entity.
677
                if ($actualValue instanceof PersistentCollection) {
678
                    $owner = $actualValue->getOwner();
679
                    if (null === $owner) { // cloned
680
                        $actualValue->setOwner($entity, $assoc);
681
                    } elseif ($owner !== $entity) { // no clone, we have to fix
682
                        if (!$actualValue->isInitialized()) {
683
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
684
                        }
685
                        $newValue = clone $actualValue;
686
                        $newValue->setOwner($entity, $assoc);
687
                        $class->reflFields[$propName]->setValue($entity, $newValue);
688
                    }
689
                }
690
691
                if ($orgValue instanceof PersistentCollection) {
692
                    // A PersistentCollection was de-referenced, so delete it.
693
                    $coid = spl_object_hash($orgValue);
694
695
                    if (isset($this->collectionDeletions[$coid])) {
696
                        continue;
697
                    }
698
699
                    $this->collectionDeletions[$coid] = $orgValue;
700
                    $changeSet[$propName]             = $orgValue; // Signal changeset, to-many assocs will be ignored.
701
702
                    continue;
703
                }
704
705
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
706
                    if ($assoc['isOwningSide']) {
707
                        $changeSet[$propName] = [$orgValue, $actualValue];
708
                    }
709
710
                    if (null !== $orgValue && $assoc['orphanRemoval']) {
711
                        $this->scheduleOrphanRemoval($orgValue);
712
                    }
713
                }
714
            }
715
716
            if ($changeSet) {
717
                $this->entityChangeSets[$oid]   = $changeSet;
718
                $this->originalEntityData[$oid] = $actualData;
719
                $this->entityUpdates[$oid]      = $entity;
720
            }
721
        }
722
723
        // Look for changes in associations of the entity
724
        foreach ($class->associationMappings as $field => $assoc) {
725
            if (null === ($val = $class->reflFields[$field]->getValue($entity))) {
726
                continue;
727
            }
728
729
            $this->computeAssociationChanges($assoc, $val);
730
731
            if (!isset($this->entityChangeSets[$oid]) &&
732
                $assoc['isOwningSide'] &&
733
                ClassMetadata::MANY_TO_MANY == $assoc['type'] &&
734
                $val instanceof PersistentCollection &&
735
                $val->isDirty()) {
736
                $this->entityChangeSets[$oid]   = [];
737
                $this->originalEntityData[$oid] = $actualData;
738
                $this->entityUpdates[$oid]      = $entity;
739
            }
740
        }
741
    }
742
743
    /**
744
     * Computes all the changes that have been done to entities and collections
745
     * since the last commit and stores these changes in the _entityChangeSet map
746
     * temporarily for access by the persisters, until the UoW commit is finished.
747
     */
748
    public function computeChangeSets()
749
    {
750
        // Compute changes for INSERTed entities first. This must always happen.
751
        $this->computeScheduleInsertsChangeSets();
752
753
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
754
        foreach ($this->identityMap as $className => $entities) {
755
            $class = $this->em->getClassMetadata($className);
756
757
            // Skip class if instances are read-only
758
            if ($class->isReadOnly) {
0 ignored issues
show
Accessing isReadOnly on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
759
                continue;
760
            }
761
762
            // If change tracking is explicit or happens through notification, then only compute
763
            // changes on entities of that type that are explicitly marked for synchronization.
764
            switch (true) {
765
                case $class->isChangeTrackingDeferredImplicit():
766
                    $entitiesToProcess = $entities;
767
                    break;
768
769
                case isset($this->scheduledForSynchronization[$className]):
770
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
771
                    break;
772
773
                default:
774
                    $entitiesToProcess = [];
775
            }
776
777
            foreach ($entitiesToProcess as $entity) {
778
                // Ignore uninitialized proxy objects
779
                if ($entity instanceof Proxy && !$entity->__isInitialized__) {
780
                    continue;
781
                }
782
783
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
784
                $oid = spl_object_hash($entity);
785
786
                if (!isset($this->entityInsertions[$oid]) && !isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
787
                    $this->computeChangeSet($class, $entity);
788
                }
789
            }
790
        }
791
    }
792
793
    /**
794
     * Computes the changes of an association.
795
     *
796
     * @param array $assoc the association mapping
797
     * @param mixed $value the value of the association
798
     *
799
     * @throws ORMInvalidArgumentException
800
     * @throws ORMException
801
     */
802
    private function computeAssociationChanges($assoc, $value)
803
    {
804
        if ($value instanceof Proxy && !$value->__isInitialized__) {
0 ignored issues
show
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...
805
            return;
806
        }
807
808
        if ($value instanceof PersistentCollection && $value->isDirty()) {
809
            $coid = spl_object_hash($value);
810
811
            $this->collectionUpdates[$coid]  = $value;
812
            $this->visitedCollections[$coid] = $value;
813
        }
814
815
        // Look through the entities, and in any of their associations,
816
        // for transient (new) entities, recursively. ("Persistence by reachability")
817
        // Unwrap. Uninitialized collections will simply be empty.
818
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
819
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
820
821
        foreach ($unwrappedValue as $key => $entry) {
822
            if (!($entry instanceof $targetClass->name)) {
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
823
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
824
            }
825
826
            $state = $this->getEntityState($entry, self::STATE_NEW);
827
828
            if (!($entry instanceof $assoc['targetEntity'])) {
829
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
830
            }
831
832
            switch ($state) {
833
                case self::STATE_NEW:
834
                    if (!$assoc['isCascadePersist']) {
835
                        throw ORMInvalidArgumentException::newEntityFoundThroughRelationship($assoc, $entry);
836
                    }
837
838
                    $this->persistNew($targetClass, $entry);
839
                    $this->computeChangeSet($targetClass, $entry);
840
                    break;
841
842
                case self::STATE_REMOVED:
843
                    // Consume the $value as array (it's either an array or an ArrayAccess)
844
                    // and remove the element from Collection.
845
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
846
                        unset($value[$key]);
847
                    }
848
                    break;
849
850
                case self::STATE_DETACHED:
851
                    // Can actually not happen right now as we assume STATE_NEW,
852
                    // so the exception will be raised from the DBAL layer (constraint violation).
853
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
854
                    break;
855
856
                default:
857
                    // MANAGED associated entities are already taken into account
858
                    // during changeset calculation anyway, since they are in the identity map.
859
            }
860
        }
861
    }
862
863
    /**
864
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
865
     * @param object                              $entity
866
     */
867
    private function persistNew($class, $entity)
868
    {
869
        $oid    = spl_object_hash($entity);
870
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
871
872
        if (ListenersInvoker::INVOKE_NONE !== $invoke) {
873
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
874
        }
875
876
        $idGen = $class->idGenerator;
877
878
        if (!$idGen->isPostInsertGenerator()) {
879
            $idValue = $idGen->generate($this->em, $entity);
880
881
            if (!$idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
882
                $idValue = [$class->identifier[0] => $idValue];
883
884
                $class->setIdentifierValues($entity, $idValue);
885
            }
886
887
            $this->entityIdentifiers[$oid] = $idValue;
888
        }
889
890
        $this->entityStates[$oid] = self::STATE_MANAGED;
891
892
        $this->scheduleForInsert($entity);
893
    }
894
895
    /**
896
     * INTERNAL:
897
     * Computes the changeset of an individual entity, independently of the
898
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
899
     *
900
     * The passed entity must be a managed entity. If the entity already has a change set
901
     * because this method is invoked during a commit cycle then the change sets are added.
902
     * whereby changes detected in this method prevail.
903
     *
904
     * @ignore
905
     *
906
     * @param ClassMetadata $class  the class descriptor of the entity
907
     * @param object        $entity the entity for which to (re)calculate the change set
908
     *
909
     * @throws ORMInvalidArgumentException if the passed entity is not MANAGED
910
     */
911
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
912
    {
913
        $oid = spl_object_hash($entity);
914
915
        if (!isset($this->entityStates[$oid]) || self::STATE_MANAGED != $this->entityStates[$oid]) {
916
            throw ORMInvalidArgumentException::entityNotManaged($entity);
917
        }
918
919
        // skip if change tracking is "NOTIFY"
920
        if ($class->isChangeTrackingNotify()) {
921
            return;
922
        }
923
924
        if (!$class->isInheritanceTypeNone()) {
925
            $class = $this->em->getClassMetadata(get_class($entity));
926
        }
927
928
        $actualData = [];
929
930
        foreach ($class->reflFields as $name => $refProp) {
931
            if ((!$class->isIdentifier($name) || !$class->isIdGeneratorIdentity())
932
                && ($name !== $class->versionField)
933
                && !$class->isCollectionValuedAssociation($name)) {
934
                $actualData[$name] = $refProp->getValue($entity);
935
            }
936
        }
937
938
        if (!isset($this->originalEntityData[$oid])) {
939
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
940
        }
941
942
        $originalData = $this->originalEntityData[$oid];
943
        $changeSet    = [];
944
945
        foreach ($actualData as $propName => $actualValue) {
946
            $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
947
948
            if ($orgValue !== $actualValue) {
949
                $changeSet[$propName] = [$orgValue, $actualValue];
950
            }
951
        }
952
953
        if ($changeSet) {
954
            if (isset($this->entityChangeSets[$oid])) {
955
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
956
            } elseif (!isset($this->entityInsertions[$oid])) {
957
                $this->entityChangeSets[$oid] = $changeSet;
958
                $this->entityUpdates[$oid]    = $entity;
959
            }
960
            $this->originalEntityData[$oid] = $actualData;
961
        }
962
    }
963
964
    /**
965
     * Executes all entity insertions for entities of the specified type.
966
     *
967
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
968
     */
969
    private function executeInserts($class)
970
    {
971
        $entities   = [];
972
        $className  = $class->name;
973
        $persister  = $this->getEntityPersister($className);
974
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
975
976
        foreach ($this->entityInsertions as $oid => $entity) {
977
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
978
                continue;
979
            }
980
981
            $persister->addInsert($entity);
982
983
            unset($this->entityInsertions[$oid]);
984
985
            if (ListenersInvoker::INVOKE_NONE !== $invoke) {
986
                $entities[] = $entity;
987
            }
988
        }
989
990
        $postInsertIds = $persister->executeInserts();
991
992
        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...
993
            // Persister returned post-insert IDs
994
            foreach ($postInsertIds as $postInsertId) {
995
                $id      = $postInsertId['generatedId'];
996
                $entity  = $postInsertId['entity'];
997
                $oid     = spl_object_hash($entity);
998
                $idField = $class->identifier[0];
999
1000
                $class->reflFields[$idField]->setValue($entity, $id);
1001
1002
                $this->entityIdentifiers[$oid]            = [$idField => $id];
1003
                $this->entityStates[$oid]                 = self::STATE_MANAGED;
1004
                $this->originalEntityData[$oid][$idField] = $id;
1005
1006
                $this->addToIdentityMap($entity);
1007
            }
1008
        }
1009
1010
        foreach ($entities as $entity) {
1011
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1012
        }
1013
    }
1014
1015
    /**
1016
     * Executes all entity updates for entities of the specified type.
1017
     *
1018
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1019
     */
1020
    private function executeUpdates($class)
1021
    {
1022
        $className          = $class->name;
1023
        $persister          = $this->getEntityPersister($className);
1024
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1025
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1026
1027
        foreach ($this->entityUpdates as $oid => $entity) {
1028
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1029
                continue;
1030
            }
1031
1032
            if (ListenersInvoker::INVOKE_NONE != $preUpdateInvoke) {
1033
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->entityChangeSets[$oid]), $preUpdateInvoke);
1034
                $this->recomputeSingleEntityChangeSet($class, $entity);
1035
            }
1036
1037
            if (!empty($this->entityChangeSets[$oid])) {
1038
                $persister->update($entity);
1039
            }
1040
1041
            unset($this->entityUpdates[$oid]);
1042
1043
            if (ListenersInvoker::INVOKE_NONE != $postUpdateInvoke) {
1044
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1045
            }
1046
        }
1047
    }
1048
1049
    /**
1050
     * Executes all entity deletions for entities of the specified type.
1051
     *
1052
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1053
     */
1054
    private function executeDeletions($class)
1055
    {
1056
        $className  = $class->name;
1057
        $persister  = $this->getEntityPersister($className);
1058
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1059
1060
        foreach ($this->entityDeletions as $oid => $entity) {
1061
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1062
                continue;
1063
            }
1064
1065
            $persister->delete($entity);
1066
1067
            unset(
1068
                $this->entityDeletions[$oid],
1069
                $this->entityIdentifiers[$oid],
1070
                $this->originalEntityData[$oid],
1071
                $this->entityStates[$oid]
1072
            );
1073
1074
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1075
            // is obtained by a new entity because the old one went out of scope.
1076
            //$this->entityStates[$oid] = self::STATE_NEW;
1077
            if (!$class->isIdentifierNatural()) {
1078
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1079
            }
1080
1081
            if (ListenersInvoker::INVOKE_NONE !== $invoke) {
1082
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1083
            }
1084
        }
1085
    }
1086
1087
    /**
1088
     * Gets the commit order.
1089
     *
1090
     * @return array
1091
     */
1092
    private function getCommitOrder(array $entityChangeSet = null)
1093
    {
1094
        if (null === $entityChangeSet) {
1095
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1096
        }
1097
1098
        $calc = $this->getCommitOrderCalculator();
1099
1100
        // See if there are any new classes in the changeset, that are not in the
1101
        // commit order graph yet (don't have a node).
1102
        // We have to inspect changeSet to be able to correctly build dependencies.
1103
        // It is not possible to use IdentityMap here because post inserted ids
1104
        // are not yet available.
1105
        $newNodes = [];
1106
1107
        foreach ($entityChangeSet as $entity) {
1108
            $class = $this->em->getClassMetadata(get_class($entity));
1109
1110
            if ($calc->hasClass($class->name)) {
0 ignored issues
show
The method hasClass() does not exist on Doctrine\ORM\Internal\CommitOrderCalculator. ( Ignorable by Annotation )

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

1110
            if ($calc->/** @scrutinizer ignore-call */ hasClass($class->name)) {

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...
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1111
                continue;
1112
            }
1113
1114
            $calc->addClass($class);
0 ignored issues
show
The method addClass() does not exist on Doctrine\ORM\Internal\CommitOrderCalculator. ( Ignorable by Annotation )

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

1114
            $calc->/** @scrutinizer ignore-call */ 
1115
                   addClass($class);

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...
1115
1116
            $newNodes[] = $class;
1117
        }
1118
1119
        // Calculate dependencies for new nodes
1120
        while ($class = array_pop($newNodes)) {
1121
            foreach ($class->associationMappings as $assoc) {
1122
                if (!($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1123
                    continue;
1124
                }
1125
1126
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1127
1128
                if (!$calc->hasClass($targetClass->name)) {
1129
                    $calc->addClass($targetClass);
1130
1131
                    $newNodes[] = $targetClass;
1132
                }
1133
1134
                $calc->addDependency($targetClass, $class);
0 ignored issues
show
The call to Doctrine\ORM\Internal\Co...ulator::addDependency() has too few arguments starting with weight. ( Ignorable by Annotation )

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

1134
                $calc->/** @scrutinizer ignore-call */ 
1135
                       addDependency($targetClass, $class);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1135
1136
                // If the target class has mapped subclasses, these share the same dependency.
1137
                if (!$targetClass->subClasses) {
1138
                    continue;
1139
                }
1140
1141
                foreach ($targetClass->subClasses as $subClassName) {
1142
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1143
1144
                    if (!$calc->hasClass($subClassName)) {
1145
                        $calc->addClass($targetSubClass);
1146
1147
                        $newNodes[] = $targetSubClass;
1148
                    }
1149
1150
                    $calc->addDependency($targetSubClass, $class);
1151
                }
1152
            }
1153
        }
1154
1155
        return $calc->getCommitOrder();
0 ignored issues
show
The method getCommitOrder() does not exist on Doctrine\ORM\Internal\CommitOrderCalculator. ( Ignorable by Annotation )

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

1155
        return $calc->/** @scrutinizer ignore-call */ getCommitOrder();

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...
1156
    }
1157
1158
    /**
1159
     * Schedules an entity for insertion into the database.
1160
     * If the entity already has an identifier, it will be added to the identity map.
1161
     *
1162
     * @param object $entity the entity to schedule for insertion
1163
     *
1164
     * @throws ORMInvalidArgumentException
1165
     * @throws \InvalidArgumentException
1166
     */
1167
    public function scheduleForInsert($entity)
1168
    {
1169
        $oid = spl_object_hash($entity);
1170
1171
        if (isset($this->entityUpdates[$oid])) {
1172
            throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
1173
        }
1174
1175
        if (isset($this->entityDeletions[$oid])) {
1176
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1177
        }
1178
        if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) {
1179
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1180
        }
1181
1182
        if (isset($this->entityInsertions[$oid])) {
1183
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1184
        }
1185
1186
        $this->entityInsertions[$oid] = $entity;
1187
1188
        if (isset($this->entityIdentifiers[$oid])) {
1189
            $this->addToIdentityMap($entity);
1190
        }
1191
1192
        if ($entity instanceof NotifyPropertyChanged) {
1193
            $entity->addPropertyChangedListener($this);
1194
        }
1195
    }
1196
1197
    /**
1198
     * Checks whether an entity is scheduled for insertion.
1199
     *
1200
     * @param object $entity
1201
     *
1202
     * @return bool
1203
     */
1204
    public function isScheduledForInsert($entity)
1205
    {
1206
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1207
    }
1208
1209
    /**
1210
     * Schedules an entity for being updated.
1211
     *
1212
     * @param object $entity the entity to schedule for being updated
1213
     *
1214
     * @throws ORMInvalidArgumentException
1215
     */
1216
    public function scheduleForUpdate($entity)
1217
    {
1218
        $oid = spl_object_hash($entity);
1219
1220
        if (!isset($this->entityIdentifiers[$oid])) {
1221
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update');
1222
        }
1223
1224
        if (isset($this->entityDeletions[$oid])) {
1225
            throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update');
1226
        }
1227
1228
        if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) {
1229
            $this->entityUpdates[$oid] = $entity;
1230
        }
1231
    }
1232
1233
    /**
1234
     * INTERNAL:
1235
     * Schedules an extra update that will be executed immediately after the
1236
     * regular entity updates within the currently running commit cycle.
1237
     *
1238
     * Extra updates for entities are stored as (entity, changeset) tuples.
1239
     *
1240
     * @ignore
1241
     *
1242
     * @param object $entity    the entity for which to schedule an extra update
1243
     * @param array  $changeset the changeset of the entity (what to update)
1244
     */
1245
    public function scheduleExtraUpdate($entity, array $changeset)
1246
    {
1247
        $oid         = spl_object_hash($entity);
1248
        $extraUpdate = [$entity, $changeset];
1249
1250
        if (isset($this->extraUpdates[$oid])) {
1251
            list($ignored, $changeset2) = $this->extraUpdates[$oid];
1252
1253
            $extraUpdate = [$entity, $changeset + $changeset2];
1254
        }
1255
1256
        $this->extraUpdates[$oid] = $extraUpdate;
1257
    }
1258
1259
    /**
1260
     * Checks whether an entity is registered as dirty in the unit of work.
1261
     * Note: Is not very useful currently as dirty entities are only registered
1262
     * at commit time.
1263
     *
1264
     * @param object $entity
1265
     *
1266
     * @return bool
1267
     */
1268
    public function isScheduledForUpdate($entity)
1269
    {
1270
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1271
    }
1272
1273
    /**
1274
     * Checks whether an entity is registered to be checked in the unit of work.
1275
     *
1276
     * @param object $entity
1277
     *
1278
     * @return bool
1279
     */
1280
    public function isScheduledForDirtyCheck($entity)
1281
    {
1282
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1283
1284
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1285
    }
1286
1287
    /**
1288
     * INTERNAL:
1289
     * Schedules an entity for deletion.
1290
     *
1291
     * @param object $entity
1292
     */
1293
    public function scheduleForDelete($entity)
1294
    {
1295
        $oid = spl_object_hash($entity);
1296
1297
        if (isset($this->entityInsertions[$oid])) {
1298
            if ($this->isInIdentityMap($entity)) {
1299
                $this->removeFromIdentityMap($entity);
1300
            }
1301
1302
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1303
1304
            return; // entity has not been persisted yet, so nothing more to do.
1305
        }
1306
1307
        if (!$this->isInIdentityMap($entity)) {
1308
            return;
1309
        }
1310
1311
        $this->removeFromIdentityMap($entity);
1312
1313
        if (isset($this->entityUpdates[$oid])) {
1314
            unset($this->entityUpdates[$oid]);
1315
        }
1316
1317
        if (!isset($this->entityDeletions[$oid])) {
1318
            $this->entityDeletions[$oid] = $entity;
1319
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1320
        }
1321
    }
1322
1323
    /**
1324
     * Checks whether an entity is registered as removed/deleted with the unit
1325
     * of work.
1326
     *
1327
     * @param object $entity
1328
     *
1329
     * @return bool
1330
     */
1331
    public function isScheduledForDelete($entity)
1332
    {
1333
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1334
    }
1335
1336
    /**
1337
     * Checks whether an entity is scheduled for insertion, update or deletion.
1338
     *
1339
     * @param object $entity
1340
     *
1341
     * @return bool
1342
     */
1343
    public function isEntityScheduled($entity)
1344
    {
1345
        $oid = spl_object_hash($entity);
1346
1347
        return isset($this->entityInsertions[$oid])
1348
            || isset($this->entityUpdates[$oid])
1349
            || isset($this->entityDeletions[$oid]);
1350
    }
1351
1352
    /**
1353
     * INTERNAL:
1354
     * Registers an entity in the identity map.
1355
     * Note that entities in a hierarchy are registered with the class name of
1356
     * the root entity.
1357
     *
1358
     * @ignore
1359
     *
1360
     * @param object $entity the entity to register
1361
     *
1362
     * @return bool TRUE if the registration was successful, FALSE if the identity of
1363
     *              the entity in question is already managed
1364
     *
1365
     * @throws ORMInvalidArgumentException
1366
     */
1367
    public function addToIdentityMap($entity)
1368
    {
1369
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1370
        $idHash        = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]);
1371
1372
        if ('' === $idHash) {
1373
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1374
        }
1375
1376
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1377
1378
        if (isset($this->identityMap[$className][$idHash])) {
1379
            return false;
1380
        }
1381
1382
        $this->identityMap[$className][$idHash] = $entity;
1383
1384
        return true;
1385
    }
1386
1387
    /**
1388
     * Gets the state of an entity with regard to the current unit of work.
1389
     *
1390
     * @param object   $entity
1391
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1392
     *                         This parameter can be set to improve performance of entity state detection
1393
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1394
     *                         is either known or does not matter for the caller of the method.
1395
     *
1396
     * @return int the entity state
1397
     */
1398
    public function getEntityState($entity, $assume = null)
1399
    {
1400
        $oid = spl_object_hash($entity);
1401
1402
        if (isset($this->entityStates[$oid])) {
1403
            return $this->entityStates[$oid];
1404
        }
1405
1406
        if (null !== $assume) {
1407
            return $assume;
1408
        }
1409
1410
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1411
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1412
        // the UoW does not hold references to such objects and the object hash can be reused.
1413
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1414
        $class = $this->em->getClassMetadata(get_class($entity));
1415
        $id    = $class->getIdentifierValues($entity);
1416
1417
        if (!$id) {
1418
            return self::STATE_NEW;
1419
        }
1420
1421
        if ($class->containsForeignIdentifier) {
0 ignored issues
show
Accessing containsForeignIdentifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1422
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1423
        }
1424
1425
        switch (true) {
1426
            case $class->isIdentifierNatural():
0 ignored issues
show
The method isIdentifierNatural() does not exist on Doctrine\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

1426
            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...
1427
                // Check for a version field, if available, to avoid a db lookup.
1428
                if ($class->isVersioned) {
0 ignored issues
show
Accessing isVersioned on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1429
                    return ($class->getFieldValue($entity, $class->versionField))
0 ignored issues
show
The method getFieldValue() does not exist on Doctrine\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

1429
                    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...
Accessing versionField on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1430
                        ? self::STATE_DETACHED
1431
                        : self::STATE_NEW;
1432
                }
1433
1434
                // Last try before db lookup: check the identity map.
1435
                if ($this->tryGetById($id, $class->rootEntityName)) {
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1436
                    return self::STATE_DETACHED;
1437
                }
1438
1439
                // db lookup
1440
                if ($this->getEntityPersister($class->name)->exists($entity)) {
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1441
                    return self::STATE_DETACHED;
1442
                }
1443
1444
                return self::STATE_NEW;
1445
1446
            case  !$class->idGenerator->isPostInsertGenerator():
0 ignored issues
show
Accessing idGenerator on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1447
                // if we have a pre insert generator we can't be sure that having an id
1448
                // really means that the entity exists. We have to verify this through
1449
                // the last resort: a db lookup
1450
1451
                // Last try before db lookup: check the identity map.
1452
                if ($this->tryGetById($id, $class->rootEntityName)) {
1453
                    return self::STATE_DETACHED;
1454
                }
1455
1456
                // db lookup
1457
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1458
                    return self::STATE_DETACHED;
1459
                }
1460
1461
                return self::STATE_NEW;
1462
1463
            default:
1464
                return self::STATE_DETACHED;
1465
        }
1466
    }
1467
1468
    /**
1469
     * INTERNAL:
1470
     * Removes an entity from the identity map. This effectively detaches the
1471
     * entity from the persistence management of Doctrine.
1472
     *
1473
     * @ignore
1474
     *
1475
     * @param object $entity
1476
     *
1477
     * @return bool
1478
     *
1479
     * @throws ORMInvalidArgumentException
1480
     */
1481
    public function removeFromIdentityMap($entity)
1482
    {
1483
        $oid           = spl_object_hash($entity);
1484
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1485
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1486
1487
        if ('' === $idHash) {
1488
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
1489
        }
1490
1491
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1492
1493
        if (isset($this->identityMap[$className][$idHash])) {
1494
            unset($this->identityMap[$className][$idHash]);
1495
            unset($this->readOnlyObjects[$oid]);
1496
1497
            //$this->entityStates[$oid] = self::STATE_DETACHED;
1498
1499
            return true;
1500
        }
1501
1502
        return false;
1503
    }
1504
1505
    /**
1506
     * INTERNAL:
1507
     * Gets an entity in the identity map by its identifier hash.
1508
     *
1509
     * @ignore
1510
     *
1511
     * @param string $idHash
1512
     * @param string $rootClassName
1513
     *
1514
     * @return object
1515
     */
1516
    public function getByIdHash($idHash, $rootClassName)
1517
    {
1518
        return $this->identityMap[$rootClassName][$idHash];
1519
    }
1520
1521
    /**
1522
     * INTERNAL:
1523
     * Tries to get an entity by its identifier hash. If no entity is found for
1524
     * the given hash, FALSE is returned.
1525
     *
1526
     * @ignore
1527
     *
1528
     * @param mixed  $idHash        (must be possible to cast it to string)
1529
     * @param string $rootClassName
1530
     *
1531
     * @return object|bool the found entity or FALSE
1532
     */
1533
    public function tryGetByIdHash($idHash, $rootClassName)
1534
    {
1535
        $stringIdHash = (string) $idHash;
1536
1537
        if (isset($this->identityMap[$rootClassName][$stringIdHash])) {
1538
            return $this->identityMap[$rootClassName][$stringIdHash];
1539
        }
1540
1541
        return false;
1542
    }
1543
1544
    /**
1545
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1546
     *
1547
     * @param object $entity
1548
     *
1549
     * @return bool
1550
     */
1551
    public function isInIdentityMap($entity)
1552
    {
1553
        $oid = spl_object_hash($entity);
1554
1555
        if (!isset($this->entityIdentifiers[$oid])) {
1556
            return false;
1557
        }
1558
1559
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1560
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1561
1562
        if ('' === $idHash) {
1563
            return false;
1564
        }
1565
1566
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1567
    }
1568
1569
    /**
1570
     * INTERNAL:
1571
     * Checks whether an identifier hash exists in the identity map.
1572
     *
1573
     * @ignore
1574
     *
1575
     * @param string $idHash
1576
     * @param string $rootClassName
1577
     *
1578
     * @return bool
1579
     */
1580
    public function containsIdHash($idHash, $rootClassName)
1581
    {
1582
        return isset($this->identityMap[$rootClassName][$idHash]);
1583
    }
1584
1585
    /**
1586
     * Persists an entity as part of the current unit of work.
1587
     *
1588
     * @param object $entity the entity to persist
1589
     */
1590
    public function persist($entity)
1591
    {
1592
        $visited = [];
1593
1594
        $this->doPersist($entity, $visited);
1595
    }
1596
1597
    /**
1598
     * Persists an entity as part of the current unit of work.
1599
     *
1600
     * This method is internally called during persist() cascades as it tracks
1601
     * the already visited entities to prevent infinite recursions.
1602
     *
1603
     * @param object $entity  the entity to persist
1604
     * @param array  $visited the already visited entities
1605
     *
1606
     * @throws ORMInvalidArgumentException
1607
     * @throws UnexpectedValueException
1608
     */
1609
    private function doPersist($entity, array &$visited)
1610
    {
1611
        $oid = spl_object_hash($entity);
1612
1613
        if (isset($visited[$oid])) {
1614
            return; // Prevent infinite recursion
1615
        }
1616
1617
        $visited[$oid] = $entity; // Mark visited
1618
1619
        $class = $this->em->getClassMetadata(get_class($entity));
1620
1621
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1622
        // If we would detect DETACHED here we would throw an exception anyway with the same
1623
        // consequences (not recoverable/programming error), so just assuming NEW here
1624
        // lets us avoid some database lookups for entities with natural identifiers.
1625
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1626
1627
        switch ($entityState) {
1628
            case self::STATE_MANAGED:
1629
                // Nothing to do, except if policy is "deferred explicit"
1630
                if ($class->isChangeTrackingDeferredExplicit()) {
0 ignored issues
show
The method isChangeTrackingDeferredExplicit() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

1630
                if ($class->/** @scrutinizer ignore-call */ isChangeTrackingDeferredExplicit()) {
Loading history...
1631
                    $this->scheduleForDirtyCheck($entity);
1632
                }
1633
                break;
1634
1635
            case self::STATE_NEW:
1636
                $this->persistNew($class, $entity);
1637
                break;
1638
1639
            case self::STATE_REMOVED:
1640
                // Entity becomes managed again
1641
                unset($this->entityDeletions[$oid]);
1642
                $this->addToIdentityMap($entity);
1643
1644
                $this->entityStates[$oid] = self::STATE_MANAGED;
1645
                break;
1646
1647
            case self::STATE_DETACHED:
1648
                // Can actually not happen right now since we assume STATE_NEW.
1649
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted');
1650
            default:
1651
                throw new UnexpectedValueException("Unexpected entity state: $entityState.".self::objToStr($entity));
1652
        }
1653
1654
        $this->cascadePersist($entity, $visited);
1655
    }
1656
1657
    /**
1658
     * Deletes an entity as part of the current unit of work.
1659
     *
1660
     * @param object $entity the entity to remove
1661
     */
1662
    public function remove($entity)
1663
    {
1664
        $visited = [];
1665
1666
        $this->doRemove($entity, $visited);
1667
    }
1668
1669
    /**
1670
     * Deletes an entity as part of the current unit of work.
1671
     *
1672
     * This method is internally called during delete() cascades as it tracks
1673
     * the already visited entities to prevent infinite recursions.
1674
     *
1675
     * @param object $entity  the entity to delete
1676
     * @param array  $visited the map of the already visited entities
1677
     *
1678
     * @throws ORMInvalidArgumentException if the instance is a detached entity
1679
     * @throws UnexpectedValueException
1680
     */
1681
    private function doRemove($entity, array &$visited)
1682
    {
1683
        $oid = spl_object_hash($entity);
1684
1685
        if (isset($visited[$oid])) {
1686
            return; // Prevent infinite recursion
1687
        }
1688
1689
        $visited[$oid] = $entity; // mark visited
1690
1691
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1692
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1693
        $this->cascadeRemove($entity, $visited);
1694
1695
        $class       = $this->em->getClassMetadata(get_class($entity));
1696
        $entityState = $this->getEntityState($entity);
1697
1698
        switch ($entityState) {
1699
            case self::STATE_NEW:
1700
            case self::STATE_REMOVED:
1701
                // nothing to do
1702
                break;
1703
1704
            case self::STATE_MANAGED:
1705
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1706
1707
                if (ListenersInvoker::INVOKE_NONE !== $invoke) {
1708
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1709
                }
1710
1711
                $this->scheduleForDelete($entity);
1712
                break;
1713
1714
            case self::STATE_DETACHED:
1715
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed');
1716
            default:
1717
                throw new UnexpectedValueException("Unexpected entity state: $entityState.".self::objToStr($entity));
1718
        }
1719
    }
1720
1721
    /**
1722
     * Merges the state of the given detached entity into this UnitOfWork.
1723
     *
1724
     * @param object $entity
1725
     *
1726
     * @return object the managed copy of the entity
1727
     *
1728
     * @throws OptimisticLockException if the entity uses optimistic locking through a version
1729
     *                                 attribute and the version check against the managed copy fails
1730
     *
1731
     * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
1732
     */
1733
    public function merge($entity)
1734
    {
1735
        $visited = [];
1736
1737
        return $this->doMerge($entity, $visited);
1738
    }
1739
1740
    /**
1741
     * Executes a merge operation on an entity.
1742
     *
1743
     * @param object      $entity
1744
     * @param object|null $prevManagedCopy
1745
     * @param array|null  $assoc
1746
     *
1747
     * @return object the managed copy of the entity
1748
     *
1749
     * @throws OptimisticLockException     if the entity uses optimistic locking through a version
1750
     *                                     attribute and the version check against the managed copy fails
1751
     * @throws ORMInvalidArgumentException if the entity instance is NEW
1752
     * @throws EntityNotFoundException     if an assigned identifier is used in the entity, but none is provided
1753
     */
1754
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null)
1755
    {
1756
        $oid = spl_object_hash($entity);
1757
1758
        if (isset($visited[$oid])) {
1759
            $managedCopy = $visited[$oid];
1760
1761
            if (null !== $prevManagedCopy) {
1762
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
0 ignored issues
show
It seems like $assoc can also be of type null; however, parameter $association of Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() 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

1762
                $this->updateAssociationWithMergedEntity($entity, /** @scrutinizer ignore-type */ $assoc, $prevManagedCopy, $managedCopy);
Loading history...
1763
            }
1764
1765
            return $managedCopy;
1766
        }
1767
1768
        $class = $this->em->getClassMetadata(get_class($entity));
1769
1770
        // First we assume DETACHED, although it can still be NEW but we can avoid
1771
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1772
        // we need to fetch it from the db anyway in order to merge.
1773
        // MANAGED entities are ignored by the merge operation.
1774
        $managedCopy = $entity;
1775
1776
        if (self::STATE_MANAGED !== $this->getEntityState($entity, self::STATE_DETACHED)) {
1777
            // Try to look the entity up in the identity map.
1778
            $id = $class->getIdentifierValues($entity);
1779
1780
            // If there is no ID, it is actually NEW.
1781
            if (!$id) {
1782
                $managedCopy = $this->newInstance($class);
1783
1784
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1785
                $this->persistNew($class, $managedCopy);
1786
            } else {
1787
                $flatId = ($class->containsForeignIdentifier)
0 ignored issues
show
Accessing containsForeignIdentifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1788
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1789
                    : $id;
1790
1791
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1792
1793
                if ($managedCopy) {
1794
                    // We have the entity in-memory already, just make sure its not removed.
1795
                    if (self::STATE_REMOVED == $this->getEntityState($managedCopy)) {
1796
                        throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, 'merge');
0 ignored issues
show
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

1796
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, 'merge');
Loading history...
1797
                    }
1798
                } else {
1799
                    // We need to fetch the managed copy in order to merge.
1800
                    $managedCopy = $this->em->find($class->name, $flatId);
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1801
                }
1802
1803
                if (null === $managedCopy) {
1804
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1805
                    // since the managed entity was not found.
1806
                    if (!$class->isIdentifierNatural()) {
1807
                        throw EntityNotFoundException::fromClassNameAndIdentifier($class->getName(), $this->identifierFlattener->flattenIdentifier($class, $id));
1808
                    }
1809
1810
                    $managedCopy = $this->newInstance($class);
1811
                    $class->setIdentifierValues($managedCopy, $id);
0 ignored issues
show
The method setIdentifierValues() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

1811
                    $class->/** @scrutinizer ignore-call */ 
1812
                            setIdentifierValues($managedCopy, $id);
Loading history...
1812
1813
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1814
                    $this->persistNew($class, $managedCopy);
1815
                } else {
1816
                    $this->ensureVersionMatch($class, $entity, $managedCopy);
0 ignored issues
show
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

1816
                    $this->ensureVersionMatch($class, $entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1817
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
0 ignored issues
show
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

1817
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1818
                }
1819
            }
1820
1821
            $visited[$oid] = $managedCopy; // mark visited
1822
1823
            if ($class->isChangeTrackingDeferredExplicit()) {
1824
                $this->scheduleForDirtyCheck($entity);
1825
            }
1826
        }
1827
1828
        if (null !== $prevManagedCopy) {
1829
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
0 ignored issues
show
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

1829
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1830
        }
1831
1832
        // Mark the managed copy visited as well
1833
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
0 ignored issues
show
It seems like $managedCopy can also be of type null and true; however, parameter $object 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

1833
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
1834
1835
        $this->cascadeMerge($entity, $managedCopy, $visited);
0 ignored issues
show
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

1835
        $this->cascadeMerge($entity, /** @scrutinizer ignore-type */ $managedCopy, $visited);
Loading history...
1836
1837
        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...
1838
    }
1839
1840
    /**
1841
     * @param object $entity
1842
     * @param object $managedCopy
1843
     *
1844
     * @throws OptimisticLockException
1845
     */
1846
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
1847
    {
1848
        if (!($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
1849
            return;
1850
        }
1851
1852
        $reflField          = $class->reflFields[$class->versionField];
1853
        $managedCopyVersion = $reflField->getValue($managedCopy);
1854
        $entityVersion      = $reflField->getValue($entity);
1855
1856
        // Throw exception if versions don't match.
1857
        if ($managedCopyVersion == $entityVersion) {
1858
            return;
1859
        }
1860
1861
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
1862
    }
1863
1864
    /**
1865
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy.
1866
     *
1867
     * @param object $entity
1868
     *
1869
     * @return bool
1870
     */
1871
    private function isLoaded($entity)
1872
    {
1873
        return !($entity instanceof Proxy) || $entity->__isInitialized();
1874
    }
1875
1876
    /**
1877
     * Sets/adds associated managed copies into the previous entity's association field.
1878
     *
1879
     * @param object $entity
1880
     * @param object $previousManagedCopy
1881
     * @param object $managedCopy
1882
     */
1883
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
1884
    {
1885
        $assocField = $association['fieldName'];
1886
        $prevClass  = $this->em->getClassMetadata(get_class($previousManagedCopy));
1887
1888
        if ($association['type'] & ClassMetadata::TO_ONE) {
1889
            $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1890
1891
            return;
1892
        }
1893
1894
        $value   = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
1895
        $value[] = $managedCopy;
1896
1897
        if (ClassMetadata::ONE_TO_MANY == $association['type']) {
1898
            $class = $this->em->getClassMetadata(get_class($entity));
1899
1900
            $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
1901
        }
1902
    }
1903
1904
    /**
1905
     * Detaches an entity from the persistence management. It's persistence will
1906
     * no longer be managed by Doctrine.
1907
     *
1908
     * @param object $entity the entity to detach
1909
     */
1910
    public function detach($entity)
1911
    {
1912
        $visited = [];
1913
1914
        $this->doDetach($entity, $visited);
1915
    }
1916
1917
    /**
1918
     * Executes a detach operation on the given entity.
1919
     *
1920
     * @param object $entity
1921
     * @param bool   $noCascade if true, don't cascade detach operation
1922
     */
1923
    private function doDetach($entity, array &$visited, $noCascade = false)
1924
    {
1925
        $oid = spl_object_hash($entity);
1926
1927
        if (isset($visited[$oid])) {
1928
            return; // Prevent infinite recursion
1929
        }
1930
1931
        $visited[$oid] = $entity; // mark visited
1932
1933
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
1934
            case self::STATE_MANAGED:
1935
                if ($this->isInIdentityMap($entity)) {
1936
                    $this->removeFromIdentityMap($entity);
1937
                }
1938
1939
                unset(
1940
                    $this->entityInsertions[$oid],
1941
                    $this->entityUpdates[$oid],
1942
                    $this->entityDeletions[$oid],
1943
                    $this->entityIdentifiers[$oid],
1944
                    $this->entityStates[$oid],
1945
                    $this->originalEntityData[$oid]
1946
                );
1947
                break;
1948
            case self::STATE_NEW:
1949
            case self::STATE_DETACHED:
1950
                return;
1951
        }
1952
1953
        if (!$noCascade) {
1954
            $this->cascadeDetach($entity, $visited);
1955
        }
1956
    }
1957
1958
    /**
1959
     * Refreshes the state of the given entity from the database, overwriting
1960
     * any local, unpersisted changes.
1961
     *
1962
     * @param object $entity the entity to refresh
1963
     *
1964
     * @throws InvalidArgumentException if the entity is not MANAGED
1965
     */
1966
    public function refresh($entity)
1967
    {
1968
        $visited = [];
1969
1970
        $this->doRefresh($entity, $visited);
1971
    }
1972
1973
    /**
1974
     * Executes a refresh operation on an entity.
1975
     *
1976
     * @param object $entity  the entity to refresh
1977
     * @param array  $visited the already visited entities during cascades
1978
     *
1979
     * @throws ORMInvalidArgumentException if the entity is not MANAGED
1980
     */
1981
    private function doRefresh($entity, array &$visited)
1982
    {
1983
        $oid = spl_object_hash($entity);
1984
1985
        if (isset($visited[$oid])) {
1986
            return; // Prevent infinite recursion
1987
        }
1988
1989
        $visited[$oid] = $entity; // mark visited
1990
1991
        $class = $this->em->getClassMetadata(get_class($entity));
1992
1993
        if (self::STATE_MANAGED !== $this->getEntityState($entity)) {
1994
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1995
        }
1996
1997
        $this->getEntityPersister($class->name)->refresh(
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1998
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1999
            $entity
2000
        );
2001
2002
        $this->cascadeRefresh($entity, $visited);
2003
    }
2004
2005
    /**
2006
     * Cascades a refresh operation to associated entities.
2007
     *
2008
     * @param object $entity
2009
     */
2010
    private function cascadeRefresh($entity, array &$visited)
2011
    {
2012
        $class = $this->em->getClassMetadata(get_class($entity));
2013
2014
        $associationMappings = array_filter(
2015
            $class->associationMappings,
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2016
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2017
        );
2018
2019
        foreach ($associationMappings as $assoc) {
2020
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2021
2022
            switch (true) {
2023
                case $relatedEntities instanceof PersistentCollection:
2024
                    // Unwrap so that foreach() does not initialize
2025
                    $relatedEntities = $relatedEntities->unwrap();
2026
                    // break; is commented intentionally!
2027
2028
                    // no break
2029
                case $relatedEntities instanceof Collection:
2030
                case is_array($relatedEntities):
2031
                    foreach ($relatedEntities as $relatedEntity) {
2032
                        $this->doRefresh($relatedEntity, $visited);
2033
                    }
2034
                    break;
2035
2036
                case null !== $relatedEntities:
2037
                    $this->doRefresh($relatedEntities, $visited);
2038
                    break;
2039
2040
                default:
2041
                    // Do nothing
2042
            }
2043
        }
2044
    }
2045
2046
    /**
2047
     * Cascades a detach operation to associated entities.
2048
     *
2049
     * @param object $entity
2050
     */
2051
    private function cascadeDetach($entity, array &$visited)
2052
    {
2053
        $class = $this->em->getClassMetadata(get_class($entity));
2054
2055
        $associationMappings = array_filter(
2056
            $class->associationMappings,
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2057
            function ($assoc) { return $assoc['isCascadeDetach']; }
2058
        );
2059
2060
        foreach ($associationMappings as $assoc) {
2061
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2062
2063
            switch (true) {
2064
                case $relatedEntities instanceof PersistentCollection:
2065
                    // Unwrap so that foreach() does not initialize
2066
                    $relatedEntities = $relatedEntities->unwrap();
2067
                    // break; is commented intentionally!
2068
2069
                    // no break
2070
                case $relatedEntities instanceof Collection:
2071
                case is_array($relatedEntities):
2072
                    foreach ($relatedEntities as $relatedEntity) {
2073
                        $this->doDetach($relatedEntity, $visited);
2074
                    }
2075
                    break;
2076
2077
                case null !== $relatedEntities:
2078
                    $this->doDetach($relatedEntities, $visited);
2079
                    break;
2080
2081
                default:
2082
                    // Do nothing
2083
            }
2084
        }
2085
    }
2086
2087
    /**
2088
     * Cascades a merge operation to associated entities.
2089
     *
2090
     * @param object $entity
2091
     * @param object $managedCopy
2092
     */
2093
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2094
    {
2095
        $class = $this->em->getClassMetadata(get_class($entity));
2096
2097
        $associationMappings = array_filter(
2098
            $class->associationMappings,
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2099
            function ($assoc) { return $assoc['isCascadeMerge']; }
2100
        );
2101
2102
        foreach ($associationMappings as $assoc) {
2103
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2104
2105
            if ($relatedEntities instanceof Collection) {
2106
                if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2107
                    continue;
2108
                }
2109
2110
                if ($relatedEntities instanceof PersistentCollection) {
2111
                    // Unwrap so that foreach() does not initialize
2112
                    $relatedEntities = $relatedEntities->unwrap();
2113
                }
2114
2115
                foreach ($relatedEntities as $relatedEntity) {
2116
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2117
                }
2118
            } elseif (null !== $relatedEntities) {
2119
                $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2120
            }
2121
        }
2122
    }
2123
2124
    /**
2125
     * Cascades the save operation to associated entities.
2126
     *
2127
     * @param object $entity
2128
     */
2129
    private function cascadePersist($entity, array &$visited)
2130
    {
2131
        $class = $this->em->getClassMetadata(get_class($entity));
2132
2133
        $associationMappings = array_filter(
2134
            $class->associationMappings,
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2135
            function ($assoc) { return $assoc['isCascadePersist']; }
2136
        );
2137
2138
        foreach ($associationMappings as $assoc) {
2139
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2140
2141
            switch (true) {
2142
                case $relatedEntities instanceof PersistentCollection:
2143
                    // Unwrap so that foreach() does not initialize
2144
                    $relatedEntities = $relatedEntities->unwrap();
2145
                    // break; is commented intentionally!
2146
2147
                    // no break
2148
                case $relatedEntities instanceof Collection:
2149
                case is_array($relatedEntities):
2150
                    if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
2151
                        throw ORMInvalidArgumentException::invalidAssociation($this->em->getClassMetadata($assoc['targetEntity']), $assoc, $relatedEntities);
2152
                    }
2153
2154
                    foreach ($relatedEntities as $relatedEntity) {
2155
                        $this->doPersist($relatedEntity, $visited);
2156
                    }
2157
2158
                    break;
2159
2160
                case null !== $relatedEntities:
2161
                    if (!$relatedEntities instanceof $assoc['targetEntity']) {
2162
                        throw ORMInvalidArgumentException::invalidAssociation($this->em->getClassMetadata($assoc['targetEntity']), $assoc, $relatedEntities);
2163
                    }
2164
2165
                    $this->doPersist($relatedEntities, $visited);
2166
                    break;
2167
2168
                default:
2169
                    // Do nothing
2170
            }
2171
        }
2172
    }
2173
2174
    /**
2175
     * Cascades the delete operation to associated entities.
2176
     *
2177
     * @param object $entity
2178
     */
2179
    private function cascadeRemove($entity, array &$visited)
2180
    {
2181
        $class = $this->em->getClassMetadata(get_class($entity));
2182
2183
        $associationMappings = array_filter(
2184
            $class->associationMappings,
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2185
            function ($assoc) { return $assoc['isCascadeRemove']; }
2186
        );
2187
2188
        $entitiesToCascade = [];
2189
2190
        foreach ($associationMappings as $assoc) {
2191
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
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...
2192
                $entity->__load();
2193
            }
2194
2195
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2196
2197
            switch (true) {
2198
                case $relatedEntities instanceof Collection:
2199
                case is_array($relatedEntities):
2200
                    // If its a PersistentCollection initialization is intended! No unwrap!
2201
                    foreach ($relatedEntities as $relatedEntity) {
2202
                        $entitiesToCascade[] = $relatedEntity;
2203
                    }
2204
                    break;
2205
2206
                case null !== $relatedEntities:
2207
                    $entitiesToCascade[] = $relatedEntities;
2208
                    break;
2209
2210
                default:
2211
                    // Do nothing
2212
            }
2213
        }
2214
2215
        foreach ($entitiesToCascade as $relatedEntity) {
2216
            $this->doRemove($relatedEntity, $visited);
2217
        }
2218
    }
2219
2220
    /**
2221
     * Acquire a lock on the given entity.
2222
     *
2223
     * @param object $entity
2224
     * @param int    $lockMode
2225
     * @param int    $lockVersion
2226
     *
2227
     * @throws ORMInvalidArgumentException
2228
     * @throws TransactionRequiredException
2229
     * @throws OptimisticLockException
2230
     */
2231
    public function lock($entity, $lockMode, $lockVersion = null)
2232
    {
2233
        if (null === $entity) {
2234
            throw new \InvalidArgumentException('No entity passed to UnitOfWork#lock().');
2235
        }
2236
2237
        if (self::STATE_MANAGED != $this->getEntityState($entity, self::STATE_DETACHED)) {
2238
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2239
        }
2240
2241
        $class = $this->em->getClassMetadata(get_class($entity));
2242
2243
        switch (true) {
2244
            case LockMode::OPTIMISTIC === $lockMode:
2245
                if (!$class->isVersioned) {
2246
                    throw OptimisticLockException::notVersioned($class->name);
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2247
                }
2248
2249
                if (null === $lockVersion) {
2250
                    return;
2251
                }
2252
2253
                if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
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...
2254
                    $entity->__load();
2255
                }
2256
2257
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
0 ignored issues
show
Accessing versionField on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2258
2259
                if ($entityVersion != $lockVersion) {
2260
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2261
                }
2262
2263
                break;
2264
2265
            case LockMode::NONE === $lockMode:
2266
            case LockMode::PESSIMISTIC_READ === $lockMode:
2267
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2268
                if (!$this->em->getConnection()->isTransactionActive()) {
2269
                    throw TransactionRequiredException::transactionRequired();
2270
                }
2271
2272
                $oid = spl_object_hash($entity);
2273
2274
                $this->getEntityPersister($class->name)->lock(
2275
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2276
                    $lockMode
2277
                );
2278
                break;
2279
2280
            default:
2281
                // Do nothing
2282
        }
2283
    }
2284
2285
    /**
2286
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
2287
     *
2288
     * @return \Doctrine\ORM\Internal\CommitOrderCalculator
2289
     */
2290
    public function getCommitOrderCalculator()
2291
    {
2292
        if (null === $this->commitOrderCalculator) {
2293
            $this->commitOrderCalculator = new Internal\CommitOrderCalculator();
2294
        }
2295
2296
        return $this->commitOrderCalculator;
2297
    }
2298
2299
    /**
2300
     * Clears the UnitOfWork.
2301
     *
2302
     * @param string|null $entityName if given, only entities of this type will get detached
2303
     */
2304
    public function clear($entityName = null)
2305
    {
2306
        if (null === $entityName) {
2307
            $this->identityMap                 =
2308
            $this->entityIdentifiers           =
2309
            $this->originalEntityData          =
2310
            $this->entityChangeSets            =
2311
            $this->entityStates                =
2312
            $this->scheduledForSynchronization =
2313
            $this->entityInsertions            =
2314
            $this->entityUpdates               =
2315
            $this->entityDeletions             =
2316
            $this->collectionDeletions         =
2317
            $this->collectionUpdates           =
2318
            $this->extraUpdates                =
2319
            $this->readOnlyObjects             =
2320
            $this->visitedCollections          =
2321
            $this->orphanRemovals              = [];
2322
2323
            if (null !== $this->commitOrderCalculator) {
2324
                $this->commitOrderCalculator->clear();
0 ignored issues
show
The method clear() does not exist on Doctrine\ORM\Internal\CommitOrderCalculator. ( Ignorable by Annotation )

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

2324
                $this->commitOrderCalculator->/** @scrutinizer ignore-call */ 
2325
                                              clear();

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...
2325
            }
2326
        } else {
2327
            $this->clearIdentityMapForEntityName($entityName);
2328
            $this->clearEntityInsertionsForEntityName($entityName);
2329
        }
2330
2331
        if ($this->evm->hasListeners(Events::onClear)) {
2332
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
2333
        }
2334
    }
2335
2336
    /**
2337
     * INTERNAL:
2338
     * Schedules an orphaned entity for removal. The remove() operation will be
2339
     * invoked on that entity at the beginning of the next commit of this
2340
     * UnitOfWork.
2341
     *
2342
     * @ignore
2343
     *
2344
     * @param object $entity
2345
     */
2346
    public function scheduleOrphanRemoval($entity)
2347
    {
2348
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2349
    }
2350
2351
    /**
2352
     * INTERNAL:
2353
     * Schedules a complete collection for removal when this UnitOfWork commits.
2354
     */
2355
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2356
    {
2357
        $coid = spl_object_hash($coll);
2358
2359
        // TODO: if $coll is already scheduled for recreation ... what to do?
2360
        // Just remove $coll from the scheduled recreations?
2361
        if (isset($this->collectionUpdates[$coid])) {
2362
            unset($this->collectionUpdates[$coid]);
2363
        }
2364
2365
        $this->collectionDeletions[$coid] = $coll;
2366
    }
2367
2368
    /**
2369
     * @return bool
2370
     */
2371
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2372
    {
2373
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2374
    }
2375
2376
    /**
2377
     * @param ClassMetadata $class
2378
     *
2379
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
2380
     */
2381
    private function newInstance($class)
2382
    {
2383
        $entity = $class->newInstance();
2384
2385
        if ($entity instanceof \Doctrine\Common\Persistence\ObjectManagerAware) {
2386
            $entity->injectObjectManager($this->em, $class);
2387
        }
2388
2389
        return $entity;
2390
    }
2391
2392
    /**
2393
     * INTERNAL:
2394
     * Creates an entity. Used for reconstitution of persistent entities.
2395
     *
2396
     * Internal note: Highly performance-sensitive method.
2397
     *
2398
     * @ignore
2399
     *
2400
     * @param string $className the name of the entity class
2401
     * @param array  $data      the data for the entity
2402
     * @param array  $hints     any hints to account for during reconstitution/lookup of the entity
2403
     *
2404
     * @return object the managed entity instance
2405
     *
2406
     * @todo Rename: getOrCreateEntity
2407
     */
2408
    public function createEntity($className, array $data, &$hints = [])
2409
    {
2410
        $class = $this->em->getClassMetadata($className);
2411
        //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]);
2412
2413
        $id     = $this->identifierFlattener->flattenIdentifier($class, $data);
2414
        $idHash = implode(' ', $id);
2415
2416
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2417
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
2418
            $oid    = spl_object_hash($entity);
2419
2420
            if (
2421
                isset($hints[Query::HINT_REFRESH])
2422
                && isset($hints[Query::HINT_REFRESH_ENTITY])
2423
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
2424
                && $unmanagedProxy instanceof Proxy
2425
                && $this->isIdentifierEquals($unmanagedProxy, $entity)
2426
            ) {
2427
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
2428
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
2429
                // refreshed object may be anything
2430
2431
                foreach ($class->identifier as $fieldName) {
0 ignored issues
show
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2432
                    $class->reflFields[$fieldName]->setValue($unmanagedProxy, null);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2433
                }
2434
2435
                return $unmanagedProxy;
2436
            }
2437
2438
            if ($entity instanceof Proxy && !$entity->__isInitialized()) {
2439
                $entity->__setInitialized(true);
2440
2441
                $overrideLocalValues = true;
2442
2443
                if ($entity instanceof NotifyPropertyChanged) {
2444
                    $entity->addPropertyChangedListener($this);
2445
                }
2446
            } else {
2447
                $overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
2448
2449
                // If only a specific entity is set to refresh, check that it's the one
2450
                if (isset($hints[Query::HINT_REFRESH_ENTITY])) {
2451
                    $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity;
2452
                }
2453
            }
2454
2455
            if ($overrideLocalValues) {
2456
                // inject ObjectManager upon refresh.
2457
                if ($entity instanceof ObjectManagerAware) {
2458
                    $entity->injectObjectManager($this->em, $class);
2459
                }
2460
2461
                $this->originalEntityData[$oid] = $data;
2462
            }
2463
        } else {
2464
            $entity = $this->newInstance($class);
2465
            $oid    = spl_object_hash($entity);
2466
2467
            $this->entityIdentifiers[$oid]  = $id;
2468
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2469
            $this->originalEntityData[$oid] = $data;
2470
2471
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
2472
2473
            if ($entity instanceof NotifyPropertyChanged) {
2474
                $entity->addPropertyChangedListener($this);
2475
            }
2476
2477
            $overrideLocalValues = true;
2478
        }
2479
2480
        if (!$overrideLocalValues) {
2481
            return $entity;
2482
        }
2483
2484
        foreach ($data as $field => $value) {
2485
            if (isset($class->fieldMappings[$field])) {
0 ignored issues
show
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2486
                $class->reflFields[$field]->setValue($entity, $value);
2487
            }
2488
        }
2489
2490
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2491
        unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2492
2493
        if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && !$this->eagerLoadingEntities[$class->rootEntityName]) {
2494
            unset($this->eagerLoadingEntities[$class->rootEntityName]);
2495
        }
2496
2497
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2498
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2499
            return $entity;
2500
        }
2501
2502
        foreach ($class->associationMappings as $field => $assoc) {
2503
            // Check if the association is not among the fetch-joined associations already.
2504
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2505
                continue;
2506
            }
2507
2508
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2509
2510
            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...
2511
                case $assoc['type'] & ClassMetadata::TO_ONE:
2512
                    if (!$assoc['isOwningSide']) {
2513
                        // use the given entity association
2514
                        if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2515
                            $this->originalEntityData[$oid][$field] = $data[$field];
2516
2517
                            $class->reflFields[$field]->setValue($entity, $data[$field]);
2518
                            $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
2519
2520
                            continue 2;
2521
                        }
2522
2523
                        // Inverse side of x-to-one can never be lazy
2524
                        $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
2525
2526
                        continue 2;
2527
                    }
2528
2529
                    // use the entity association
2530
                    if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2531
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2532
                        $this->originalEntityData[$oid][$field] = $data[$field];
2533
2534
                        break;
2535
                    }
2536
2537
                    $associatedId = [];
2538
2539
                    // TODO: Is this even computed right in all cases of composite keys?
2540
                    foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
2541
                        $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null;
2542
2543
                        if (null !== $joinColumnValue) {
2544
                            if ($targetClass->containsForeignIdentifier) {
0 ignored issues
show
Accessing containsForeignIdentifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2545
                                $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
0 ignored issues
show
The method getFieldForColumn() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

2545
                                $associatedId[$targetClass->/** @scrutinizer ignore-call */ getFieldForColumn($targetColumn)] = $joinColumnValue;
Loading history...
2546
                            } else {
2547
                                $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
0 ignored issues
show
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2548
                            }
2549
                        } elseif ($targetClass->containsForeignIdentifier
2550
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2551
                        ) {
2552
                            // the missing key is part of target's entity primary key
2553
                            $associatedId = [];
2554
                            break;
2555
                        }
2556
                    }
2557
2558
                    if (!$associatedId) {
2559
                        // Foreign key is NULL
2560
                        $class->reflFields[$field]->setValue($entity, null);
2561
                        $this->originalEntityData[$oid][$field] = null;
2562
2563
                        break;
2564
                    }
2565
2566
                    if (!isset($hints['fetchMode'][$class->name][$field])) {
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2567
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2568
                    }
2569
2570
                    // Foreign key is set
2571
                    // Check identity map first
2572
                    // FIXME: Can break easily with composite keys if join column values are in
2573
                    //        wrong order. The correct order is the one in ClassMetadata#identifier.
2574
                    $relatedIdHash = implode(' ', $associatedId);
2575
2576
                    switch (true) {
2577
                        case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]):
2578
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2579
2580
                            // If this is an uninitialized proxy, we are deferring eager loads,
2581
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2582
                            // then we can append this entity for eager loading!
2583
                            if (ClassMetadata::FETCH_EAGER == $hints['fetchMode'][$class->name][$field] &&
2584
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2585
                                !$targetClass->isIdentifierComposite &&
0 ignored issues
show
Accessing isIdentifierComposite on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2586
                                $newValue instanceof Proxy &&
2587
                                false === $newValue->__isInitialized__) {
2588
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2589
                            }
2590
2591
                            break;
2592
2593
                        case $targetClass->subClasses:
2594
                            // If it might be a subtype, it can not be lazy. There isn't even
2595
                            // a way to solve this with deferred eager loading, which means putting
2596
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2597
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2598
                            break;
2599
2600
                        default:
2601
                            switch (true) {
2602
                                // We are negating the condition here. Other cases will assume it is valid!
2603
                                case ClassMetadata::FETCH_EAGER !== $hints['fetchMode'][$class->name][$field]:
2604
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2605
                                    break;
2606
2607
                                // Deferred eager load only works for single identifier classes
2608
                                case isset($hints[self::HINT_DEFEREAGERLOAD]) && !$targetClass->isIdentifierComposite:
2609
                                    // TODO: Is there a faster approach?
2610
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2611
2612
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2613
                                    break;
2614
2615
                                default:
2616
                                    // TODO: This is very imperformant, ignore it?
2617
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2618
                                    break;
2619
                            }
2620
2621
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2622
                            $newValueOid                                                     = spl_object_hash($newValue);
2623
                            $this->entityIdentifiers[$newValueOid]                           = $associatedId;
2624
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2625
2626
                            if (
2627
                                $newValue instanceof NotifyPropertyChanged &&
2628
                                (!$newValue instanceof Proxy || $newValue->__isInitialized())
2629
                            ) {
2630
                                $newValue->addPropertyChangedListener($this);
2631
                            }
2632
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2633
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2634
                            break;
2635
                    }
2636
2637
                    $this->originalEntityData[$oid][$field] = $newValue;
2638
                    $class->reflFields[$field]->setValue($entity, $newValue);
2639
2640
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2641
                        $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2642
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2643
                    }
2644
2645
                    break;
2646
2647
                default:
2648
                    // Ignore if its a cached collection
2649
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2650
                        break;
2651
                    }
2652
2653
                    // use the given collection
2654
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2655
                        $data[$field]->setOwner($entity, $assoc);
2656
2657
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2658
                        $this->originalEntityData[$oid][$field] = $data[$field];
2659
2660
                        break;
2661
                    }
2662
2663
                    // Inject collection
2664
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
2665
                    $pColl->setOwner($entity, $assoc);
2666
                    $pColl->setInitialized(false);
2667
2668
                    $reflField = $class->reflFields[$field];
2669
                    $reflField->setValue($entity, $pColl);
2670
2671
                    if (ClassMetadata::FETCH_EAGER == $assoc['fetch']) {
2672
                        $this->loadCollection($pColl);
2673
                        $pColl->takeSnapshot();
2674
                    }
2675
2676
                    $this->originalEntityData[$oid][$field] = $pColl;
2677
                    break;
2678
            }
2679
        }
2680
2681
        if ($overrideLocalValues) {
0 ignored issues
show
The condition $overrideLocalValues is always true.
Loading history...
2682
            // defer invoking of postLoad event to hydration complete step
2683
            $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2684
        }
2685
2686
        return $entity;
2687
    }
2688
2689
    public function triggerEagerLoads()
2690
    {
2691
        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...
2692
            return;
2693
        }
2694
2695
        // avoid infinite recursion
2696
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2697
        $this->eagerLoadingEntities = [];
2698
2699
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2700
            if (!$ids) {
2701
                continue;
2702
            }
2703
2704
            $class = $this->em->getClassMetadata($entityName);
2705
2706
            $this->getEntityPersister($entityName)->loadAll(
2707
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2708
            );
2709
        }
2710
    }
2711
2712
    /**
2713
     * Initializes (loads) an uninitialized persistent collection of an entity.
2714
     *
2715
     * @param \Doctrine\ORM\PersistentCollection $collection the collection to initialize
2716
     *
2717
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2718
     */
2719
    public function loadCollection(PersistentCollection $collection)
2720
    {
2721
        $assoc     = $collection->getMapping();
2722
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2723
2724
        switch ($assoc['type']) {
2725
            case ClassMetadata::ONE_TO_MANY:
2726
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2727
                break;
2728
2729
            case ClassMetadata::MANY_TO_MANY:
2730
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2731
                break;
2732
        }
2733
2734
        $collection->setInitialized(true);
2735
    }
2736
2737
    /**
2738
     * Gets the identity map of the UnitOfWork.
2739
     *
2740
     * @return array
2741
     */
2742
    public function getIdentityMap()
2743
    {
2744
        return $this->identityMap;
2745
    }
2746
2747
    /**
2748
     * Gets the original data of an entity. The original data is the data that was
2749
     * present at the time the entity was reconstituted from the database.
2750
     *
2751
     * @param object $entity
2752
     *
2753
     * @return array
2754
     */
2755
    public function getOriginalEntityData($entity)
2756
    {
2757
        $oid = spl_object_hash($entity);
2758
2759
        if (isset($this->originalEntityData[$oid])) {
2760
            return $this->originalEntityData[$oid];
2761
        }
2762
2763
        return [];
2764
    }
2765
2766
    /**
2767
     * @ignore
2768
     *
2769
     * @param object $entity
2770
     */
2771
    public function setOriginalEntityData($entity, array $data)
2772
    {
2773
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2774
    }
2775
2776
    /**
2777
     * INTERNAL:
2778
     * Sets a property value of the original data array of an entity.
2779
     *
2780
     * @ignore
2781
     *
2782
     * @param string $oid
2783
     * @param string $property
2784
     * @param mixed  $value
2785
     */
2786
    public function setOriginalEntityProperty($oid, $property, $value)
2787
    {
2788
        $this->originalEntityData[$oid][$property] = $value;
2789
    }
2790
2791
    /**
2792
     * Gets the identifier of an entity.
2793
     * The returned value is always an array of identifier values. If the entity
2794
     * has a composite identifier then the identifier values are in the same
2795
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2796
     *
2797
     * @param object $entity
2798
     *
2799
     * @return array the identifier values
2800
     */
2801
    public function getEntityIdentifier($entity)
2802
    {
2803
        return $this->entityIdentifiers[spl_object_hash($entity)];
2804
    }
2805
2806
    /**
2807
     * Processes an entity instance to extract their identifier values.
2808
     *
2809
     * @param object $entity the entity instance
2810
     *
2811
     * @return mixed a scalar value
2812
     *
2813
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
2814
     */
2815
    public function getSingleIdentifierValue($entity)
2816
    {
2817
        $class = $this->em->getClassMetadata(get_class($entity));
2818
2819
        if ($class->isIdentifierComposite) {
0 ignored issues
show
Accessing isIdentifierComposite on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2820
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2821
        }
2822
2823
        $values = $this->isInIdentityMap($entity)
2824
            ? $this->getEntityIdentifier($entity)
2825
            : $class->getIdentifierValues($entity);
2826
2827
        return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null;
0 ignored issues
show
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2828
    }
2829
2830
    /**
2831
     * Tries to find an entity with the given identifier in the identity map of
2832
     * this UnitOfWork.
2833
     *
2834
     * @param mixed  $id            the entity identifier to look for
2835
     * @param string $rootClassName the name of the root class of the mapped entity hierarchy
2836
     *
2837
     * @return object|bool returns the entity with the specified identifier if it exists in
2838
     *                     this UnitOfWork, FALSE otherwise
2839
     */
2840
    public function tryGetById($id, $rootClassName)
2841
    {
2842
        $idHash = implode(' ', (array) $id);
2843
2844
        if (isset($this->identityMap[$rootClassName][$idHash])) {
2845
            return $this->identityMap[$rootClassName][$idHash];
2846
        }
2847
2848
        return false;
2849
    }
2850
2851
    /**
2852
     * Schedules an entity for dirty-checking at commit-time.
2853
     *
2854
     * @param object $entity the entity to schedule for dirty-checking
2855
     *
2856
     * @todo Rename: scheduleForSynchronization
2857
     */
2858
    public function scheduleForDirtyCheck($entity)
2859
    {
2860
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2861
2862
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
2863
    }
2864
2865
    /**
2866
     * Checks whether the UnitOfWork has any pending insertions.
2867
     *
2868
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise
2869
     */
2870
    public function hasPendingInsertions()
2871
    {
2872
        return !empty($this->entityInsertions);
2873
    }
2874
2875
    /**
2876
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2877
     * number of entities in the identity map.
2878
     *
2879
     * @return int
2880
     */
2881
    public function size()
2882
    {
2883
        $countArray = array_map(function ($item) { return count($item); }, $this->identityMap);
2884
2885
        return array_sum($countArray);
2886
    }
2887
2888
    /**
2889
     * Gets the EntityPersister for an Entity.
2890
     *
2891
     * @param string $entityName the name of the Entity
2892
     *
2893
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
2894
     */
2895
    public function getEntityPersister($entityName)
2896
    {
2897
        if (isset($this->persisters[$entityName])) {
2898
            return $this->persisters[$entityName];
2899
        }
2900
2901
        $class = $this->em->getClassMetadata($entityName);
2902
2903
        switch (true) {
2904
            case $class->isInheritanceTypeNone():
0 ignored issues
show
The method isInheritanceTypeNone() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

2904
            case $class->/** @scrutinizer ignore-call */ isInheritanceTypeNone():
Loading history...
2905
                $persister = new BasicEntityPersister($this->em, $class);
2906
                break;
2907
2908
            case $class->isInheritanceTypeSingleTable():
0 ignored issues
show
The method isInheritanceTypeSingleTable() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

2908
            case $class->/** @scrutinizer ignore-call */ isInheritanceTypeSingleTable():
Loading history...
2909
                $persister = new SingleTablePersister($this->em, $class);
2910
                break;
2911
2912
            case $class->isInheritanceTypeJoined():
0 ignored issues
show
The method isInheritanceTypeJoined() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

2912
            case $class->/** @scrutinizer ignore-call */ isInheritanceTypeJoined():
Loading history...
2913
                $persister = new JoinedSubclassPersister($this->em, $class);
2914
                break;
2915
2916
            default:
2917
                throw new \RuntimeException('No persister found for entity.');
2918
        }
2919
2920
        if ($this->hasCache && null !== $class->cache) {
2921
            $persister = $this->em->getConfiguration()
2922
                ->getSecondLevelCacheConfiguration()
2923
                ->getCacheFactory()
2924
                ->buildCachedEntityPersister($this->em, $persister, $class);
2925
        }
2926
2927
        $this->persisters[$entityName] = $persister;
2928
2929
        return $this->persisters[$entityName];
2930
    }
2931
2932
    /**
2933
     * Gets a collection persister for a collection-valued association.
2934
     *
2935
     * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister
2936
     */
2937
    public function getCollectionPersister(array $association)
2938
    {
2939
        $role = isset($association['cache'])
2940
            ? $association['sourceEntity'].'::'.$association['fieldName']
2941
            : $association['type'];
2942
2943
        if (isset($this->collectionPersisters[$role])) {
2944
            return $this->collectionPersisters[$role];
2945
        }
2946
2947
        $persister = ClassMetadata::ONE_TO_MANY === $association['type']
2948
            ? new OneToManyPersister($this->em)
2949
            : new ManyToManyPersister($this->em);
2950
2951
        if ($this->hasCache && isset($association['cache'])) {
2952
            $persister = $this->em->getConfiguration()
2953
                ->getSecondLevelCacheConfiguration()
2954
                ->getCacheFactory()
2955
                ->buildCachedCollectionPersister($this->em, $persister, $association);
2956
        }
2957
2958
        $this->collectionPersisters[$role] = $persister;
2959
2960
        return $this->collectionPersisters[$role];
2961
    }
2962
2963
    /**
2964
     * INTERNAL:
2965
     * Registers an entity as managed.
2966
     *
2967
     * @param object $entity the entity
2968
     * @param array  $id     the identifier values
2969
     * @param array  $data   the original entity data
2970
     */
2971
    public function registerManaged($entity, array $id, array $data)
2972
    {
2973
        $oid = spl_object_hash($entity);
2974
2975
        $this->entityIdentifiers[$oid]  = $id;
2976
        $this->entityStates[$oid]       = self::STATE_MANAGED;
2977
        $this->originalEntityData[$oid] = $data;
2978
2979
        $this->addToIdentityMap($entity);
2980
2981
        if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) {
2982
            $entity->addPropertyChangedListener($this);
2983
        }
2984
    }
2985
2986
    /**
2987
     * INTERNAL:
2988
     * Clears the property changeset of the entity with the given OID.
2989
     *
2990
     * @param string $oid the entity's OID
2991
     */
2992
    public function clearEntityChangeSet($oid)
2993
    {
2994
        $this->entityChangeSets[$oid] = [];
2995
    }
2996
2997
    /* PropertyChangedListener implementation */
2998
2999
    /**
3000
     * Notifies this UnitOfWork of a property change in an entity.
3001
     *
3002
     * @param object $entity       the entity that owns the property
3003
     * @param string $propertyName the name of the property that changed
3004
     * @param mixed  $oldValue     the old value of the property
3005
     * @param mixed  $newValue     the new value of the property
3006
     */
3007
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
3008
    {
3009
        $oid   = spl_object_hash($entity);
3010
        $class = $this->em->getClassMetadata(get_class($entity));
3011
3012
        $isAssocField = isset($class->associationMappings[$propertyName]);
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3013
3014
        if (!$isAssocField && !isset($class->fieldMappings[$propertyName])) {
0 ignored issues
show
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3015
            return; // ignore non-persistent fields
3016
        }
3017
3018
        // Update changeset and mark entity for synchronization
3019
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
3020
3021
        if (!isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
0 ignored issues
show
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3022
            $this->scheduleForDirtyCheck($entity);
3023
        }
3024
    }
3025
3026
    /**
3027
     * Gets the currently scheduled entity insertions in this UnitOfWork.
3028
     *
3029
     * @return array
3030
     */
3031
    public function getScheduledEntityInsertions()
3032
    {
3033
        return $this->entityInsertions;
3034
    }
3035
3036
    /**
3037
     * Gets the currently scheduled entity updates in this UnitOfWork.
3038
     *
3039
     * @return array
3040
     */
3041
    public function getScheduledEntityUpdates()
3042
    {
3043
        return $this->entityUpdates;
3044
    }
3045
3046
    /**
3047
     * Gets the currently scheduled entity deletions in this UnitOfWork.
3048
     *
3049
     * @return array
3050
     */
3051
    public function getScheduledEntityDeletions()
3052
    {
3053
        return $this->entityDeletions;
3054
    }
3055
3056
    /**
3057
     * Gets the currently scheduled complete collection deletions.
3058
     *
3059
     * @return array
3060
     */
3061
    public function getScheduledCollectionDeletions()
3062
    {
3063
        return $this->collectionDeletions;
3064
    }
3065
3066
    /**
3067
     * Gets the currently scheduled collection inserts, updates and deletes.
3068
     *
3069
     * @return array
3070
     */
3071
    public function getScheduledCollectionUpdates()
3072
    {
3073
        return $this->collectionUpdates;
3074
    }
3075
3076
    /**
3077
     * Helper method to initialize a lazy loading proxy or persistent collection.
3078
     *
3079
     * @param object $obj
3080
     */
3081
    public function initializeObject($obj)
3082
    {
3083
        if ($obj instanceof Proxy) {
3084
            $obj->__load();
3085
3086
            return;
3087
        }
3088
3089
        if ($obj instanceof PersistentCollection) {
3090
            $obj->initialize();
3091
        }
3092
    }
3093
3094
    /**
3095
     * Helper method to show an object as string.
3096
     *
3097
     * @param object $obj
3098
     *
3099
     * @return string
3100
     */
3101
    private static function objToStr($obj)
3102
    {
3103
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj).'@'.spl_object_hash($obj);
3104
    }
3105
3106
    /**
3107
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3108
     *
3109
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3110
     * on this object that might be necessary to perform a correct update.
3111
     *
3112
     * @param object $object
3113
     *
3114
     * @throws ORMInvalidArgumentException
3115
     */
3116
    public function markReadOnly($object)
3117
    {
3118
        if (!is_object($object) || !$this->isInIdentityMap($object)) {
3119
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3120
        }
3121
3122
        $this->readOnlyObjects[spl_object_hash($object)] = true;
3123
    }
3124
3125
    /**
3126
     * Is this entity read only?
3127
     *
3128
     * @param object $object
3129
     *
3130
     * @return bool
3131
     *
3132
     * @throws ORMInvalidArgumentException
3133
     */
3134
    public function isReadOnly($object)
3135
    {
3136
        if (!is_object($object)) {
3137
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3138
        }
3139
3140
        return isset($this->readOnlyObjects[spl_object_hash($object)]);
3141
    }
3142
3143
    /**
3144
     * Perform whatever processing is encapsulated here after completion of the transaction.
3145
     */
3146
    private function afterTransactionComplete()
3147
    {
3148
        if (!$this->hasCache) {
3149
            return;
3150
        }
3151
3152
        foreach ($this->persisters as $persister) {
3153
            if ($persister instanceof CachedPersister) {
3154
                $persister->afterTransactionComplete();
3155
            }
3156
        }
3157
3158
        foreach ($this->collectionPersisters as $persister) {
3159
            if ($persister instanceof CachedPersister) {
3160
                $persister->afterTransactionComplete();
3161
            }
3162
        }
3163
    }
3164
3165
    /**
3166
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
3167
     */
3168
    private function afterTransactionRolledBack()
3169
    {
3170
        if (!$this->hasCache) {
3171
            return;
3172
        }
3173
3174
        foreach ($this->persisters as $persister) {
3175
            if ($persister instanceof CachedPersister) {
3176
                $persister->afterTransactionRolledBack();
3177
            }
3178
        }
3179
3180
        foreach ($this->collectionPersisters as $persister) {
3181
            if ($persister instanceof CachedPersister) {
3182
                $persister->afterTransactionRolledBack();
3183
            }
3184
        }
3185
    }
3186
3187
    private function dispatchOnFlushEvent()
3188
    {
3189
        if ($this->evm->hasListeners(Events::onFlush)) {
3190
            $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3191
        }
3192
    }
3193
3194
    private function dispatchPostFlushEvent()
3195
    {
3196
        if ($this->evm->hasListeners(Events::postFlush)) {
3197
            $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3198
        }
3199
    }
3200
3201
    /**
3202
     * Verifies if two given entities actually are the same based on identifier comparison.
3203
     *
3204
     * @param object $entity1
3205
     * @param object $entity2
3206
     *
3207
     * @return bool
3208
     */
3209
    private function isIdentifierEquals($entity1, $entity2)
3210
    {
3211
        if ($entity1 === $entity2) {
3212
            return true;
3213
        }
3214
3215
        $class = $this->em->getClassMetadata(get_class($entity1));
3216
3217
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
3218
            return false;
3219
        }
3220
3221
        $oid1 = spl_object_hash($entity1);
3222
        $oid2 = spl_object_hash($entity2);
3223
3224
        $id1 = isset($this->entityIdentifiers[$oid1])
3225
            ? $this->entityIdentifiers[$oid1]
3226
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
3227
        $id2 = isset($this->entityIdentifiers[$oid2])
3228
            ? $this->entityIdentifiers[$oid2]
3229
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
3230
3231
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
3232
    }
3233
3234
    /**
3235
     * @param object $entity
3236
     * @param object $managedCopy
3237
     *
3238
     * @throws ORMException
3239
     * @throws OptimisticLockException
3240
     * @throws TransactionRequiredException
3241
     */
3242
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
3243
    {
3244
        if (!$this->isLoaded($entity)) {
3245
            return;
3246
        }
3247
3248
        if (!$this->isLoaded($managedCopy)) {
3249
            $managedCopy->__load();
3250
        }
3251
3252
        $class = $this->em->getClassMetadata(get_class($entity));
3253
3254
        foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
3255
            $name = $prop->name;
3256
3257
            $prop->setAccessible(true);
3258
3259
            if (!isset($class->associationMappings[$name])) {
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3260
                if (!$class->isIdentifier($name)) {
3261
                    $prop->setValue($managedCopy, $prop->getValue($entity));
3262
                }
3263
            } else {
3264
                $assoc2 = $class->associationMappings[$name];
3265
3266
                if ($assoc2['type'] & ClassMetadata::TO_ONE) {
3267
                    $other = $prop->getValue($entity);
3268
                    if (null === $other) {
3269
                        $prop->setValue($managedCopy, null);
3270
                    } else {
3271
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
3272
                            // do not merge fields marked lazy that have not been fetched.
3273
                            continue;
3274
                        }
3275
3276
                        if (!$assoc2['isCascadeMerge']) {
3277
                            if (self::STATE_DETACHED === $this->getEntityState($other)) {
3278
                                $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
3279
                                $relatedId   = $targetClass->getIdentifierValues($other);
3280
3281
                                if ($targetClass->subClasses) {
0 ignored issues
show
Accessing subClasses on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3282
                                    $other = $this->em->find($targetClass->name, $relatedId);
0 ignored issues
show
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3283
                                } else {
3284
                                    $other = $this->em->getProxyFactory()->getProxy(
3285
                                        $assoc2['targetEntity'],
3286
                                        $relatedId
3287
                                    );
3288
                                    $this->registerManaged($other, $relatedId, []);
3289
                                }
3290
                            }
3291
3292
                            $prop->setValue($managedCopy, $other);
3293
                        }
3294
                    }
3295
                } else {
3296
                    $mergeCol = $prop->getValue($entity);
3297
3298
                    if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) {
3299
                        // do not merge fields marked lazy that have not been fetched.
3300
                        // keep the lazy persistent collection of the managed copy.
3301
                        continue;
3302
                    }
3303
3304
                    $managedCol = $prop->getValue($managedCopy);
3305
3306
                    if (!$managedCol) {
3307
                        $managedCol = new PersistentCollection(
3308
                            $this->em,
3309
                            $this->em->getClassMetadata($assoc2['targetEntity']),
3310
                            new ArrayCollection()
3311
                        );
3312
                        $managedCol->setOwner($managedCopy, $assoc2);
3313
                        $prop->setValue($managedCopy, $managedCol);
3314
                    }
3315
3316
                    if ($assoc2['isCascadeMerge']) {
3317
                        $managedCol->initialize();
3318
3319
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
3320
                        if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
3321
                            $managedCol->unwrap()->clear();
3322
                            $managedCol->setDirty(true);
3323
3324
                            if ($assoc2['isOwningSide']
3325
                                && ClassMetadata::MANY_TO_MANY == $assoc2['type']
3326
                                && $class->isChangeTrackingNotify()
3327
                            ) {
3328
                                $this->scheduleForDirtyCheck($managedCopy);
3329
                            }
3330
                        }
3331
                    }
3332
                }
3333
            }
3334
3335
            if ($class->isChangeTrackingNotify()) {
3336
                // Just treat all properties as changed, there is no other choice.
3337
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
3338
            }
3339
        }
3340
    }
3341
3342
    /**
3343
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
3344
     * Unit of work able to fire deferred events, related to loading events here.
3345
     *
3346
     * @internal should be called internally from object hydrators
3347
     */
3348
    public function hydrationComplete()
3349
    {
3350
        $this->hydrationCompleteHandler->hydrationComplete();
3351
    }
3352
3353
    /**
3354
     * @param string $entityName
3355
     */
3356
    private function clearIdentityMapForEntityName($entityName)
3357
    {
3358
        if (!isset($this->identityMap[$entityName])) {
3359
            return;
3360
        }
3361
3362
        $visited = [];
3363
3364
        foreach ($this->identityMap[$entityName] as $entity) {
3365
            $this->doDetach($entity, $visited, false);
3366
        }
3367
    }
3368
3369
    /**
3370
     * @param string $entityName
3371
     */
3372
    private function clearEntityInsertionsForEntityName($entityName)
3373
    {
3374
        foreach ($this->entityInsertions as $hash => $entity) {
3375
            if (get_class($entity) === $entityName) {
3376
                unset($this->entityInsertions[$hash]);
3377
            }
3378
        }
3379
    }
3380
}
3381