Completed
Pull Request — master (#14)
by Pavel
03:40
created

UnitOfWork::recomputeSingleEntityChangeSet()   C

Complexity

Conditions 13
Paths 64

Size

Total Lines 38
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 27.152

Importance

Changes 0
Metric Value
dl 0
loc 38
c 0
b 0
f 0
ccs 18
cts 32
cp 0.5625
rs 5.1234
cc 13
eloc 25
nc 64
nop 2
crap 27.152

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
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\Mapping\ApiMetadata;
12
use Bankiru\Api\Doctrine\Mapping\EntityMetadata;
13
use Bankiru\Api\Doctrine\Persister\ApiPersister;
14
use Bankiru\Api\Doctrine\Persister\CollectionPersister;
15
use Bankiru\Api\Doctrine\Persister\EntityPersister;
16
use Bankiru\Api\Doctrine\Proxy\ApiCollection;
17
use Bankiru\Api\Doctrine\Rpc\CrudsApiInterface;
18
use Bankiru\Api\Doctrine\Utility\IdentifierFlattener;
19
use Bankiru\Api\Doctrine\Utility\ReflectionPropertiesGetter;
20
use Doctrine\Common\Collections\ArrayCollection;
21
use Doctrine\Common\Collections\Collection;
22
use Doctrine\Common\NotifyPropertyChanged;
23
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
24
use Doctrine\Common\Persistence\ObjectManagerAware;
25
use Doctrine\Common\PropertyChangedListener;
26
use Doctrine\Common\Proxy\Proxy;
27
28
class UnitOfWork implements PropertyChangedListener
29
{
30
    /**
31
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
32
     */
33
    const STATE_MANAGED = 1;
34
    /**
35
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
36
     * and is not (yet) managed by an EntityManager.
37
     */
38
    const STATE_NEW = 2;
39
    /**
40
     * A detached entity is an instance with persistent state and identity that is not
41
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
42
     */
43
    const STATE_DETACHED = 3;
44
    /**
45
     * A removed entity instance is an instance with a persistent identity,
46
     * associated with an EntityManager, whose persistent state will be deleted
47
     * on commit.
48
     */
49
    const STATE_REMOVED = 4;
50
51
    /**
52
     * The (cached) states of any known entities.
53
     * Keys are object ids (spl_object_hash).
54
     *
55
     * @var array
56
     */
57
    private $entityStates = [];
58
59
    /** @var  EntityManager */
60
    private $manager;
61
    /** @var EntityPersister[] */
62
    private $persisters = [];
63
    /** @var CollectionPersister[] */
64
    private $collectionPersisters = [];
65
    /** @var  array */
66
    private $entityIdentifiers = [];
67
    /** @var  object[][] */
68
    private $identityMap = [];
69
    /** @var IdentifierFlattener */
70
    private $identifierFlattener;
71
    /** @var  array */
72
    private $originalEntityData = [];
73
    /** @var  array */
74
    private $entityDeletions = [];
75
    /** @var  array */
76
    private $entityChangeSets = [];
77
    /** @var  array */
78
    private $entityInsertions = [];
79
    /** @var  array */
80
    private $entityUpdates = [];
81
    /** @var  array */
82
    private $readOnlyObjects = [];
83
    /** @var  array */
84
    private $scheduledForSynchronization = [];
85
    /** @var  array */
86
    private $orphanRemovals = [];
87
    /** @var  ApiCollection[] */
88
    private $collectionDeletions = [];
89
    /** @var  array */
90
    private $extraUpdates = [];
91
    /** @var  ApiCollection[] */
92
    private $collectionUpdates = [];
93
    /** @var  ApiCollection[] */
94
    private $visitedCollections = [];
95
    /** @var ReflectionPropertiesGetter */
96
    private $reflectionPropertiesGetter;
97
98
    /**
99
     * UnitOfWork constructor.
100
     *
101
     * @param EntityManager $manager
102
     */
103 18
    public function __construct(EntityManager $manager)
104
    {
105 18
        $this->manager                    = $manager;
106 18
        $this->identifierFlattener        = new IdentifierFlattener($this->manager);
107 18
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
108 18
    }
109
110
    /**
111
     * Helper method to show an object as string.
112
     *
113
     * @param object $obj
114
     *
115
     * @return string
116
     */
117
    private static function objToStr($obj)
118
    {
119
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj);
120
    }
121
122
    /**
123
     * @param $className
124
     *
125
     * @return EntityPersister
126
     */
127 17
    public function getEntityPersister($className)
128
    {
129 17
        if (!array_key_exists($className, $this->persisters)) {
130
            /** @var ApiMetadata $classMetadata */
131 17
            $classMetadata = $this->manager->getClassMetadata($className);
132
133 17
            $api = $this->createApi($classMetadata);
134
135 17
            if ($api instanceof EntityCacheAwareInterface) {
136 17
                $api->setEntityCache($this->createEntityCache($classMetadata));
137 17
            }
138
139 17
            $this->persisters[$className] = new ApiPersister($this->manager, $api);
140 17
        }
141
142 17
        return $this->persisters[$className];
143
    }
144
145
    /**
146
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
147
     *
148
     * @param object $entity
149
     *
150
     * @return boolean
151
     */
152
    public function isInIdentityMap($entity)
153
    {
154
        $oid = spl_object_hash($entity);
155
156
        if (!isset($this->entityIdentifiers[$oid])) {
157
            return false;
158
        }
159
160
        /** @var EntityMetadata $classMetadata */
161
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
162
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
163
164
        if ($idHash === '') {
165
            return false;
166
        }
167
168
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
169
    }
170
171
    /**
172
     * Gets the identifier of an entity.
173
     * The returned value is always an array of identifier values. If the entity
174
     * has a composite identifier then the identifier values are in the same
175
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
176
     *
177
     * @param object $entity
178
     *
179
     * @return array The identifier values.
180
     */
181 1
    public function getEntityIdentifier($entity)
182
    {
183 1
        return $this->entityIdentifiers[spl_object_hash($entity)];
184
    }
185
186
    /**
187
     * @param             $className
188
     * @param \stdClass   $data
189
     *
190
     * @return ObjectManagerAware|object
191
     * @throws MappingException
192
     */
193 12
    public function getOrCreateEntity($className, \stdClass $data)
194
    {
195
        /** @var EntityMetadata $class */
196 12
        $class    = $this->manager->getClassMetadata($className);
197 12
        $hydrator = new EntityHydrator($this->manager, $class);
198
199 12
        $tmpEntity = $hydrator->hydarate($data);
200
201 12
        $id     = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($tmpEntity));
202 12
        $idHash = implode(' ', $id);
203
204 12
        $overrideLocalValues = false;
205 12
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
206 2
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
207 2
            $oid    = spl_object_hash($entity);
208
209 2
            if ($entity instanceof Proxy && !$entity->__isInitialized()) {
210 2
                $entity->__setInitialized(true);
211
212 2
                $overrideLocalValues            = true;
213 2
                $this->originalEntityData[$oid] = $data;
214
215 2
                if ($entity instanceof NotifyPropertyChanged) {
216
                    $entity->addPropertyChangedListener($this);
217
                }
218 2
            }
219 2
        } else {
220 11
            $entity                                             = $this->newInstance($class);
221 11
            $oid                                                = spl_object_hash($entity);
222 11
            $this->entityIdentifiers[$oid]                      = $id;
223 11
            $this->entityStates[$oid]                           = self::STATE_MANAGED;
224 11
            $this->originalEntityData[$oid]                     = $data;
225 11
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
226 11
            if ($entity instanceof NotifyPropertyChanged) {
227
                $entity->addPropertyChangedListener($this);
228
            }
229 11
            $overrideLocalValues = true;
230
        }
231
232 12
        if (!$overrideLocalValues) {
233
            return $entity;
234
        }
235
236 12
        $entity = $hydrator->hydarate($data, $entity);
237
238 12
        return $entity;
239
    }
240
241
    /**
242
     * INTERNAL:
243
     * Registers an entity as managed.
244
     *
245
     * @param object         $entity The entity.
246
     * @param array          $id     The identifier values.
247
     * @param \stdClass|null $data   The original entity data.
248
     *
249
     * @return void
250
     */
251 3
    public function registerManaged($entity, array $id, \stdClass $data = null)
252
    {
253 3
        $oid = spl_object_hash($entity);
254
255 3
        $this->entityIdentifiers[$oid]  = $id;
256 3
        $this->entityStates[$oid]       = self::STATE_MANAGED;
257 3
        $this->originalEntityData[$oid] = $data;
258
259 3
        $this->addToIdentityMap($entity);
260
261 3
        if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) {
262
            $entity->addPropertyChangedListener($this);
263
        }
264 3
    }
265
266
    /**
267
     * INTERNAL:
268
     * Registers an entity in the identity map.
269
     * Note that entities in a hierarchy are registered with the class name of
270
     * the root entity.
271
     *
272
     * @ignore
273
     *
274
     * @param object $entity The entity to register.
275
     *
276
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
277
     *                 the entity in question is already managed.
278
     *
279
     */
280 7
    public function addToIdentityMap($entity)
281
    {
282
        /** @var EntityMetadata $classMetadata */
283 7
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
284 7
        $idHash        = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]);
285
286 7
        if ($idHash === '') {
287
            throw new \InvalidArgumentException('Entitty does not have valid identifiers to be stored at identity map');
288
        }
289
290 7
        $className = $classMetadata->rootEntityName;
291
292 7
        if (isset($this->identityMap[$className][$idHash])) {
293
            return false;
294
        }
295
296 7
        $this->identityMap[$className][$idHash] = $entity;
297
298 7
        return true;
299
    }
300
301
    /**
302
     * Gets the identity map of the UnitOfWork.
303
     *
304
     * @return array
305
     */
306
    public function getIdentityMap()
307
    {
308
        return $this->identityMap;
309
    }
310
311
    /**
312
     * Gets the original data of an entity. The original data is the data that was
313
     * present at the time the entity was reconstituted from the database.
314
     *
315
     * @param object $entity
316
     *
317
     * @return array
318
     */
319
    public function getOriginalEntityData($entity)
320
    {
321
        $oid = spl_object_hash($entity);
322
323
        if (isset($this->originalEntityData[$oid])) {
324
            return $this->originalEntityData[$oid];
325
        }
326
327
        return [];
328
    }
329
330
    /**
331
     * INTERNAL:
332
     * Checks whether an identifier hash exists in the identity map.
333
     *
334
     * @ignore
335
     *
336
     * @param string $idHash
337
     * @param string $rootClassName
338
     *
339
     * @return boolean
340
     */
341
    public function containsIdHash($idHash, $rootClassName)
342
    {
343
        return isset($this->identityMap[$rootClassName][$idHash]);
344
    }
345
346
    /**
347
     * INTERNAL:
348
     * Gets an entity in the identity map by its identifier hash.
349
     *
350
     * @ignore
351
     *
352
     * @param string $idHash
353
     * @param string $rootClassName
354
     *
355
     * @return object
356
     */
357
    public function getByIdHash($idHash, $rootClassName)
358
    {
359
        return $this->identityMap[$rootClassName][$idHash];
360
    }
361
362
    /**
363
     * INTERNAL:
364
     * Tries to get an entity by its identifier hash. If no entity is found for
365
     * the given hash, FALSE is returned.
366
     *
367
     * @ignore
368
     *
369
     * @param mixed  $idHash (must be possible to cast it to string)
370
     * @param string $rootClassName
371
     *
372
     * @return object|bool The found entity or FALSE.
373
     */
374
    public function tryGetByIdHash($idHash, $rootClassName)
375
    {
376
        $stringIdHash = (string)$idHash;
377
378
        if (isset($this->identityMap[$rootClassName][$stringIdHash])) {
379
            return $this->identityMap[$rootClassName][$stringIdHash];
380
        }
381
382
        return false;
383
    }
384
385
    /**
386
     * Gets the state of an entity with regard to the current unit of work.
387
     *
388
     * @param object   $entity
389
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
390
     *                         This parameter can be set to improve performance of entity state detection
391
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
392
     *                         is either known or does not matter for the caller of the method.
393
     *
394
     * @return int The entity state.
395
     */
396 4
    public function getEntityState($entity, $assume = null)
397
    {
398 4
        $oid = spl_object_hash($entity);
399 4
        if (isset($this->entityStates[$oid])) {
400 2
            return $this->entityStates[$oid];
401
        }
402 4
        if ($assume !== null) {
403 4
            return $assume;
404
        }
405
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
406
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
407
        // the UoW does not hold references to such objects and the object hash can be reused.
408
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
409
        $class = $this->manager->getClassMetadata(get_class($entity));
410
        $id    = $class->getIdentifierValues($entity);
411
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
412
            return self::STATE_NEW;
413
        }
414
415
        return self::STATE_DETACHED;
416
    }
417
418
    /**
419
     * Tries to find an entity with the given identifier in the identity map of
420
     * this UnitOfWork.
421
     *
422
     * @param mixed  $id            The entity identifier to look for.
423
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
424
     *
425
     * @return object|bool Returns the entity with the specified identifier if it exists in
426
     *                     this UnitOfWork, FALSE otherwise.
427
     */
428 11
    public function tryGetById($id, $rootClassName)
429
    {
430
        /** @var EntityMetadata $metadata */
431 11
        $metadata = $this->manager->getClassMetadata($rootClassName);
432 11
        $idHash   = implode(' ', (array)$this->identifierFlattener->flattenIdentifier($metadata, $id));
433
434 11
        if (isset($this->identityMap[$rootClassName][$idHash])) {
435 4
            return $this->identityMap[$rootClassName][$idHash];
436
        }
437
438 11
        return false;
439
    }
440
441
    /**
442
     * Notifies this UnitOfWork of a property change in an entity.
443
     *
444
     * @param object $entity       The entity that owns the property.
445
     * @param string $propertyName The name of the property that changed.
446
     * @param mixed  $oldValue     The old value of the property.
447
     * @param mixed  $newValue     The new value of the property.
448
     *
449
     * @return void
450
     */
451
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
452
    {
453
        $oid          = spl_object_hash($entity);
454
        $class        = $this->manager->getClassMetadata(get_class($entity));
455
        $isAssocField = $class->hasAssociation($propertyName);
456
        if (!$isAssocField && !$class->hasField($propertyName)) {
457
            return; // ignore non-persistent fields
458
        }
459
        // Update changeset and mark entity for synchronization
460
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
461
        if (!isset($this->scheduledForSynchronization[$class->getRootEntityName()][$oid])) {
462
            $this->scheduleForDirtyCheck($entity);
463
        }
464
    }
465
466
    /**
467
     * Persists an entity as part of the current unit of work.
468
     *
469
     * @param object $entity The entity to persist.
470
     *
471
     * @return void
472
     */
473 4
    public function persist($entity)
474
    {
475 4
        $visited = [];
476 4
        $this->doPersist($entity, $visited);
477 4
    }
478
479
    /**
480
     * @param ApiMetadata $class
481
     * @param             $entity
482
     *
483
     * @throws \InvalidArgumentException
484
     * @throws \RuntimeException
485
     */
486 1
    public function recomputeSingleEntityChangeSet(ApiMetadata $class, $entity)
487
    {
488 1
        $oid = spl_object_hash($entity);
489 1
        if (!isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
490
            throw new \InvalidArgumentException('Entity is not managed');
491
        }
492
493 1
        $actualData = [];
494 1
        foreach ($class->getReflectionProperties() as $name => $refProp) {
495 1
            if (!$class->isIdentifier($name) && !$class->isCollectionValuedAssociation($name)) {
496 1
                $actualData[$name] = $refProp->getValue($entity);
497 1
            }
498 1
        }
499 1
        if (!isset($this->originalEntityData[$oid])) {
500
            throw new \RuntimeException(
501
                'Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'
502
            );
503
        }
504 1
        $originalData = $this->originalEntityData[$oid];
505 1
        $changeSet    = [];
506 1
        foreach ($actualData as $propName => $actualValue) {
507 1
            $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
508 1
            if ($orgValue !== $actualValue) {
509
                $changeSet[$propName] = [$orgValue, $actualValue];
510
            }
511 1
        }
512 1
        if ($changeSet) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeSet of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
513
            if (isset($this->entityChangeSets[$oid])) {
514
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
515
            } else {
516
                if (!isset($this->entityInsertions[$oid])) {
517
                    $this->entityChangeSets[$oid] = $changeSet;
518
                    $this->entityUpdates[$oid]    = $entity;
519
                }
520
            }
521
            $this->originalEntityData[$oid] = $actualData;
522
        }
523 1
    }
524
525
    /**
526
     * Schedules an entity for insertion into the database.
527
     * If the entity already has an identifier, it will be added to the identity map.
528
     *
529
     * @param object $entity The entity to schedule for insertion.
530
     *
531
     * @return void
532
     *
533
     * @throws \InvalidArgumentException
534
     */
535 4
    public function scheduleForInsert($entity)
536
    {
537 4
        $oid = spl_object_hash($entity);
538 4
        if (isset($this->entityUpdates[$oid])) {
539
            throw new \InvalidArgumentException('Dirty entity cannot be scheduled for insertion');
540
        }
541 4
        if (isset($this->entityDeletions[$oid])) {
542
            throw new \InvalidArgumentException('Removed entity scheduled for insertion');
543
        }
544 4
        if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) {
545
            throw new \InvalidArgumentException('Managed entity scheduled for insertion');
546
        }
547 4
        if (isset($this->entityInsertions[$oid])) {
548
            throw new \InvalidArgumentException('Entity scheduled for insertion twice');
549
        }
550 4
        $this->entityInsertions[$oid] = $entity;
551 4
        if (isset($this->entityIdentifiers[$oid])) {
552
            $this->addToIdentityMap($entity);
553
        }
554 4
        if ($entity instanceof NotifyPropertyChanged) {
555
            $entity->addPropertyChangedListener($this);
556
        }
557 4
    }
558
559
    /**
560
     * Checks whether an entity is scheduled for insertion.
561
     *
562
     * @param object $entity
563
     *
564
     * @return boolean
565
     */
566 1
    public function isScheduledForInsert($entity)
567
    {
568 1
        return isset($this->entityInsertions[spl_object_hash($entity)]);
569
    }
570
571
    /**
572
     * Schedules an entity for being updated.
573
     *
574
     * @param object $entity The entity to schedule for being updated.
575
     *
576
     * @return void
577
     *
578
     * @throws \InvalidArgumentException
579
     */
580
    public function scheduleForUpdate($entity)
581
    {
582
        $oid = spl_object_hash($entity);
583
        if (!isset($this->entityIdentifiers[$oid])) {
584
            throw new \InvalidArgumentException('Entity has no identity');
585
        }
586
        if (isset($this->entityDeletions[$oid])) {
587
            throw new \InvalidArgumentException('Entity is removed');
588
        }
589
        if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) {
590
            $this->entityUpdates[$oid] = $entity;
591
        }
592
    }
593
594
    /**
595
     * Checks whether an entity is registered as dirty in the unit of work.
596
     * Note: Is not very useful currently as dirty entities are only registered
597
     * at commit time.
598
     *
599
     * @param object $entity
600
     *
601
     * @return boolean
602
     */
603
    public function isScheduledForUpdate($entity)
604
    {
605
        return isset($this->entityUpdates[spl_object_hash($entity)]);
606
    }
607
608
    /**
609
     * Checks whether an entity is registered to be checked in the unit of work.
610
     *
611
     * @param object $entity
612
     *
613
     * @return boolean
614
     */
615
    public function isScheduledForDirtyCheck($entity)
616
    {
617
        $rootEntityName = $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
618
619
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
620
    }
621
622
    /**
623
     * INTERNAL:
624
     * Schedules an entity for deletion.
625
     *
626
     * @param object $entity
627
     *
628
     * @return void
629
     */
630
    public function scheduleForDelete($entity)
631
    {
632
        $oid = spl_object_hash($entity);
633
        if (isset($this->entityInsertions[$oid])) {
634
            if ($this->isInIdentityMap($entity)) {
635
                $this->removeFromIdentityMap($entity);
636
            }
637
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
638
639
            return; // entity has not been persisted yet, so nothing more to do.
640
        }
641
        if (!$this->isInIdentityMap($entity)) {
642
            return;
643
        }
644
        $this->removeFromIdentityMap($entity);
645
        unset($this->entityUpdates[$oid]);
646
        if (!isset($this->entityDeletions[$oid])) {
647
            $this->entityDeletions[$oid] = $entity;
648
            $this->entityStates[$oid]    = self::STATE_REMOVED;
649
        }
650
    }
651
652
    /**
653
     * Checks whether an entity is registered as removed/deleted with the unit
654
     * of work.
655
     *
656
     * @param object $entity
657
     *
658
     * @return boolean
659
     */
660
    public function isScheduledForDelete($entity)
661
    {
662
        return isset($this->entityDeletions[spl_object_hash($entity)]);
663
    }
664
665
    /**
666
     * Checks whether an entity is scheduled for insertion, update or deletion.
667
     *
668
     * @param object $entity
669
     *
670
     * @return boolean
671
     */
672
    public function isEntityScheduled($entity)
673
    {
674
        $oid = spl_object_hash($entity);
675
676
        return isset($this->entityInsertions[$oid])
677
        || isset($this->entityUpdates[$oid])
678
        || isset($this->entityDeletions[$oid]);
679
    }
680
681
    /**
682
     * INTERNAL:
683
     * Removes an entity from the identity map. This effectively detaches the
684
     * entity from the persistence management of Doctrine.
685
     *
686
     * @ignore
687
     *
688
     * @param object $entity
689
     *
690
     * @return boolean
691
     *
692
     * @throws \InvalidArgumentException
693
     */
694
    public function removeFromIdentityMap($entity)
695
    {
696
        $oid           = spl_object_hash($entity);
697
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
698
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
699
        if ($idHash === '') {
700
            throw new \InvalidArgumentException('Entity has no identity');
701
        }
702
        $className = $classMetadata->getRootEntityName();
703
        if (isset($this->identityMap[$className][$idHash])) {
704
            unset($this->identityMap[$className][$idHash]);
705
            unset($this->readOnlyObjects[$oid]);
706
707
            //$this->entityStates[$oid] = self::STATE_DETACHED;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
708
            return true;
709
        }
710
711
        return false;
712
    }
713
714
    /**
715
     * Commits the UnitOfWork, executing all operations that have been postponed
716
     * up to this point. The state of all managed entities will be synchronized with
717
     * the database.
718
     *
719
     * The operations are executed in the following order:
720
     *
721
     * 1) All entity insertions
722
     * 2) All entity updates
723
     * 3) All collection deletions
724
     * 4) All collection updates
725
     * 5) All entity deletions
726
     *
727
     * @param null|object|array $entity
728
     *
729
     * @return void
730
     *
731
     * @throws \Exception
732
     */
733 4
    public function commit($entity = null)
734
    {
735
        // Compute changes done since last commit.
736 4
        if ($entity === null) {
737 4
            $this->computeChangeSets();
738 4
        } elseif (is_object($entity)) {
739
            $this->computeSingleEntityChangeSet($entity);
740
        } elseif (is_array($entity)) {
741
            foreach ((array)$entity as $object) {
742
                $this->computeSingleEntityChangeSet($object);
743
            }
744
        }
745 4
        if (!($this->entityInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
751 4
        ) {
752 1
            return; // Nothing to do.
753
        }
754 4
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
755
            foreach ($this->orphanRemovals as $orphan) {
756
                $this->remove($orphan);
757
            }
758
        }
759
        // Now we need a commit order to maintain referential integrity
760 4
        $commitOrder = $this->getCommitOrder();
761
762
        // Collection deletions (deletions of complete collections)
763
        // foreach ($this->collectionDeletions as $collectionToDelete) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
764
        //       //fixme: collection mutations
765
        //       $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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

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

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

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

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

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

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

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

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

Loading history...
779
            $this->executeExtraUpdates();
780
        }
781
        // Collection updates (deleteRows, updateRows, insertRows)
782 4
        foreach ($this->collectionUpdates as $collectionToUpdate) {
0 ignored issues
show
Unused Code introduced by
This foreach statement is empty and can be removed.

This check looks for foreach loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
783
            //fixme: decide what to do with collection mutation if API does not support this
784
            //$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
0 ignored issues
show
Unused Code Comprehensibility introduced by
82% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
785 4
        }
786
        // Entity deletions come last and need to be in reverse commit order
787 4
        if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
789
                $this->executeDeletions($commitOrder[$i]);
790
            }
791
        }
792
793
        // Take new snapshots from visited collections
794 4
        foreach ($this->visitedCollections as $coll) {
795 2
            $coll->takeSnapshot();
796 4
        }
797
798
        // Clear up
799 4
        $this->entityInsertions =
800 4
        $this->entityUpdates =
801 4
        $this->entityDeletions =
802 4
        $this->extraUpdates =
803 4
        $this->entityChangeSets =
804 4
        $this->collectionUpdates =
805 4
        $this->collectionDeletions =
806 4
        $this->visitedCollections =
807 4
        $this->scheduledForSynchronization =
808 4
        $this->orphanRemovals = [];
809 4
    }
810
811
    /**
812
     * Gets the changeset for an entity.
813
     *
814
     * @param object $entity
815
     *
816
     * @return array
817
     */
818 1
    public function & getEntityChangeSet($entity)
819
    {
820 1
        $oid  = spl_object_hash($entity);
821 1
        $data = [];
822 1
        if (!isset($this->entityChangeSets[$oid])) {
823
            return $data;
824
        }
825
826 1
        return $this->entityChangeSets[$oid];
827
    }
828
829
    /**
830
     * Computes the changes that happened to a single entity.
831
     *
832
     * Modifies/populates the following properties:
833
     *
834
     * {@link _originalEntityData}
835
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
836
     * then it was not fetched from the database and therefore we have no original
837
     * entity data yet. All of the current entity data is stored as the original entity data.
838
     *
839
     * {@link _entityChangeSets}
840
     * The changes detected on all properties of the entity are stored there.
841
     * A change is a tuple array where the first entry is the old value and the second
842
     * entry is the new value of the property. Changesets are used by persisters
843
     * to INSERT/UPDATE the persistent entity state.
844
     *
845
     * {@link _entityUpdates}
846
     * If the entity is already fully MANAGED (has been fetched from the database before)
847
     * and any changes to its properties are detected, then a reference to the entity is stored
848
     * there to mark it for an update.
849
     *
850
     * {@link _collectionDeletions}
851
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
852
     * then this collection is marked for deletion.
853
     *
854
     * @ignore
855
     *
856
     * @internal Don't call from the outside.
857
     *
858
     * @param ApiMetadata $class  The class descriptor of the entity.
859
     * @param object      $entity The entity for which to compute the changes.
860
     *
861
     * @return void
862
     */
863 4
    public function computeChangeSet(ApiMetadata $class, $entity)
864
    {
865 4
        $oid = spl_object_hash($entity);
866 4
        if (isset($this->readOnlyObjects[$oid])) {
867
            return;
868
        }
869
        //        if ( ! $class->isInheritanceTypeNone()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
870
        //            $class = $this->em->getClassMetadata(get_class($entity));
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
871
        //        }
872
        //        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
873
        //        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
874
        //            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
875
        //        }
876 4
        $actualData = [];
877 4
        foreach ($class->getReflectionProperties() as $name => $refProp) {
878 4
            $value = $refProp->getValue($entity);
879 4
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
880 2
                if ($value instanceof ApiCollection) {
881 1
                    if ($value->getOwner() === $entity) {
882 1
                        continue;
883
                    }
884
                    $value = new ArrayCollection($value->getValues());
885
                }
886
                // If $value is not a Collection then use an ArrayCollection.
887 2
                if (!$value instanceof Collection) {
888
                    $value = new ArrayCollection($value);
889
                }
890 2
                $assoc = $class->getAssociationMapping($name);
891
                // Inject PersistentCollection
892 2
                $value = new ApiCollection(
893 2
                    $this->manager,
894 2
                    $this->manager->getClassMetadata($assoc['target']),
895
                    $value
896 2
                );
897 2
                $value->setOwner($entity, $assoc);
898 2
                $value->setDirty(!$value->isEmpty());
899 2
                $class->getReflectionProperty($name)->setValue($entity, $value);
900 2
                $actualData[$name] = $value;
901 2
                continue;
902
            }
903 4
            if (!$class->isIdentifier($name)) {
904 4
                $actualData[$name] = $value;
905 4
            }
906 4
        }
907 4
        if (!isset($this->originalEntityData[$oid])) {
908
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
909
            // These result in an INSERT.
910 4
            $this->originalEntityData[$oid] = $actualData;
911 4
            $changeSet                      = [];
912 4
            foreach ($actualData as $propName => $actualValue) {
913 4 View Code Duplication
                if (!$class->hasAssociation($propName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
914 4
                    $changeSet[$propName] = [null, $actualValue];
915 4
                    continue;
916
                }
917 4
                $assoc = $class->getAssociationMapping($propName);
918 4
                if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) {
919 4
                    $changeSet[$propName] = [null, $actualValue];
920 4
                }
921 4
            }
922 4
            $this->entityChangeSets[$oid] = $changeSet;
923 4
        } else {
924
            // Entity is "fully" MANAGED: it was already fully persisted before
925
            // and we have a copy of the original data
926 2
            $originalData           = $this->originalEntityData[$oid];
927 2
            $isChangeTrackingNotify = false;
928 2
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
929 2
                ? $this->entityChangeSets[$oid]
930 2
                : [];
931 2
            foreach ($actualData as $propName => $actualValue) {
932
                // skip field, its a partially omitted one!
933 2
                if (!(isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
934
                    continue;
935
                }
936 2
                $orgValue = $originalData[$propName];
937
                // skip if value haven't changed
938 2
                if ($orgValue === $actualValue) {
939 2
                    continue;
940
                }
941
                // if regular field
942 1 View Code Duplication
                if (!$class->hasAssociation($propName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
943
                    if ($isChangeTrackingNotify) {
944
                        continue;
945
                    }
946
                    $changeSet[$propName] = [$orgValue, $actualValue];
947
                    continue;
948
                }
949 1
                $assoc = $class->getAssociationMapping($propName);
950
                // Persistent collection was exchanged with the "originally"
951
                // created one. This can only mean it was cloned and replaced
952
                // on another entity.
953 1
                if ($actualValue instanceof ApiCollection) {
954
                    $owner = $actualValue->getOwner();
955
                    if ($owner === null) { // cloned
956
                        $actualValue->setOwner($entity, $assoc);
957
                    } else {
958
                        if ($owner !== $entity) { // no clone, we have to fix
959
                            if (!$actualValue->isInitialized()) {
960
                                $actualValue->initialize(); // we have to do this otherwise the cols share state
961
                            }
962
                            $newValue = clone $actualValue;
963
                            $newValue->setOwner($entity, $assoc);
964
                            $class->getReflectionProperty($propName)->setValue($entity, $newValue);
965
                        }
966
                    }
967
                }
968 1
                if ($orgValue instanceof ApiCollection) {
969
                    // A PersistentCollection was de-referenced, so delete it.
970
                    $coid = spl_object_hash($orgValue);
971
                    if (isset($this->collectionDeletions[$coid])) {
972
                        continue;
973
                    }
974
                    $this->collectionDeletions[$coid] = $orgValue;
975
                    $changeSet[$propName]             = $orgValue; // Signal changeset, to-many assocs will be ignored.
976
                    continue;
977
                }
978 1
                if ($assoc['type'] & ApiMetadata::TO_ONE) {
979 1
                    if ($assoc['isOwningSide']) {
980 1
                        $changeSet[$propName] = [$orgValue, $actualValue];
981 1
                    }
982 1
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
983
                        $this->scheduleOrphanRemoval($orgValue);
984
                    }
985 1
                }
986 2
            }
987 2
            if ($changeSet) {
988 1
                $this->entityChangeSets[$oid]   = $changeSet;
989 1
                $this->originalEntityData[$oid] = $actualData;
990 1
                $this->entityUpdates[$oid]      = $entity;
991 1
            }
992
        }
993
        // Look for changes in associations of the entity
994 4
        foreach ($class->getAssociationMappings() as $field => $assoc) {
995 4
            if (($val = $class->getReflectionProperty($field)->getValue($entity)) === null) {
996 4
                continue;
997
            }
998 2
            $this->computeAssociationChanges($assoc, $val);
999 2
            if (!isset($this->entityChangeSets[$oid]) &&
1000 2
                $assoc['isOwningSide'] &&
1001 2
                $assoc['type'] == ApiMetadata::MANY_TO_MANY &&
1002 2
                $val instanceof ApiCollection &&
1003
                $val->isDirty()
1004 2
            ) {
1005
                $this->entityChangeSets[$oid]   = [];
1006
                $this->originalEntityData[$oid] = $actualData;
1007
                $this->entityUpdates[$oid]      = $entity;
1008
            }
1009 4
        }
1010 4
    }
1011
1012
    /**
1013
     * Computes all the changes that have been done to entities and collections
1014
     * since the last commit and stores these changes in the _entityChangeSet map
1015
     * temporarily for access by the persisters, until the UoW commit is finished.
1016
     *
1017
     * @return void
1018
     */
1019 4
    public function computeChangeSets()
1020
    {
1021
        // Compute changes for INSERTed entities first. This must always happen.
1022 4
        $this->computeScheduleInsertsChangeSets();
1023
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
1024 4
        foreach ($this->identityMap as $className => $entities) {
1025 2
            $class = $this->manager->getClassMetadata($className);
1026
            // Skip class if instances are read-only
1027 2
            if ($class->isReadOnly()) {
1028
                continue;
1029
            }
1030
            // If change tracking is explicit or happens through notification, then only compute
1031
            // changes on entities of that type that are explicitly marked for synchronization.
1032 2
            switch (true) {
1033 2
                case ($class->isChangeTrackingDeferredImplicit()):
1034 2
                    $entitiesToProcess = $entities;
1035 2
                    break;
1036
                case (isset($this->scheduledForSynchronization[$className])):
1037
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
1038
                    break;
1039
                default:
1040
                    $entitiesToProcess = [];
1041
            }
1042 2
            foreach ($entitiesToProcess as $entity) {
1043
                // Ignore uninitialized proxy objects
1044 2
                if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1045
                    continue;
1046
                }
1047
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1048 2
                $oid = spl_object_hash($entity);
1049 2 View Code Duplication
                if (!isset($this->entityInsertions[$oid]) &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1050 2
                    !isset($this->entityDeletions[$oid]) &&
1051 2
                    isset($this->entityStates[$oid])
1052 2
                ) {
1053 2
                    $this->computeChangeSet($class, $entity);
1054 2
                }
1055 2
            }
1056 4
        }
1057 4
    }
1058
1059
    /**
1060
     * INTERNAL:
1061
     * Schedules an orphaned entity for removal. The remove() operation will be
1062
     * invoked on that entity at the beginning of the next commit of this
1063
     * UnitOfWork.
1064
     *
1065
     * @ignore
1066
     *
1067
     * @param object $entity
1068
     *
1069
     * @return void
1070
     */
1071
    public function scheduleOrphanRemoval($entity)
1072
    {
1073
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
1074
    }
1075
1076 2
    public function loadCollection(ApiCollection $collection)
1077
    {
1078 2
        $assoc     = $collection->getMapping();
1079 2
        $persister = $this->getEntityPersister($assoc['target']);
1080 2
        switch ($assoc['type']) {
1081 2
            case ApiMetadata::ONE_TO_MANY:
1082 2
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
1083 2
                break;
1084 2
        }
1085 2
        $collection->setInitialized(true);
1086 2
    }
1087
1088
    public function getCollectionPersister($association)
1089
    {
1090
        $role = isset($association['cache'])
1091
            ? $association['sourceEntity'].'::'.$association['fieldName']
1092
            : $association['type'];
1093
        if (array_key_exists($role, $this->collectionPersisters)) {
1094
            return $this->collectionPersisters[$role];
1095
        }
1096
        $this->collectionPersisters[$role] = new CollectionPersister($this->manager);
0 ignored issues
show
Unused Code introduced by
The call to CollectionPersister::__construct() has too many arguments starting with $this->manager.

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

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

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1097
1098
        return $this->collectionPersisters[$role];
1099
    }
1100
1101
    public function scheduleCollectionDeletion(Collection $collection)
0 ignored issues
show
Unused Code introduced by
The parameter $collection is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1102
    {
1103
    }
1104
1105 2
    public function cancelOrphanRemoval($value)
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1106
    {
1107 2
    }
1108
1109
    /**
1110
     * INTERNAL:
1111
     * Sets a property value of the original data array of an entity.
1112
     *
1113
     * @ignore
1114
     *
1115
     * @param string $oid
1116
     * @param string $property
1117
     * @param mixed  $value
1118
     *
1119
     * @return void
1120
     */
1121 9
    public function setOriginalEntityProperty($oid, $property, $value)
1122
    {
1123 9
        if (!array_key_exists($oid, $this->originalEntityData)) {
1124 9
            $this->originalEntityData[$oid] = new \stdClass();
1125 9
        }
1126
1127 9
        $this->originalEntityData[$oid]->$property = $value;
1128 9
    }
1129
1130
    public function scheduleExtraUpdate($entity, $changeset)
1131
    {
1132
        $oid         = spl_object_hash($entity);
1133
        $extraUpdate = [$entity, $changeset];
1134
        if (isset($this->extraUpdates[$oid])) {
1135
            list(, $changeset2) = $this->extraUpdates[$oid];
1136
            $extraUpdate = [$entity, $changeset + $changeset2];
1137
        }
1138
        $this->extraUpdates[$oid] = $extraUpdate;
1139
    }
1140
1141
    /**
1142
     * Refreshes the state of the given entity from the database, overwriting
1143
     * any local, unpersisted changes.
1144
     *
1145
     * @param object $entity The entity to refresh.
1146
     *
1147
     * @return void
1148
     *
1149
     * @throws InvalidArgumentException If the entity is not MANAGED.
1150
     */
1151
    public function refresh($entity)
1152
    {
1153
        $visited = [];
1154
        $this->doRefresh($entity, $visited);
1155
    }
1156
1157
    /**
1158
     * Clears the UnitOfWork.
1159
     *
1160
     * @param string|null $entityName if given, only entities of this type will get detached.
1161
     *
1162
     * @return void
1163
     */
1164
    public function clear($entityName = null)
1165
    {
1166
        if ($entityName === null) {
1167
            $this->identityMap =
1168
            $this->entityIdentifiers =
1169
            $this->originalEntityData =
1170
            $this->entityChangeSets =
1171
            $this->entityStates =
1172
            $this->scheduledForSynchronization =
1173
            $this->entityInsertions =
1174
            $this->entityUpdates =
1175
            $this->entityDeletions =
1176
            $this->collectionDeletions =
1177
            $this->collectionUpdates =
1178
            $this->extraUpdates =
1179
            $this->readOnlyObjects =
1180
            $this->visitedCollections =
1181
            $this->orphanRemovals = [];
1182
        } else {
1183
            $this->clearIdentityMapForEntityName($entityName);
0 ignored issues
show
Bug introduced by
The method clearIdentityMapForEntityName() does not seem to exist on object<Bankiru\Api\Doctrine\UnitOfWork>.

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

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

Loading history...
1184
            $this->clearEntityInsertionsForEntityName($entityName);
0 ignored issues
show
Bug introduced by
The method clearEntityInsertionsForEntityName() does not seem to exist on object<Bankiru\Api\Doctrine\UnitOfWork>.

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

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

Loading history...
1185
        }
1186
    }
1187
1188
    /**
1189
     * @param PersistentCollection $coll
1190
     *
1191
     * @return bool
1192
     */
1193
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
1194
    {
1195
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
1196
    }
1197
1198
    /**
1199
     * Schedules an entity for dirty-checking at commit-time.
1200
     *
1201
     * @param object $entity The entity to schedule for dirty-checking.
1202
     *
1203
     * @return void
1204
     *
1205
     * @todo Rename: scheduleForSynchronization
1206
     */
1207
    public function scheduleForDirtyCheck($entity)
1208
    {
1209
        $rootClassName                                                               =
1210
            $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
1211
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
1212
    }
1213
1214
    /**
1215
     * Deletes an entity as part of the current unit of work.
1216
     *
1217
     * @param object $entity The entity to remove.
1218
     *
1219
     * @return void
1220
     */
1221
    public function remove($entity)
1222
    {
1223
        $visited = [];
1224
        $this->doRemove($entity, $visited);
1225
    }
1226
1227
    /**
1228
     * Merges the state of the given detached entity into this UnitOfWork.
1229
     *
1230
     * @param object $entity
1231
     *
1232
     * @return object The managed copy of the entity.
1233
     */
1234
    public function merge($entity)
1235
    {
1236
        $visited = [];
1237
1238
        return $this->doMerge($entity, $visited);
1239
    }
1240
1241
    /**
1242
     * Detaches an entity from the persistence management. It's persistence will
1243
     * no longer be managed by Doctrine.
1244
     *
1245
     * @param object $entity The entity to detach.
1246
     *
1247
     * @return void
1248
     */
1249
    public function detach($entity)
1250
    {
1251
        $visited = [];
1252
        $this->doDetach($entity, $visited);
1253
    }
1254
1255
    /**
1256
     * @param ApiMetadata $class
1257
     *
1258
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
1259
     */
1260 11
    private function newInstance(ApiMetadata $class)
1261
    {
1262 11
        $entity = $class->newInstance();
1263
1264 11
        if ($entity instanceof ObjectManagerAware) {
1265
            $entity->injectObjectManager($this->manager, $class);
1266
        }
1267
1268 11
        return $entity;
1269
    }
1270
1271
    /**
1272
     * @param ApiMetadata $classMetadata
1273
     *
1274
     * @return EntityDataCacheInterface
1275
     */
1276 17
    private function createEntityCache(ApiMetadata $classMetadata)
1277
    {
1278 17
        $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName());
1279 17
        $cache         = new VoidEntityCache($classMetadata);
1280 17
        if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) {
1281
            $cache =
1282 1
                new LoggingCache(
1283 1
                    new ApiEntityCache(
1284 1
                        $this->manager->getConfiguration()->getApiCache(),
1285 1
                        $classMetadata,
1286
                        $configuration
1287 1
                    ),
1288 1
                    $this->manager->getConfiguration()->getApiCacheLogger()
1289 1
                );
1290
1291 1
            return $cache;
1292
        }
1293
1294 16
        return $cache;
1295
    }
1296
1297
    /**
1298
     * @param ApiMetadata $classMetadata
1299
     *
1300
     * @return CrudsApiInterface
1301
     */
1302 17
    private function createApi(ApiMetadata $classMetadata)
1303
    {
1304 17
        $client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName());
1305
1306 17
        $api = $this->manager
1307 17
            ->getConfiguration()
1308 17
            ->getFactoryRegistry()
1309 17
            ->create(
1310 17
                $classMetadata->getApiFactory(),
1311 17
                $client,
1312
                $classMetadata
1313 17
            );
1314
1315 17
        return $api;
1316
    }
1317
1318 4
    private function doPersist($entity, $visited)
1319
    {
1320 4
        $oid = spl_object_hash($entity);
1321 4
        if (isset($visited[$oid])) {
1322
            return; // Prevent infinite recursion
1323
        }
1324 4
        $visited[$oid] = $entity; // Mark visited
1325 4
        $class         = $this->manager->getClassMetadata(get_class($entity));
1326
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1327
        // If we would detect DETACHED here we would throw an exception anyway with the same
1328
        // consequences (not recoverable/programming error), so just assuming NEW here
1329
        // lets us avoid some database lookups for entities with natural identifiers.
1330 4
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1331
        switch ($entityState) {
1332 4
            case self::STATE_MANAGED:
1333
                $this->scheduleForDirtyCheck($entity);
1334
                break;
1335 4
            case self::STATE_NEW:
1336 4
                $this->persistNew($class, $entity);
1337 4
                break;
1338
            case self::STATE_REMOVED:
1339
                // Entity becomes managed again
1340
                unset($this->entityDeletions[$oid]);
1341
                $this->addToIdentityMap($entity);
1342
                $this->entityStates[$oid] = self::STATE_MANAGED;
1343
                break;
1344
            case self::STATE_DETACHED:
1345
                // Can actually not happen right now since we assume STATE_NEW.
1346
                throw new \InvalidArgumentException('Detached entity cannot be persisted');
1347
            default:
1348
                throw new \UnexpectedValueException("Unexpected entity state: $entityState.".self::objToStr($entity));
1349
        }
1350 4
        $this->cascadePersist($entity, $visited);
1351 4
    }
1352
1353
    /**
1354
     * Cascades the save operation to associated entities.
1355
     *
1356
     * @param object $entity
1357
     * @param array  $visited
1358
     *
1359
     * @return void
1360
     * @throws \InvalidArgumentException
1361
     * @throws MappingException
1362
     */
1363 4
    private function cascadePersist($entity, array &$visited)
1364
    {
1365 4
        $class               = $this->manager->getClassMetadata(get_class($entity));
1366 4
        $associationMappings = [];
1367 4
        foreach ($class->getAssociationNames() as $name) {
1368 4
            $assoc = $class->getAssociationMapping($name);
1369 4
            if ($assoc['isCascadePersist']) {
1370
                $associationMappings[$name] = $assoc;
1371
            }
1372 4
        }
1373 4
        foreach ($associationMappings as $assoc) {
1374
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1375
            switch (true) {
1376
                case ($relatedEntities instanceof ApiCollection):
1377
                    // Unwrap so that foreach() does not initialize
1378
                    $relatedEntities = $relatedEntities->unwrap();
1379
                // break; is commented intentionally!
1380
                case ($relatedEntities instanceof Collection):
1381
                case (is_array($relatedEntities)):
1382
                    if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) {
1383
                        throw new \InvalidArgumentException('Invalid association for cascade');
1384
                    }
1385
                    foreach ($relatedEntities as $relatedEntity) {
1386
                        $this->doPersist($relatedEntity, $visited);
1387
                    }
1388
                    break;
1389
                case ($relatedEntities !== null):
1390
                    if (!$relatedEntities instanceof $assoc['target']) {
1391
                        throw new \InvalidArgumentException('Invalid association for cascade');
1392
                    }
1393
                    $this->doPersist($relatedEntities, $visited);
1394
                    break;
1395
                default:
1396
                    // Do nothing
1397
            }
1398 4
        }
1399 4
    }
1400
1401
    /**
1402
     * @param ApiMetadata $class
1403
     * @param object      $entity
1404
     *
1405
     * @return void
1406
     */
1407 4
    private function persistNew($class, $entity)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1408
    {
1409 4
        $oid = spl_object_hash($entity);
1410
        //        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1411
        //        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1412
        //            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1413
        //        }
1414
        //        $idGen = $class->idGenerator;
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1415
        //        if ( ! $idGen->isPostInsertGenerator()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1416
        //            $idValue = $idGen->generate($this->em, $entity);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1417
        //            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1418
        //                $idValue = array($class->identifier[0] => $idValue);
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1419
        //                $class->setIdentifierValues($entity, $idValue);
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1420
        //            }
1421
        //            $this->entityIdentifiers[$oid] = $idValue;
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1422
        //        }
1423 4
        $this->entityStates[$oid] = self::STATE_MANAGED;
1424 4
        $this->scheduleForInsert($entity);
1425 4
    }
1426
1427
    /**
1428
     * Gets the commit order.
1429
     *
1430
     * @param array|null $entityChangeSet
1431
     *
1432
     * @return array
1433
     */
1434 4
    private function getCommitOrder(array $entityChangeSet = null)
1435
    {
1436 4
        if ($entityChangeSet === null) {
1437 4
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1438 4
        }
1439 4
        $calc = $this->getCommitOrderCalculator();
1440
        // See if there are any new classes in the changeset, that are not in the
1441
        // commit order graph yet (don't have a node).
1442
        // We have to inspect changeSet to be able to correctly build dependencies.
1443
        // It is not possible to use IdentityMap here because post inserted ids
1444
        // are not yet available.
1445
        /** @var ApiMetadata[] $newNodes */
1446 4
        $newNodes = [];
1447 4
        foreach ((array)$entityChangeSet as $entity) {
1448 4
            $class = $this->manager->getClassMetadata(get_class($entity));
1449 4
            if ($calc->hasNode($class->getName())) {
1450
                continue;
1451
            }
1452 4
            $calc->addNode($class->getName(), $class);
1453 4
            $newNodes[] = $class;
1454 4
        }
1455
        // Calculate dependencies for new nodes
1456 4
        while ($class = array_pop($newNodes)) {
1457 4
            foreach ($class->getAssociationMappings() as $assoc) {
1458 4
                if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) {
1459 4
                    continue;
1460
                }
1461 4
                $targetClass = $this->manager->getClassMetadata($assoc['target']);
1462 4
                if (!$calc->hasNode($targetClass->getName())) {
1463 2
                    $calc->addNode($targetClass->getName(), $targetClass);
1464 2
                    $newNodes[] = $targetClass;
1465 2
                }
1466 4
                $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable']));
1467
                // If the target class has mapped subclasses, these share the same dependency.
1468 4
                if (!$targetClass->getSubclasses()) {
1469 4
                    continue;
1470
                }
1471
                foreach ($targetClass->getSubclasses() as $subClassName) {
1472
                    $targetSubClass = $this->manager->getClassMetadata($subClassName);
1473
                    if (!$calc->hasNode($subClassName)) {
1474
                        $calc->addNode($targetSubClass->name, $targetSubClass);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Bankiru\Api\Doctrine\Mapping\ApiMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1475
                        $newNodes[] = $targetSubClass;
1476
                    }
1477
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Bankiru\Api\Doctrine\Mapping\ApiMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1478
                }
1479 4
            }
1480 4
        }
1481
1482 4
        return $calc->sort();
1483
    }
1484
1485 4
    private function getCommitOrderCalculator()
1486
    {
1487 4
        return new Utility\CommitOrderCalculator();
1488
    }
1489
1490
    /**
1491
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
1492
     *
1493
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
1494
     * 2. Read Only entities are skipped.
1495
     * 3. Proxies are skipped.
1496
     * 4. Only if entity is properly managed.
1497
     *
1498
     * @param object $entity
1499
     *
1500
     * @return void
1501
     *
1502
     * @throws \InvalidArgumentException
1503
     */
1504
    private function computeSingleEntityChangeSet($entity)
1505
    {
1506
        $state = $this->getEntityState($entity);
1507
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
1508
            throw new \InvalidArgumentException(
1509
                "Entity has to be managed or scheduled for removal for single computation ".self::objToStr($entity)
1510
            );
1511
        }
1512
        $class = $this->manager->getClassMetadata(get_class($entity));
1513
        // Compute changes for INSERTed entities first. This must always happen even in this case.
1514
        $this->computeScheduleInsertsChangeSets();
1515
        if ($class->isReadOnly()) {
1516
            return;
1517
        }
1518
        // Ignore uninitialized proxy objects
1519
        if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1520
            return;
1521
        }
1522
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1523
        $oid = spl_object_hash($entity);
1524 View Code Duplication
        if (!isset($this->entityInsertions[$oid]) &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1525
            !isset($this->entityDeletions[$oid]) &&
1526
            isset($this->entityStates[$oid])
1527
        ) {
1528
            $this->computeChangeSet($class, $entity);
1529
        }
1530
    }
1531
1532
    /**
1533
     * Computes the changesets of all entities scheduled for insertion.
1534
     *
1535
     * @return void
1536
     */
1537 4
    private function computeScheduleInsertsChangeSets()
1538
    {
1539 4
        foreach ($this->entityInsertions as $entity) {
1540 4
            $class = $this->manager->getClassMetadata(get_class($entity));
1541 4
            $this->computeChangeSet($class, $entity);
1542 4
        }
1543 4
    }
1544
1545
    /**
1546
     * Computes the changes of an association.
1547
     *
1548
     * @param array $assoc The association mapping.
1549
     * @param mixed $value The value of the association.
1550
     *
1551
     * @throws \InvalidArgumentException
1552
     * @throws \UnexpectedValueException
1553
     *
1554
     * @return void
1555
     */
1556 2
    private function computeAssociationChanges($assoc, $value)
1557
    {
1558 2
        if ($value instanceof Proxy && !$value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1559
            return;
1560
        }
1561 2
        if ($value instanceof ApiCollection && $value->isDirty()) {
1562 2
            $coid                            = spl_object_hash($value);
1563 2
            $this->collectionUpdates[$coid]  = $value;
1564 2
            $this->visitedCollections[$coid] = $value;
1565 2
        }
1566
        // Look through the entities, and in any of their associations,
1567
        // for transient (new) entities, recursively. ("Persistence by reachability")
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1568
        // Unwrap. Uninitialized collections will simply be empty.
1569 2
        $unwrappedValue  = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap();
1570 2
        $targetClass     = $this->manager->getClassMetadata($assoc['target']);
1571 2
        $targetClassName = $targetClass->getName();
1572 2
        foreach ($unwrappedValue as $key => $entry) {
1573 2
            if (!($entry instanceof $targetClassName)) {
1574
                throw new \InvalidArgumentException('Invalid association');
1575
            }
1576 2
            $state = $this->getEntityState($entry, self::STATE_NEW);
1577 2
            if (!($entry instanceof $assoc['target'])) {
1578
                throw new \UnexpectedValueException('Unexpected association');
1579
            }
1580
            switch ($state) {
1581 2
                case self::STATE_NEW:
1582
                    if (!$assoc['isCascadePersist']) {
1583
                        throw new \InvalidArgumentException('New entity through relationship');
1584
                    }
1585
                    $this->persistNew($targetClass, $entry);
1586
                    $this->computeChangeSet($targetClass, $entry);
1587
                    break;
1588 2
                case self::STATE_REMOVED:
1589
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1590
                    // and remove the element from Collection.
1591
                    if ($assoc['type'] & ApiMetadata::TO_MANY) {
1592
                        unset($value[$key]);
1593
                    }
1594
                    break;
1595 2
                case self::STATE_DETACHED:
1596
                    // Can actually not happen right now as we assume STATE_NEW,
1597
                    // so the exception will be raised from the DBAL layer (constraint violation).
1598
                    throw new \InvalidArgumentException('Detached entity through relationship');
1599
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1600 2
                default:
1601
                    // MANAGED associated entities are already taken into account
1602
                    // during changeset calculation anyway, since they are in the identity map.
1603 2
            }
1604 2
        }
1605 2
    }
1606
1607 4
    private function executeInserts(ApiMetadata $class)
1608
    {
1609 4
        $className = $class->getName();
1610 4
        $persister = $this->getEntityPersister($className);
1611 4
        foreach ($this->entityInsertions as $oid => $entity) {
1612 4
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1613 4
                continue;
1614
            }
1615 4
            $persister->pushNewEntity($entity);
1616 4
            unset($this->entityInsertions[$oid]);
1617 4
        }
1618 4
        $postInsertIds = $persister->flushNewEntities();
1619 4
        if ($postInsertIds) {
1620
            // Persister returned post-insert IDs
1621 4
            foreach ($postInsertIds as $postInsertId) {
1622 4
                $id      = $postInsertId['generatedId'];
1623 4
                $entity  = $postInsertId['entity'];
1624 4
                $oid     = spl_object_hash($entity);
1625 4
                $idField = $class->getIdentifierFieldNames()[0];
1626 4
                $class->getReflectionProperty($idField)->setValue($entity, $id);
1627 4
                $this->entityIdentifiers[$oid]            = [$idField => $id];
1628 4
                $this->entityStates[$oid]                 = self::STATE_MANAGED;
1629 4
                $this->originalEntityData[$oid][$idField] = $id;
1630 4
                $this->addToIdentityMap($entity);
1631 4
            }
1632 4
        }
1633 4
    }
1634
1635 1
    private function executeUpdates($class)
1636
    {
1637 1
        $className = $class->name;
1638 1
        $persister = $this->getEntityPersister($className);
1639 1
        foreach ($this->entityUpdates as $oid => $entity) {
1640 1
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1641 1
                continue;
1642
            }
1643 1
            $this->recomputeSingleEntityChangeSet($class, $entity);
1644
1645 1
            if (!empty($this->entityChangeSets[$oid])) {
1646 1
                $persister->update($entity);
1647 1
            }
1648 1
            unset($this->entityUpdates[$oid]);
1649 1
        }
1650 1
    }
1651
1652
    /**
1653
     * Executes a refresh operation on an entity.
1654
     *
1655
     * @param object $entity  The entity to refresh.
1656
     * @param array  $visited The already visited entities during cascades.
1657
     *
1658
     * @return void
1659
     *
1660
     * @throws \InvalidArgumentException If the entity is not MANAGED.
1661
     */
1662
    private function doRefresh($entity, array &$visited)
1663
    {
1664
        $oid = spl_object_hash($entity);
1665
        if (isset($visited[$oid])) {
1666
            return; // Prevent infinite recursion
1667
        }
1668
        $visited[$oid] = $entity; // mark visited
1669
        $class         = $this->manager->getClassMetadata(get_class($entity));
1670
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1671
            throw new \InvalidArgumentException('Entity not managed');
1672
        }
1673
        $this->getEntityPersister($class->getName())->refresh(
1674
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1675
            $entity
1676
        );
1677
        $this->cascadeRefresh($entity, $visited);
1678
    }
1679
1680
    /**
1681
     * Cascades a refresh operation to associated entities.
1682
     *
1683
     * @param object $entity
1684
     * @param array  $visited
1685
     *
1686
     * @return void
1687
     */
1688 View Code Duplication
    private function cascadeRefresh($entity, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1689
    {
1690
        $class               = $this->manager->getClassMetadata(get_class($entity));
1691
        $associationMappings = array_filter(
1692
            $class->getAssociationMappings(),
1693
            function ($assoc) {
1694
                return $assoc['isCascadeRefresh'];
1695
            }
1696
        );
1697
        foreach ($associationMappings as $assoc) {
1698
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1699
            switch (true) {
1700
                case ($relatedEntities instanceof ApiCollection):
1701
                    // Unwrap so that foreach() does not initialize
1702
                    $relatedEntities = $relatedEntities->unwrap();
1703
                // break; is commented intentionally!
1704
                case ($relatedEntities instanceof Collection):
1705
                case (is_array($relatedEntities)):
1706
                    foreach ($relatedEntities as $relatedEntity) {
1707
                        $this->doRefresh($relatedEntity, $visited);
1708
                    }
1709
                    break;
1710
                case ($relatedEntities !== null):
1711
                    $this->doRefresh($relatedEntities, $visited);
1712
                    break;
1713
                default:
1714
                    // Do nothing
1715
            }
1716
        }
1717
    }
1718
1719
    /**
1720
     * Cascades a detach operation to associated entities.
1721
     *
1722
     * @param object $entity
1723
     * @param array  $visited
1724
     *
1725
     * @return void
1726
     */
1727 View Code Duplication
    private function cascadeDetach($entity, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1728
    {
1729
        $class               = $this->manager->getClassMetadata(get_class($entity));
1730
        $associationMappings = array_filter(
1731
            $class->getAssociationMappings(),
1732
            function ($assoc) {
1733
                return $assoc['isCascadeDetach'];
1734
            }
1735
        );
1736
        foreach ($associationMappings as $assoc) {
1737
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1738
            switch (true) {
1739
                case ($relatedEntities instanceof ApiCollection):
1740
                    // Unwrap so that foreach() does not initialize
1741
                    $relatedEntities = $relatedEntities->unwrap();
1742
                // break; is commented intentionally!
1743
                case ($relatedEntities instanceof Collection):
1744
                case (is_array($relatedEntities)):
1745
                    foreach ($relatedEntities as $relatedEntity) {
1746
                        $this->doDetach($relatedEntity, $visited);
1747
                    }
1748
                    break;
1749
                case ($relatedEntities !== null):
1750
                    $this->doDetach($relatedEntities, $visited);
1751
                    break;
1752
                default:
1753
                    // Do nothing
1754
            }
1755
        }
1756
    }
1757
1758
    /**
1759
     * Cascades a merge operation to associated entities.
1760
     *
1761
     * @param object $entity
1762
     * @param object $managedCopy
1763
     * @param array  $visited
1764
     *
1765
     * @return void
1766
     */
1767
    private function cascadeMerge($entity, $managedCopy, array &$visited)
1768
    {
1769
        $class               = $this->manager->getClassMetadata(get_class($entity));
1770
        $associationMappings = array_filter(
1771
            $class->getAssociationMappings(),
1772
            function ($assoc) {
1773
                return $assoc['isCascadeMerge'];
1774
            }
1775
        );
1776
        foreach ($associationMappings as $assoc) {
1777
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1778
            if ($relatedEntities instanceof Collection) {
1779
                if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) {
1780
                    continue;
1781
                }
1782
                if ($relatedEntities instanceof ApiCollection) {
1783
                    // Unwrap so that foreach() does not initialize
1784
                    $relatedEntities = $relatedEntities->unwrap();
1785
                }
1786
                foreach ($relatedEntities as $relatedEntity) {
1787
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
1788
                }
1789
            } else {
1790
                if ($relatedEntities !== null) {
1791
                    $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
1792
                }
1793
            }
1794
        }
1795
    }
1796
1797
    /**
1798
     * Cascades the delete operation to associated entities.
1799
     *
1800
     * @param object $entity
1801
     * @param array  $visited
1802
     *
1803
     * @return void
1804
     */
1805
    private function cascadeRemove($entity, array &$visited)
1806
    {
1807
        $class               = $this->manager->getClassMetadata(get_class($entity));
1808
        $associationMappings = array_filter(
1809
            $class->getAssociationMappings(),
1810
            function ($assoc) {
1811
                return $assoc['isCascadeRemove'];
1812
            }
1813
        );
1814
        $entitiesToCascade   = [];
1815
        foreach ($associationMappings as $assoc) {
1816
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1817
                $entity->__load();
1818
            }
1819
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1820
            switch (true) {
1821
                case ($relatedEntities instanceof Collection):
1822
                case (is_array($relatedEntities)):
1823
                    // If its a PersistentCollection initialization is intended! No unwrap!
1824
                    foreach ($relatedEntities as $relatedEntity) {
1825
                        $entitiesToCascade[] = $relatedEntity;
1826
                    }
1827
                    break;
1828
                case ($relatedEntities !== null):
1829
                    $entitiesToCascade[] = $relatedEntities;
1830
                    break;
1831
                default:
1832
                    // Do nothing
1833
            }
1834
        }
1835
        foreach ($entitiesToCascade as $relatedEntity) {
1836
            $this->doRemove($relatedEntity, $visited);
1837
        }
1838
    }
1839
1840
    /**
1841
     * Executes any extra updates that have been scheduled.
1842
     */
1843
    private function executeExtraUpdates()
1844
    {
1845
        foreach ($this->extraUpdates as $oid => $update) {
1846
            list ($entity, $changeset) = $update;
1847
            $this->entityChangeSets[$oid] = $changeset;
1848
            $this->getEntityPersister(get_class($entity))->update($entity);
1849
        }
1850
        $this->extraUpdates = [];
1851
    }
1852
1853
    private function executeDeletions(ApiMetadata $class)
1854
    {
1855
        $className = $class->getName();
1856
        $persister = $this->getEntityPersister($className);
1857
        foreach ($this->entityDeletions as $oid => $entity) {
1858
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1859
                continue;
1860
            }
1861
            $persister->delete($entity);
1862
            unset(
1863
                $this->entityDeletions[$oid],
1864
                $this->entityIdentifiers[$oid],
1865
                $this->originalEntityData[$oid],
1866
                $this->entityStates[$oid]
1867
            );
1868
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1869
            // is obtained by a new entity because the old one went out of scope.
1870
            //$this->entityStates[$oid] = self::STATE_NEW;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1871
            //            if ( ! $class->isIdentifierNatural()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1872
            //                $class->getReflectionProperty($class->getIdentifierFieldNames()[0])->setValue($entity, null);
0 ignored issues
show
Unused Code Comprehensibility introduced by
79% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1873
            //            }
1874
        }
1875
    }
1876
1877
    /**
1878
     * @param object $entity
1879
     * @param object $managedCopy
1880
     */
1881
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
1882
    {
1883
        $class = $this->manager->getClassMetadata(get_class($entity));
1884
        foreach ($this->reflectionPropertiesGetter->getProperties($class->getName()) as $prop) {
1885
            $name = $prop->name;
1886
            $prop->setAccessible(true);
1887
            if ($class->hasAssociation($name)) {
1888
                if (!$class->isIdentifier($name)) {
1889
                    $prop->setValue($managedCopy, $prop->getValue($entity));
1890
                }
1891
            } else {
1892
                $assoc2 = $class->getAssociationMapping($name);
1893
                if ($assoc2['type'] & ApiMetadata::TO_ONE) {
1894
                    $other = $prop->getValue($entity);
1895
                    if ($other === null) {
1896
                        $prop->setValue($managedCopy, null);
1897
                    } else {
1898
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
1899
                            // do not merge fields marked lazy that have not been fetched.
1900
                            continue;
1901
                        }
1902
                        if (!$assoc2['isCascadeMerge']) {
1903
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
1904
                                $targetClass = $this->manager->getClassMetadata($assoc2['targetEntity']);
1905
                                $relatedId   = $targetClass->getIdentifierValues($other);
1906
                                if ($targetClass->getSubclasses()) {
1907
                                    $other = $this->manager->find($targetClass->getName(), $relatedId);
1908
                                } else {
1909
                                    $other = $this->manager->getProxyFactory()->getProxy(
1910
                                        $assoc2['targetEntity'],
1911
                                        $relatedId
1912
                                    );
1913
                                    $this->registerManaged($other, $relatedId, []);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a null|object<stdClass>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1914
                                }
1915
                            }
1916
                            $prop->setValue($managedCopy, $other);
1917
                        }
1918
                    }
1919
                } else {
1920
                    $mergeCol = $prop->getValue($entity);
1921
                    if ($mergeCol instanceof ApiCollection && !$mergeCol->isInitialized()) {
1922
                        // do not merge fields marked lazy that have not been fetched.
1923
                        // keep the lazy persistent collection of the managed copy.
1924
                        continue;
1925
                    }
1926
                    $managedCol = $prop->getValue($managedCopy);
1927
                    if (!$managedCol) {
1928
                        $managedCol = new ApiCollection(
1929
                            $this->manager,
1930
                            $this->manager->getClassMetadata($assoc2['target']),
1931
                            new ArrayCollection
1932
                        );
1933
                        $managedCol->setOwner($managedCopy, $assoc2);
1934
                        $prop->setValue($managedCopy, $managedCol);
1935
                    }
1936
                    if ($assoc2['isCascadeMerge']) {
1937
                        $managedCol->initialize();
1938
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
1939
                        if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
1940
                            $managedCol->unwrap()->clear();
1941
                            $managedCol->setDirty(true);
1942
                            if ($assoc2['isOwningSide']
1943
                                && $assoc2['type'] == ApiMetadata::MANY_TO_MANY
1944
                                && $class->isChangeTrackingNotify()
1945
                            ) {
1946
                                $this->scheduleForDirtyCheck($managedCopy);
1947
                            }
1948
                        }
1949
                    }
1950
                }
1951
            }
1952
            if ($class->isChangeTrackingNotify()) {
1953
                // Just treat all properties as changed, there is no other choice.
1954
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1955
            }
1956
        }
1957
    }
1958
1959
    /**
1960
     * Deletes an entity as part of the current unit of work.
1961
     *
1962
     * This method is internally called during delete() cascades as it tracks
1963
     * the already visited entities to prevent infinite recursions.
1964
     *
1965
     * @param object $entity  The entity to delete.
1966
     * @param array  $visited The map of the already visited entities.
1967
     *
1968
     * @return void
1969
     *
1970
     * @throws \InvalidArgumentException If the instance is a detached entity.
1971
     * @throws \UnexpectedValueException
1972
     */
1973
    private function doRemove($entity, array &$visited)
1974
    {
1975
        $oid = spl_object_hash($entity);
1976
        if (isset($visited[$oid])) {
1977
            return; // Prevent infinite recursion
1978
        }
1979
        $visited[$oid] = $entity; // mark visited
1980
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1981
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1982
        $this->cascadeRemove($entity, $visited);
1983
        $class       = $this->manager->getClassMetadata(get_class($entity));
0 ignored issues
show
Unused Code introduced by
$class is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1984
        $entityState = $this->getEntityState($entity);
1985
        switch ($entityState) {
1986
            case self::STATE_NEW:
1987
            case self::STATE_REMOVED:
1988
                // nothing to do
1989
                break;
1990
            case self::STATE_MANAGED:
1991
                $this->scheduleForDelete($entity);
1992
                break;
1993
            case self::STATE_DETACHED:
1994
                throw new \InvalidArgumentException('Detached entity cannot be removed');
1995
            default:
1996
                throw new \UnexpectedValueException("Unexpected entity state: $entityState.".self::objToStr($entity));
1997
        }
1998
    }
1999
2000
    /**
2001
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2002
     *
2003
     * @param object $entity
2004
     *
2005
     * @return bool
2006
     */
2007
    private function isLoaded($entity)
2008
    {
2009
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2010
    }
2011
2012
    /**
2013
     * Sets/adds associated managed copies into the previous entity's association field
2014
     *
2015
     * @param object $entity
2016
     * @param array  $association
2017
     * @param object $previousManagedCopy
2018
     * @param object $managedCopy
2019
     *
2020
     * @return void
2021
     */
2022
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2023
    {
2024
        $assocField = $association['fieldName'];
2025
        $prevClass  = $this->manager->getClassMetadata(get_class($previousManagedCopy));
2026
        if ($association['type'] & ApiMetadata::TO_ONE) {
2027
            $prevClass->getReflectionProperty($assocField)->setValue($previousManagedCopy, $managedCopy);
2028
2029
            return;
2030
        }
2031
        /** @var array $value */
2032
        $value   = $prevClass->getReflectionProperty($assocField)->getValue($previousManagedCopy);
2033
        $value[] = $managedCopy;
2034
        if ($association['type'] == ApiMetadata::ONE_TO_MANY) {
2035
            $class = $this->manager->getClassMetadata(get_class($entity));
2036
            $class->getReflectionProperty($association['mappedBy'])->setValue($managedCopy, $previousManagedCopy);
2037
        }
2038
    }
2039
2040
    /**
2041
     * Executes a merge operation on an entity.
2042
     *
2043
     * @param object      $entity
2044
     * @param array       $visited
2045
     * @param object|null $prevManagedCopy
2046
     * @param array|null  $assoc
2047
     *
2048
     * @return object The managed copy of the entity.
2049
     *
2050
     * @throws \InvalidArgumentException If the entity instance is NEW.
2051
     * @throws \OutOfBoundsException
2052
     */
2053
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
2054
    {
2055
        $oid = spl_object_hash($entity);
2056
        if (isset($visited[$oid])) {
2057
            $managedCopy = $visited[$oid];
2058
            if ($prevManagedCopy !== null) {
2059
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2060
            }
2061
2062
            return $managedCopy;
2063
        }
2064
        $class = $this->manager->getClassMetadata(get_class($entity));
2065
        // First we assume DETACHED, although it can still be NEW but we can avoid
2066
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
2067
        // we need to fetch it from the db anyway in order to merge.
2068
        // MANAGED entities are ignored by the merge operation.
2069
        $managedCopy = $entity;
2070
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2071
            // Try to look the entity up in the identity map.
2072
            $id = $class->getIdentifierValues($entity);
2073
            // If there is no ID, it is actually NEW.
2074
            if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2075
                $managedCopy = $this->newInstance($class);
2076
                $this->persistNew($class, $managedCopy);
2077
            } else {
2078
                $flatId      = ($class->containsForeignIdentifier())
2079
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
2080
                    : $id;
2081
                $managedCopy = $this->tryGetById($flatId, $class->getRootEntityName());
2082
                if ($managedCopy) {
2083
                    // We have the entity in-memory already, just make sure its not removed.
2084
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
2085
                        throw new \InvalidArgumentException('Removed entity cannot be merged');
2086
                    }
2087
                } else {
2088
                    // We need to fetch the managed copy in order to merge.
2089
                    $managedCopy = $this->manager->find($class->getName(), $flatId);
2090
                }
2091
                if ($managedCopy === null) {
2092
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
2093
                    // since the managed entity was not found.
2094
                    if (!$class->isIdentifierNatural()) {
2095
                        throw new \OutOfBoundsException('Entity not found');
2096
                    }
2097
                    $managedCopy = $this->newInstance($class);
2098
                    $class->setIdentifierValues($managedCopy, $id);
2099
                    $this->persistNew($class, $managedCopy);
2100
                }
2101
            }
2102
2103
            $visited[$oid] = $managedCopy; // mark visited
2104
            if ($this->isLoaded($entity)) {
2105
                if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized()) {
2106
                    $managedCopy->__load();
2107
                }
2108
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
2109
            }
2110
            if ($class->isChangeTrackingDeferredExplicit()) {
2111
                $this->scheduleForDirtyCheck($entity);
2112
            }
2113
        }
2114
        if ($prevManagedCopy !== null) {
2115
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2116
        }
2117
        // Mark the managed copy visited as well
2118
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
2119
        $this->cascadeMerge($entity, $managedCopy, $visited);
2120
2121
        return $managedCopy;
2122
    }
2123
2124
    /**
2125
     * Executes a detach operation on the given entity.
2126
     *
2127
     * @param object  $entity
2128
     * @param array   $visited
2129
     * @param boolean $noCascade if true, don't cascade detach operation.
2130
     *
2131
     * @return void
2132
     */
2133
    private function doDetach($entity, array &$visited, $noCascade = false)
2134
    {
2135
        $oid = spl_object_hash($entity);
2136
        if (isset($visited[$oid])) {
2137
            return; // Prevent infinite recursion
2138
        }
2139
        $visited[$oid] = $entity; // mark visited
2140
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2141
            case self::STATE_MANAGED:
2142
                if ($this->isInIdentityMap($entity)) {
2143
                    $this->removeFromIdentityMap($entity);
2144
                }
2145
                unset(
2146
                    $this->entityInsertions[$oid],
2147
                    $this->entityUpdates[$oid],
2148
                    $this->entityDeletions[$oid],
2149
                    $this->entityIdentifiers[$oid],
2150
                    $this->entityStates[$oid],
2151
                    $this->originalEntityData[$oid]
2152
                );
2153
                break;
2154
            case self::STATE_NEW:
2155
            case self::STATE_DETACHED:
2156
                return;
2157
        }
2158
        if (!$noCascade) {
2159
            $this->cascadeDetach($entity, $visited);
2160
        }
2161
    }
2162
2163
}
2164