Code

< 40 %
40-60 %
> 60 %
1
<?php
2
3
namespace Bankiru\Api\Doctrine;
4
5
use Bankiru\Api\Doctrine\Cache\ApiEntityCache;
6
use Bankiru\Api\Doctrine\Cache\EntityCacheAwareInterface;
7
use Bankiru\Api\Doctrine\Cache\LoggingCache;
8
use Bankiru\Api\Doctrine\Cache\VoidEntityCache;
9
use Bankiru\Api\Doctrine\Exception\MappingException;
10
use Bankiru\Api\Doctrine\Hydration\EntityHydrator;
11
use Bankiru\Api\Doctrine\Hydration\Hydrator;
12
use Bankiru\Api\Doctrine\Mapping\ApiMetadata;
13
use Bankiru\Api\Doctrine\Mapping\EntityMetadata;
14
use Bankiru\Api\Doctrine\Persister\ApiPersister;
15
use Bankiru\Api\Doctrine\Persister\CollectionMatcher;
16
use Bankiru\Api\Doctrine\Persister\CollectionPersister;
17
use Bankiru\Api\Doctrine\Persister\EntityPersister;
18
use Bankiru\Api\Doctrine\Proxy\ApiCollection;
19
use Bankiru\Api\Doctrine\Rpc\CrudsApiInterface;
20
use Bankiru\Api\Doctrine\Utility\IdentifierFlattener;
21
use Bankiru\Api\Doctrine\Utility\ReflectionPropertiesGetter;
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\Common\Proxy\Proxy;
29
30
class UnitOfWork implements PropertyChangedListener
31
{
32
    /**
33
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
34
     */
35
    const STATE_MANAGED = 1;
36
    /**
37
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
38
     * and is not (yet) managed by an EntityManager.
39
     */
40
    const STATE_NEW = 2;
41
    /**
42
     * A detached entity is an instance with persistent state and identity that is not
43
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
44
     */
45
    const STATE_DETACHED = 3;
46
    /**
47
     * A removed entity instance is an instance with a persistent identity,
48
     * associated with an EntityManager, whose persistent state will be deleted
49
     * on commit.
50
     */
51
    const STATE_REMOVED = 4;
52
53
    /**
54
     * The (cached) states of any known entities.
55
     * Keys are object ids (spl_object_hash).
56
     *
57
     * @var array
58
     */
59
    private $entityStates = [];
60
61
    /** @var  EntityManager */
62
    private $manager;
63
    /** @var EntityPersister[] */
64
    private $persisters = [];
65
    /** @var CollectionPersister[] */
66
    private $collectionPersisters = [];
67
    /** @var  array */
68
    private $entityIdentifiers = [];
69
    /** @var  object[][] */
70
    private $identityMap = [];
71
    /** @var IdentifierFlattener */
72
    private $identifierFlattener;
73
    /** @var  array */
74
    private $originalEntityData = [];
75
    /** @var  array */
76
    private $entityDeletions = [];
77
    /** @var  array */
78
    private $entityChangeSets = [];
79
    /** @var  array */
80
    private $entityInsertions = [];
81
    /** @var  array */
82
    private $entityUpdates = [];
83
    /** @var  array */
84
    private $readOnlyObjects = [];
85
    /** @var  array */
86
    private $scheduledForSynchronization = [];
87
    /** @var  array */
88
    private $orphanRemovals = [];
89
    /** @var  ApiCollection[] */
90
    private $collectionDeletions = [];
91
    /** @var  array */
92
    private $extraUpdates = [];
93
    /** @var  ApiCollection[] */
94
    private $collectionUpdates = [];
95
    /** @var  ApiCollection[] */
96
    private $visitedCollections = [];
97
    /** @var ReflectionPropertiesGetter */
98
    private $reflectionPropertiesGetter;
99
    /** @var Hydrator[] */
100
    private $hydrators = [];
101
    /** @var CrudsApiInterface[] */
102
    private $apis = [];
103
104
    /**
105
     * UnitOfWork constructor.
106
     *
107
     * @param EntityManager $manager
108
     */
109
    public function __construct(EntityManager $manager)
110
    {
111
        $this->manager                    = $manager;
112
        $this->identifierFlattener        = new IdentifierFlattener($this->manager);
113
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
114
    }
115
116
    /**
117
     * @param $className
118
     *
119
     * @return EntityPersister
120
     */
121
    public function getEntityPersister($className)
122
    {
123
        if (!array_key_exists($className, $this->persisters)) {
124
            /** @var ApiMetadata $classMetadata */
125
            $classMetadata = $this->manager->getClassMetadata($className);
126
127
            $api = $this->getCrudsApi($classMetadata);
128
129
            if ($api instanceof EntityCacheAwareInterface) {
130
                $api->setEntityCache($this->createEntityCache($classMetadata));
131
            }
132
133
            $this->persisters[$className] = new ApiPersister($this->manager, $api);
134
        }
135
136
        return $this->persisters[$className];
137
    }
138
139
    /**
140
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
141
     *
142
     * @param object $entity
143
     *
144
     * @return boolean
145
     */
146
    public function isInIdentityMap($entity)
147
    {
148
        $oid = spl_object_hash($entity);
149
150
        if (!isset($this->entityIdentifiers[$oid])) {
151
            return false;
152
        }
153
154
        /** @var EntityMetadata $classMetadata */
155
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
156
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
157
158
        if ($idHash === '') {
159
            return false;
160
        }
161
162
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
163
    }
164
165
    /**
166
     * Gets the identifier of an entity.
167
     * The returned value is always an array of identifier values. If the entity
168
     * has a composite identifier then the identifier values are in the same
169
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
170
     *
171
     * @param object $entity
172
     *
173
     * @return array The identifier values.
174
     */
175
    public function getEntityIdentifier($entity)
176
    {
177
        return $this->entityIdentifiers[spl_object_hash($entity)];
178
    }
179
180
    /**
181
     * @param             string $className
182
     * @param \stdClass   $data
183
     *
184
     * @return ObjectManagerAware|object
185
     * @throws MappingException
186
     */
187
    public function getOrCreateEntity($className, \stdClass $data)
188
    {
189
        /** @var EntityMetadata $class */
190
        $class     = $this->resolveSourceMetadataForClass($data, $className);
191
        $tmpEntity = $this->getHydratorForClass($class)->hydarate($data);
192
193
        $id     = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($tmpEntity));
194
        $idHash = implode(' ', $id);
195
196
        $overrideLocalValues = false;
197
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
198
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
199
            $oid    = spl_object_hash($entity);
200
201
            if ($entity instanceof Proxy && !$entity->__isInitialized()) {
202
                $entity->__setInitialized(true);
203
204
                $overrideLocalValues            = true;
205
                $this->originalEntityData[$oid] = $data;
206
207
                if ($entity instanceof NotifyPropertyChanged) {
208
                    $entity->addPropertyChangedListener($this);
209
                }
210
            }
211
        } else {
212
            $entity                                             = $this->newInstance($class);
213
            $oid                                                = spl_object_hash($entity);
214
            $this->entityIdentifiers[$oid]                      = $id;
215
            $this->entityStates[$oid]                           = self::STATE_MANAGED;
216
            $this->originalEntityData[$oid]                     = $data;
217
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
218
            if ($entity instanceof NotifyPropertyChanged) {
219
                $entity->addPropertyChangedListener($this);
220
            }
221
            $overrideLocalValues = true;
222
        }
223
224
        if (!$overrideLocalValues) {
225
            return $entity;
226
        }
227
228
        $entity = $this->getHydratorForClass($class)->hydarate($data, $entity);
229
230
        return $entity;
231
    }
232
233
    /**
234
     * INTERNAL:
235
     * Registers an entity as managed.
236
     *
237
     * @param Proxy         $entity The entity.
238
     * @param array          $id     The identifier values.
239
     * @param \stdClass|null $data   The original entity data.
240
     *
241
     * @return void
242
     */
243
    public function registerManaged($entity, array $id, \stdClass $data = null)
244
    {
245
        $oid = spl_object_hash($entity);
246
247
        $this->entityIdentifiers[$oid]  = $id;
248
        $this->entityStates[$oid]       = self::STATE_MANAGED;
249
        $this->originalEntityData[$oid] = $data;
250
251
        $this->addToIdentityMap($entity);
252
253
        if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) {
254
            $entity->addPropertyChangedListener($this);
255
        }
256
    }
257
258
    /**
259
     * INTERNAL:
260
     * Registers an entity in the identity map.
261
     * Note that entities in a hierarchy are registered with the class name of
262
     * the root entity.
263
     *
264
     * @ignore
265
     *
266
     * @param object $entity The entity to register.
267
     *
268
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
269
     *                 the entity in question is already managed.
270
     *
271
     */
272
    public function addToIdentityMap($entity)
273
    {
274
        /** @var EntityMetadata $classMetadata */
275
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
276
        $idHash        = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]);
277
278
        if ($idHash === '') {
279
            throw new \InvalidArgumentException('Entitty does not have valid identifiers to be stored at identity map');
280
        }
281
282
        $className = $classMetadata->rootEntityName;
283
284
        if (isset($this->identityMap[$className][$idHash])) {
285
            return false;
286
        }
287
288
        $this->identityMap[$className][$idHash] = $entity;
289
290
        return true;
291
    }
292
293
    /**
294
     * Gets the identity map of the UnitOfWork.
295
     *
296
     * @return array
297
     */
298
    public function getIdentityMap()
299
    {
300
        return $this->identityMap;
301
    }
302
303
    /**
304
     * Gets the original data of an entity. The original data is the data that was
305
     * present at the time the entity was reconstituted from the database.
306
     *
307
     * @param object $entity
308
     *
309
     * @return array
310
     */
311
    public function getOriginalEntityData($entity)
312
    {
313
        $oid = spl_object_hash($entity);
314
315
        if (isset($this->originalEntityData[$oid])) {
316
            return $this->originalEntityData[$oid];
317
        }
318
319
        return [];
320
    }
321
322
    /**
323
     * INTERNAL:
324
     * Checks whether an identifier hash exists in the identity map.
325
     *
326
     * @ignore
327
     *
328
     * @param string $idHash
329
     * @param string $rootClassName
330
     *
331
     * @return boolean
332
     */
333
    public function containsIdHash($idHash, $rootClassName)
334
    {
335
        return isset($this->identityMap[$rootClassName][$idHash]);
336
    }
337
338
    /**
339
     * INTERNAL:
340
     * Gets an entity in the identity map by its identifier hash.
341
     *
342
     * @ignore
343
     *
344
     * @param string $idHash
345
     * @param string $rootClassName
346
     *
347
     * @return object
348
     */
349
    public function getByIdHash($idHash, $rootClassName)
350
    {
351
        return $this->identityMap[$rootClassName][$idHash];
352
    }
353
354
    /**
355
     * INTERNAL:
356
     * Tries to get an entity by its identifier hash. If no entity is found for
357
     * the given hash, FALSE is returned.
358
     *
359
     * @ignore
360
     *
361
     * @param mixed  $idHash (must be possible to cast it to string)
362
     * @param string $rootClassName
363
     *
364
     * @return object|bool The found entity or FALSE.
365
     */
366
    public function tryGetByIdHash($idHash, $rootClassName)
367
    {
368
        $stringIdHash = (string)$idHash;
369
370
        if (isset($this->identityMap[$rootClassName][$stringIdHash])) {
371
            return $this->identityMap[$rootClassName][$stringIdHash];
372
        }
373
374
        return false;
375
    }
376
377
    /**
378
     * Gets the state of an entity with regard to the current unit of work.
379
     *
380
     * @param object   $entity
381
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
382
     *                         This parameter can be set to improve performance of entity state detection
383
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
384
     *                         is either known or does not matter for the caller of the method.
385
     *
386
     * @return int The entity state.
387
     */
388
    public function getEntityState($entity, $assume = null)
389
    {
390
        $oid = spl_object_hash($entity);
391
        if (isset($this->entityStates[$oid])) {
392
            return $this->entityStates[$oid];
393
        }
394
        if ($assume !== null) {
395
            return $assume;
396
        }
397
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
398
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
399
        // the UoW does not hold references to such objects and the object hash can be reused.
400
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
401
        $class = $this->manager->getClassMetadata(get_class($entity));
402
        $id    = $class->getIdentifierValues($entity);
403
        if (!$id) {
404
            return self::STATE_NEW;
405
        }
406
407
        return self::STATE_DETACHED;
408
    }
409
410
    /**
411
     * Tries to find an entity with the given identifier in the identity map of
412
     * this UnitOfWork.
413
     *
414
     * @param mixed  $id            The entity identifier to look for.
415
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
416
     *
417
     * @return object|bool Returns the entity with the specified identifier if it exists in
418
     *                     this UnitOfWork, FALSE otherwise.
419
     */
420
    public function tryGetById($id, $rootClassName)
421
    {
422
        /** @var EntityMetadata $metadata */
423
        $metadata = $this->manager->getClassMetadata($rootClassName);
424
        $idHash   = implode(' ', (array)$this->identifierFlattener->flattenIdentifier($metadata, $id));
425
426
        if (isset($this->identityMap[$rootClassName][$idHash])) {
427
            return $this->identityMap[$rootClassName][$idHash];
428
        }
429
430
        return false;
431
    }
432
433
    /**
434
     * Notifies this UnitOfWork of a property change in an entity.
435
     *
436
     * @param object $entity       The entity that owns the property.
437
     * @param string $propertyName The name of the property that changed.
438
     * @param mixed  $oldValue     The old value of the property.
439
     * @param mixed  $newValue     The new value of the property.
440
     *
441
     * @return void
442
     */
443
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
444
    {
445
        $oid          = spl_object_hash($entity);
446
        $class        = $this->manager->getClassMetadata(get_class($entity));
447
        $isAssocField = $class->hasAssociation($propertyName);
448
        if (!$isAssocField && !$class->hasField($propertyName)) {
449
            return; // ignore non-persistent fields
450
        }
451
        // Update changeset and mark entity for synchronization
452
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
453
        if (!isset($this->scheduledForSynchronization[$class->getRootEntityName()][$oid])) {
454
            $this->scheduleForDirtyCheck($entity);
455
        }
456
    }
457
458
    /**
459
     * Persists an entity as part of the current unit of work.
460
     *
461
     * @param object $entity The entity to persist.
462
     *
463
     * @return void
464
     */
465
    public function persist($entity)
466
    {
467
        $visited = [];
468
        $this->doPersist($entity, $visited);
469
    }
470
471
    /**
472
     * @param ApiMetadata $class
473
     * @param             $entity
474
     *
475
     * @throws \InvalidArgumentException
476
     * @throws \RuntimeException
477
     */
478
    public function recomputeSingleEntityChangeSet(ApiMetadata $class, $entity)
479
    {
480
        $oid = spl_object_hash($entity);
481
        if (!isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
482
            throw new \InvalidArgumentException('Entity is not managed');
483
        }
484
485
        $actualData = [];
486
        foreach ($class->getReflectionProperties() as $name => $refProp) {
487
            if (!$class->isIdentifier($name) && !$class->isCollectionValuedAssociation($name)) {
488
                $actualData[$name] = $refProp->getValue($entity);
489
            }
490
        }
491
        if (!isset($this->originalEntityData[$oid])) {
492
            throw new \RuntimeException(
493
                'Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'
494
            );
495
        }
496
        $originalData = $this->originalEntityData[$oid];
497
        $changeSet    = [];
498
        foreach ($actualData as $propName => $actualValue) {
499
            $orgValue = isset($originalData->$propName) ? $originalData->$propName : null;
500
            if ($orgValue !== $actualValue) {
501
                $changeSet[$propName] = [$orgValue, $actualValue];
502
            }
503
        }
504
        if ($changeSet) {
505
            if (isset($this->entityChangeSets[$oid])) {
506
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
507
            } else {
508
                if (!isset($this->entityInsertions[$oid])) {
509
                    $this->entityChangeSets[$oid] = $changeSet;
510
                    $this->entityUpdates[$oid]    = $entity;
511
                }
512
            }
513
            $this->originalEntityData[$oid] = (object)$actualData;
514
        }
515
    }
516
517
    /**
518
     * Schedules an entity for insertion into the database.
519
     * If the entity already has an identifier, it will be added to the identity map.
520
     *
521
     * @param object $entity The entity to schedule for insertion.
522
     *
523
     * @return void
524
     *
525
     * @throws \InvalidArgumentException
526
     */
527
    public function scheduleForInsert($entity)
528
    {
529
        $oid = spl_object_hash($entity);
530
        if (isset($this->entityUpdates[$oid])) {
531
            throw new \InvalidArgumentException('Dirty entity cannot be scheduled for insertion');
532
        }
533
        if (isset($this->entityDeletions[$oid])) {
534
            throw new \InvalidArgumentException('Removed entity scheduled for insertion');
535
        }
536
        if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) {
537
            throw new \InvalidArgumentException('Managed entity scheduled for insertion');
538
        }
539
        if (isset($this->entityInsertions[$oid])) {
540
            throw new \InvalidArgumentException('Entity scheduled for insertion twice');
541
        }
542
        $this->entityInsertions[$oid] = $entity;
543
        if (isset($this->entityIdentifiers[$oid])) {
544
            $this->addToIdentityMap($entity);
545
        }
546
        if ($entity instanceof NotifyPropertyChanged) {
547
            $entity->addPropertyChangedListener($this);
548
        }
549
    }
550
551
    /**
552
     * Checks whether an entity is scheduled for insertion.
553
     *
554
     * @param object $entity
555
     *
556
     * @return boolean
557
     */
558
    public function isScheduledForInsert($entity)
559
    {
560
        return isset($this->entityInsertions[spl_object_hash($entity)]);
561
    }
562
563
    /**
564
     * Schedules an entity for being updated.
565
     *
566
     * @param object $entity The entity to schedule for being updated.
567
     *
568
     * @return void
569
     *
570
     * @throws \InvalidArgumentException
571
     */
572
    public function scheduleForUpdate($entity)
573
    {
574
        $oid = spl_object_hash($entity);
575
        if (!isset($this->entityIdentifiers[$oid])) {
576
            throw new \InvalidArgumentException('Entity has no identity');
577
        }
578
        if (isset($this->entityDeletions[$oid])) {
579
            throw new \InvalidArgumentException('Entity is removed');
580
        }
581
        if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) {
582
            $this->entityUpdates[$oid] = $entity;
583
        }
584
    }
585
586
    /**
587
     * Checks whether an entity is registered as dirty in the unit of work.
588
     * Note: Is not very useful currently as dirty entities are only registered
589
     * at commit time.
590
     *
591
     * @param object $entity
592
     *
593
     * @return boolean
594
     */
595
    public function isScheduledForUpdate($entity)
596
    {
597
        return isset($this->entityUpdates[spl_object_hash($entity)]);
598
    }
599
600
    /**
601
     * Checks whether an entity is registered to be checked in the unit of work.
602
     *
603
     * @param object $entity
604
     *
605
     * @return boolean
606
     */
607
    public function isScheduledForDirtyCheck($entity)
608
    {
609
        $rootEntityName = $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
610
611
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
612
    }
613
614
    /**
615
     * INTERNAL:
616
     * Schedules an entity for deletion.
617
     *
618
     * @param object $entity
619
     *
620
     * @return void
621
     */
622
    public function scheduleForDelete($entity)
623
    {
624
        $oid = spl_object_hash($entity);
625
        if (isset($this->entityInsertions[$oid])) {
626
            if ($this->isInIdentityMap($entity)) {
627
                $this->removeFromIdentityMap($entity);
628
            }
629
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
630
631
            return; // entity has not been persisted yet, so nothing more to do.
632
        }
633
        if (!$this->isInIdentityMap($entity)) {
634
            return;
635
        }
636
        $this->removeFromIdentityMap($entity);
637
        unset($this->entityUpdates[$oid]);
638
        if (!isset($this->entityDeletions[$oid])) {
639
            $this->entityDeletions[$oid] = $entity;
640
            $this->entityStates[$oid]    = self::STATE_REMOVED;
641
        }
642
    }
643
644
    /**
645
     * Checks whether an entity is registered as removed/deleted with the unit
646
     * of work.
647
     *
648
     * @param object $entity
649
     *
650
     * @return boolean
651
     */
652
    public function isScheduledForDelete($entity)
653
    {
654
        return isset($this->entityDeletions[spl_object_hash($entity)]);
655
    }
656
657
    /**
658
     * Checks whether an entity is scheduled for insertion, update or deletion.
659
     *
660
     * @param object $entity
661
     *
662
     * @return boolean
663
     */
664
    public function isEntityScheduled($entity)
665
    {
666
        $oid = spl_object_hash($entity);
667
668
        return isset($this->entityInsertions[$oid])
669
               || isset($this->entityUpdates[$oid])
670
               || isset($this->entityDeletions[$oid]);
671
    }
672
673
    /**
674
     * INTERNAL:
675
     * Removes an entity from the identity map. This effectively detaches the
676
     * entity from the persistence management of Doctrine.
677
     *
678
     * @ignore
679
     *
680
     * @param object $entity
681
     *
682
     * @return boolean
683
     *
684
     * @throws \InvalidArgumentException
685
     */
686
    public function removeFromIdentityMap($entity)
687
    {
688
        $oid           = spl_object_hash($entity);
689
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
690
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
691
        if ($idHash === '') {
692
            throw new \InvalidArgumentException('Entity has no identity');
693
        }
694
        $className = $classMetadata->getRootEntityName();
695
        if (isset($this->identityMap[$className][$idHash])) {
696
            unset($this->identityMap[$className][$idHash]);
697
            unset($this->readOnlyObjects[$oid]);
698
699
            //$this->entityStates[$oid] = self::STATE_DETACHED;
700
            return true;
701
        }
702
703
        return false;
704
    }
705
706
    /**
707
     * Commits the UnitOfWork, executing all operations that have been postponed
708
     * up to this point. The state of all managed entities will be synchronized with
709
     * the database.
710
     *
711
     * The operations are executed in the following order:
712
     *
713
     * 1) All entity insertions
714
     * 2) All entity updates
715
     * 3) All collection deletions
716
     * 4) All collection updates
717
     * 5) All entity deletions
718
     *
719
     * @param null|object|array $entity
720
     *
721
     * @return void
722
     *
723
     * @throws \Exception
724
     */
725
    public function commit($entity = null)
726
    {
727
        // Compute changes done since last commit.
728
        if ($entity === null) {
729
            $this->computeChangeSets();
730
        } elseif (is_object($entity)) {
731
            $this->computeSingleEntityChangeSet($entity);
732
        } elseif (is_array($entity)) {
733
            foreach ((array)$entity as $object) {
734
                $this->computeSingleEntityChangeSet($object);
735
            }
736
        }
737
        if (!($this->entityInsertions ||
738
              $this->entityDeletions ||
739
              $this->entityUpdates ||
740
              $this->collectionUpdates ||
741
              $this->collectionDeletions ||
742
              $this->orphanRemovals)
743
        ) {
744
            return; // Nothing to do.
745
        }
746
        if ($this->orphanRemovals) {
747
            foreach ($this->orphanRemovals as $orphan) {
748
                $this->remove($orphan);
749
            }
750
        }
751
        // Now we need a commit order to maintain referential integrity
752
        $commitOrder = $this->getCommitOrder();
753
754
        // Collection deletions (deletions of complete collections)
755
        // foreach ($this->collectionDeletions as $collectionToDelete) {
756
        //       //fixme: collection mutations
757
        //       $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
758
        // }
759
        if ($this->entityInsertions) {
760
            foreach ($commitOrder as $class) {
761
                $this->executeInserts($class);
762
            }
763
        }
764
        if ($this->entityUpdates) {
765
            foreach ($commitOrder as $class) {
766
                $this->executeUpdates($class);
767
            }
768
        }
769
        // Extra updates that were requested by persisters.
770
        if ($this->extraUpdates) {
771
            $this->executeExtraUpdates();
772
        }
773
        // Collection updates (deleteRows, updateRows, insertRows)
774
        foreach ($this->collectionUpdates as $collectionToUpdate) {
775
            //fixme: decide what to do with collection mutation if API does not support this
776
            //$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
777
        }
778
        // Entity deletions come last and need to be in reverse commit order
779
        if ($this->entityDeletions) {
780
            for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) {
781
                $this->executeDeletions($commitOrder[$i]);
782
            }
783
        }
784
785
        // Take new snapshots from visited collections
786
        foreach ($this->visitedCollections as $coll) {
787
            $coll->takeSnapshot();
788
        }
789
790
        // Clear up
791
        $this->entityInsertions =
792
        $this->entityUpdates =
793
        $this->entityDeletions =
794
        $this->extraUpdates =
795
        $this->entityChangeSets =
796
        $this->collectionUpdates =
797
        $this->collectionDeletions =
798
        $this->visitedCollections =
799
        $this->scheduledForSynchronization =
800
        $this->orphanRemovals = [];
801
    }
802
803
    /**
804
     * Gets the changeset for an entity.
805
     *
806
     * @param object $entity
807
     *
808
     * @return array
809
     */
810
    public function & getEntityChangeSet($entity)
811
    {
812
        $oid  = spl_object_hash($entity);
813
        $data = [];
814
        if (!isset($this->entityChangeSets[$oid])) {
815
            return $data;
816
        }
817
818
        return $this->entityChangeSets[$oid];
819
    }
820
821
    /**
822
     * Computes the changes that happened to a single entity.
823
     *
824
     * Modifies/populates the following properties:
825
     *
826
     * {@link _originalEntityData}
827
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
828
     * then it was not fetched from the database and therefore we have no original
829
     * entity data yet. All of the current entity data is stored as the original entity data.
830
     *
831
     * {@link _entityChangeSets}
832
     * The changes detected on all properties of the entity are stored there.
833
     * A change is a tuple array where the first entry is the old value and the second
834
     * entry is the new value of the property. Changesets are used by persisters
835
     * to INSERT/UPDATE the persistent entity state.
836
     *
837
     * {@link _entityUpdates}
838
     * If the entity is already fully MANAGED (has been fetched from the database before)
839
     * and any changes to its properties are detected, then a reference to the entity is stored
840
     * there to mark it for an update.
841
     *
842
     * {@link _collectionDeletions}
843
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
844
     * then this collection is marked for deletion.
845
     *
846
     * @ignore
847
     *
848
     * @internal Don't call from the outside.
849
     *
850
     * @param ApiMetadata $class  The class descriptor of the entity.
851
     * @param object      $entity The entity for which to compute the changes.
852
     *
853
     * @return void
854
     */
855
    public function computeChangeSet(ApiMetadata $class, $entity)
856
    {
857
        $oid = spl_object_hash($entity);
858
        if (isset($this->readOnlyObjects[$oid])) {
859
            return;
860
        }
861
862
        $actualData = [];
863
        foreach ($class->getReflectionProperties() as $name => $refProp) {
864
            $value = $refProp->getValue($entity);
865
            if (null !== $value && $class->isCollectionValuedAssociation($name)) {
866
                if ($value instanceof ApiCollection) {
867
                    if ($value->getOwner() === $entity) {
868
                        continue;
869
                    }
870
                    $value = new ArrayCollection($value->getValues());
871
                }
872
                // If $value is not a Collection then use an ArrayCollection.
873
                if (!$value instanceof Collection) {
874
                    $value = new ArrayCollection($value);
875
                }
876
                $assoc = $class->getAssociationMapping($name);
877
                // Inject PersistentCollection
878
                $value = new ApiCollection(
879
                    $this->manager,
880
                    $this->manager->getClassMetadata($assoc['targetEntity']),
881
                    $value
882
                );
883
                $value->setOwner($entity, $assoc);
884
                $value->setDirty(!$value->isEmpty());
885
                $class->getReflectionProperty($name)->setValue($entity, $value);
886
                $actualData[$name] = $value;
887
                continue;
888
            }
889
            if (!$class->isIdentifier($name)) {
890
                $actualData[$name] = $value;
891
            }
892
        }
893
        if (!isset($this->originalEntityData[$oid])) {
894
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
895
            // These result in an INSERT.
896
            $this->originalEntityData[$oid] = (object)$actualData;
897
            $changeSet                      = [];
898
            foreach ($actualData as $propName => $actualValue) {
899
                if (!$class->hasAssociation($propName)) {
900
                    $changeSet[$propName] = [null, $actualValue];
901
                    continue;
902
                }
903
                $assoc = $class->getAssociationMapping($propName);
904
                if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) {
905
                    $changeSet[$propName] = [null, $actualValue];
906
                }
907
            }
908
            $this->entityChangeSets[$oid] = $changeSet;
909
        } else {
910
911
            // Entity is "fully" MANAGED: it was already fully persisted before
912
            // and we have a copy of the original data
913
            $originalData           = $this->originalEntityData[$oid];
914
            $isChangeTrackingNotify = false;
915
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
916
                ? $this->entityChangeSets[$oid]
917
                : [];
918
919
            foreach ($actualData as $propName => $actualValue) {
920
                // skip field, its a partially omitted one!
921
                if (!property_exists($originalData, $propName)) {
922
                    continue;
923
                }
924
                $orgValue = $originalData->$propName;
925
                // skip if value haven't changed
926
                if ($orgValue === $actualValue) {
927
928
                    continue;
929
                }
930
                // if regular field
931
                if (!$class->hasAssociation($propName)) {
932
                    if ($isChangeTrackingNotify) {
933
                        continue;
934
                    }
935
                    $changeSet[$propName] = [$orgValue, $actualValue];
936
                    continue;
937
                }
938
939
                $assoc = $class->getAssociationMapping($propName);
940
                // Persistent collection was exchanged with the "originally"
941
                // created one. This can only mean it was cloned and replaced
942
                // on another entity.
943
                if ($actualValue instanceof ApiCollection) {
944
                    $owner = $actualValue->getOwner();
945
                    if ($owner === null) { // cloned
946
                        $actualValue->setOwner($entity, $assoc);
947
                    } else {
948
                        if ($owner !== $entity) { // no clone, we have to fix
949
                            if (!$actualValue->isInitialized()) {
950
                                $actualValue->initialize(); // we have to do this otherwise the cols share state
951
                            }
952
                            $newValue = clone $actualValue;
953
                            $newValue->setOwner($entity, $assoc);
954
                            $class->getReflectionProperty($propName)->setValue($entity, $newValue);
955
                        }
956
                    }
957
                }
958
                if ($orgValue instanceof ApiCollection) {
959
                    // A PersistentCollection was de-referenced, so delete it.
960
                    $coid = spl_object_hash($orgValue);
961
                    if (isset($this->collectionDeletions[$coid])) {
962
                        continue;
963
                    }
964
                    $this->collectionDeletions[$coid] = $orgValue;
965
                    $changeSet[$propName]             = $orgValue; // Signal changeset, to-many assocs will be ignored.
966
                    continue;
967
                }
968
                if ($assoc['type'] & ApiMetadata::TO_ONE) {
969
                    if ($assoc['isOwningSide']) {
970
                        $changeSet[$propName] = [$orgValue, $actualValue];
971
                    }
972
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
973
                        $this->scheduleOrphanRemoval($orgValue);
974
                    }
975
                }
976
            }
977
            if ($changeSet) {
978
                $this->entityChangeSets[$oid]   = $changeSet;
979
                $this->originalEntityData[$oid] = (object)$actualData;
980
                $this->entityUpdates[$oid]      = $entity;
981
            }
982
        }
983
        // Look for changes in associations of the entity
984
        foreach ($class->getAssociationMappings() as $field => $assoc) {
985
            if (($val = $class->getReflectionProperty($field)->getValue($entity)) === null) {
986
                continue;
987
            }
988
            $this->computeAssociationChanges($assoc, $val);
989
            if (!isset($this->entityChangeSets[$oid]) &&
990
                $assoc['isOwningSide'] &&
991
                $assoc['type'] == ApiMetadata::MANY_TO_MANY &&
992
                $val instanceof ApiCollection &&
993
                $val->isDirty()
994
            ) {
995
                $this->entityChangeSets[$oid]   = [];
996
                $this->originalEntityData[$oid] = (object)$actualData;
997
                $this->entityUpdates[$oid]      = $entity;
998
            }
999
        }
1000
    }
1001
1002
    /**
1003
     * Computes all the changes that have been done to entities and collections
1004
     * since the last commit and stores these changes in the _entityChangeSet map
1005
     * temporarily for access by the persisters, until the UoW commit is finished.
1006
     *
1007
     * @return void
1008
     */
1009
    public function computeChangeSets()
1010
    {
1011
        // Compute changes for INSERTed entities first. This must always happen.
1012
        $this->computeScheduleInsertsChangeSets();
1013
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
1014
        foreach ($this->identityMap as $className => $entities) {
1015
            $class = $this->manager->getClassMetadata($className);
1016
            // Skip class if instances are read-only
1017
            if ($class->isReadOnly()) {
1018
                continue;
1019
            }
1020
            // If change tracking is explicit or happens through notification, then only compute
1021
            // changes on entities of that type that are explicitly marked for synchronization.
1022
            switch (true) {
1023
                case ($class->isChangeTrackingDeferredImplicit()):
1024
                    $entitiesToProcess = $entities;
1025
                    break;
1026
                case (isset($this->scheduledForSynchronization[$className])):
1027
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
1028
                    break;
1029
                default:
1030
                    $entitiesToProcess = [];
1031
            }
1032
            foreach ($entitiesToProcess as $entity) {
1033
                // Ignore uninitialized proxy objects
1034
                if ($entity instanceof Proxy && !$entity->__isInitialized__) {
1035
                    continue;
1036
                }
1037
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1038
                $oid = spl_object_hash($entity);
1039
                if (!isset($this->entityInsertions[$oid]) &&
1040
                    !isset($this->entityDeletions[$oid]) &&
1041
                    isset($this->entityStates[$oid])
1042
                ) {
1043
                    $this->computeChangeSet($class, $entity);
1044
                }
1045
            }
1046
        }
1047
    }
1048
1049
    /**
1050
     * INTERNAL:
1051
     * Schedules an orphaned entity for removal. The remove() operation will be
1052
     * invoked on that entity at the beginning of the next commit of this
1053
     * UnitOfWork.
1054
     *
1055
     * @ignore
1056
     *
1057
     * @param object $entity
1058
     *
1059
     * @return void
1060
     */
1061
    public function scheduleOrphanRemoval($entity)
1062
    {
1063
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
1064
    }
1065
1066
    public function loadCollection(ApiCollection $collection)
1067
    {
1068
        $assoc     = $collection->getMapping();
1069
        $persister = $this->getEntityPersister($assoc['targetEntity']);
1070
        switch ($assoc['type']) {
1071
            case ApiMetadata::ONE_TO_MANY:
1072
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
1073
                break;
1074
        }
1075
        $collection->setInitialized(true);
1076
    }
1077
1078
    public function getCollectionPersister($association)
1079
    {
1080
        $targetMetadata = $this->manager->getClassMetadata($association['targetEntity']);
1081
        $role           = $association['sourceEntity'] . '::' . $association['field'];
1082
1083
        if (!array_key_exists($role, $this->collectionPersisters)) {
1084
            $this->collectionPersisters[$role] = new CollectionPersister(
1085
                $this->manager,
1086
                $targetMetadata,
1087
                new CollectionMatcher($this->manager, $this->getCrudsApi($targetMetadata)),
1088
                $association
1089
            );
1090
        }
1091
1092
        return $this->collectionPersisters[$role];
1093
    }
1094
1095
    public function scheduleCollectionDeletion(Collection $collection)
1096
    {
1097
    }
1098
1099
    public function cancelOrphanRemoval($value)
1100
    {
1101
    }
1102
1103
    /**
1104
     * INTERNAL:
1105
     * Sets a property value of the original data array of an entity.
1106
     *
1107
     * @ignore
1108
     *
1109
     * @param string $oid
1110
     * @param string $property
1111
     * @param mixed  $value
1112
     *
1113
     * @return void
1114
     */
1115
    public function setOriginalEntityProperty($oid, $property, $value)
1116
    {
1117
        if (!array_key_exists($oid, $this->originalEntityData)) {
1118
            $this->originalEntityData[$oid] = new \stdClass();
1119
        }
1120
1121
        $this->originalEntityData[$oid]->$property = $value;
1122
    }
1123
1124
    public function scheduleExtraUpdate($entity, $changeset)
1125
    {
1126
        $oid         = spl_object_hash($entity);
1127
        $extraUpdate = [$entity, $changeset];
1128
        if (isset($this->extraUpdates[$oid])) {
1129
            list(, $changeset2) = $this->extraUpdates[$oid];
1130
            $extraUpdate = [$entity, $changeset + $changeset2];
1131
        }
1132
        $this->extraUpdates[$oid] = $extraUpdate;
1133
    }
1134
1135
    /**
1136
     * Refreshes the state of the given entity from the database, overwriting
1137
     * any local, unpersisted changes.
1138
     *
1139
     * @param object $entity The entity to refresh.
1140
     *
1141
     * @return void
1142
     *
1143
     * @throws InvalidArgumentException If the entity is not MANAGED.
1144
     */
1145
    public function refresh($entity)
1146
    {
1147
        $visited = [];
1148
        $this->doRefresh($entity, $visited);
1149
    }
1150
1151
    /**
1152
     * Clears the UnitOfWork.
1153
     *
1154
     * @param string|null $entityName if given, only entities of this type will get detached.
1155
     *
1156
     * @return void
1157
     */
1158
    public function clear($entityName = null)
1159
    {
1160
        if ($entityName === null) {
1161
            $this->identityMap =
1162
            $this->entityIdentifiers =
1163
            $this->originalEntityData =
1164
            $this->entityChangeSets =
1165
            $this->entityStates =
1166
            $this->scheduledForSynchronization =
1167
            $this->entityInsertions =
1168
            $this->entityUpdates =
1169
            $this->entityDeletions =
1170
            $this->collectionDeletions =
1171
            $this->collectionUpdates =
1172
            $this->extraUpdates =
1173
            $this->readOnlyObjects =
1174
            $this->visitedCollections =
1175
            $this->orphanRemovals = [];
1176
        } else {
1177
            $this->clearIdentityMapForEntityName($entityName);
1178
            $this->clearEntityInsertionsForEntityName($entityName);
1179
        }
1180
    }
1181
1182
    /**
1183
     * @param PersistentCollection $coll
1184
     *
1185
     * @return bool
1186
     */
1187
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
1188
    {
1189
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
1190
    }
1191
1192
    /**
1193
     * Schedules an entity for dirty-checking at commit-time.
1194
     *
1195
     * @param object $entity The entity to schedule for dirty-checking.
1196
     *
1197
     * @return void
1198
     *
1199
     * @todo Rename: scheduleForSynchronization
1200
     */
1201
    public function scheduleForDirtyCheck($entity)
1202
    {
1203
        $rootClassName                                                               =
1204
            $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
1205
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
1206
    }
1207
1208
    /**
1209
     * Deletes an entity as part of the current unit of work.
1210
     *
1211
     * @param object $entity The entity to remove.
1212
     *
1213
     * @return void
1214
     */
1215
    public function remove($entity)
1216
    {
1217
        $visited = [];
1218
        $this->doRemove($entity, $visited);
1219
    }
1220
1221
    /**
1222
     * Merges the state of the given detached entity into this UnitOfWork.
1223
     *
1224
     * @param object $entity
1225
     *
1226
     * @return object The managed copy of the entity.
1227
     */
1228
    public function merge($entity)
1229
    {
1230
        $visited = [];
1231
1232
        return $this->doMerge($entity, $visited);
1233
    }
1234
1235
    /**
1236
     * Detaches an entity from the persistence management. It's persistence will
1237
     * no longer be managed by Doctrine.
1238
     *
1239
     * @param object $entity The entity to detach.
1240
     *
1241
     * @return void
1242
     */
1243
    public function detach($entity)
1244
    {
1245
        $visited = [];
1246
        $this->doDetach($entity, $visited);
1247
    }
1248
1249
    /**
1250
     * Resolve metadata against source data and root class
1251
     *
1252
     * @param \stdClass $data
1253
     * @param string    $class
1254
     *
1255
     * @return ApiMetadata
1256
     * @throws MappingException
1257
     */
1258
    private function resolveSourceMetadataForClass(\stdClass $data, $class)
1259
    {
1260
        $metadata           = $this->manager->getClassMetadata($class);
1261
        $discriminatorValue = $metadata->getDiscriminatorValue();
1262
        if ($metadata->getDiscriminatorField()) {
1263
            $property = $metadata->getDiscriminatorField()['fieldName'];
1264
            if (isset($data->$property)) {
1265
                $discriminatorValue = $data->$property;
1266
            }
1267
        }
1268
1269
        $map = $metadata->getDiscriminatorMap();
1270
1271
        if (!array_key_exists($discriminatorValue, $map)) {
1272
            throw MappingException::unknownDiscriminatorValue($discriminatorValue, $class);
1273
        }
1274
1275
        $realClass = $map[$discriminatorValue];
1276
1277
        return $this->manager->getClassMetadata($realClass);
1278
    }
1279
1280
    /**
1281
     * Helper method to show an object as string.
1282
     *
1283
     * @param object $obj
1284
     *
1285
     * @return string
1286
     */
1287
    private static function objToStr($obj)
1288
    {
1289
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
1290
    }
1291
1292
    /**
1293
     * @param ApiMetadata $class
1294
     *
1295
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
1296
     */
1297
    private function newInstance(ApiMetadata $class)
1298
    {
1299
        $entity = $class->newInstance();
1300
1301
        if ($entity instanceof ObjectManagerAware) {
1302
            $entity->injectObjectManager($this->manager, $class);
1303
        }
1304
1305
        return $entity;
1306
    }
1307
1308
    /**
1309
     * @param ApiMetadata $classMetadata
1310
     *
1311
     * @return EntityDataCacheInterface
1312
     */
1313
    private function createEntityCache(ApiMetadata $classMetadata)
1314
    {
1315
        $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName());
1316
        $cache         = new VoidEntityCache($classMetadata);
1317
        if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) {
1318
            $cache =
1319
                new LoggingCache(
1320
                    new ApiEntityCache(
1321
                        $this->manager->getConfiguration()->getApiCache(),
1322
                        $classMetadata,
1323
                        $configuration
1324
                    ),
1325
                    $this->manager->getConfiguration()->getApiCacheLogger()
1326
                );
1327
1328
            return $cache;
1329
        }
1330
1331
        return $cache;
1332
    }
1333
1334
    /**
1335
     * @param ApiMetadata $classMetadata
1336
     *
1337
     * @return CrudsApiInterface
1338
     */
1339
    private function getCrudsApi(ApiMetadata $classMetadata)
1340
    {
1341
        if (!array_key_exists($classMetadata->getName(), $this->apis)) {
1342
            $client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName());
1343
1344
            $api = $this->manager
1345
                ->getConfiguration()
1346
                ->getFactoryRegistry()
1347
                ->create(
1348
                    $classMetadata->getApiFactory(),
1349
                    $client,
1350
                    $classMetadata
1351
                );
1352
1353
            $this->apis[$classMetadata->getName()] = $api;
1354
        }
1355
1356
        return $this->apis[$classMetadata->getName()];
1357
    }
1358
1359
    private function doPersist($entity, $visited)
1360
    {
1361
        $oid = spl_object_hash($entity);
1362
        if (isset($visited[$oid])) {
1363
            return; // Prevent infinite recursion
1364
        }
1365
        $visited[$oid] = $entity; // Mark visited
1366
        $class         = $this->manager->getClassMetadata(get_class($entity));
1367
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1368
        // If we would detect DETACHED here we would throw an exception anyway with the same
1369
        // consequences (not recoverable/programming error), so just assuming NEW here
1370
        // lets us avoid some database lookups for entities with natural identifiers.
1371
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1372
        switch ($entityState) {
1373
            case self::STATE_MANAGED:
1374
                $this->scheduleForDirtyCheck($entity);
1375
                break;
1376
            case self::STATE_NEW:
1377
                $this->persistNew($class, $entity);
1378
                break;
1379
            case self::STATE_REMOVED:
1380
                // Entity becomes managed again
1381
                unset($this->entityDeletions[$oid]);
1382
                $this->addToIdentityMap($entity);
1383
                $this->entityStates[$oid] = self::STATE_MANAGED;
1384
                break;
1385
            case self::STATE_DETACHED:
1386
                // Can actually not happen right now since we assume STATE_NEW.
1387
                throw new \InvalidArgumentException('Detached entity cannot be persisted');
1388
            default:
1389
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1390
        }
1391
        $this->cascadePersist($entity, $visited);
1392
    }
1393
1394
    /**
1395
     * Cascades the save operation to associated entities.
1396
     *
1397
     * @param object $entity
1398
     * @param array  $visited
1399
     *
1400
     * @return void
1401
     * @throws \InvalidArgumentException
1402
     * @throws MappingException
1403
     */
1404
    private function cascadePersist($entity, array &$visited)
1405
    {
1406
        $class               = $this->manager->getClassMetadata(get_class($entity));
1407
        $associationMappings = [];
1408
        foreach ($class->getAssociationNames() as $name) {
1409
            $assoc = $class->getAssociationMapping($name);
1410
            if ($assoc['isCascadePersist']) {
1411
                $associationMappings[$name] = $assoc;
1412
            }
1413
        }
1414
        foreach ($associationMappings as $assoc) {
1415
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1416
            switch (true) {
1417
                case ($relatedEntities instanceof ApiCollection):
1418
                    // Unwrap so that foreach() does not initialize
1419
                    $relatedEntities = $relatedEntities->unwrap();
1420
                // break; is commented intentionally!
1421
                case ($relatedEntities instanceof Collection):
1422
                case (is_array($relatedEntities)):
1423
                    if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) {
1424
                        throw new \InvalidArgumentException('Invalid association for cascade');
1425
                    }
1426
                    foreach ($relatedEntities as $relatedEntity) {
1427
                        $this->doPersist($relatedEntity, $visited);
1428
                    }
1429
                    break;
1430
                case ($relatedEntities !== null):
1431
                    if (!$relatedEntities instanceof $assoc['targetEntity']) {
1432
                        throw new \InvalidArgumentException('Invalid association for cascade');
1433
                    }
1434
                    $this->doPersist($relatedEntities, $visited);
1435
                    break;
1436
                default:
1437
                    // Do nothing
1438
            }
1439
        }
1440
    }
1441
1442
    /**
1443
     * @param ApiMetadata $class
1444
     * @param object      $entity
1445
     *
1446
     * @return void
1447
     */
1448
    private function persistNew($class, $entity)
1449
    {
1450
        $oid = spl_object_hash($entity);
1451
        //        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
1452
        //        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1453
        //            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1454
        //        }
1455
        //        $idGen = $class->idGenerator;
1456
        //        if ( ! $idGen->isPostInsertGenerator()) {
1457
        //            $idValue = $idGen->generate($this->em, $entity);
1458
        //            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
1459
        //                $idValue = array($class->identifier[0] => $idValue);
1460
        //                $class->setIdentifierValues($entity, $idValue);
1461
        //            }
1462
        //            $this->entityIdentifiers[$oid] = $idValue;
1463
        //        }
1464
        $this->entityStates[$oid] = self::STATE_MANAGED;
1465
        $this->scheduleForInsert($entity);
1466
    }
1467
1468
    /**
1469
     * Gets the commit order.
1470
     *
1471
     * @param array|null $entityChangeSet
1472
     *
1473
     * @return array
1474
     */
1475
    private function getCommitOrder(array $entityChangeSet = null)
1476
    {
1477
        if ($entityChangeSet === null) {
1478
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1479
        }
1480
        $calc = $this->getCommitOrderCalculator();
1481
        // See if there are any new classes in the changeset, that are not in the
1482
        // commit order graph yet (don't have a node).
1483
        // We have to inspect changeSet to be able to correctly build dependencies.
1484
        // It is not possible to use IdentityMap here because post inserted ids
1485
        // are not yet available.
1486
        /** @var ApiMetadata[] $newNodes */
1487
        $newNodes = [];
1488
        foreach ((array)$entityChangeSet as $entity) {
1489
            $class = $this->manager->getClassMetadata(get_class($entity));
1490
            if ($calc->hasNode($class->getName())) {
1491
                continue;
1492
            }
1493
            $calc->addNode($class->getName(), $class);
1494
            $newNodes[] = $class;
1495
        }
1496
        // Calculate dependencies for new nodes
1497
        while ($class = array_pop($newNodes)) {
1498
            foreach ($class->getAssociationMappings() as $assoc) {
1499
                if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) {
1500
                    continue;
1501
                }
1502
                $targetClass = $this->manager->getClassMetadata($assoc['targetEntity']);
1503
                if (!$calc->hasNode($targetClass->getName())) {
1504
                    $calc->addNode($targetClass->getName(), $targetClass);
1505
                    $newNodes[] = $targetClass;
1506
                }
1507
                $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable']));
1508
                // If the target class has mapped subclasses, these share the same dependency.
1509
                if (!$targetClass->getSubclasses()) {
1510
                    continue;
1511
                }
1512
                foreach ($targetClass->getSubclasses() as $subClassName) {
1513
                    $targetSubClass = $this->manager->getClassMetadata($subClassName);
1514
                    if (!$calc->hasNode($subClassName)) {
1515
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1516
                        $newNodes[] = $targetSubClass;
1517
                    }
1518
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1519
                }
1520
            }
1521
        }
1522
1523
        return $calc->sort();
1524
    }
1525
1526
    private function getCommitOrderCalculator()
1527
    {
1528
        return new Utility\CommitOrderCalculator();
1529
    }
1530
1531
    /**
1532
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
1533
     *
1534
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
1535
     * 2. Read Only entities are skipped.
1536
     * 3. Proxies are skipped.
1537
     * 4. Only if entity is properly managed.
1538
     *
1539
     * @param object $entity
1540
     *
1541
     * @return void
1542
     *
1543
     * @throws \InvalidArgumentException
1544
     */
1545
    private function computeSingleEntityChangeSet($entity)
1546
    {
1547
        $state = $this->getEntityState($entity);
1548
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
1549
            throw new \InvalidArgumentException(
1550
                "Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)
1551
            );
1552
        }
1553
        $class = $this->manager->getClassMetadata(get_class($entity));
1554
        // Compute changes for INSERTed entities first. This must always happen even in this case.
1555
        $this->computeScheduleInsertsChangeSets();
1556
        if ($class->isReadOnly()) {
1557
            return;
1558
        }
1559
        // Ignore uninitialized proxy objects
1560
        if ($entity instanceof Proxy && !$entity->__isInitialized__) {
1561
            return;
1562
        }
1563
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1564
        $oid = spl_object_hash($entity);
1565
        if (!isset($this->entityInsertions[$oid]) &&
1566
            !isset($this->entityDeletions[$oid]) &&
1567
            isset($this->entityStates[$oid])
1568
        ) {
1569
            $this->computeChangeSet($class, $entity);
1570
        }
1571
    }
1572
1573
    /**
1574
     * Computes the changesets of all entities scheduled for insertion.
1575
     *
1576
     * @return void
1577
     */
1578
    private function computeScheduleInsertsChangeSets()
1579
    {
1580
        foreach ($this->entityInsertions as $entity) {
1581
            $class = $this->manager->getClassMetadata(get_class($entity));
1582
            $this->computeChangeSet($class, $entity);
1583
        }
1584
    }
1585
1586
    /**
1587
     * Computes the changes of an association.
1588
     *
1589
     * @param array $assoc The association mapping.
1590
     * @param mixed $value The value of the association.
1591
     *
1592
     * @throws \InvalidArgumentException
1593
     * @throws \UnexpectedValueException
1594
     *
1595
     * @return void
1596
     */
1597
    private function computeAssociationChanges($assoc, $value)
1598
    {
1599
        if ($value instanceof Proxy && !$value->__isInitialized__) {
1600
            return;
1601
        }
1602
        if ($value instanceof ApiCollection && $value->isDirty()) {
1603
            $coid                            = spl_object_hash($value);
1604
            $this->collectionUpdates[$coid]  = $value;
1605
            $this->visitedCollections[$coid] = $value;
1606
        }
1607
        // Look through the entities, and in any of their associations,
1608
        // for transient (new) entities, recursively. ("Persistence by reachability")
1609
        // Unwrap. Uninitialized collections will simply be empty.
1610
        $unwrappedValue  = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap();
1611
        $targetClass     = $this->manager->getClassMetadata($assoc['targetEntity']);
1612
        $targetClassName = $targetClass->getName();
1613
        foreach ($unwrappedValue as $key => $entry) {
1614
            if (!($entry instanceof $targetClassName)) {
1615
                throw new \InvalidArgumentException('Invalid association');
1616
            }
1617
            $state = $this->getEntityState($entry, self::STATE_NEW);
1618
            if (!($entry instanceof $assoc['targetEntity'])) {
1619
                throw new \UnexpectedValueException('Unexpected association');
1620
            }
1621
            switch ($state) {
1622
                case self::STATE_NEW:
1623
                    if (!$assoc['isCascadePersist']) {
1624
                        throw new \InvalidArgumentException('New entity through relationship');
1625
                    }
1626
                    $this->persistNew($targetClass, $entry);
1627
                    $this->computeChangeSet($targetClass, $entry);
1628
                    break;
1629
                case self::STATE_REMOVED:
1630
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1631
                    // and remove the element from Collection.
1632
                    if ($assoc['type'] & ApiMetadata::TO_MANY) {
1633
                        unset($value[$key]);
1634
                    }
1635
                    break;
1636
                case self::STATE_DETACHED:
1637
                    // Can actually not happen right now as we assume STATE_NEW,
1638
                    // so the exception will be raised from the DBAL layer (constraint violation).
1639
                    throw new \InvalidArgumentException('Detached entity through relationship');
1640
                    break;
1641
                default:
1642
                    // MANAGED associated entities are already taken into account
1643
                    // during changeset calculation anyway, since they are in the identity map.
1644
            }
1645
        }
1646
    }
1647
1648
    private function executeInserts(ApiMetadata $class)
1649
    {
1650
        $className = $class->getName();
1651
        $persister = $this->getEntityPersister($className);
1652
        foreach ($this->entityInsertions as $oid => $entity) {
1653
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1654
                continue;
1655
            }
1656
            $persister->pushNewEntity($entity);
1657
            unset($this->entityInsertions[$oid]);
1658
        }
1659
        $postInsertIds = $persister->flushNewEntities();
1660
        if ($postInsertIds) {
1661
            // Persister returned post-insert IDs
1662
            foreach ($postInsertIds as $postInsertId) {
1663
                $id     = $postInsertId['generatedId'];
1664
                $entity = $postInsertId['entity'];
1665
                $oid    = spl_object_hash($entity);
1666
1667
                if ($id instanceof \stdClass) {
1668
                    $id = (array)$id;
1669
                }
1670
                if (!is_array($id)) {
1671
                    $id = [$class->getApiFieldName($class->getIdentifierFieldNames()[0]) => $id];
1672
                }
1673
1674
                if (!array_key_exists($oid, $this->originalEntityData)) {
1675
                    $this->originalEntityData[$oid] = new \stdClass();
1676
                }
1677
1678
                $idValues = [];
1679
                foreach ((array)$id as $apiIdField => $idValue) {
1680
                    $idName   = $class->getFieldName($apiIdField);
1681
                    $typeName = $class->getTypeOfField($idName);
1682
                    $type     = $this->manager->getConfiguration()->getTypeRegistry()->get($typeName);
1683
                    $idValue  = $type->toApiValue($idValue, $class->getFieldOptions($idName));
1684
                    $class->getReflectionProperty($idName)->setValue($entity, $idValue);
1685
                    $idValues[$idName]                       = $idValue;
1686
                    $this->originalEntityData[$oid]->$idName = $idValue;
1687
                }
1688
1689
                $this->entityIdentifiers[$oid] = $idValues;
1690
                $this->entityStates[$oid]      = self::STATE_MANAGED;
1691
                $this->addToIdentityMap($entity);
1692
            }
1693
        }
1694
    }
1695
1696
    private function executeUpdates($class)
1697
    {
1698
        $className = $class->name;
1699
        $persister = $this->getEntityPersister($className);
1700
        foreach ($this->entityUpdates as $oid => $entity) {
1701
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1702
                continue;
1703
            }
1704
            $this->recomputeSingleEntityChangeSet($class, $entity);
1705
1706
            if (!empty($this->entityChangeSets[$oid])) {
1707
                $persister->update($entity);
1708
            }
1709
            unset($this->entityUpdates[$oid]);
1710
        }
1711
    }
1712
1713
    /**
1714
     * Executes a refresh operation on an entity.
1715
     *
1716
     * @param object $entity  The entity to refresh.
1717
     * @param array  $visited The already visited entities during cascades.
1718
     *
1719
     * @return void
1720
     *
1721
     * @throws \InvalidArgumentException If the entity is not MANAGED.
1722
     */
1723
    private function doRefresh($entity, array &$visited)
1724
    {
1725
        $oid = spl_object_hash($entity);
1726
        if (isset($visited[$oid])) {
1727
            return; // Prevent infinite recursion
1728
        }
1729
        $visited[$oid] = $entity; // mark visited
1730
        $class         = $this->manager->getClassMetadata(get_class($entity));
1731
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1732
            throw new \InvalidArgumentException('Entity not managed');
1733
        }
1734
        $this->getEntityPersister($class->getName())->refresh(
1735
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1736
            $entity
1737
        );
1738
        $this->cascadeRefresh($entity, $visited);
1739
    }
1740
1741
    /**
1742
     * Cascades a refresh operation to associated entities.
1743
     *
1744
     * @param object $entity
1745
     * @param array  $visited
1746
     *
1747
     * @return void
1748
     */
1749
    private function cascadeRefresh($entity, array &$visited)
1750
    {
1751
        $class               = $this->manager->getClassMetadata(get_class($entity));
1752
        $associationMappings = array_filter(
1753
            $class->getAssociationMappings(),
1754
            function($assoc) {
1755
                return $assoc['isCascadeRefresh'];
1756
            }
1757
        );
1758
        foreach ($associationMappings as $assoc) {
1759
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1760
            switch (true) {
1761
                case ($relatedEntities instanceof ApiCollection):
1762
                    // Unwrap so that foreach() does not initialize
1763
                    $relatedEntities = $relatedEntities->unwrap();
1764
                // break; is commented intentionally!
1765
                case ($relatedEntities instanceof Collection):
1766
                case (is_array($relatedEntities)):
1767
                    foreach ($relatedEntities as $relatedEntity) {
1768
                        $this->doRefresh($relatedEntity, $visited);
1769
                    }
1770
                    break;
1771
                case ($relatedEntities !== null):
1772
                    $this->doRefresh($relatedEntities, $visited);
1773
                    break;
1774
                default:
1775
                    // Do nothing
1776
            }
1777
        }
1778
    }
1779
1780
    /**
1781
     * Cascades a detach operation to associated entities.
1782
     *
1783
     * @param object $entity
1784
     * @param array  $visited
1785
     *
1786
     * @return void
1787
     */
1788
    private function cascadeDetach($entity, array &$visited)
1789
    {
1790
        $class               = $this->manager->getClassMetadata(get_class($entity));
1791
        $associationMappings = array_filter(
1792
            $class->getAssociationMappings(),
1793
            function($assoc) {
1794
                return $assoc['isCascadeDetach'];
1795
            }
1796
        );
1797
        foreach ($associationMappings as $assoc) {
1798
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1799
            switch (true) {
1800
                case ($relatedEntities instanceof ApiCollection):
1801
                    // Unwrap so that foreach() does not initialize
1802
                    $relatedEntities = $relatedEntities->unwrap();
1803
                // break; is commented intentionally!
1804
                case ($relatedEntities instanceof Collection):
1805
                case (is_array($relatedEntities)):
1806
                    foreach ($relatedEntities as $relatedEntity) {
1807
                        $this->doDetach($relatedEntity, $visited);
1808
                    }
1809
                    break;
1810
                case ($relatedEntities !== null):
1811
                    $this->doDetach($relatedEntities, $visited);
1812
                    break;
1813
                default:
1814
                    // Do nothing
1815
            }
1816
        }
1817
    }
1818
1819
    /**
1820
     * Cascades a merge operation to associated entities.
1821
     *
1822
     * @param object $entity
1823
     * @param object $managedCopy
1824
     * @param array  $visited
1825
     *
1826
     * @return void
1827
     */
1828
    private function cascadeMerge($entity, $managedCopy, array &$visited)
1829
    {
1830
        $class               = $this->manager->getClassMetadata(get_class($entity));
1831
        $associationMappings = array_filter(
1832
            $class->getAssociationMappings(),
1833
            function($assoc) {
1834
                return $assoc['isCascadeMerge'];
1835
            }
1836
        );
1837
        foreach ($associationMappings as $assoc) {
1838
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1839
            if ($relatedEntities instanceof Collection) {
1840
                if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) {
1841
                    continue;
1842
                }
1843
                if ($relatedEntities instanceof ApiCollection) {
1844
                    // Unwrap so that foreach() does not initialize
1845
                    $relatedEntities = $relatedEntities->unwrap();
1846
                }
1847
                foreach ($relatedEntities as $relatedEntity) {
1848
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
1849
                }
1850
            } else {
1851
                if ($relatedEntities !== null) {
1852
                    $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
1853
                }
1854
            }
1855
        }
1856
    }
1857
1858
    /**
1859
     * Cascades the delete operation to associated entities.
1860
     *
1861
     * @param object $entity
1862
     * @param array  $visited
1863
     *
1864
     * @return void
1865
     */
1866
    private function cascadeRemove($entity, array &$visited)
1867
    {
1868
        $class               = $this->manager->getClassMetadata(get_class($entity));
1869
        $associationMappings = array_filter(
1870
            $class->getAssociationMappings(),
1871
            function($assoc) {
1872
                return $assoc['isCascadeRemove'];
1873
            }
1874
        );
1875
        $entitiesToCascade = [];
1876
        foreach ($associationMappings as $assoc) {
1877
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
1878
                $entity->__load();
1879
            }
1880
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1881
            switch (true) {
1882
                case ($relatedEntities instanceof Collection):
1883
                case (is_array($relatedEntities)):
1884
                    // If its a PersistentCollection initialization is intended! No unwrap!
1885
                    foreach ($relatedEntities as $relatedEntity) {
1886
                        $entitiesToCascade[] = $relatedEntity;
1887
                    }
1888
                    break;
1889
                case ($relatedEntities !== null):
1890
                    $entitiesToCascade[] = $relatedEntities;
1891
                    break;
1892
                default:
1893
                    // Do nothing
1894
            }
1895
        }
1896
        foreach ($entitiesToCascade as $relatedEntity) {
1897
            $this->doRemove($relatedEntity, $visited);
1898
        }
1899
    }
1900
1901
    /**
1902
     * Executes any extra updates that have been scheduled.
1903
     */
1904
    private function executeExtraUpdates()
1905
    {
1906
        foreach ($this->extraUpdates as $oid => $update) {
1907
            list ($entity, $changeset) = $update;
1908
            $this->entityChangeSets[$oid] = $changeset;
1909
            $this->getEntityPersister(get_class($entity))->update($entity);
1910
        }
1911
        $this->extraUpdates = [];
1912
    }
1913
1914
    private function executeDeletions(ApiMetadata $class)
1915
    {
1916
        $className = $class->getName();
1917
        $persister = $this->getEntityPersister($className);
1918
        foreach ($this->entityDeletions as $oid => $entity) {
1919
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1920
                continue;
1921
            }
1922
            $persister->delete($entity);
1923
            unset(
1924
                $this->entityDeletions[$oid],
1925
                $this->entityIdentifiers[$oid],
1926
                $this->originalEntityData[$oid],
1927
                $this->entityStates[$oid]
1928
            );
1929
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1930
            // is obtained by a new entity because the old one went out of scope.
1931
            //$this->entityStates[$oid] = self::STATE_NEW;
1932
            //            if ( ! $class->isIdentifierNatural()) {
1933
            //                $class->getReflectionProperty($class->getIdentifierFieldNames()[0])->setValue($entity, null);
1934
            //            }
1935
        }
1936
    }
1937
1938
    /**
1939
     * @param object $entity
1940
     * @param object $managedCopy
1941
     */
1942
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
1943
    {
1944
        $class = $this->manager->getClassMetadata(get_class($entity));
1945
        foreach ($this->reflectionPropertiesGetter->getProperties($class->getName()) as $prop) {
1946
            $name = $prop->name;
1947
            $prop->setAccessible(true);
1948
            if ($class->hasAssociation($name)) {
1949
                if (!$class->isIdentifier($name)) {
1950
                    $prop->setValue($managedCopy, $prop->getValue($entity));
1951
                }
1952
            } else {
1953
                $assoc2 = $class->getAssociationMapping($name);
1954
                if ($assoc2['type'] & ApiMetadata::TO_ONE) {
1955
                    $other = $prop->getValue($entity);
1956
                    if ($other === null) {
1957
                        $prop->setValue($managedCopy, null);
1958
                    } else {
1959
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
1960
                            // do not merge fields marked lazy that have not been fetched.
1961
                            continue;
1962
                        }
1963
                        if (!$assoc2['isCascadeMerge']) {
1964
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
1965
                                $targetClass = $this->manager->getClassMetadata($assoc2['targetEntity']);
1966
                                $relatedId   = $targetClass->getIdentifierValues($other);
1967
                                if ($targetClass->getSubclasses()) {
1968
                                    $other = $this->manager->find($targetClass->getName(), $relatedId);
1969
                                } else {
1970
                                    $other = $this->manager->getProxyFactory()->getProxy(
1971
                                        $assoc2['targetEntity'],
1972
                                        $relatedId
1973
                                    );
1974
                                    $this->registerManaged($other, $relatedId, []);
1975
                                }
1976
                            }
1977
                            $prop->setValue($managedCopy, $other);
1978
                        }
1979
                    }
1980
                } else {
1981
                    $mergeCol = $prop->getValue($entity);
1982
                    if ($mergeCol instanceof ApiCollection && !$mergeCol->isInitialized()) {
1983
                        // do not merge fields marked lazy that have not been fetched.
1984
                        // keep the lazy persistent collection of the managed copy.
1985
                        continue;
1986
                    }
1987
                    $managedCol = $prop->getValue($managedCopy);
1988
                    if (!$managedCol) {
1989
                        $managedCol = new ApiCollection(
1990
                            $this->manager,
1991
                            $this->manager->getClassMetadata($assoc2['targetEntity']),
1992
                            new ArrayCollection
1993
                        );
1994
                        $managedCol->setOwner($managedCopy, $assoc2);
1995
                        $prop->setValue($managedCopy, $managedCol);
1996
                    }
1997
                    if ($assoc2['isCascadeMerge']) {
1998
                        $managedCol->initialize();
1999
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
2000
                        if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
2001
                            $managedCol->unwrap()->clear();
2002
                            $managedCol->setDirty(true);
2003
                            if ($assoc2['isOwningSide']
2004
                                && $assoc2['type'] == ApiMetadata::MANY_TO_MANY
2005
                                && $class->isChangeTrackingNotify()
2006
                            ) {
2007
                                $this->scheduleForDirtyCheck($managedCopy);
2008
                            }
2009
                        }
2010
                    }
2011
                }
2012
            }
2013
            if ($class->isChangeTrackingNotify()) {
2014
                // Just treat all properties as changed, there is no other choice.
2015
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2016
            }
2017
        }
2018
    }
2019
2020
    /**
2021
     * Deletes an entity as part of the current unit of work.
2022
     *
2023
     * This method is internally called during delete() cascades as it tracks
2024
     * the already visited entities to prevent infinite recursions.
2025
     *
2026
     * @param object $entity  The entity to delete.
2027
     * @param array  $visited The map of the already visited entities.
2028
     *
2029
     * @return void
2030
     *
2031
     * @throws \InvalidArgumentException If the instance is a detached entity.
2032
     * @throws \UnexpectedValueException
2033
     */
2034
    private function doRemove($entity, array &$visited)
2035
    {
2036
        $oid = spl_object_hash($entity);
2037
        if (isset($visited[$oid])) {
2038
            return; // Prevent infinite recursion
2039
        }
2040
        $visited[$oid] = $entity; // mark visited
2041
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
2042
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
2043
        $this->cascadeRemove($entity, $visited);
2044
        $class       = $this->manager->getClassMetadata(get_class($entity));
2045
        $entityState = $this->getEntityState($entity);
2046
        switch ($entityState) {
2047
            case self::STATE_NEW:
2048
            case self::STATE_REMOVED:
2049
                // nothing to do
2050
                break;
2051
            case self::STATE_MANAGED:
2052
                $this->scheduleForDelete($entity);
2053
                break;
2054
            case self::STATE_DETACHED:
2055
                throw new \InvalidArgumentException('Detached entity cannot be removed');
2056
            default:
2057
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
2058
        }
2059
    }
2060
2061
    /**
2062
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2063
     *
2064
     * @param object $entity
2065
     *
2066
     * @return bool
2067
     */
2068
    private function isLoaded($entity)
2069
    {
2070
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2071
    }
2072
2073
    /**
2074
     * Sets/adds associated managed copies into the previous entity's association field
2075
     *
2076
     * @param object $entity
2077
     * @param array  $association
2078
     * @param object $previousManagedCopy
2079
     * @param object $managedCopy
2080
     *
2081
     * @return void
2082
     */
2083
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2084
    {
2085
        $assocField = $association['fieldName'];
2086
        $prevClass  = $this->manager->getClassMetadata(get_class($previousManagedCopy));
2087
        if ($association['type'] & ApiMetadata::TO_ONE) {
2088
            $prevClass->getReflectionProperty($assocField)->setValue($previousManagedCopy, $managedCopy);
2089
2090
            return;
2091
        }
2092
        /** @var array $value */
2093
        $value   = $prevClass->getReflectionProperty($assocField)->getValue($previousManagedCopy);
2094
        $value[] = $managedCopy;
2095
        if ($association['type'] == ApiMetadata::ONE_TO_MANY) {
2096
            $class = $this->manager->getClassMetadata(get_class($entity));
2097
            $class->getReflectionProperty($association['mappedBy'])->setValue($managedCopy, $previousManagedCopy);
2098
        }
2099
    }
2100
2101
    /**
2102
     * Executes a merge operation on an entity.
2103
     *
2104
     * @param object      $entity
2105
     * @param array       $visited
2106
     * @param object|null $prevManagedCopy
2107
     * @param array|null  $assoc
2108
     *
2109
     * @return object The managed copy of the entity.
2110
     *
2111
     * @throws \InvalidArgumentException If the entity instance is NEW.
2112
     * @throws \OutOfBoundsException
2113
     */
2114
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
2115
    {
2116
        $oid = spl_object_hash($entity);
2117
        if (isset($visited[$oid])) {
2118
            $managedCopy = $visited[$oid];
2119
            if ($prevManagedCopy !== null) {
2120
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2121
            }
2122
2123
            return $managedCopy;
2124
        }
2125
        $class = $this->manager->getClassMetadata(get_class($entity));
2126
        // First we assume DETACHED, although it can still be NEW but we can avoid
2127
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
2128
        // we need to fetch it from the db anyway in order to merge.
2129
        // MANAGED entities are ignored by the merge operation.
2130
        $managedCopy = $entity;
2131
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2132
            // Try to look the entity up in the identity map.
2133
            $id = $class->getIdentifierValues($entity);
2134
            // If there is no ID, it is actually NEW.
2135
            if (!$id) {
2136
                $managedCopy = $this->newInstance($class);
2137
                $this->persistNew($class, $managedCopy);
2138
            } else {
2139
                $flatId      = ($class->containsForeignIdentifier())
2140
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
2141
                    : $id;
2142
                $managedCopy = $this->tryGetById($flatId, $class->getRootEntityName());
2143
                if ($managedCopy) {
2144
                    // We have the entity in-memory already, just make sure its not removed.
2145
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
2146
                        throw new \InvalidArgumentException('Removed entity cannot be merged');
2147
                    }
2148
                } else {
2149
                    // We need to fetch the managed copy in order to merge.
2150
                    $managedCopy = $this->manager->find($class->getName(), $flatId);
2151
                }
2152
                if ($managedCopy === null) {
2153
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
2154
                    // since the managed entity was not found.
2155
                    if (!$class->isIdentifierNatural()) {
2156
                        throw new \OutOfBoundsException('Entity not found');
2157
                    }
2158
                    $managedCopy = $this->newInstance($class);
2159
                    $class->setIdentifierValues($managedCopy, $id);
2160
                    $this->persistNew($class, $managedCopy);
2161
                }
2162
            }
2163
2164
            $visited[$oid] = $managedCopy; // mark visited
2165
            if ($this->isLoaded($entity)) {
2166
                if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized()) {
2167
                    $managedCopy->__load();
2168
                }
2169
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
2170
            }
2171
            if ($class->isChangeTrackingDeferredExplicit()) {
2172
                $this->scheduleForDirtyCheck($entity);
2173
            }
2174
        }
2175
        if ($prevManagedCopy !== null) {
2176
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2177
        }
2178
        // Mark the managed copy visited as well
2179
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
2180
        $this->cascadeMerge($entity, $managedCopy, $visited);
2181
2182
        return $managedCopy;
2183
    }
2184
2185
    /**
2186
     * Executes a detach operation on the given entity.
2187
     *
2188
     * @param object  $entity
2189
     * @param array   $visited
2190
     * @param boolean $noCascade if true, don't cascade detach operation.
2191
     *
2192
     * @return void
2193
     */
2194
    private function doDetach($entity, array &$visited, $noCascade = false)
2195
    {
2196
        $oid = spl_object_hash($entity);
2197
        if (isset($visited[$oid])) {
2198
            return; // Prevent infinite recursion
2199
        }
2200
        $visited[$oid] = $entity; // mark visited
2201
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2202
            case self::STATE_MANAGED:
2203
                if ($this->isInIdentityMap($entity)) {
2204
                    $this->removeFromIdentityMap($entity);
2205
                }
2206
                unset(
2207
                    $this->entityInsertions[$oid],
2208
                    $this->entityUpdates[$oid],
2209
                    $this->entityDeletions[$oid],
2210
                    $this->entityIdentifiers[$oid],
2211
                    $this->entityStates[$oid],
2212
                    $this->originalEntityData[$oid]
2213
                );
2214
                break;
2215
            case self::STATE_NEW:
2216
            case self::STATE_DETACHED:
2217
                return;
2218
        }
2219
        if (!$noCascade) {
2220
            $this->cascadeDetach($entity, $visited);
2221
        }
2222
    }
2223
2224
    /**
2225
     * @param ApiMetadata $class
2226
     *
2227
     * @return EntityHydrator
2228
     */
2229
    private function getHydratorForClass(ApiMetadata $class)
2230
    {
2231
        if (!array_key_exists($class->getName(), $this->hydrators)) {
2232
            $this->hydrators[$class->getName()] = new EntityHydrator($this->manager, $class);
2233
        }
2234
2235
        return $this->hydrators[$class->getName()];
2236
    }
2237
}
2238