Failed Conditions
Push — master ( 2ade86...13f838 )
by Jonathan
18s
created

lib/Doctrine/ORM/UnitOfWork.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\NotifyPropertyChanged;
25
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
26
use Doctrine\Common\Persistence\ObjectManagerAware;
27
use Doctrine\Common\PropertyChangedListener;
28
use Doctrine\DBAL\LockMode;
29
use Doctrine\ORM\Cache\Persister\CachedPersister;
30
use Doctrine\ORM\Event\LifecycleEventArgs;
31
use Doctrine\ORM\Event\ListenersInvoker;
32
use Doctrine\ORM\Event\OnFlushEventArgs;
33
use Doctrine\ORM\Event\PostFlushEventArgs;
34
use Doctrine\ORM\Event\PreFlushEventArgs;
35
use Doctrine\ORM\Event\PreUpdateEventArgs;
36
use Doctrine\ORM\Internal\HydrationCompleteHandler;
37
use Doctrine\ORM\Mapping\ClassMetadata;
38
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
39
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
40
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
41
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
42
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
43
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
44
use Doctrine\ORM\Proxy\Proxy;
45
use Doctrine\ORM\Utility\IdentifierFlattener;
46
use InvalidArgumentException;
47
use Throwable;
48
use UnexpectedValueException;
49
50
/**
51
 * The UnitOfWork is responsible for tracking changes to objects during an
52
 * "object-level" transaction and for writing out changes to the database
53
 * in the correct order.
54
 *
55
 * Internal note: This class contains highly performance-sensitive code.
56
 *
57
 * @since       2.0
58
 * @author      Benjamin Eberlei <[email protected]>
59
 * @author      Guilherme Blanco <[email protected]>
60
 * @author      Jonathan Wage <[email protected]>
61
 * @author      Roman Borschel <[email protected]>
62
 * @author      Rob Caiger <[email protected]>
63
 */
64
class UnitOfWork implements PropertyChangedListener
65
{
66
    /**
67
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
68
     */
69
    const STATE_MANAGED = 1;
70
71
    /**
72
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
73
     * and is not (yet) managed by an EntityManager.
74
     */
75
    const STATE_NEW = 2;
76
77
    /**
78
     * A detached entity is an instance with persistent state and identity that is not
79
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
80
     */
81
    const STATE_DETACHED = 3;
82
83
    /**
84
     * A removed entity instance is an instance with a persistent identity,
85
     * associated with an EntityManager, whose persistent state will be deleted
86
     * on commit.
87
     */
88
    const STATE_REMOVED = 4;
89
90
    /**
91
     * Hint used to collect all primary keys of associated entities during hydration
92
     * and execute it in a dedicated query afterwards
93
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
94
     */
95
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
96
97
    /**
98
     * The identity map that holds references to all managed entities that have
99
     * an identity. The entities are grouped by their class name.
100
     * Since all classes in a hierarchy must share the same identifier set,
101
     * we always take the root class name of the hierarchy.
102
     *
103
     * @var array
104
     */
105
    private $identityMap = [];
106
107
    /**
108
     * Map of all identifiers of managed entities.
109
     * Keys are object ids (spl_object_hash).
110
     *
111
     * @var array
112
     */
113
    private $entityIdentifiers = [];
114
115
    /**
116
     * Map of the original entity data of managed entities.
117
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
118
     * at commit time.
119
     *
120
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
121
     *                A value will only really be copied if the value in the entity is modified
122
     *                by the user.
123
     *
124
     * @var array
125
     */
126
    private $originalEntityData = [];
127
128
    /**
129
     * Map of entity changes. Keys are object ids (spl_object_hash).
130
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
131
     *
132
     * @var array
133
     */
134
    private $entityChangeSets = [];
135
136
    /**
137
     * The (cached) states of any known entities.
138
     * Keys are object ids (spl_object_hash).
139
     *
140
     * @var array
141
     */
142
    private $entityStates = [];
143
144
    /**
145
     * Map of entities that are scheduled for dirty checking at commit time.
146
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
147
     * Keys are object ids (spl_object_hash).
148
     *
149
     * @var array
150
     */
151
    private $scheduledForSynchronization = [];
152
153
    /**
154
     * A list of all pending entity insertions.
155
     *
156
     * @var array
157
     */
158
    private $entityInsertions = [];
159
160
    /**
161
     * A list of all pending entity updates.
162
     *
163
     * @var array
164
     */
165
    private $entityUpdates = [];
166
167
    /**
168
     * Any pending extra updates that have been scheduled by persisters.
169
     *
170
     * @var array
171
     */
172
    private $extraUpdates = [];
173
174
    /**
175
     * A list of all pending entity deletions.
176
     *
177
     * @var array
178
     */
179
    private $entityDeletions = [];
180
181
    /**
182
     * New entities that were discovered through relationships that were not
183
     * marked as cascade-persist. During flush, this array is populated and
184
     * then pruned of any entities that were discovered through a valid
185
     * cascade-persist path. (Leftovers cause an error.)
186
     *
187
     * Keys are OIDs, payload is a two-item array describing the association
188
     * and the entity.
189
     *
190
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
191
     */
192
    private $nonCascadedNewDetectedEntities = [];
193
194
    /**
195
     * All pending collection deletions.
196
     *
197
     * @var array
198
     */
199
    private $collectionDeletions = [];
200
201
    /**
202
     * All pending collection updates.
203
     *
204
     * @var array
205
     */
206
    private $collectionUpdates = [];
207
208
    /**
209
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
210
     * At the end of the UnitOfWork all these collections will make new snapshots
211
     * of their data.
212
     *
213
     * @var array
214
     */
215
    private $visitedCollections = [];
216
217
    /**
218
     * The EntityManager that "owns" this UnitOfWork instance.
219
     *
220
     * @var EntityManagerInterface
221
     */
222
    private $em;
223
224
    /**
225
     * The entity persister instances used to persist entity instances.
226
     *
227
     * @var array
228
     */
229
    private $persisters = [];
230
231
    /**
232
     * The collection persister instances used to persist collections.
233
     *
234
     * @var array
235
     */
236
    private $collectionPersisters = [];
237
238
    /**
239
     * The EventManager used for dispatching events.
240
     *
241
     * @var \Doctrine\Common\EventManager
242
     */
243
    private $evm;
244
245
    /**
246
     * The ListenersInvoker used for dispatching events.
247
     *
248
     * @var \Doctrine\ORM\Event\ListenersInvoker
249
     */
250
    private $listenersInvoker;
251
252
    /**
253
     * The IdentifierFlattener used for manipulating identifiers
254
     *
255
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
256
     */
257
    private $identifierFlattener;
258
259
    /**
260
     * Orphaned entities that are scheduled for removal.
261
     *
262
     * @var array
263
     */
264
    private $orphanRemovals = [];
265
266
    /**
267
     * Read-Only objects are never evaluated
268
     *
269
     * @var array
270
     */
271
    private $readOnlyObjects = [];
272
273
    /**
274
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
275
     *
276
     * @var array
277
     */
278
    private $eagerLoadingEntities = [];
279
280
    /**
281
     * @var boolean
282
     */
283
    protected $hasCache = false;
284
285
    /**
286
     * Helper for handling completion of hydration
287
     *
288
     * @var HydrationCompleteHandler
289
     */
290
    private $hydrationCompleteHandler;
291
292
    /**
293
     * @var ReflectionPropertiesGetter
294
     */
295
    private $reflectionPropertiesGetter;
296
297
    /**
298
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
299
     *
300
     * @param EntityManagerInterface $em
301
     */
302 2413
    public function __construct(EntityManagerInterface $em)
303
    {
304 2413
        $this->em                         = $em;
305 2413
        $this->evm                        = $em->getEventManager();
306 2413
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2413
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2413
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2413
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2413
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2413
    }
312
313
    /**
314
     * Commits the UnitOfWork, executing all operations that have been postponed
315
     * up to this point. The state of all managed entities will be synchronized with
316
     * the database.
317
     *
318
     * The operations are executed in the following order:
319
     *
320
     * 1) All entity insertions
321
     * 2) All entity updates
322
     * 3) All collection deletions
323
     * 4) All collection updates
324
     * 5) All entity deletions
325
     *
326
     * @param null|object|array $entity
327
     *
328
     * @return void
329
     *
330
     * @throws \Exception
331
     */
332 1051
    public function commit($entity = null)
333
    {
334
        // Raise preFlush
335 1051
        if ($this->evm->hasListeners(Events::preFlush)) {
336 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
337
        }
338
339
        // Compute changes done since last commit.
340 1051
        if (null === $entity) {
341 1041
            $this->computeChangeSets();
342 18
        } elseif (is_object($entity)) {
343 16
            $this->computeSingleEntityChangeSet($entity);
344 2
        } elseif (is_array($entity)) {
345 2
            foreach ($entity as $object) {
346 2
                $this->computeSingleEntityChangeSet($object);
347
            }
348
        }
349
350 1050
        if ( ! ($this->entityInsertions ||
351 170
                $this->entityDeletions ||
352 134
                $this->entityUpdates ||
353 41
                $this->collectionUpdates ||
354 37
                $this->collectionDeletions ||
355 1050
                $this->orphanRemovals)) {
356 25
            $this->dispatchOnFlushEvent();
357 25
            $this->dispatchPostFlushEvent();
358
359 25
            return; // Nothing to do.
360
        }
361
362 1046
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
363
364 1044
        if ($this->orphanRemovals) {
365 16
            foreach ($this->orphanRemovals as $orphan) {
366 16
                $this->remove($orphan);
367
            }
368
        }
369
370 1044
        $this->dispatchOnFlushEvent();
371
372
        // Now we need a commit order to maintain referential integrity
373 1044
        $commitOrder = $this->getCommitOrder();
374
375 1044
        $conn = $this->em->getConnection();
376 1044
        $conn->beginTransaction();
377
378
        try {
379
            // Collection deletions (deletions of complete collections)
380 1044
            foreach ($this->collectionDeletions as $collectionToDelete) {
381 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
382
            }
383
384 1044
            if ($this->entityInsertions) {
385 1040
                foreach ($commitOrder as $class) {
386 1040
                    $this->executeInserts($class);
387
                }
388
            }
389
390 1043
            if ($this->entityUpdates) {
391 119
                foreach ($commitOrder as $class) {
392 119
                    $this->executeUpdates($class);
393
                }
394
            }
395
396
            // Extra updates that were requested by persisters.
397 1039
            if ($this->extraUpdates) {
398 44
                $this->executeExtraUpdates();
399
            }
400
401
            // Collection updates (deleteRows, updateRows, insertRows)
402 1039
            foreach ($this->collectionUpdates as $collectionToUpdate) {
403 535
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
404
            }
405
406
            // Entity deletions come last and need to be in reverse commit order
407 1039
            if ($this->entityDeletions) {
408 63
                for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) {
409 63
                    $this->executeDeletions($commitOrder[$i]);
410
                }
411
            }
412
413 1039
            $conn->commit();
414 11
        } catch (Throwable $e) {
415 11
            $this->em->close();
416 11
            $conn->rollBack();
417
418 11
            $this->afterTransactionRolledBack();
419
420 11
            throw $e;
421
        }
422
423 1039
        $this->afterTransactionComplete();
424
425
        // Take new snapshots from visited collections
426 1039
        foreach ($this->visitedCollections as $coll) {
427 534
            $coll->takeSnapshot();
428
        }
429
430 1039
        $this->dispatchPostFlushEvent();
431
432 1038
        $this->postCommitCleanup($entity);
433 1038
    }
434
435
    /**
436
     * @param null|object|object[] $entity
437
     */
438 1038
    private function postCommitCleanup($entity) : void
439
    {
440 1038
        $this->entityInsertions =
441 1038
        $this->entityUpdates =
442 1038
        $this->entityDeletions =
443 1038
        $this->extraUpdates =
444 1038
        $this->collectionUpdates =
445 1038
        $this->nonCascadedNewDetectedEntities =
446 1038
        $this->collectionDeletions =
447 1038
        $this->visitedCollections =
448 1038
        $this->orphanRemovals = [];
449
450 1038
        if (null === $entity) {
451 1029
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
452
453 1029
            return;
454
        }
455
456 15
        $entities = \is_object($entity)
457 13
            ? [$entity]
458 15
            : $entity;
459
460 15
        foreach ($entities as $object) {
461 15
            $oid = \spl_object_hash($object);
462
463 15
            $this->clearEntityChangeSet($oid);
464
465 15
            unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]);
466
        }
467 15
    }
468
469
    /**
470
     * Computes the changesets of all entities scheduled for insertion.
471
     *
472
     * @return void
473
     */
474 1050
    private function computeScheduleInsertsChangeSets()
475
    {
476 1050
        foreach ($this->entityInsertions as $entity) {
477 1042
            $class = $this->em->getClassMetadata(get_class($entity));
478
479 1042
            $this->computeChangeSet($class, $entity);
480
        }
481 1050
    }
482
483
    /**
484
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
485
     *
486
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
487
     * 2. Read Only entities are skipped.
488
     * 3. Proxies are skipped.
489
     * 4. Only if entity is properly managed.
490
     *
491
     * @param object $entity
492
     *
493
     * @return void
494
     *
495
     * @throws \InvalidArgumentException
496
     */
497 18
    private function computeSingleEntityChangeSet($entity)
498
    {
499 18
        $state = $this->getEntityState($entity);
500
501 18
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
502 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
503
        }
504
505 17
        $class = $this->em->getClassMetadata(get_class($entity));
506
507 17
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
508 16
            $this->persist($entity);
509
        }
510
511
        // Compute changes for INSERTed entities first. This must always happen even in this case.
512 17
        $this->computeScheduleInsertsChangeSets();
513
514 17
        if ($class->isReadOnly) {
515
            return;
516
        }
517
518
        // Ignore uninitialized proxy objects
519 17
        if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
520 2
            return;
521
        }
522
523
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
524 15
        $oid = spl_object_hash($entity);
525
526 15 View Code Duplication
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
527 6
            $this->computeChangeSet($class, $entity);
528
        }
529 15
    }
530
531
    /**
532
     * Executes any extra updates that have been scheduled.
533
     */
534 44
    private function executeExtraUpdates()
535
    {
536 44
        foreach ($this->extraUpdates as $oid => $update) {
537 44
            list ($entity, $changeset) = $update;
538
539 44
            $this->entityChangeSets[$oid] = $changeset;
540 44
            $this->getEntityPersister(get_class($entity))->update($entity);
541
        }
542
543 44
        $this->extraUpdates = [];
544 44
    }
545
546
    /**
547
     * Gets the changeset for an entity.
548
     *
549
     * @param object $entity
550
     *
551
     * @return array
552
     */
553 1039
    public function & getEntityChangeSet($entity)
554
    {
555 1039
        $oid  = spl_object_hash($entity);
556 1039
        $data = [];
557
558 1039
        if (!isset($this->entityChangeSets[$oid])) {
559 3
            return $data;
560
        }
561
562 1039
        return $this->entityChangeSets[$oid];
563
    }
564
565
    /**
566
     * Computes the changes that happened to a single entity.
567
     *
568
     * Modifies/populates the following properties:
569
     *
570
     * {@link _originalEntityData}
571
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
572
     * then it was not fetched from the database and therefore we have no original
573
     * entity data yet. All of the current entity data is stored as the original entity data.
574
     *
575
     * {@link _entityChangeSets}
576
     * The changes detected on all properties of the entity are stored there.
577
     * A change is a tuple array where the first entry is the old value and the second
578
     * entry is the new value of the property. Changesets are used by persisters
579
     * to INSERT/UPDATE the persistent entity state.
580
     *
581
     * {@link _entityUpdates}
582
     * If the entity is already fully MANAGED (has been fetched from the database before)
583
     * and any changes to its properties are detected, then a reference to the entity is stored
584
     * there to mark it for an update.
585
     *
586
     * {@link _collectionDeletions}
587
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
588
     * then this collection is marked for deletion.
589
     *
590
     * @ignore
591
     *
592
     * @internal Don't call from the outside.
593
     *
594
     * @param ClassMetadata $class  The class descriptor of the entity.
595
     * @param object        $entity The entity for which to compute the changes.
596
     *
597
     * @return void
598
     */
599 1052
    public function computeChangeSet(ClassMetadata $class, $entity)
600
    {
601 1052
        $oid = spl_object_hash($entity);
602
603 1052
        if (isset($this->readOnlyObjects[$oid])) {
604 2
            return;
605
        }
606
607 1052
        if ( ! $class->isInheritanceTypeNone()) {
608 323
            $class = $this->em->getClassMetadata(get_class($entity));
609
        }
610
611 1052
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
612
613 1052 View Code Duplication
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
614 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
615
        }
616
617 1052
        $actualData = [];
618
619 1052
        foreach ($class->reflFields as $name => $refProp) {
620 1052
            $value = $refProp->getValue($entity);
621
622 1052
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
623 792
                if ($value instanceof PersistentCollection) {
624 202
                    if ($value->getOwner() === $entity) {
625 202
                        continue;
626
                    }
627
628 5
                    $value = new ArrayCollection($value->getValues());
629
                }
630
631
                // If $value is not a Collection then use an ArrayCollection.
632 787
                if ( ! $value instanceof Collection) {
633 242
                    $value = new ArrayCollection($value);
634
                }
635
636 787
                $assoc = $class->associationMappings[$name];
637
638
                // Inject PersistentCollection
639 787
                $value = new PersistentCollection(
640 787
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
641
                );
642 787
                $value->setOwner($entity, $assoc);
643 787
                $value->setDirty( ! $value->isEmpty());
644
645 787
                $class->reflFields[$name]->setValue($entity, $value);
646
647 787
                $actualData[$name] = $value;
648
649 787
                continue;
650
            }
651
652 1052
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
653 1052
                $actualData[$name] = $value;
654
            }
655
        }
656
657 1052
        if ( ! isset($this->originalEntityData[$oid])) {
658
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
659
            // These result in an INSERT.
660 1048
            $this->originalEntityData[$oid] = $actualData;
661 1048
            $changeSet = [];
662
663 1048
            foreach ($actualData as $propName => $actualValue) {
664 1026 View Code Duplication
                if ( ! isset($class->associationMappings[$propName])) {
665 974
                    $changeSet[$propName] = [null, $actualValue];
666
667 974
                    continue;
668
                }
669
670 916
                $assoc = $class->associationMappings[$propName];
671
672 916
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
673 916
                    $changeSet[$propName] = [null, $actualValue];
674
                }
675
            }
676
677 1048
            $this->entityChangeSets[$oid] = $changeSet;
678
        } else {
679
            // Entity is "fully" MANAGED: it was already fully persisted before
680
            // and we have a copy of the original data
681 270
            $originalData           = $this->originalEntityData[$oid];
682 270
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
683 270
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
684
                ? $this->entityChangeSets[$oid]
685 270
                : [];
686
687 270
            foreach ($actualData as $propName => $actualValue) {
688
                // skip field, its a partially omitted one!
689 255
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
690 8
                    continue;
691
                }
692
693 255
                $orgValue = $originalData[$propName];
694
695
                // skip if value haven't changed
696 255
                if ($orgValue === $actualValue) {
697 239
                    continue;
698
                }
699
700
                // if regular field
701 115 View Code Duplication
                if ( ! isset($class->associationMappings[$propName])) {
702 60
                    if ($isChangeTrackingNotify) {
703
                        continue;
704
                    }
705
706 60
                    $changeSet[$propName] = [$orgValue, $actualValue];
707
708 60
                    continue;
709
                }
710
711 59
                $assoc = $class->associationMappings[$propName];
712
713
                // Persistent collection was exchanged with the "originally"
714
                // created one. This can only mean it was cloned and replaced
715
                // on another entity.
716 59
                if ($actualValue instanceof PersistentCollection) {
717 8
                    $owner = $actualValue->getOwner();
718 8
                    if ($owner === null) { // cloned
719
                        $actualValue->setOwner($entity, $assoc);
720 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
721
                        if (!$actualValue->isInitialized()) {
722
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
723
                        }
724
                        $newValue = clone $actualValue;
725
                        $newValue->setOwner($entity, $assoc);
726
                        $class->reflFields[$propName]->setValue($entity, $newValue);
727
                    }
728
                }
729
730 59
                if ($orgValue instanceof PersistentCollection) {
731
                    // A PersistentCollection was de-referenced, so delete it.
732 8
                    $coid = spl_object_hash($orgValue);
733
734 8
                    if (isset($this->collectionDeletions[$coid])) {
735
                        continue;
736
                    }
737
738 8
                    $this->collectionDeletions[$coid] = $orgValue;
739 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
740
741 8
                    continue;
742
                }
743
744 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
745 50
                    if ($assoc['isOwningSide']) {
746 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
747
                    }
748
749 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
750 51
                        $this->scheduleOrphanRemoval($orgValue);
751
                    }
752
                }
753
            }
754
755 270
            if ($changeSet) {
756 88
                $this->entityChangeSets[$oid]   = $changeSet;
757 88
                $this->originalEntityData[$oid] = $actualData;
758 88
                $this->entityUpdates[$oid]      = $entity;
759
            }
760
        }
761
762
        // Look for changes in associations of the entity
763 1052
        foreach ($class->associationMappings as $field => $assoc) {
764 916
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
765 644
                continue;
766
            }
767
768 887
            $this->computeAssociationChanges($assoc, $val);
769
770 881
            if ( ! isset($this->entityChangeSets[$oid]) &&
771 881
                $assoc['isOwningSide'] &&
772 881
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
773 881
                $val instanceof PersistentCollection &&
774 881
                $val->isDirty()) {
775
776 35
                $this->entityChangeSets[$oid]   = [];
777 35
                $this->originalEntityData[$oid] = $actualData;
778 881
                $this->entityUpdates[$oid]      = $entity;
779
            }
780
        }
781 1046
    }
782
783
    /**
784
     * Computes all the changes that have been done to entities and collections
785
     * since the last commit and stores these changes in the _entityChangeSet map
786
     * temporarily for access by the persisters, until the UoW commit is finished.
787
     *
788
     * @return void
789
     */
790 1041
    public function computeChangeSets()
791
    {
792
        // Compute changes for INSERTed entities first. This must always happen.
793 1041
        $this->computeScheduleInsertsChangeSets();
794
795
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
796 1041
        foreach ($this->identityMap as $className => $entities) {
797 462
            $class = $this->em->getClassMetadata($className);
798
799
            // Skip class if instances are read-only
800 462
            if ($class->isReadOnly) {
801 1
                continue;
802
            }
803
804
            // If change tracking is explicit or happens through notification, then only compute
805
            // changes on entities of that type that are explicitly marked for synchronization.
806
            switch (true) {
807 461
                case ($class->isChangeTrackingDeferredImplicit()):
808 459
                    $entitiesToProcess = $entities;
809 459
                    break;
810
811 3
                case (isset($this->scheduledForSynchronization[$className])):
812 3
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
813 3
                    break;
814
815
                default:
816 1
                    $entitiesToProcess = [];
817
818
            }
819
820 461
            foreach ($entitiesToProcess as $entity) {
821
                // Ignore uninitialized proxy objects
822 441
                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
823 36
                    continue;
824
                }
825
826
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
827 440
                $oid = spl_object_hash($entity);
828
829 440 View Code Duplication
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
830 461
                    $this->computeChangeSet($class, $entity);
831
                }
832
            }
833
        }
834 1041
    }
835
836
    /**
837
     * Computes the changes of an association.
838
     *
839
     * @param array $assoc The association mapping.
840
     * @param mixed $value The value of the association.
841
     *
842
     * @throws ORMInvalidArgumentException
843
     * @throws ORMException
844
     *
845
     * @return void
846
     */
847 887
    private function computeAssociationChanges($assoc, $value)
848
    {
849 887
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
850 29
            return;
851
        }
852
853 886
        if ($value instanceof PersistentCollection && $value->isDirty()) {
854 537
            $coid = spl_object_hash($value);
855
856 537
            $this->collectionUpdates[$coid] = $value;
857 537
            $this->visitedCollections[$coid] = $value;
858
        }
859
860
        // Look through the entities, and in any of their associations,
861
        // for transient (new) entities, recursively. ("Persistence by reachability")
862
        // Unwrap. Uninitialized collections will simply be empty.
863 886
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
864 886
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
865
866 886
        foreach ($unwrappedValue as $key => $entry) {
867 738
            if (! ($entry instanceof $targetClass->name)) {
868 6
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
869
            }
870
871 732
            $state = $this->getEntityState($entry, self::STATE_NEW);
872
873 732
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
874
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
875
            }
876
877
            switch ($state) {
878 732
                case self::STATE_NEW:
879 42
                    if ( ! $assoc['isCascadePersist']) {
880
                        /*
881
                         * For now just record the details, because this may
882
                         * not be an issue if we later discover another pathway
883
                         * through the object-graph where cascade-persistence
884
                         * is enabled for this object.
885
                         */
886 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
887
888 6
                        break;
889
                    }
890
891 37
                    $this->persistNew($targetClass, $entry);
892 37
                    $this->computeChangeSet($targetClass, $entry);
893
894 37
                    break;
895
896 724
                case self::STATE_REMOVED:
897
                    // Consume the $value as array (it's either an array or an ArrayAccess)
898
                    // and remove the element from Collection.
899 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
900 3
                        unset($value[$key]);
901
                    }
902 4
                    break;
903
904 724
                case self::STATE_DETACHED:
905
                    // Can actually not happen right now as we assume STATE_NEW,
906
                    // so the exception will be raised from the DBAL layer (constraint violation).
907
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
908
                    break;
909
910 732
                default:
911
                    // MANAGED associated entities are already taken into account
912
                    // during changeset calculation anyway, since they are in the identity map.
913
            }
914
        }
915 880
    }
916
917
    /**
918
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
919
     * @param object                              $entity
920
     *
921
     * @return void
922
     */
923 1071
    private function persistNew($class, $entity)
924
    {
925 1071
        $oid    = spl_object_hash($entity);
926 1071
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
927
928 1071 View Code Duplication
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
929 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
930
        }
931
932 1071
        $idGen = $class->idGenerator;
933
934 1071
        if ( ! $idGen->isPostInsertGenerator()) {
935 282
            $idValue = $idGen->generate($this->em, $entity);
936
937 282
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
938 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
939
940 2
                $class->setIdentifierValues($entity, $idValue);
941
            }
942
943 282
            $this->entityIdentifiers[$oid] = $idValue;
944
        }
945
946 1071
        $this->entityStates[$oid] = self::STATE_MANAGED;
947
948 1071
        $this->scheduleForInsert($entity);
949 1071
    }
950
951
    /**
952
     * INTERNAL:
953
     * Computes the changeset of an individual entity, independently of the
954
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
955
     *
956
     * The passed entity must be a managed entity. If the entity already has a change set
957
     * because this method is invoked during a commit cycle then the change sets are added.
958
     * whereby changes detected in this method prevail.
959
     *
960
     * @ignore
961
     *
962
     * @param ClassMetadata $class  The class descriptor of the entity.
963
     * @param object        $entity The entity for which to (re)calculate the change set.
964
     *
965
     * @return void
966
     *
967
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
968
     */
969 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
970
    {
971 16
        $oid = spl_object_hash($entity);
972
973 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
974
            throw ORMInvalidArgumentException::entityNotManaged($entity);
975
        }
976
977
        // skip if change tracking is "NOTIFY"
978 16
        if ($class->isChangeTrackingNotify()) {
979
            return;
980
        }
981
982 16
        if ( ! $class->isInheritanceTypeNone()) {
983 3
            $class = $this->em->getClassMetadata(get_class($entity));
984
        }
985
986 16
        $actualData = [];
987
988 16
        foreach ($class->reflFields as $name => $refProp) {
989 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
990 16
                && ($name !== $class->versionField)
991 16
                && ! $class->isCollectionValuedAssociation($name)) {
992 16
                $actualData[$name] = $refProp->getValue($entity);
993
            }
994
        }
995
996 16
        if ( ! isset($this->originalEntityData[$oid])) {
997
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
998
        }
999
1000 16
        $originalData = $this->originalEntityData[$oid];
1001 16
        $changeSet = [];
1002
1003 16
        foreach ($actualData as $propName => $actualValue) {
1004 16
            $orgValue = $originalData[$propName] ?? null;
1005
1006 16
            if ($orgValue !== $actualValue) {
1007 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1008
            }
1009
        }
1010
1011 16
        if ($changeSet) {
1012 7
            if (isset($this->entityChangeSets[$oid])) {
1013 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1014 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1015 1
                $this->entityChangeSets[$oid] = $changeSet;
1016 1
                $this->entityUpdates[$oid]    = $entity;
1017
            }
1018 7
            $this->originalEntityData[$oid] = $actualData;
1019
        }
1020 16
    }
1021
1022
    /**
1023
     * Executes all entity insertions for entities of the specified type.
1024
     *
1025
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1026
     *
1027
     * @return void
1028
     */
1029 1040
    private function executeInserts($class)
1030
    {
1031 1040
        $entities   = [];
1032 1040
        $className  = $class->name;
1033 1040
        $persister  = $this->getEntityPersister($className);
1034 1040
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1035
1036 1040
        foreach ($this->entityInsertions as $oid => $entity) {
1037
1038 1040
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
1039 882
                continue;
1040
            }
1041
1042 1040
            $persister->addInsert($entity);
1043
1044 1040
            unset($this->entityInsertions[$oid]);
1045
1046 1040
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1047 1040
                $entities[] = $entity;
1048
            }
1049
        }
1050
1051 1040
        $postInsertIds = $persister->executeInserts();
1052
1053 1040
        if ($postInsertIds) {
1054
            // Persister returned post-insert IDs
1055 943
            foreach ($postInsertIds as $postInsertId) {
1056 943
                $idField = $class->getSingleIdentifierFieldName();
1057 943
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1058
1059 943
                $entity  = $postInsertId['entity'];
1060 943
                $oid     = spl_object_hash($entity);
1061
1062 943
                $class->reflFields[$idField]->setValue($entity, $idValue);
1063
1064 943
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1065 943
                $this->entityStates[$oid] = self::STATE_MANAGED;
1066 943
                $this->originalEntityData[$oid][$idField] = $idValue;
1067
1068 943
                $this->addToIdentityMap($entity);
1069
            }
1070
        }
1071
1072 1040
        foreach ($entities as $entity) {
1073 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1074
        }
1075 1040
    }
1076
1077
    /**
1078
     * Executes all entity updates for entities of the specified type.
1079
     *
1080
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1081
     *
1082
     * @return void
1083
     */
1084 119
    private function executeUpdates($class)
1085
    {
1086 119
        $className          = $class->name;
1087 119
        $persister          = $this->getEntityPersister($className);
1088 119
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1089 119
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1090
1091 119
        foreach ($this->entityUpdates as $oid => $entity) {
1092 119
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
1093 77
                continue;
1094
            }
1095
1096 119
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1097 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1098
1099 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1100
            }
1101
1102 119
            if ( ! empty($this->entityChangeSets[$oid])) {
1103 85
                $persister->update($entity);
1104
            }
1105
1106 115
            unset($this->entityUpdates[$oid]);
1107
1108 115 View Code Duplication
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1109 115
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1110
            }
1111
        }
1112 115
    }
1113
1114
    /**
1115
     * Executes all entity deletions for entities of the specified type.
1116
     *
1117
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1118
     *
1119
     * @return void
1120
     */
1121 63
    private function executeDeletions($class)
1122
    {
1123 63
        $className  = $class->name;
1124 63
        $persister  = $this->getEntityPersister($className);
1125 63
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1126
1127 63
        foreach ($this->entityDeletions as $oid => $entity) {
1128 63
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
1129 25
                continue;
1130
            }
1131
1132 63
            $persister->delete($entity);
1133
1134
            unset(
1135 63
                $this->entityDeletions[$oid],
1136 63
                $this->entityIdentifiers[$oid],
1137 63
                $this->originalEntityData[$oid],
1138 63
                $this->entityStates[$oid]
1139
            );
1140
1141
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1142
            // is obtained by a new entity because the old one went out of scope.
1143
            //$this->entityStates[$oid] = self::STATE_NEW;
1144 63
            if ( ! $class->isIdentifierNatural()) {
1145 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1146
            }
1147
1148 63 View Code Duplication
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1149 63
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1150
            }
1151
        }
1152 62
    }
1153
1154
    /**
1155
     * Gets the commit order.
1156
     *
1157
     * @param array|null $entityChangeSet
1158
     *
1159
     * @return array
1160
     */
1161 1044
    private function getCommitOrder(array $entityChangeSet = null)
1162
    {
1163 1044
        if ($entityChangeSet === null) {
1164 1044
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1165
        }
1166
1167 1044
        $calc = $this->getCommitOrderCalculator();
1168
1169
        // See if there are any new classes in the changeset, that are not in the
1170
        // commit order graph yet (don't have a node).
1171
        // We have to inspect changeSet to be able to correctly build dependencies.
1172
        // It is not possible to use IdentityMap here because post inserted ids
1173
        // are not yet available.
1174 1044
        $newNodes = [];
1175
1176 1044
        foreach ($entityChangeSet as $entity) {
1177 1044
            $class = $this->em->getClassMetadata(get_class($entity));
1178
1179 1044
            if ($calc->hasNode($class->name)) {
1180 638
                continue;
1181
            }
1182
1183 1044
            $calc->addNode($class->name, $class);
1184
1185 1044
            $newNodes[] = $class;
1186
        }
1187
1188
        // Calculate dependencies for new nodes
1189 1044
        while ($class = array_pop($newNodes)) {
1190 1044
            foreach ($class->associationMappings as $assoc) {
1191 907
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1192 863
                    continue;
1193
                }
1194
1195 858
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1196
1197 858
                if ( ! $calc->hasNode($targetClass->name)) {
1198 660
                    $calc->addNode($targetClass->name, $targetClass);
1199
1200 660
                    $newNodes[] = $targetClass;
1201
                }
1202
1203 858
                $joinColumns = reset($assoc['joinColumns']);
1204
1205 858
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1206
1207
                // If the target class has mapped subclasses, these share the same dependency.
1208 858
                if ( ! $targetClass->subClasses) {
1209 851
                    continue;
1210
                }
1211
1212 226
                foreach ($targetClass->subClasses as $subClassName) {
1213 226
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1214
1215 226
                    if ( ! $calc->hasNode($subClassName)) {
1216 196
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1217
1218 196
                        $newNodes[] = $targetSubClass;
1219
                    }
1220
1221 226
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1222
                }
1223
            }
1224
        }
1225
1226 1044
        return $calc->sort();
1227
    }
1228
1229
    /**
1230
     * Schedules an entity for insertion into the database.
1231
     * If the entity already has an identifier, it will be added to the identity map.
1232
     *
1233
     * @param object $entity The entity to schedule for insertion.
1234
     *
1235
     * @return void
1236
     *
1237
     * @throws ORMInvalidArgumentException
1238
     * @throws \InvalidArgumentException
1239
     */
1240 1072
    public function scheduleForInsert($entity)
1241
    {
1242 1072
        $oid = spl_object_hash($entity);
1243
1244 1072
        if (isset($this->entityUpdates[$oid])) {
1245
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1246
        }
1247
1248 1072
        if (isset($this->entityDeletions[$oid])) {
1249 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1250
        }
1251 1072
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1252 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1253
        }
1254
1255 1072
        if (isset($this->entityInsertions[$oid])) {
1256 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1257
        }
1258
1259 1072
        $this->entityInsertions[$oid] = $entity;
1260
1261 1072
        if (isset($this->entityIdentifiers[$oid])) {
1262 282
            $this->addToIdentityMap($entity);
1263
        }
1264
1265 1072
        if ($entity instanceof NotifyPropertyChanged) {
1266 7
            $entity->addPropertyChangedListener($this);
1267
        }
1268 1072
    }
1269
1270
    /**
1271
     * Checks whether an entity is scheduled for insertion.
1272
     *
1273
     * @param object $entity
1274
     *
1275
     * @return boolean
1276
     */
1277 646
    public function isScheduledForInsert($entity)
1278
    {
1279 646
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1280
    }
1281
1282
    /**
1283
     * Schedules an entity for being updated.
1284
     *
1285
     * @param object $entity The entity to schedule for being updated.
1286
     *
1287
     * @return void
1288
     *
1289
     * @throws ORMInvalidArgumentException
1290
     */
1291 1
    public function scheduleForUpdate($entity)
1292
    {
1293 1
        $oid = spl_object_hash($entity);
1294
1295 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1296
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1297
        }
1298
1299 1
        if (isset($this->entityDeletions[$oid])) {
1300
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1301
        }
1302
1303 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1304 1
            $this->entityUpdates[$oid] = $entity;
1305
        }
1306 1
    }
1307
1308
    /**
1309
     * INTERNAL:
1310
     * Schedules an extra update that will be executed immediately after the
1311
     * regular entity updates within the currently running commit cycle.
1312
     *
1313
     * Extra updates for entities are stored as (entity, changeset) tuples.
1314
     *
1315
     * @ignore
1316
     *
1317
     * @param object $entity    The entity for which to schedule an extra update.
1318
     * @param array  $changeset The changeset of the entity (what to update).
1319
     *
1320
     * @return void
1321
     */
1322 44
    public function scheduleExtraUpdate($entity, array $changeset)
1323
    {
1324 44
        $oid         = spl_object_hash($entity);
1325 44
        $extraUpdate = [$entity, $changeset];
1326
1327 44
        if (isset($this->extraUpdates[$oid])) {
1328 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1329
1330 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1331
        }
1332
1333 44
        $this->extraUpdates[$oid] = $extraUpdate;
1334 44
    }
1335
1336
    /**
1337
     * Checks whether an entity is registered as dirty in the unit of work.
1338
     * Note: Is not very useful currently as dirty entities are only registered
1339
     * at commit time.
1340
     *
1341
     * @param object $entity
1342
     *
1343
     * @return boolean
1344
     */
1345
    public function isScheduledForUpdate($entity)
1346
    {
1347
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1348
    }
1349
1350
    /**
1351
     * Checks whether an entity is registered to be checked in the unit of work.
1352
     *
1353
     * @param object $entity
1354
     *
1355
     * @return boolean
1356
     */
1357 1
    public function isScheduledForDirtyCheck($entity)
1358
    {
1359 1
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
1360
1361 1
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1362
    }
1363
1364
    /**
1365
     * INTERNAL:
1366
     * Schedules an entity for deletion.
1367
     *
1368
     * @param object $entity
1369
     *
1370
     * @return void
1371
     */
1372 66
    public function scheduleForDelete($entity)
1373
    {
1374 66
        $oid = spl_object_hash($entity);
1375
1376 66
        if (isset($this->entityInsertions[$oid])) {
1377 1
            if ($this->isInIdentityMap($entity)) {
1378
                $this->removeFromIdentityMap($entity);
1379
            }
1380
1381 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1382
1383 1
            return; // entity has not been persisted yet, so nothing more to do.
1384
        }
1385
1386 66
        if ( ! $this->isInIdentityMap($entity)) {
1387 1
            return;
1388
        }
1389
1390 65
        $this->removeFromIdentityMap($entity);
1391
1392 65
        unset($this->entityUpdates[$oid]);
1393
1394 65
        if ( ! isset($this->entityDeletions[$oid])) {
1395 65
            $this->entityDeletions[$oid] = $entity;
1396 65
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1397
        }
1398 65
    }
1399
1400
    /**
1401
     * Checks whether an entity is registered as removed/deleted with the unit
1402
     * of work.
1403
     *
1404
     * @param object $entity
1405
     *
1406
     * @return boolean
1407
     */
1408 17
    public function isScheduledForDelete($entity)
1409
    {
1410 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1411
    }
1412
1413
    /**
1414
     * Checks whether an entity is scheduled for insertion, update or deletion.
1415
     *
1416
     * @param object $entity
1417
     *
1418
     * @return boolean
1419
     */
1420
    public function isEntityScheduled($entity)
1421
    {
1422
        $oid = spl_object_hash($entity);
1423
1424
        return isset($this->entityInsertions[$oid])
1425
            || isset($this->entityUpdates[$oid])
1426
            || isset($this->entityDeletions[$oid]);
1427
    }
1428
1429
    /**
1430
     * INTERNAL:
1431
     * Registers an entity in the identity map.
1432
     * Note that entities in a hierarchy are registered with the class name of
1433
     * the root entity.
1434
     *
1435
     * @ignore
1436
     *
1437
     * @param object $entity The entity to register.
1438
     *
1439
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1440
     *                 the entity in question is already managed.
1441
     *
1442
     * @throws ORMInvalidArgumentException
1443
     */
1444 1138
    public function addToIdentityMap($entity)
1445
    {
1446 1138
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1447 1138
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1448
1449 1138
        if (empty($identifier) || in_array(null, $identifier, true)) {
1450 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
1451
        }
1452
1453 1132
        $idHash    = implode(' ', $identifier);
1454 1132
        $className = $classMetadata->rootEntityName;
1455
1456 1132
        if (isset($this->identityMap[$className][$idHash])) {
1457 86
            return false;
1458
        }
1459
1460 1132
        $this->identityMap[$className][$idHash] = $entity;
1461
1462 1132
        return true;
1463
    }
1464
1465
    /**
1466
     * Gets the state of an entity with regard to the current unit of work.
1467
     *
1468
     * @param object   $entity
1469
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1470
     *                         This parameter can be set to improve performance of entity state detection
1471
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1472
     *                         is either known or does not matter for the caller of the method.
1473
     *
1474
     * @return int The entity state.
1475
     */
1476 1086
    public function getEntityState($entity, $assume = null)
1477
    {
1478 1086
        $oid = spl_object_hash($entity);
1479
1480 1086
        if (isset($this->entityStates[$oid])) {
1481 802
            return $this->entityStates[$oid];
1482
        }
1483
1484 1080
        if ($assume !== null) {
1485 1076
            return $assume;
1486
        }
1487
1488
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1489
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1490
        // the UoW does not hold references to such objects and the object hash can be reused.
1491
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1492 13
        $class = $this->em->getClassMetadata(get_class($entity));
1493 13
        $id    = $class->getIdentifierValues($entity);
1494
1495 13
        if ( ! $id) {
1496 5
            return self::STATE_NEW;
1497
        }
1498
1499 10
        if ($class->containsForeignIdentifier) {
1500 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1501
        }
1502
1503
        switch (true) {
1504 10
            case ($class->isIdentifierNatural()):
1505
                // Check for a version field, if available, to avoid a db lookup.
1506 5
                if ($class->isVersioned) {
1507 1
                    return ($class->getFieldValue($entity, $class->versionField))
1508
                        ? self::STATE_DETACHED
1509 1
                        : self::STATE_NEW;
1510
                }
1511
1512
                // Last try before db lookup: check the identity map.
1513 4
                if ($this->tryGetById($id, $class->rootEntityName)) {
1514 1
                    return self::STATE_DETACHED;
1515
                }
1516
1517
                // db lookup
1518 4
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1519
                    return self::STATE_DETACHED;
1520
                }
1521
1522 4
                return self::STATE_NEW;
1523
1524 5
            case ( ! $class->idGenerator->isPostInsertGenerator()):
1525
                // if we have a pre insert generator we can't be sure that having an id
1526
                // really means that the entity exists. We have to verify this through
1527
                // the last resort: a db lookup
1528
1529
                // Last try before db lookup: check the identity map.
1530
                if ($this->tryGetById($id, $class->rootEntityName)) {
1531
                    return self::STATE_DETACHED;
1532
                }
1533
1534
                // db lookup
1535
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1536
                    return self::STATE_DETACHED;
1537
                }
1538
1539
                return self::STATE_NEW;
1540
1541
            default:
1542 5
                return self::STATE_DETACHED;
1543
        }
1544
    }
1545
1546
    /**
1547
     * INTERNAL:
1548
     * Removes an entity from the identity map. This effectively detaches the
1549
     * entity from the persistence management of Doctrine.
1550
     *
1551
     * @ignore
1552
     *
1553
     * @param object $entity
1554
     *
1555
     * @return boolean
1556
     *
1557
     * @throws ORMInvalidArgumentException
1558
     */
1559 78
    public function removeFromIdentityMap($entity)
1560
    {
1561 78
        $oid           = spl_object_hash($entity);
1562 78
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1563 78
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1564
1565 78
        if ($idHash === '') {
1566
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1567
        }
1568
1569 78
        $className = $classMetadata->rootEntityName;
1570
1571 78
        if (isset($this->identityMap[$className][$idHash])) {
1572 78
            unset($this->identityMap[$className][$idHash]);
1573 78
            unset($this->readOnlyObjects[$oid]);
1574
1575
            //$this->entityStates[$oid] = self::STATE_DETACHED;
1576
1577 78
            return true;
1578
        }
1579
1580
        return false;
1581
    }
1582
1583
    /**
1584
     * INTERNAL:
1585
     * Gets an entity in the identity map by its identifier hash.
1586
     *
1587
     * @ignore
1588
     *
1589
     * @param string $idHash
1590
     * @param string $rootClassName
1591
     *
1592
     * @return object
1593
     */
1594 6
    public function getByIdHash($idHash, $rootClassName)
1595
    {
1596 6
        return $this->identityMap[$rootClassName][$idHash];
1597
    }
1598
1599
    /**
1600
     * INTERNAL:
1601
     * Tries to get an entity by its identifier hash. If no entity is found for
1602
     * the given hash, FALSE is returned.
1603
     *
1604
     * @ignore
1605
     *
1606
     * @param mixed  $idHash        (must be possible to cast it to string)
1607
     * @param string $rootClassName
1608
     *
1609
     * @return object|bool The found entity or FALSE.
1610
     */
1611 35 View Code Duplication
    public function tryGetByIdHash($idHash, $rootClassName)
1612
    {
1613 35
        $stringIdHash = (string) $idHash;
1614
1615 35
        return isset($this->identityMap[$rootClassName][$stringIdHash])
1616 35
            ? $this->identityMap[$rootClassName][$stringIdHash]
1617 35
            : false;
1618
    }
1619
1620
    /**
1621
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1622
     *
1623
     * @param object $entity
1624
     *
1625
     * @return boolean
1626
     */
1627 217
    public function isInIdentityMap($entity)
1628
    {
1629 217
        $oid = spl_object_hash($entity);
1630
1631 217
        if (empty($this->entityIdentifiers[$oid])) {
1632 33
            return false;
1633
        }
1634
1635 201
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1636 201
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1637
1638 201
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
1639
    }
1640
1641
    /**
1642
     * INTERNAL:
1643
     * Checks whether an identifier hash exists in the identity map.
1644
     *
1645
     * @ignore
1646
     *
1647
     * @param string $idHash
1648
     * @param string $rootClassName
1649
     *
1650
     * @return boolean
1651
     */
1652
    public function containsIdHash($idHash, $rootClassName)
1653
    {
1654
        return isset($this->identityMap[$rootClassName][$idHash]);
1655
    }
1656
1657
    /**
1658
     * Persists an entity as part of the current unit of work.
1659
     *
1660
     * @param object $entity The entity to persist.
1661
     *
1662
     * @return void
1663
     */
1664 1067
    public function persist($entity)
1665
    {
1666 1067
        $visited = [];
1667
1668 1067
        $this->doPersist($entity, $visited);
1669 1060
    }
1670
1671
    /**
1672
     * Persists an entity as part of the current unit of work.
1673
     *
1674
     * This method is internally called during persist() cascades as it tracks
1675
     * the already visited entities to prevent infinite recursions.
1676
     *
1677
     * @param object $entity  The entity to persist.
1678
     * @param array  $visited The already visited entities.
1679
     *
1680
     * @return void
1681
     *
1682
     * @throws ORMInvalidArgumentException
1683
     * @throws UnexpectedValueException
1684
     */
1685 1067
    private function doPersist($entity, array &$visited)
1686
    {
1687 1067
        $oid = spl_object_hash($entity);
1688
1689 1067
        if (isset($visited[$oid])) {
1690 110
            return; // Prevent infinite recursion
1691
        }
1692
1693 1067
        $visited[$oid] = $entity; // Mark visited
1694
1695 1067
        $class = $this->em->getClassMetadata(get_class($entity));
1696
1697
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1698
        // If we would detect DETACHED here we would throw an exception anyway with the same
1699
        // consequences (not recoverable/programming error), so just assuming NEW here
1700
        // lets us avoid some database lookups for entities with natural identifiers.
1701 1067
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1702
1703
        switch ($entityState) {
1704 1067
            case self::STATE_MANAGED:
1705
                // Nothing to do, except if policy is "deferred explicit"
1706 238
                if ($class->isChangeTrackingDeferredExplicit()) {
1707 2
                    $this->scheduleForDirtyCheck($entity);
1708
                }
1709 238
                break;
1710
1711 1067
            case self::STATE_NEW:
1712 1066
                $this->persistNew($class, $entity);
1713 1066
                break;
1714
1715 1
            case self::STATE_REMOVED:
1716
                // Entity becomes managed again
1717 1
                unset($this->entityDeletions[$oid]);
1718 1
                $this->addToIdentityMap($entity);
1719
1720 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1721 1
                break;
1722
1723
            case self::STATE_DETACHED:
1724
                // Can actually not happen right now since we assume STATE_NEW.
1725
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1726
1727
            default:
1728
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1729
        }
1730
1731 1067
        $this->cascadePersist($entity, $visited);
1732 1060
    }
1733
1734
    /**
1735
     * Deletes an entity as part of the current unit of work.
1736
     *
1737
     * @param object $entity The entity to remove.
1738
     *
1739
     * @return void
1740
     */
1741 65
    public function remove($entity)
1742
    {
1743 65
        $visited = [];
1744
1745 65
        $this->doRemove($entity, $visited);
1746 65
    }
1747
1748
    /**
1749
     * Deletes an entity as part of the current unit of work.
1750
     *
1751
     * This method is internally called during delete() cascades as it tracks
1752
     * the already visited entities to prevent infinite recursions.
1753
     *
1754
     * @param object $entity  The entity to delete.
1755
     * @param array  $visited The map of the already visited entities.
1756
     *
1757
     * @return void
1758
     *
1759
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1760
     * @throws UnexpectedValueException
1761
     */
1762 65
    private function doRemove($entity, array &$visited)
1763
    {
1764 65
        $oid = spl_object_hash($entity);
1765
1766 65
        if (isset($visited[$oid])) {
1767 1
            return; // Prevent infinite recursion
1768
        }
1769
1770 65
        $visited[$oid] = $entity; // mark visited
1771
1772
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1773
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1774 65
        $this->cascadeRemove($entity, $visited);
1775
1776 65
        $class       = $this->em->getClassMetadata(get_class($entity));
1777 65
        $entityState = $this->getEntityState($entity);
1778
1779
        switch ($entityState) {
1780 65
            case self::STATE_NEW:
1781 65
            case self::STATE_REMOVED:
1782
                // nothing to do
1783 2
                break;
1784
1785 65
            case self::STATE_MANAGED:
1786 65
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1787
1788 65 View Code Duplication
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1789 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1790
                }
1791
1792 65
                $this->scheduleForDelete($entity);
1793 65
                break;
1794
1795
            case self::STATE_DETACHED:
1796
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1797
            default:
1798
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1799
        }
1800
1801 65
    }
1802
1803
    /**
1804
     * Merges the state of the given detached entity into this UnitOfWork.
1805
     *
1806
     * @param object $entity
1807
     *
1808
     * @return object The managed copy of the entity.
1809
     *
1810
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1811
     *         attribute and the version check against the managed copy fails.
1812
     *
1813
     * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
1814
     */
1815 43
    public function merge($entity)
1816
    {
1817 43
        $visited = [];
1818
1819 43
        return $this->doMerge($entity, $visited);
1820
    }
1821
1822
    /**
1823
     * Executes a merge operation on an entity.
1824
     *
1825
     * @param object      $entity
1826
     * @param array       $visited
1827
     * @param object|null $prevManagedCopy
1828
     * @param array|null  $assoc
1829
     *
1830
     * @return object The managed copy of the entity.
1831
     *
1832
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1833
     *         attribute and the version check against the managed copy fails.
1834
     * @throws ORMInvalidArgumentException If the entity instance is NEW.
1835
     * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided
1836
     */
1837 43
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
1838
    {
1839 43
        $oid = spl_object_hash($entity);
1840
1841 43
        if (isset($visited[$oid])) {
1842 4
            $managedCopy = $visited[$oid];
1843
1844 4
            if ($prevManagedCopy !== null) {
1845 4
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1846
            }
1847
1848 4
            return $managedCopy;
1849
        }
1850
1851 43
        $class = $this->em->getClassMetadata(get_class($entity));
1852
1853
        // First we assume DETACHED, although it can still be NEW but we can avoid
1854
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1855
        // we need to fetch it from the db anyway in order to merge.
1856
        // MANAGED entities are ignored by the merge operation.
1857 43
        $managedCopy = $entity;
1858
1859 43
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1860
            // Try to look the entity up in the identity map.
1861 42
            $id = $class->getIdentifierValues($entity);
1862
1863
            // If there is no ID, it is actually NEW.
1864 42
            if ( ! $id) {
1865 6
                $managedCopy = $this->newInstance($class);
1866
1867 6
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1868 6
                $this->persistNew($class, $managedCopy);
1869
            } else {
1870 37
                $flatId = ($class->containsForeignIdentifier)
1871 3
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1872 37
                    : $id;
1873
1874 37
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
1875
1876 37
                if ($managedCopy) {
1877
                    // We have the entity in-memory already, just make sure its not removed.
1878 15
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
1879 15
                        throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge");
1880
                    }
1881
                } else {
1882
                    // We need to fetch the managed copy in order to merge.
1883 25
                    $managedCopy = $this->em->find($class->name, $flatId);
1884
                }
1885
1886 37
                if ($managedCopy === null) {
1887
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1888
                    // since the managed entity was not found.
1889 3
                    if ( ! $class->isIdentifierNatural()) {
1890 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1891 1
                            $class->getName(),
1892 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1893
                        );
1894
                    }
1895
1896 2
                    $managedCopy = $this->newInstance($class);
1897 2
                    $class->setIdentifierValues($managedCopy, $id);
1898
1899 2
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1900 2
                    $this->persistNew($class, $managedCopy);
1901
                } else {
1902 34
                    $this->ensureVersionMatch($class, $entity, $managedCopy);
1903 33
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1904
                }
1905
            }
1906
1907 40
            $visited[$oid] = $managedCopy; // mark visited
1908
1909 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1910
                $this->scheduleForDirtyCheck($entity);
1911
            }
1912
        }
1913
1914 41
        if ($prevManagedCopy !== null) {
1915 6
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1916
        }
1917
1918
        // Mark the managed copy visited as well
1919 41
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1920
1921 41
        $this->cascadeMerge($entity, $managedCopy, $visited);
1922
1923 41
        return $managedCopy;
1924
    }
1925
1926
    /**
1927
     * @param ClassMetadata $class
1928
     * @param object        $entity
1929
     * @param object        $managedCopy
1930
     *
1931
     * @return void
1932
     *
1933
     * @throws OptimisticLockException
1934
     */
1935 34
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
1936
    {
1937 34
        if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
1938 31
            return;
1939
        }
1940
1941 4
        $reflField          = $class->reflFields[$class->versionField];
1942 4
        $managedCopyVersion = $reflField->getValue($managedCopy);
1943 4
        $entityVersion      = $reflField->getValue($entity);
1944
1945
        // Throw exception if versions don't match.
1946 4
        if ($managedCopyVersion == $entityVersion) {
1947 3
            return;
1948
        }
1949
1950 1
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
1951
    }
1952
1953
    /**
1954
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
1955
     *
1956
     * @param object $entity
1957
     *
1958
     * @return bool
1959
     */
1960 41
    private function isLoaded($entity)
1961
    {
1962 41
        return !($entity instanceof Proxy) || $entity->__isInitialized();
1963
    }
1964
1965
    /**
1966
     * Sets/adds associated managed copies into the previous entity's association field
1967
     *
1968
     * @param object $entity
1969
     * @param array  $association
1970
     * @param object $previousManagedCopy
1971
     * @param object $managedCopy
1972
     *
1973
     * @return void
1974
     */
1975 6
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
1976
    {
1977 6
        $assocField = $association['fieldName'];
1978 6
        $prevClass  = $this->em->getClassMetadata(get_class($previousManagedCopy));
1979
1980 6
        if ($association['type'] & ClassMetadata::TO_ONE) {
1981 6
            $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
1982
1983 6
            return;
1984
        }
1985
1986 1
        $value   = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
1987 1
        $value[] = $managedCopy;
1988
1989 1 View Code Duplication
        if ($association['type'] == ClassMetadata::ONE_TO_MANY) {
1990 1
            $class = $this->em->getClassMetadata(get_class($entity));
1991
1992 1
            $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
1993
        }
1994 1
    }
1995
1996
    /**
1997
     * Detaches an entity from the persistence management. It's persistence will
1998
     * no longer be managed by Doctrine.
1999
     *
2000
     * @param object $entity The entity to detach.
2001
     *
2002
     * @return void
2003
     */
2004 12
    public function detach($entity)
2005
    {
2006 12
        $visited = [];
2007
2008 12
        $this->doDetach($entity, $visited);
2009 12
    }
2010
2011
    /**
2012
     * Executes a detach operation on the given entity.
2013
     *
2014
     * @param object  $entity
2015
     * @param array   $visited
2016
     * @param boolean $noCascade if true, don't cascade detach operation.
2017
     *
2018
     * @return void
2019
     */
2020 16
    private function doDetach($entity, array &$visited, $noCascade = false)
2021
    {
2022 16
        $oid = spl_object_hash($entity);
2023
2024 16
        if (isset($visited[$oid])) {
2025
            return; // Prevent infinite recursion
2026
        }
2027
2028 16
        $visited[$oid] = $entity; // mark visited
2029
2030 16
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2031 16
            case self::STATE_MANAGED:
2032 14
                if ($this->isInIdentityMap($entity)) {
2033 13
                    $this->removeFromIdentityMap($entity);
2034
                }
2035
2036
                unset(
2037 14
                    $this->entityInsertions[$oid],
2038 14
                    $this->entityUpdates[$oid],
2039 14
                    $this->entityDeletions[$oid],
2040 14
                    $this->entityIdentifiers[$oid],
2041 14
                    $this->entityStates[$oid],
2042 14
                    $this->originalEntityData[$oid]
2043
                );
2044 14
                break;
2045 3
            case self::STATE_NEW:
2046 3
            case self::STATE_DETACHED:
2047 3
                return;
2048
        }
2049
2050 14
        if ( ! $noCascade) {
2051 14
            $this->cascadeDetach($entity, $visited);
2052
        }
2053 14
    }
2054
2055
    /**
2056
     * Refreshes the state of the given entity from the database, overwriting
2057
     * any local, unpersisted changes.
2058
     *
2059
     * @param object $entity The entity to refresh.
2060
     *
2061
     * @return void
2062
     *
2063
     * @throws InvalidArgumentException If the entity is not MANAGED.
2064
     */
2065 17
    public function refresh($entity)
2066
    {
2067 17
        $visited = [];
2068
2069 17
        $this->doRefresh($entity, $visited);
2070 17
    }
2071
2072
    /**
2073
     * Executes a refresh operation on an entity.
2074
     *
2075
     * @param object $entity  The entity to refresh.
2076
     * @param array  $visited The already visited entities during cascades.
2077
     *
2078
     * @return void
2079
     *
2080
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
2081
     */
2082 17
    private function doRefresh($entity, array &$visited)
2083
    {
2084 17
        $oid = spl_object_hash($entity);
2085
2086 17
        if (isset($visited[$oid])) {
2087
            return; // Prevent infinite recursion
2088
        }
2089
2090 17
        $visited[$oid] = $entity; // mark visited
2091
2092 17
        $class = $this->em->getClassMetadata(get_class($entity));
2093
2094 17
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
2095
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2096
        }
2097
2098 17
        $this->getEntityPersister($class->name)->refresh(
2099 17
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2100 17
            $entity
2101
        );
2102
2103 17
        $this->cascadeRefresh($entity, $visited);
2104 17
    }
2105
2106
    /**
2107
     * Cascades a refresh operation to associated entities.
2108
     *
2109
     * @param object $entity
2110
     * @param array  $visited
2111
     *
2112
     * @return void
2113
     */
2114 17 View Code Duplication
    private function cascadeRefresh($entity, array &$visited)
2115
    {
2116 17
        $class = $this->em->getClassMetadata(get_class($entity));
2117
2118 17
        $associationMappings = array_filter(
2119 17
            $class->associationMappings,
2120
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2121
        );
2122
2123 17
        foreach ($associationMappings as $assoc) {
2124 5
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2125
2126
            switch (true) {
2127 5
                case ($relatedEntities instanceof PersistentCollection):
2128
                    // Unwrap so that foreach() does not initialize
2129 5
                    $relatedEntities = $relatedEntities->unwrap();
2130
                    // break; is commented intentionally!
2131
2132
                case ($relatedEntities instanceof Collection):
2133
                case (is_array($relatedEntities)):
2134 5
                    foreach ($relatedEntities as $relatedEntity) {
2135
                        $this->doRefresh($relatedEntity, $visited);
2136
                    }
2137 5
                    break;
2138
2139
                case ($relatedEntities !== null):
2140
                    $this->doRefresh($relatedEntities, $visited);
2141
                    break;
2142
2143 5
                default:
2144
                    // Do nothing
2145
            }
2146
        }
2147 17
    }
2148
2149
    /**
2150
     * Cascades a detach operation to associated entities.
2151
     *
2152
     * @param object $entity
2153
     * @param array  $visited
2154
     *
2155
     * @return void
2156
     */
2157 14 View Code Duplication
    private function cascadeDetach($entity, array &$visited)
2158
    {
2159 14
        $class = $this->em->getClassMetadata(get_class($entity));
2160
2161 14
        $associationMappings = array_filter(
2162 14
            $class->associationMappings,
2163
            function ($assoc) { return $assoc['isCascadeDetach']; }
2164
        );
2165
2166 14
        foreach ($associationMappings as $assoc) {
2167 3
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2168
2169
            switch (true) {
2170 3
                case ($relatedEntities instanceof PersistentCollection):
2171
                    // Unwrap so that foreach() does not initialize
2172 2
                    $relatedEntities = $relatedEntities->unwrap();
2173
                    // break; is commented intentionally!
2174
2175 1
                case ($relatedEntities instanceof Collection):
2176
                case (is_array($relatedEntities)):
2177 3
                    foreach ($relatedEntities as $relatedEntity) {
2178 1
                        $this->doDetach($relatedEntity, $visited);
2179
                    }
2180 3
                    break;
2181
2182
                case ($relatedEntities !== null):
2183
                    $this->doDetach($relatedEntities, $visited);
2184
                    break;
2185
2186 3
                default:
2187
                    // Do nothing
2188
            }
2189
        }
2190 14
    }
2191
2192
    /**
2193
     * Cascades a merge operation to associated entities.
2194
     *
2195
     * @param object $entity
2196
     * @param object $managedCopy
2197
     * @param array  $visited
2198
     *
2199
     * @return void
2200
     */
2201 41
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2202
    {
2203 41
        $class = $this->em->getClassMetadata(get_class($entity));
2204
2205 41
        $associationMappings = array_filter(
2206 41
            $class->associationMappings,
2207
            function ($assoc) { return $assoc['isCascadeMerge']; }
2208
        );
2209
2210 41
        foreach ($associationMappings as $assoc) {
2211 16
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2212
2213 16
            if ($relatedEntities instanceof Collection) {
2214 10
                if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2215 1
                    continue;
2216
                }
2217
2218 9
                if ($relatedEntities instanceof PersistentCollection) {
2219
                    // Unwrap so that foreach() does not initialize
2220 5
                    $relatedEntities = $relatedEntities->unwrap();
2221
                }
2222
2223 9
                foreach ($relatedEntities as $relatedEntity) {
2224 9
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2225
                }
2226 7
            } else if ($relatedEntities !== null) {
2227 15
                $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2228
            }
2229
        }
2230 41
    }
2231
2232
    /**
2233
     * Cascades the save operation to associated entities.
2234
     *
2235
     * @param object $entity
2236
     * @param array  $visited
2237
     *
2238
     * @return void
2239
     */
2240 1067
    private function cascadePersist($entity, array &$visited)
2241
    {
2242 1067
        $class = $this->em->getClassMetadata(get_class($entity));
2243
2244 1067
        $associationMappings = array_filter(
2245 1067
            $class->associationMappings,
2246
            function ($assoc) { return $assoc['isCascadePersist']; }
2247
        );
2248
2249 1067
        foreach ($associationMappings as $assoc) {
2250 663
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2251
2252
            switch (true) {
2253 663
                case ($relatedEntities instanceof PersistentCollection):
2254
                    // Unwrap so that foreach() does not initialize
2255 21
                    $relatedEntities = $relatedEntities->unwrap();
2256
                    // break; is commented intentionally!
2257
2258 663
                case ($relatedEntities instanceof Collection):
2259 602
                case (is_array($relatedEntities)):
2260 565
                    if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
2261 3
                        throw ORMInvalidArgumentException::invalidAssociation(
2262 3
                            $this->em->getClassMetadata($assoc['targetEntity']),
2263 3
                            $assoc,
2264 3
                            $relatedEntities
2265
                        );
2266
                    }
2267
2268 562
                    foreach ($relatedEntities as $relatedEntity) {
2269 283
                        $this->doPersist($relatedEntity, $visited);
2270
                    }
2271
2272 562
                    break;
2273
2274 592
                case ($relatedEntities !== null):
2275 253
                    if (! $relatedEntities instanceof $assoc['targetEntity']) {
2276 4
                        throw ORMInvalidArgumentException::invalidAssociation(
2277 4
                            $this->em->getClassMetadata($assoc['targetEntity']),
2278 4
                            $assoc,
2279 4
                            $relatedEntities
2280
                        );
2281
                    }
2282
2283 249
                    $this->doPersist($relatedEntities, $visited);
2284 249
                    break;
2285
2286 657
                default:
2287
                    // Do nothing
2288
            }
2289
        }
2290 1060
    }
2291
2292
    /**
2293
     * Cascades the delete operation to associated entities.
2294
     *
2295
     * @param object $entity
2296
     * @param array  $visited
2297
     *
2298
     * @return void
2299
     */
2300 65
    private function cascadeRemove($entity, array &$visited)
2301
    {
2302 65
        $class = $this->em->getClassMetadata(get_class($entity));
2303
2304 65
        $associationMappings = array_filter(
2305 65
            $class->associationMappings,
2306
            function ($assoc) { return $assoc['isCascadeRemove']; }
2307
        );
2308
2309 65
        $entitiesToCascade = [];
2310
2311 65
        foreach ($associationMappings as $assoc) {
2312 26
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
2313 6
                $entity->__load();
2314
            }
2315
2316 26
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2317
2318
            switch (true) {
2319 26
                case ($relatedEntities instanceof Collection):
2320 19
                case (is_array($relatedEntities)):
2321
                    // If its a PersistentCollection initialization is intended! No unwrap!
2322 20
                    foreach ($relatedEntities as $relatedEntity) {
2323 10
                        $entitiesToCascade[] = $relatedEntity;
2324
                    }
2325 20
                    break;
2326
2327 19
                case ($relatedEntities !== null):
2328 7
                    $entitiesToCascade[] = $relatedEntities;
2329 7
                    break;
2330
2331 26
                default:
2332
                    // Do nothing
2333
            }
2334
        }
2335
2336 65
        foreach ($entitiesToCascade as $relatedEntity) {
2337 16
            $this->doRemove($relatedEntity, $visited);
2338
        }
2339 65
    }
2340
2341
    /**
2342
     * Acquire a lock on the given entity.
2343
     *
2344
     * @param object $entity
2345
     * @param int    $lockMode
2346
     * @param int    $lockVersion
2347
     *
2348
     * @return void
2349
     *
2350
     * @throws ORMInvalidArgumentException
2351
     * @throws TransactionRequiredException
2352
     * @throws OptimisticLockException
2353
     */
2354 10
    public function lock($entity, $lockMode, $lockVersion = null)
2355
    {
2356 10
        if ($entity === null) {
2357 1
            throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock().");
2358
        }
2359
2360 9
        if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2361 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2362
        }
2363
2364 8
        $class = $this->em->getClassMetadata(get_class($entity));
2365
2366
        switch (true) {
2367 8
            case LockMode::OPTIMISTIC === $lockMode:
2368 6
                if ( ! $class->isVersioned) {
2369 2
                    throw OptimisticLockException::notVersioned($class->name);
2370
                }
2371
2372 4
                if ($lockVersion === null) {
2373
                    return;
2374
                }
2375
2376 4
                if ($entity instanceof Proxy && !$entity->__isInitialized__) {
2377 1
                    $entity->__load();
2378
                }
2379
2380 4
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
2381
2382 4
                if ($entityVersion != $lockVersion) {
2383 2
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2384
                }
2385
2386 2
                break;
2387
2388 2
            case LockMode::NONE === $lockMode:
2389 2
            case LockMode::PESSIMISTIC_READ === $lockMode:
2390 1
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2391 2
                if (!$this->em->getConnection()->isTransactionActive()) {
2392 2
                    throw TransactionRequiredException::transactionRequired();
2393
                }
2394
2395
                $oid = spl_object_hash($entity);
2396
2397
                $this->getEntityPersister($class->name)->lock(
2398
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2399
                    $lockMode
2400
                );
2401
                break;
2402
2403
            default:
2404
                // Do nothing
2405
        }
2406 2
    }
2407
2408
    /**
2409
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
2410
     *
2411
     * @return \Doctrine\ORM\Internal\CommitOrderCalculator
2412
     */
2413 1044
    public function getCommitOrderCalculator()
2414
    {
2415 1044
        return new Internal\CommitOrderCalculator();
2416
    }
2417
2418
    /**
2419
     * Clears the UnitOfWork.
2420
     *
2421
     * @param string|null $entityName if given, only entities of this type will get detached.
2422
     *
2423
     * @return void
2424
     *
2425
     * @throws ORMInvalidArgumentException if an invalid entity name is given
2426
     */
2427 1264
    public function clear($entityName = null)
2428
    {
2429 1264
        if ($entityName === null) {
2430 1262
            $this->identityMap =
2431 1262
            $this->entityIdentifiers =
2432 1262
            $this->originalEntityData =
2433 1262
            $this->entityChangeSets =
2434 1262
            $this->entityStates =
2435 1262
            $this->scheduledForSynchronization =
2436 1262
            $this->entityInsertions =
2437 1262
            $this->entityUpdates =
2438 1262
            $this->entityDeletions =
2439 1262
            $this->nonCascadedNewDetectedEntities =
2440 1262
            $this->collectionDeletions =
2441 1262
            $this->collectionUpdates =
2442 1262
            $this->extraUpdates =
2443 1262
            $this->readOnlyObjects =
2444 1262
            $this->visitedCollections =
2445 1262
            $this->orphanRemovals = [];
2446
        } else {
2447 4
            $this->clearIdentityMapForEntityName($entityName);
2448 4
            $this->clearEntityInsertionsForEntityName($entityName);
2449
        }
2450
2451 1264
        if ($this->evm->hasListeners(Events::onClear)) {
2452 9
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
2453
        }
2454 1264
    }
2455
2456
    /**
2457
     * INTERNAL:
2458
     * Schedules an orphaned entity for removal. The remove() operation will be
2459
     * invoked on that entity at the beginning of the next commit of this
2460
     * UnitOfWork.
2461
     *
2462
     * @ignore
2463
     *
2464
     * @param object $entity
2465
     *
2466
     * @return void
2467
     */
2468 17
    public function scheduleOrphanRemoval($entity)
2469
    {
2470 17
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2471 17
    }
2472
2473
    /**
2474
     * INTERNAL:
2475
     * Cancels a previously scheduled orphan removal.
2476
     *
2477
     * @ignore
2478
     *
2479
     * @param object $entity
2480
     *
2481
     * @return void
2482
     */
2483 117
    public function cancelOrphanRemoval($entity)
2484
    {
2485 117
        unset($this->orphanRemovals[spl_object_hash($entity)]);
2486 117
    }
2487
2488
    /**
2489
     * INTERNAL:
2490
     * Schedules a complete collection for removal when this UnitOfWork commits.
2491
     *
2492
     * @param PersistentCollection $coll
2493
     *
2494
     * @return void
2495
     */
2496 14
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2497
    {
2498 14
        $coid = spl_object_hash($coll);
2499
2500
        // TODO: if $coll is already scheduled for recreation ... what to do?
2501
        // Just remove $coll from the scheduled recreations?
2502 14
        unset($this->collectionUpdates[$coid]);
2503
2504 14
        $this->collectionDeletions[$coid] = $coll;
2505 14
    }
2506
2507
    /**
2508
     * @param PersistentCollection $coll
2509
     *
2510
     * @return bool
2511
     */
2512
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2513
    {
2514
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2515
    }
2516
2517
    /**
2518
     * @param ClassMetadata $class
2519
     *
2520
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
2521
     */
2522 700
    private function newInstance($class)
2523
    {
2524 700
        $entity = $class->newInstance();
2525
2526 700
        if ($entity instanceof \Doctrine\Common\Persistence\ObjectManagerAware) {
2527 4
            $entity->injectObjectManager($this->em, $class);
2528
        }
2529
2530 700
        return $entity;
2531
    }
2532
2533
    /**
2534
     * INTERNAL:
2535
     * Creates an entity. Used for reconstitution of persistent entities.
2536
     *
2537
     * Internal note: Highly performance-sensitive method.
2538
     *
2539
     * @ignore
2540
     *
2541
     * @param string $className The name of the entity class.
2542
     * @param array  $data      The data for the entity.
2543
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the entity.
2544
     *
2545
     * @return object The managed entity instance.
2546
     *
2547
     * @todo Rename: getOrCreateEntity
2548
     */
2549 842
    public function createEntity($className, array $data, &$hints = [])
2550
    {
2551 842
        $class = $this->em->getClassMetadata($className);
2552
2553 842
        $id = $this->identifierFlattener->flattenIdentifier($class, $data);
2554 842
        $idHash = implode(' ', $id);
2555
2556 842
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
2557 324
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
2558 324
            $oid = spl_object_hash($entity);
2559
2560
            if (
2561 324
                isset($hints[Query::HINT_REFRESH])
2562 324
                && isset($hints[Query::HINT_REFRESH_ENTITY])
2563 324
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
2564 324
                && $unmanagedProxy instanceof Proxy
2565 324
                && $this->isIdentifierEquals($unmanagedProxy, $entity)
2566
            ) {
2567
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
2568
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
2569
                // refreshed object may be anything
2570
2571 2
                foreach ($class->identifier as $fieldName) {
2572 2
                    $class->reflFields[$fieldName]->setValue($unmanagedProxy, null);
2573
                }
2574
2575 2
                return $unmanagedProxy;
2576
            }
2577
2578 322
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2579 23
                $entity->__setInitialized(true);
2580
2581 23
                if ($entity instanceof NotifyPropertyChanged) {
2582 23
                    $entity->addPropertyChangedListener($this);
2583
                }
2584
            } else {
2585 301
                if ( ! isset($hints[Query::HINT_REFRESH])
2586 301
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2587 230
                    return $entity;
2588
                }
2589
            }
2590
2591
            // inject ObjectManager upon refresh.
2592 115
            if ($entity instanceof ObjectManagerAware) {
2593 3
                $entity->injectObjectManager($this->em, $class);
2594
            }
2595
2596 115
            $this->originalEntityData[$oid] = $data;
2597
        } else {
2598 695
            $entity = $this->newInstance($class);
2599 695
            $oid    = spl_object_hash($entity);
2600
2601 695
            $this->entityIdentifiers[$oid]  = $id;
2602 695
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2603 695
            $this->originalEntityData[$oid] = $data;
2604
2605 695
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
2606
2607 695
            if ($entity instanceof NotifyPropertyChanged) {
2608 2
                $entity->addPropertyChangedListener($this);
2609
            }
2610
        }
2611
2612 733
        foreach ($data as $field => $value) {
2613 733
            if (isset($class->fieldMappings[$field])) {
2614 733
                $class->reflFields[$field]->setValue($entity, $value);
2615
            }
2616
        }
2617
2618
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2619 733
        unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2620
2621 733
        if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) {
2622
            unset($this->eagerLoadingEntities[$class->rootEntityName]);
2623
        }
2624
2625
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2626 733
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2627 34
            return $entity;
2628
        }
2629
2630 699
        foreach ($class->associationMappings as $field => $assoc) {
2631
            // Check if the association is not among the fetch-joined associations already.
2632 603
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2633 259
                continue;
2634
            }
2635
2636 579
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2637
2638
            switch (true) {
2639 579
                case ($assoc['type'] & ClassMetadata::TO_ONE):
2640 499
                    if ( ! $assoc['isOwningSide']) {
2641
2642
                        // use the given entity association
2643 67
                        if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2644
2645 3
                            $this->originalEntityData[$oid][$field] = $data[$field];
2646
2647 3
                            $class->reflFields[$field]->setValue($entity, $data[$field]);
2648 3
                            $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
2649
2650 3
                            continue 2;
2651
                        }
2652
2653
                        // Inverse side of x-to-one can never be lazy
2654 64
                        $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
2655
2656 64
                        continue 2;
2657
                    }
2658
2659
                    // use the entity association
2660 499
                    if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2661 38
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2662 38
                        $this->originalEntityData[$oid][$field] = $data[$field];
2663
2664 38
                        continue;
2665
                    }
2666
2667 492
                    $associatedId = [];
2668
2669
                    // TODO: Is this even computed right in all cases of composite keys?
2670 492
                    foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
2671 492
                        $joinColumnValue = $data[$srcColumn] ?? null;
2672
2673 492
                        if ($joinColumnValue !== null) {
2674 296
                            if ($targetClass->containsForeignIdentifier) {
2675 11
                                $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
2676
                            } else {
2677 296
                                $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
2678
                            }
2679 289
                        } elseif ($targetClass->containsForeignIdentifier
2680 289
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2681
                        ) {
2682
                            // the missing key is part of target's entity primary key
2683 7
                            $associatedId = [];
2684 492
                            break;
2685
                        }
2686
                    }
2687
2688 492
                    if ( ! $associatedId) {
2689
                        // Foreign key is NULL
2690 289
                        $class->reflFields[$field]->setValue($entity, null);
2691 289
                        $this->originalEntityData[$oid][$field] = null;
2692
2693 289
                        continue;
2694
                    }
2695
2696 296
                    if ( ! isset($hints['fetchMode'][$class->name][$field])) {
2697 293
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2698
                    }
2699
2700
                    // Foreign key is set
2701
                    // Check identity map first
2702
                    // FIXME: Can break easily with composite keys if join column values are in
2703
                    //        wrong order. The correct order is the one in ClassMetadata#identifier.
2704 296
                    $relatedIdHash = implode(' ', $associatedId);
2705
2706
                    switch (true) {
2707 296
                        case (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])):
2708 170
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2709
2710
                            // If this is an uninitialized proxy, we are deferring eager loads,
2711
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2712
                            // then we can append this entity for eager loading!
2713 170
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2714 170
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2715 170
                                !$targetClass->isIdentifierComposite &&
2716 170
                                $newValue instanceof Proxy &&
2717 170
                                $newValue->__isInitialized__ === false) {
2718
2719
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2720
                            }
2721
2722 170
                            break;
2723
2724 202
                        case ($targetClass->subClasses):
2725
                            // If it might be a subtype, it can not be lazy. There isn't even
2726
                            // a way to solve this with deferred eager loading, which means putting
2727
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2728 32
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2729 32
                            break;
2730
2731
                        default:
2732
                            switch (true) {
2733
                                // We are negating the condition here. Other cases will assume it is valid!
2734 172
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2735 165
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2736 165
                                    break;
2737
2738
                                // Deferred eager load only works for single identifier classes
2739 7
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
2740
                                    // TODO: Is there a faster approach?
2741 7
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2742
2743 7
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2744 7
                                    break;
2745
2746
                                default:
2747
                                    // TODO: This is very imperformant, ignore it?
2748
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2749
                                    break;
2750
                            }
2751
2752
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2753 172
                            $newValueOid = spl_object_hash($newValue);
2754 172
                            $this->entityIdentifiers[$newValueOid] = $associatedId;
2755 172
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2756
2757
                            if (
2758 172
                                $newValue instanceof NotifyPropertyChanged &&
2759 172
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
2760
                            ) {
2761
                                $newValue->addPropertyChangedListener($this);
2762
                            }
2763 172
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2764
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2765 172
                            break;
2766
                    }
2767
2768 296
                    $this->originalEntityData[$oid][$field] = $newValue;
2769 296
                    $class->reflFields[$field]->setValue($entity, $newValue);
2770
2771 296
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2772 57
                        $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
2773 57
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2774
                    }
2775
2776 296
                    break;
2777
2778
                default:
2779
                    // Ignore if its a cached collection
2780 492
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2781
                        break;
2782
                    }
2783
2784
                    // use the given collection
2785 492
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2786
2787 3
                        $data[$field]->setOwner($entity, $assoc);
2788
2789 3
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2790 3
                        $this->originalEntityData[$oid][$field] = $data[$field];
2791
2792 3
                        break;
2793
                    }
2794
2795
                    // Inject collection
2796 492
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2797 492
                    $pColl->setOwner($entity, $assoc);
2798 492
                    $pColl->setInitialized(false);
2799
2800 492
                    $reflField = $class->reflFields[$field];
2801 492
                    $reflField->setValue($entity, $pColl);
2802
2803 492
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2804 4
                        $this->loadCollection($pColl);
2805 4
                        $pColl->takeSnapshot();
2806
                    }
2807
2808 492
                    $this->originalEntityData[$oid][$field] = $pColl;
2809 579
                    break;
2810
            }
2811
        }
2812
2813
        // defer invoking of postLoad event to hydration complete step
2814 699
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

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