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