Completed
Pull Request — master (#14)
by Pavel
28:57
created

UnitOfWork::commit()   F

Complexity

Conditions 23
Paths 644

Size

Total Lines 77
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 77
rs 2.5974
c 0
b 0
f 0
cc 23
eloc 43
nc 644
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Bankiru\Api\Doctrine;
4
5
use Bankiru\Api\Doctrine\Cache\ApiEntityCache;
6
use Bankiru\Api\Doctrine\Cache\EntityCacheAwareInterface;
7
use Bankiru\Api\Doctrine\Cache\LoggingCache;
8
use Bankiru\Api\Doctrine\Cache\VoidEntityCache;
9
use Bankiru\Api\Doctrine\Exception\MappingException;
10
use Bankiru\Api\Doctrine\Hydration\EntityHydrator;
11
use Bankiru\Api\Doctrine\Mapping\ApiMetadata;
12
use Bankiru\Api\Doctrine\Mapping\EntityMetadata;
13
use Bankiru\Api\Doctrine\Persister\ApiPersister;
14
use Bankiru\Api\Doctrine\Persister\CollectionPersister;
15
use Bankiru\Api\Doctrine\Persister\EntityPersister;
16
use Bankiru\Api\Doctrine\Proxy\ApiCollection;
17
use Bankiru\Api\Doctrine\Rpc\CrudsApiInterface;
18
use Bankiru\Api\Doctrine\Utility\IdentifierFlattener;
19
use Doctrine\Common\Collections\ArrayCollection;
20
use Doctrine\Common\Collections\Collection;
21
use Doctrine\Common\NotifyPropertyChanged;
22
use Doctrine\Common\Persistence\ObjectManagerAware;
23
use Doctrine\Common\PropertyChangedListener;
24
use Doctrine\Common\Proxy\Proxy;
25
26
class UnitOfWork implements PropertyChangedListener
27
{
28
    /**
29
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
30
     */
31
    const STATE_MANAGED = 1;
32
    /**
33
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
34
     * and is not (yet) managed by an EntityManager.
35
     */
36
    const STATE_NEW = 2;
37
    /**
38
     * A detached entity is an instance with persistent state and identity that is not
39
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
40
     */
41
    const STATE_DETACHED = 3;
42
    /**
43
     * A removed entity instance is an instance with a persistent identity,
44
     * associated with an EntityManager, whose persistent state will be deleted
45
     * on commit.
46
     */
47
    const STATE_REMOVED = 4;
48
49
    /**
50
     * The (cached) states of any known entities.
51
     * Keys are object ids (spl_object_hash).
52
     *
53
     * @var array
54
     */
55
    private $entityStates = [];
56
57
    /** @var  EntityManager */
58
    private $manager;
59
    /** @var EntityPersister[] */
60
    private $persisters = [];
61
    /** @var CollectionPersister[] */
62
    private $collectionPersisters = [];
63
    /** @var  array */
64
    private $entityIdentifiers = [];
65
    /** @var  object[][] */
66
    private $identityMap = [];
67
    /** @var IdentifierFlattener */
68
    private $identifierFlattener;
69
    /** @var  array */
70
    private $originalEntityData = [];
71
    /** @var  array */
72
    private $entityDeletions = [];
73
    /** @var  array */
74
    private $entityChangeSets = [];
75
    /** @var  array */
76
    private $entityInsertions = [];
77
    /** @var  array */
78
    private $entityUpdates = [];
79
    /** @var  array */
80
    private $readOnlyObjects = [];
81
    /** @var  array */
82
    private $scheduledForSynchronization = [];
83
    /** @var  array */
84
    private $orphanRemovals = [];
85
    /** @var  ApiCollection[] */
86
    private $collectionDeletions = [];
87
    /** @var  array */
88
    private $extraUpdates = [];
89
    /** @var  ApiCollection[] */
90
    private $collectionUpdates = [];
91
    /** @var  ApiCollection[] */
92
    private $visitedCollections = [];
93
94
    /**
95
     * UnitOfWork constructor.
96
     *
97
     * @param EntityManager $manager
98
     */
99
    public function __construct(EntityManager $manager)
100
    {
101
        $this->manager             = $manager;
102
        $this->identifierFlattener = new IdentifierFlattener($this->manager);
103
    }
104
105
    /**
106
     * @param $className
107
     *
108
     * @return EntityPersister
109
     */
110
    public function getEntityPersister($className)
111
    {
112
        if (!array_key_exists($className, $this->persisters)) {
113
            /** @var ApiMetadata $classMetadata */
114
            $classMetadata = $this->manager->getClassMetadata($className);
115
116
            $api = $this->createApi($classMetadata);
117
118
            if ($api instanceof EntityCacheAwareInterface) {
119
                $api->setEntityCache($this->createEntityCache($classMetadata));
120
            }
121
122
            $this->persisters[$className] = new ApiPersister($this->manager, $api);
123
        }
124
125
        return $this->persisters[$className];
126
    }
127
128
    /**
129
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
130
     *
131
     * @param object $entity
132
     *
133
     * @return boolean
134
     */
135
    public function isInIdentityMap($entity)
136
    {
137
        $oid = spl_object_hash($entity);
138
139
        if (!isset($this->entityIdentifiers[$oid])) {
140
            return false;
141
        }
142
143
        /** @var EntityMetadata $classMetadata */
144
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
145
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
146
147
        if ($idHash === '') {
148
            return false;
149
        }
150
151
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
152
    }
153
154
    /**
155
     * Gets the identifier of an entity.
156
     * The returned value is always an array of identifier values. If the entity
157
     * has a composite identifier then the identifier values are in the same
158
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
159
     *
160
     * @param object $entity
161
     *
162
     * @return array The identifier values.
163
     */
164
    public function getEntityIdentifier($entity)
165
    {
166
        return $this->entityIdentifiers[spl_object_hash($entity)];
167
    }
168
169
    /**
170
     * @param             $className
171
     * @param \stdClass   $data
172
     *
173
     * @return ObjectManagerAware|object
174
     * @throws MappingException
175
     */
176
    public function getOrCreateEntity($className, \stdClass $data)
177
    {
178
        /** @var EntityMetadata $class */
179
        $class    = $this->manager->getClassMetadata($className);
180
        $hydrator = new EntityHydrator($this->manager, $class);
181
182
        $tmpEntity = $hydrator->hydarate($data);
183
184
        $id     = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($tmpEntity));
185
        $idHash = implode(' ', $id);
186
187
        $overrideLocalValues = false;
188
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
189
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
190
            $oid    = spl_object_hash($entity);
191
192
            if ($entity instanceof Proxy && !$entity->__isInitialized()) {
193
                $entity->__setInitialized(true);
194
195
                $overrideLocalValues            = true;
196
                $this->originalEntityData[$oid] = $data;
197
198
                if ($entity instanceof NotifyPropertyChanged) {
199
                    $entity->addPropertyChangedListener($this);
200
                }
201
            }
202
        } else {
203
            $entity                                             = $this->newInstance($class);
204
            $oid                                                = spl_object_hash($entity);
205
            $this->entityIdentifiers[$oid]                      = $id;
206
            $this->entityStates[$oid]                           = self::STATE_MANAGED;
207
            $this->originalEntityData[$oid]                     = $data;
208
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
209
            if ($entity instanceof NotifyPropertyChanged) {
210
                $entity->addPropertyChangedListener($this);
211
            }
212
            $overrideLocalValues = true;
213
        }
214
215
        if (!$overrideLocalValues) {
216
            return $entity;
217
        }
218
219
        $entity = $hydrator->hydarate($data, $entity);
220
221
        return $entity;
222
    }
223
224
    /**
225
     * INTERNAL:
226
     * Registers an entity as managed.
227
     *
228
     * @param object         $entity The entity.
229
     * @param array          $id     The identifier values.
230
     * @param \stdClass|null $data   The original entity data.
231
     *
232
     * @return void
233
     */
234
    public function registerManaged($entity, array $id, \stdClass $data = null)
235
    {
236
        $oid = spl_object_hash($entity);
237
238
        $this->entityIdentifiers[$oid]  = $id;
239
        $this->entityStates[$oid]       = self::STATE_MANAGED;
240
        $this->originalEntityData[$oid] = $data;
241
242
        $this->addToIdentityMap($entity);
243
244
        if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) {
245
            $entity->addPropertyChangedListener($this);
246
        }
247
    }
248
249
    /**
250
     * INTERNAL:
251
     * Registers an entity in the identity map.
252
     * Note that entities in a hierarchy are registered with the class name of
253
     * the root entity.
254
     *
255
     * @ignore
256
     *
257
     * @param object $entity The entity to register.
258
     *
259
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
260
     *                 the entity in question is already managed.
261
     *
262
     */
263
    public function addToIdentityMap($entity)
264
    {
265
        /** @var EntityMetadata $classMetadata */
266
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
267
        $idHash        = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]);
268
269
        if ($idHash === '') {
270
            throw new \InvalidArgumentException('Entitty does not have valid identifiers to be stored at identity map');
271
        }
272
273
        $className = $classMetadata->rootEntityName;
274
275
        if (isset($this->identityMap[$className][$idHash])) {
276
            return false;
277
        }
278
279
        $this->identityMap[$className][$idHash] = $entity;
280
281
        return true;
282
    }
283
284
    /**
285
     * Gets the identity map of the UnitOfWork.
286
     *
287
     * @return array
288
     */
289
    public function getIdentityMap()
290
    {
291
        return $this->identityMap;
292
    }
293
294
    /**
295
     * Gets the original data of an entity. The original data is the data that was
296
     * present at the time the entity was reconstituted from the database.
297
     *
298
     * @param object $entity
299
     *
300
     * @return array
301
     */
302
    public function getOriginalEntityData($entity)
303
    {
304
        $oid = spl_object_hash($entity);
305
306
        if (isset($this->originalEntityData[$oid])) {
307
            return $this->originalEntityData[$oid];
308
        }
309
310
        return [];
311
    }
312
313
    /**
314
     * INTERNAL:
315
     * Checks whether an identifier hash exists in the identity map.
316
     *
317
     * @ignore
318
     *
319
     * @param string $idHash
320
     * @param string $rootClassName
321
     *
322
     * @return boolean
323
     */
324
    public function containsIdHash($idHash, $rootClassName)
325
    {
326
        return isset($this->identityMap[$rootClassName][$idHash]);
327
    }
328
329
    /**
330
     * INTERNAL:
331
     * Gets an entity in the identity map by its identifier hash.
332
     *
333
     * @ignore
334
     *
335
     * @param string $idHash
336
     * @param string $rootClassName
337
     *
338
     * @return object
339
     */
340
    public function getByIdHash($idHash, $rootClassName)
341
    {
342
        return $this->identityMap[$rootClassName][$idHash];
343
    }
344
345
    /**
346
     * INTERNAL:
347
     * Tries to get an entity by its identifier hash. If no entity is found for
348
     * the given hash, FALSE is returned.
349
     *
350
     * @ignore
351
     *
352
     * @param mixed  $idHash (must be possible to cast it to string)
353
     * @param string $rootClassName
354
     *
355
     * @return object|bool The found entity or FALSE.
356
     */
357
    public function tryGetByIdHash($idHash, $rootClassName)
358
    {
359
        $stringIdHash = (string)$idHash;
360
361
        if (isset($this->identityMap[$rootClassName][$stringIdHash])) {
362
            return $this->identityMap[$rootClassName][$stringIdHash];
363
        }
364
365
        return false;
366
    }
367
368
    /**
369
     * Gets the state of an entity with regard to the current unit of work.
370
     *
371
     * @param object   $entity
372
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
373
     *                         This parameter can be set to improve performance of entity state detection
374
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
375
     *                         is either known or does not matter for the caller of the method.
376
     *
377
     * @return int The entity state.
378
     */
379
    public function getEntityState($entity, $assume = null)
380
    {
381
        $oid = spl_object_hash($entity);
382
        if (isset($this->entityStates[$oid])) {
383
            return $this->entityStates[$oid];
384
        }
385
        if ($assume !== null) {
386
            return $assume;
387
        }
388
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
389
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
390
        // the UoW does not hold references to such objects and the object hash can be reused.
391
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
392
        $class = $this->manager->getClassMetadata(get_class($entity));
393
        $id    = $class->getIdentifierValues($entity);
394
        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...
395
            return self::STATE_NEW;
396
        }
397
398
        return self::STATE_DETACHED;
399
    }
400
401
    /**
402
     * Tries to find an entity with the given identifier in the identity map of
403
     * this UnitOfWork.
404
     *
405
     * @param mixed  $id            The entity identifier to look for.
406
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
407
     *
408
     * @return object|bool Returns the entity with the specified identifier if it exists in
409
     *                     this UnitOfWork, FALSE otherwise.
410
     */
411
    public function tryGetById($id, $rootClassName)
412
    {
413
        /** @var EntityMetadata $metadata */
414
        $metadata = $this->manager->getClassMetadata($rootClassName);
415
        $idHash   = implode(' ', (array)$this->identifierFlattener->flattenIdentifier($metadata, $id));
416
417
        if (isset($this->identityMap[$rootClassName][$idHash])) {
418
            return $this->identityMap[$rootClassName][$idHash];
419
        }
420
421
        return false;
422
    }
423
424
    /**
425
     * Notifies this UnitOfWork of a property change in an entity.
426
     *
427
     * @param object $entity       The entity that owns the property.
428
     * @param string $propertyName The name of the property that changed.
429
     * @param mixed  $oldValue     The old value of the property.
430
     * @param mixed  $newValue     The new value of the property.
431
     *
432
     * @return void
433
     */
434
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
435
    {
436
        $oid          = spl_object_hash($entity);
437
        $class        = $this->manager->getClassMetadata(get_class($entity));
438
        $isAssocField = $class->hasAssociation($propertyName);
439
        if (!$isAssocField && !$class->hasField($propertyName)) {
440
            return; // ignore non-persistent fields
441
        }
442
        // Update changeset and mark entity for synchronization
443
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
444
        if (!isset($this->scheduledForSynchronization[$class->getRootEntityName()][$oid])) {
445
            $this->scheduleForDirtyCheck($entity);
446
        }
447
    }
448
449
    /**
450
     * Persists an entity as part of the current unit of work.
451
     *
452
     * @param object $entity The entity to persist.
453
     *
454
     * @return void
455
     */
456
    public function persist($entity)
457
    {
458
        $visited = [];
459
        $this->doPersist($entity, $visited);
460
    }
461
462
    /**
463
     * @param ApiMetadata $class
464
     * @param             $entity
465
     *
466
     * @throws \InvalidArgumentException
467
     * @throws \RuntimeException
468
     */
469
    public function recomputeSingleEntityChangeSet(ApiMetadata $class, $entity)
470
    {
471
        $oid = spl_object_hash($entity);
472
        if (!isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
473
            throw new \InvalidArgumentException('Entity is not managed');
474
        }
475
476
        $actualData = [];
477
        foreach ($class->getReflectionProperties() as $name => $refProp) {
478
            if (!$class->isIdentifier($name) && !$class->isCollectionValuedAssociation($name)) {
479
                $actualData[$name] = $refProp->getValue($entity);
480
            }
481
        }
482
        if (!isset($this->originalEntityData[$oid])) {
483
            throw new \RuntimeException(
484
                'Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'
485
            );
486
        }
487
        $originalData = $this->originalEntityData[$oid];
488
        $changeSet    = [];
489
        foreach ($actualData as $propName => $actualValue) {
490
            $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
491
            if ($orgValue !== $actualValue) {
492
                $changeSet[$propName] = [$orgValue, $actualValue];
493
            }
494
        }
495
        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...
496
            if (isset($this->entityChangeSets[$oid])) {
497
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
498
            } else {
499
                if (!isset($this->entityInsertions[$oid])) {
500
                    $this->entityChangeSets[$oid] = $changeSet;
501
                    $this->entityUpdates[$oid]    = $entity;
502
                }
503
            }
504
            $this->originalEntityData[$oid] = $actualData;
505
        }
506
    }
507
508
    /**
509
     * Schedules an entity for insertion into the database.
510
     * If the entity already has an identifier, it will be added to the identity map.
511
     *
512
     * @param object $entity The entity to schedule for insertion.
513
     *
514
     * @return void
515
     *
516
     * @throws \InvalidArgumentException
517
     */
518
    public function scheduleForInsert($entity)
519
    {
520
        $oid = spl_object_hash($entity);
521
        if (isset($this->entityUpdates[$oid])) {
522
            throw new \InvalidArgumentException('Dirty entity cannot be scheduled for insertion');
523
        }
524
        if (isset($this->entityDeletions[$oid])) {
525
            throw new \InvalidArgumentException('Removed entity scheduled for insertion');
526
        }
527
        if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) {
528
            throw new \InvalidArgumentException('Managed entity scheduled for insertion');
529
        }
530
        if (isset($this->entityInsertions[$oid])) {
531
            throw new \InvalidArgumentException('Entity scheduled for insertion twice');
532
        }
533
        $this->entityInsertions[$oid] = $entity;
534
        if (isset($this->entityIdentifiers[$oid])) {
535
            $this->addToIdentityMap($entity);
536
        }
537
        if ($entity instanceof NotifyPropertyChanged) {
538
            $entity->addPropertyChangedListener($this);
539
        }
540
    }
541
542
    /**
543
     * Checks whether an entity is scheduled for insertion.
544
     *
545
     * @param object $entity
546
     *
547
     * @return boolean
548
     */
549
    public function isScheduledForInsert($entity)
550
    {
551
        return isset($this->entityInsertions[spl_object_hash($entity)]);
552
    }
553
554
    /**
555
     * Schedules an entity for being updated.
556
     *
557
     * @param object $entity The entity to schedule for being updated.
558
     *
559
     * @return void
560
     *
561
     * @throws \InvalidArgumentException
562
     */
563
    public function scheduleForUpdate($entity)
564
    {
565
        $oid = spl_object_hash($entity);
566
        if (!isset($this->entityIdentifiers[$oid])) {
567
            throw new \InvalidArgumentException('Entity has no identity');
568
        }
569
        if (isset($this->entityDeletions[$oid])) {
570
            throw new \InvalidArgumentException('Entity is removed');
571
        }
572
        if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) {
573
            $this->entityUpdates[$oid] = $entity;
574
        }
575
    }
576
577
    /**
578
     * Checks whether an entity is registered as dirty in the unit of work.
579
     * Note: Is not very useful currently as dirty entities are only registered
580
     * at commit time.
581
     *
582
     * @param object $entity
583
     *
584
     * @return boolean
585
     */
586
    public function isScheduledForUpdate($entity)
587
    {
588
        return isset($this->entityUpdates[spl_object_hash($entity)]);
589
    }
590
591
    /**
592
     * Checks whether an entity is registered to be checked in the unit of work.
593
     *
594
     * @param object $entity
595
     *
596
     * @return boolean
597
     */
598
    public function isScheduledForDirtyCheck($entity)
599
    {
600
        $rootEntityName = $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
601
602
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
603
    }
604
605
    /**
606
     * INTERNAL:
607
     * Schedules an entity for deletion.
608
     *
609
     * @param object $entity
610
     *
611
     * @return void
612
     */
613
    public function scheduleForDelete($entity)
614
    {
615
        $oid = spl_object_hash($entity);
616
        if (isset($this->entityInsertions[$oid])) {
617
            if ($this->isInIdentityMap($entity)) {
618
                $this->removeFromIdentityMap($entity);
619
            }
620
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
621
622
            return; // entity has not been persisted yet, so nothing more to do.
623
        }
624
        if (!$this->isInIdentityMap($entity)) {
625
            return;
626
        }
627
        $this->removeFromIdentityMap($entity);
628
        unset($this->entityUpdates[$oid]);
629
        if (!isset($this->entityDeletions[$oid])) {
630
            $this->entityDeletions[$oid] = $entity;
631
            $this->entityStates[$oid]    = self::STATE_REMOVED;
632
        }
633
    }
634
635
    /**
636
     * Checks whether an entity is registered as removed/deleted with the unit
637
     * of work.
638
     *
639
     * @param object $entity
640
     *
641
     * @return boolean
642
     */
643
    public function isScheduledForDelete($entity)
644
    {
645
        return isset($this->entityDeletions[spl_object_hash($entity)]);
646
    }
647
648
    /**
649
     * Checks whether an entity is scheduled for insertion, update or deletion.
650
     *
651
     * @param object $entity
652
     *
653
     * @return boolean
654
     */
655
    public function isEntityScheduled($entity)
656
    {
657
        $oid = spl_object_hash($entity);
658
659
        return isset($this->entityInsertions[$oid])
660
               || isset($this->entityUpdates[$oid])
661
               || isset($this->entityDeletions[$oid]);
662
    }
663
664
    /**
665
     * INTERNAL:
666
     * Removes an entity from the identity map. This effectively detaches the
667
     * entity from the persistence management of Doctrine.
668
     *
669
     * @ignore
670
     *
671
     * @param object $entity
672
     *
673
     * @return boolean
674
     *
675
     * @throws \InvalidArgumentException
676
     */
677
    public function removeFromIdentityMap($entity)
678
    {
679
        $oid           = spl_object_hash($entity);
680
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
681
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
682
        if ($idHash === '') {
683
            throw new \InvalidArgumentException('Entity has no identity');
684
        }
685
        $className = $classMetadata->getRootEntityName();
686
        if (isset($this->identityMap[$className][$idHash])) {
687
            unset($this->identityMap[$className][$idHash]);
688
            unset($this->readOnlyObjects[$oid]);
689
690
            //$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...
691
            return true;
692
        }
693
694
        return false;
695
    }
696
697
    /**
698
     * Commits the UnitOfWork, executing all operations that have been postponed
699
     * up to this point. The state of all managed entities will be synchronized with
700
     * the database.
701
     *
702
     * The operations are executed in the following order:
703
     *
704
     * 1) All entity insertions
705
     * 2) All entity updates
706
     * 3) All collection deletions
707
     * 4) All collection updates
708
     * 5) All entity deletions
709
     *
710
     * @param null|object|array $entity
711
     *
712
     * @return void
713
     *
714
     * @throws \Exception
715
     */
716
    public function commit($entity = null)
717
    {
718
        // Compute changes done since last commit.
719
        if ($entity === null) {
720
            $this->computeChangeSets();
721
        } elseif (is_object($entity)) {
722
            $this->computeSingleEntityChangeSet($entity);
723
        } elseif (is_array($entity)) {
724
            foreach ((array)$entity as $object) {
725
                $this->computeSingleEntityChangeSet($object);
726
            }
727
        }
728
        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...
729
              $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...
730
              $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...
731
              $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...
732
              $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...
733
              $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...
734
        ) {
735
            return; // Nothing to do.
736
        }
737
        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...
738
            foreach ($this->orphanRemovals as $orphan) {
739
                $this->remove($orphan);
0 ignored issues
show
Bug introduced by
The method remove() does not exist on Bankiru\Api\Doctrine\UnitOfWork. Did you maybe mean removeFromIdentityMap()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
740
            }
741
        }
742
        // Now we need a commit order to maintain referential integrity
743
        $commitOrder = $this->getCommitOrder();
744
745
        // Collection deletions (deletions of complete collections)
746
        // 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...
747
        //       //fixme: collection mutations
748
        //       $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...
749
        // }
750
        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...
751
            foreach ($commitOrder as $class) {
752
                $this->executeInserts($class);
753
            }
754
        }
755
        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...
756
            foreach ($commitOrder as $class) {
757
                $this->executeUpdates($class);
758
            }
759
        }
760
        // Extra updates that were requested by persisters.
761
        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...
762
            $this->executeExtraUpdates();
763
        }
764
        // Collection updates (deleteRows, updateRows, insertRows)
765
        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...
766
            //fixme: decide what to do with collection mutation if API does not support this
767
            //$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...
768
        }
769
        // Entity deletions come last and need to be in reverse commit order
770
        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...
771
            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...
772
                $this->executeDeletions($commitOrder[$i]);
773
            }
774
        }
775
776
        // Take new snapshots from visited collections
777
        foreach ($this->visitedCollections as $coll) {
778
            $coll->takeSnapshot();
779
        }
780
781
        // Clear up
782
        $this->entityInsertions =
783
        $this->entityUpdates =
784
        $this->entityDeletions =
785
        $this->extraUpdates =
786
        $this->entityChangeSets =
787
        $this->collectionUpdates =
788
        $this->collectionDeletions =
789
        $this->visitedCollections =
790
        $this->scheduledForSynchronization =
791
        $this->orphanRemovals = [];
792
    }
793
794
    /**
795
     * Gets the changeset for an entity.
796
     *
797
     * @param object $entity
798
     *
799
     * @return array
800
     */
801
    public function & getEntityChangeSet($entity)
802
    {
803
        $oid  = spl_object_hash($entity);
804
        $data = [];
805
        if (!isset($this->entityChangeSets[$oid])) {
806
            return $data;
807
        }
808
809
        return $this->entityChangeSets[$oid];
810
    }
811
812
    /**
813
     * Computes the changes that happened to a single entity.
814
     *
815
     * Modifies/populates the following properties:
816
     *
817
     * {@link _originalEntityData}
818
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
819
     * then it was not fetched from the database and therefore we have no original
820
     * entity data yet. All of the current entity data is stored as the original entity data.
821
     *
822
     * {@link _entityChangeSets}
823
     * The changes detected on all properties of the entity are stored there.
824
     * A change is a tuple array where the first entry is the old value and the second
825
     * entry is the new value of the property. Changesets are used by persisters
826
     * to INSERT/UPDATE the persistent entity state.
827
     *
828
     * {@link _entityUpdates}
829
     * If the entity is already fully MANAGED (has been fetched from the database before)
830
     * and any changes to its properties are detected, then a reference to the entity is stored
831
     * there to mark it for an update.
832
     *
833
     * {@link _collectionDeletions}
834
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
835
     * then this collection is marked for deletion.
836
     *
837
     * @ignore
838
     *
839
     * @internal Don't call from the outside.
840
     *
841
     * @param ApiMetadata $class  The class descriptor of the entity.
842
     * @param object      $entity The entity for which to compute the changes.
843
     *
844
     * @return void
845
     */
846
    public function computeChangeSet(ApiMetadata $class, $entity)
847
    {
848
        $oid = spl_object_hash($entity);
849
        if (isset($this->readOnlyObjects[$oid])) {
850
            return;
851
        }
852
        //        if ( ! $class->isInheritanceTypeNone()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
858
        //        }
859
        $actualData = [];
860
        foreach ($class->getReflectionProperties() as $name => $refProp) {
861
            $value = $refProp->getValue($entity);
862
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
863
                if ($value instanceof ApiCollection) {
864
                    if ($value->getOwner() === $entity) {
865
                        continue;
866
                    }
867
                    $value = new ArrayCollection($value->getValues());
868
                }
869
                // If $value is not a Collection then use an ArrayCollection.
870
                if (!$value instanceof Collection) {
871
                    $value = new ArrayCollection($value);
872
                }
873
                $assoc = $class->getAssociationMapping($name);
874
                // Inject PersistentCollection
875
                $value = new ApiCollection(
876
                    $this->manager,
877
                    $this->manager->getClassMetadata($assoc['target']),
878
                    $value
879
                );
880
                $value->setOwner($entity, $assoc);
881
                $value->setDirty(!$value->isEmpty());
882
                $class->getReflectionProperty($name)->setValue($entity, $value);
883
                $actualData[$name] = $value;
884
                continue;
885
            }
886
            if (!$class->isIdentifier($name)) {
887
                $actualData[$name] = $value;
888
            }
889
        }
890
        if (!isset($this->originalEntityData[$oid])) {
891
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
892
            // These result in an INSERT.
893
            $this->originalEntityData[$oid] = $actualData;
894
            $changeSet                      = [];
895
            foreach ($actualData as $propName => $actualValue) {
896 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...
897
                    $changeSet[$propName] = [null, $actualValue];
898
                    continue;
899
                }
900
                $assoc = $class->getAssociationMapping($propName);
901
                if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) {
902
                    $changeSet[$propName] = [null, $actualValue];
903
                }
904
            }
905
            $this->entityChangeSets[$oid] = $changeSet;
906
        } else {
907
            // Entity is "fully" MANAGED: it was already fully persisted before
908
            // and we have a copy of the original data
909
            $originalData           = $this->originalEntityData[$oid];
910
            $isChangeTrackingNotify = false;
911
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
912
                ? $this->entityChangeSets[$oid]
913
                : [];
914
            foreach ($actualData as $propName => $actualValue) {
915
                // skip field, its a partially omitted one!
916
                if (!(isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
917
                    continue;
918
                }
919
                $orgValue = $originalData[$propName];
920
                // skip if value haven't changed
921
                if ($orgValue === $actualValue) {
922
                    continue;
923
                }
924
                // if regular field
925 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...
926
                    if ($isChangeTrackingNotify) {
927
                        continue;
928
                    }
929
                    $changeSet[$propName] = [$orgValue, $actualValue];
930
                    continue;
931
                }
932
                $assoc = $class->getAssociationMapping($propName);
933
                // Persistent collection was exchanged with the "originally"
934
                // created one. This can only mean it was cloned and replaced
935
                // on another entity.
936
                if ($actualValue instanceof ApiCollection) {
937
                    $owner = $actualValue->getOwner();
938
                    if ($owner === null) { // cloned
939
                        $actualValue->setOwner($entity, $assoc);
940
                    } else {
941
                        if ($owner !== $entity) { // no clone, we have to fix
942
                            if (!$actualValue->isInitialized()) {
943
                                $actualValue->initialize(); // we have to do this otherwise the cols share state
944
                            }
945
                            $newValue = clone $actualValue;
946
                            $newValue->setOwner($entity, $assoc);
947
                            $class->getReflectionProperty($propName)->setValue($entity, $newValue);
948
                        }
949
                    }
950
                }
951
                if ($orgValue instanceof ApiCollection) {
952
                    // A PersistentCollection was de-referenced, so delete it.
953
                    $coid = spl_object_hash($orgValue);
954
                    if (isset($this->collectionDeletions[$coid])) {
955
                        continue;
956
                    }
957
                    $this->collectionDeletions[$coid] = $orgValue;
958
                    $changeSet[$propName]             = $orgValue; // Signal changeset, to-many assocs will be ignored.
959
                    continue;
960
                }
961
                if ($assoc['type'] & ApiMetadata::TO_ONE) {
962
                    if ($assoc['isOwningSide']) {
963
                        $changeSet[$propName] = [$orgValue, $actualValue];
964
                    }
965
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
966
                        $this->scheduleOrphanRemoval($orgValue);
967
                    }
968
                }
969
            }
970
            if ($changeSet) {
971
                $this->entityChangeSets[$oid]   = $changeSet;
972
                $this->originalEntityData[$oid] = $actualData;
973
                $this->entityUpdates[$oid]      = $entity;
974
            }
975
        }
976
        // Look for changes in associations of the entity
977
        foreach ($class->getAssociationMappings() as $field => $assoc) {
978
            if (($val = $class->getReflectionProperty($field)->getValue($entity)) === null) {
979
                continue;
980
            }
981
            $this->computeAssociationChanges($assoc, $val);
982
            if (!isset($this->entityChangeSets[$oid]) &&
983
                $assoc['isOwningSide'] &&
984
                $assoc['type'] == ApiMetadata::MANY_TO_MANY &&
985
                $val instanceof ApiCollection &&
986
                $val->isDirty()
987
            ) {
988
                $this->entityChangeSets[$oid]   = [];
989
                $this->originalEntityData[$oid] = $actualData;
990
                $this->entityUpdates[$oid]      = $entity;
991
            }
992
        }
993
    }
994
995
    /**
996
     * Computes all the changes that have been done to entities and collections
997
     * since the last commit and stores these changes in the _entityChangeSet map
998
     * temporarily for access by the persisters, until the UoW commit is finished.
999
     *
1000
     * @return void
1001
     */
1002
    public function computeChangeSets()
1003
    {
1004
        // Compute changes for INSERTed entities first. This must always happen.
1005
        $this->computeScheduleInsertsChangeSets();
1006
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
1007
        foreach ($this->identityMap as $className => $entities) {
1008
            $class = $this->manager->getClassMetadata($className);
1009
            // Skip class if instances are read-only
1010
            if ($class->isReadOnly()) {
1011
                continue;
1012
            }
1013
            // If change tracking is explicit or happens through notification, then only compute
1014
            // changes on entities of that type that are explicitly marked for synchronization.
1015
            switch (true) {
1016
                case ($class->isChangeTrackingDeferredImplicit()):
1017
                    $entitiesToProcess = $entities;
1018
                    break;
1019
                case (isset($this->scheduledForSynchronization[$className])):
1020
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
1021
                    break;
1022
                default:
1023
                    $entitiesToProcess = [];
1024
            }
1025
            foreach ($entitiesToProcess as $entity) {
1026
                // Ignore uninitialized proxy objects
1027
                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...
1028
                    continue;
1029
                }
1030
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1031
                $oid = spl_object_hash($entity);
1032 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...
1033
                    !isset($this->entityDeletions[$oid]) &&
1034
                    isset($this->entityStates[$oid])
1035
                ) {
1036
                    $this->computeChangeSet($class, $entity);
1037
                }
1038
            }
1039
        }
1040
    }
1041
1042
    /**
1043
     * INTERNAL:
1044
     * Schedules an orphaned entity for removal. The remove() operation will be
1045
     * invoked on that entity at the beginning of the next commit of this
1046
     * UnitOfWork.
1047
     *
1048
     * @ignore
1049
     *
1050
     * @param object $entity
1051
     *
1052
     * @return void
1053
     */
1054
    public function scheduleOrphanRemoval($entity)
1055
    {
1056
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
1057
    }
1058
1059
    public function loadCollection(ApiCollection $collection)
1060
    {
1061
        $assoc     = $collection->getMapping();
1062
        $persister = $this->getEntityPersister($assoc['target']);
1063
        switch ($assoc['type']) {
1064
            case ApiMetadata::ONE_TO_MANY:
1065
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
1066
                break;
1067
        }
1068
        $collection->setInitialized(true);
1069
    }
1070
1071
    public function getCollectionPersister($association)
1072
    {
1073
        $role = isset($association['cache'])
1074
            ? $association['sourceEntity'] . '::' . $association['fieldName']
1075
            : $association['type'];
1076
        if (array_key_exists($role, $this->collectionPersisters)) {
1077
            return $this->collectionPersisters[$role];
1078
        }
1079
        $this->collectionPersisters[$role] = new CollectionPersister($this->manager);
0 ignored issues
show
Unused Code introduced by
The call to CollectionPersister::__construct() has too many arguments starting with $this->manager.

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

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

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

Loading history...
1080
1081
        return $this->collectionPersisters[$role];
1082
    }
1083
1084
    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...
1085
    {
1086
    }
1087
1088
    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...
1089
    {
1090
    }
1091
1092
    /**
1093
     * INTERNAL:
1094
     * Sets a property value of the original data array of an entity.
1095
     *
1096
     * @ignore
1097
     *
1098
     * @param string $oid
1099
     * @param string $property
1100
     * @param mixed  $value
1101
     *
1102
     * @return void
1103
     */
1104
    public function setOriginalEntityProperty($oid, $property, $value)
1105
    {
1106
        if (!array_key_exists($oid, $this->originalEntityData)) {
1107
            $this->originalEntityData[$oid] = new \stdClass();
1108
        }
1109
1110
        $this->originalEntityData[$oid]->$property = $value;
1111
    }
1112
1113
    public function scheduleExtraUpdate($entity, $changeset)
1114
    {
1115
        $oid         = spl_object_hash($entity);
1116
        $extraUpdate = [$entity, $changeset];
1117
        if (isset($this->extraUpdates[$oid])) {
1118
            list(, $changeset2) = $this->extraUpdates[$oid];
1119
            $extraUpdate = [$entity, $changeset + $changeset2];
1120
        }
1121
        $this->extraUpdates[$oid] = $extraUpdate;
1122
    }
1123
1124
    /**
1125
     * Refreshes the state of the given entity from the database, overwriting
1126
     * any local, unpersisted changes.
1127
     *
1128
     * @param object $entity The entity to refresh.
1129
     *
1130
     * @return void
1131
     *
1132
     * @throws InvalidArgumentException If the entity is not MANAGED.
1133
     */
1134
    public function refresh($entity)
1135
    {
1136
        $visited = [];
1137
        $this->doRefresh($entity, $visited);
1138
    }
1139
1140
    /**
1141
     * Clears the UnitOfWork.
1142
     *
1143
     * @param string|null $entityName if given, only entities of this type will get detached.
1144
     *
1145
     * @return void
1146
     */
1147
    public function clear($entityName = null)
1148
    {
1149
        if ($entityName === null) {
1150
            $this->identityMap =
1151
            $this->entityIdentifiers =
1152
            $this->originalEntityData =
1153
            $this->entityChangeSets =
1154
            $this->entityStates =
1155
            $this->scheduledForSynchronization =
1156
            $this->entityInsertions =
1157
            $this->entityUpdates =
1158
            $this->entityDeletions =
1159
            $this->collectionDeletions =
1160
            $this->collectionUpdates =
1161
            $this->extraUpdates =
1162
            $this->readOnlyObjects =
1163
            $this->visitedCollections =
1164
            $this->orphanRemovals = [];
1165
        } else {
1166
            $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...
1167
            $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...
1168
        }
1169
    }
1170
1171
    /**
1172
     * @param PersistentCollection $coll
1173
     *
1174
     * @return bool
1175
     */
1176
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
1177
    {
1178
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
1179
    }
1180
1181
    /**
1182
     * Schedules an entity for dirty-checking at commit-time.
1183
     *
1184
     * @param object $entity The entity to schedule for dirty-checking.
1185
     *
1186
     * @return void
1187
     *
1188
     * @todo Rename: scheduleForSynchronization
1189
     */
1190
    public function scheduleForDirtyCheck($entity)
1191
    {
1192
        $rootClassName                                                               =
1193
            $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
1194
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
1195
    }
1196
1197
    /**
1198
     * @param ApiMetadata $class
1199
     *
1200
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
1201
     */
1202
    private function newInstance(ApiMetadata $class)
1203
    {
1204
        $entity = $class->newInstance();
1205
1206
        if ($entity instanceof ObjectManagerAware) {
1207
            $entity->injectObjectManager($this->manager, $class);
1208
        }
1209
1210
        return $entity;
1211
    }
1212
1213
    /**
1214
     * @param ApiMetadata $classMetadata
1215
     *
1216
     * @return EntityDataCacheInterface
1217
     */
1218
    private function createEntityCache(ApiMetadata $classMetadata)
1219
    {
1220
        $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName());
1221
        $cache         = new VoidEntityCache($classMetadata);
1222
        if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) {
1223
            $cache =
1224
                new LoggingCache(
1225
                    new ApiEntityCache(
1226
                        $this->manager->getConfiguration()->getApiCache(),
1227
                        $classMetadata,
1228
                        $configuration
1229
                    ),
1230
                    $this->manager->getConfiguration()->getApiCacheLogger()
1231
                );
1232
1233
            return $cache;
1234
        }
1235
1236
        return $cache;
1237
    }
1238
1239
    /**
1240
     * @param ApiMetadata $classMetadata
1241
     *
1242
     * @return CrudsApiInterface
1243
     */
1244
    private function createApi(ApiMetadata $classMetadata)
1245
    {
1246
        $client = $this->manager->getConfiguration()->getRegistry()->get($classMetadata->getClientName());
1247
1248
        $api = $this->manager
1249
            ->getConfiguration()
1250
            ->getResolver()
1251
            ->resolve($classMetadata->getApiName())
1252
            ->createApi(
1253
                $client,
1254
                $classMetadata
1255
            );
1256
1257
        return $api;
1258
    }
1259
1260
    private function doPersist($entity, $visited)
1261
    {
1262
        $oid = spl_object_hash($entity);
1263
        if (isset($visited[$oid])) {
1264
            return; // Prevent infinite recursion
1265
        }
1266
        $visited[$oid] = $entity; // Mark visited
1267
        $class         = $this->manager->getClassMetadata(get_class($entity));
1268
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1269
        // If we would detect DETACHED here we would throw an exception anyway with the same
1270
        // consequences (not recoverable/programming error), so just assuming NEW here
1271
        // lets us avoid some database lookups for entities with natural identifiers.
1272
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1273
        switch ($entityState) {
1274
            case self::STATE_MANAGED:
1275
                $this->scheduleForDirtyCheck($entity);
1276
                break;
1277
            case self::STATE_NEW:
1278
                $this->persistNew($class, $entity);
1279
                break;
1280
            case self::STATE_REMOVED:
1281
                // Entity becomes managed again
1282
                unset($this->entityDeletions[$oid]);
1283
                $this->addToIdentityMap($entity);
1284
                $this->entityStates[$oid] = self::STATE_MANAGED;
1285
                break;
1286
            case self::STATE_DETACHED:
1287
                // Can actually not happen right now since we assume STATE_NEW.
1288
                throw new \InvalidArgumentException('Detached entity cannot be persisted');
1289
            default:
1290
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1291
        }
1292
        $this->cascadePersist($entity, $visited);
1293
    }
1294
1295
    /**
1296
     * Cascades the save operation to associated entities.
1297
     *
1298
     * @param object $entity
1299
     * @param array  $visited
1300
     *
1301
     * @return void
1302
     * @throws \InvalidArgumentException
1303
     * @throws MappingException
1304
     */
1305
    private function cascadePersist($entity, array &$visited)
1306
    {
1307
        $class               = $this->manager->getClassMetadata(get_class($entity));
1308
        $associationMappings = [];
1309
        foreach ($class->getAssociationNames() as $name) {
1310
            $assoc = $class->getAssociationMapping($name);
1311
            if ($assoc['isCascadePersist']) {
1312
                $associationMappings[$name] = $assoc;
1313
            }
1314
        }
1315
        foreach ($associationMappings as $assoc) {
1316
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1317
            switch (true) {
1318
                case ($relatedEntities instanceof ApiCollection):
1319
                    // Unwrap so that foreach() does not initialize
1320
                    $relatedEntities = $relatedEntities->unwrap();
1321
                // break; is commented intentionally!
1322
                case ($relatedEntities instanceof Collection):
1323
                case (is_array($relatedEntities)):
1324
                    if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) {
1325
                        throw new \InvalidArgumentException('Invalid association for cascade');
1326
                    }
1327
                    foreach ($relatedEntities as $relatedEntity) {
1328
                        $this->doPersist($relatedEntity, $visited);
1329
                    }
1330
                    break;
1331
                case ($relatedEntities !== null):
1332
                    if (!$relatedEntities instanceof $assoc['target']) {
1333
                        throw new \InvalidArgumentException('Invalid association for cascade');
1334
                    }
1335
                    $this->doPersist($relatedEntities, $visited);
1336
                    break;
1337
                default:
1338
                    // Do nothing
1339
            }
1340
        }
1341
    }
1342
1343
    /**
1344
     * @param ApiMetadata $class
1345
     * @param object      $entity
1346
     *
1347
     * @return void
1348
     */
1349
    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...
1350
    {
1351
        $oid = spl_object_hash($entity);
1352
        //        $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...
1353
        //        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...
1354
        //            $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...
1355
        //        }
1356
        //        $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...
1357
        //        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...
1358
        //            $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...
1359
        //            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...
1360
        //                $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...
1361
        //                $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...
1362
        //            }
1363
        //            $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...
1364
        //        }
1365
        $this->entityStates[$oid] = self::STATE_MANAGED;
1366
        $this->scheduleForInsert($entity);
1367
    }
1368
1369
    /**
1370
     * Gets the commit order.
1371
     *
1372
     * @param array|null $entityChangeSet
1373
     *
1374
     * @return array
1375
     */
1376
    private function getCommitOrder(array $entityChangeSet = null)
1377
    {
1378
        if ($entityChangeSet === null) {
1379
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1380
        }
1381
        $calc = $this->getCommitOrderCalculator();
1382
        // See if there are any new classes in the changeset, that are not in the
1383
        // commit order graph yet (don't have a node).
1384
        // We have to inspect changeSet to be able to correctly build dependencies.
1385
        // It is not possible to use IdentityMap here because post inserted ids
1386
        // are not yet available.
1387
        /** @var ApiMetadata[] $newNodes */
1388
        $newNodes = [];
1389
        foreach ((array)$entityChangeSet as $entity) {
1390
            $class = $this->manager->getClassMetadata(get_class($entity));
1391
            if ($calc->hasNode($class->getName())) {
1392
                continue;
1393
            }
1394
            $calc->addNode($class->getName(), $class);
1395
            $newNodes[] = $class;
1396
        }
1397
        // Calculate dependencies for new nodes
1398
        while ($class = array_pop($newNodes)) {
1399
            foreach ($class->getAssociationMappings() as $assoc) {
1400
                if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) {
1401
                    continue;
1402
                }
1403
                $targetClass = $this->manager->getClassMetadata($assoc['target']);
1404
                if (!$calc->hasNode($targetClass->getName())) {
1405
                    $calc->addNode($targetClass->getName(), $targetClass);
1406
                    $newNodes[] = $targetClass;
1407
                }
1408
                $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable']));
1409
                // If the target class has mapped subclasses, these share the same dependency.
1410
                if (!$targetClass->getSubclasses()) {
1411
                    continue;
1412
                }
1413
                foreach ($targetClass->getSubclasses() as $subClassName) {
1414
                    $targetSubClass = $this->manager->getClassMetadata($subClassName);
1415
                    if (!$calc->hasNode($subClassName)) {
1416
                        $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...
1417
                        $newNodes[] = $targetSubClass;
1418
                    }
1419
                    $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...
1420
                }
1421
            }
1422
        }
1423
1424
        return $calc->sort();
1425
    }
1426
1427
    /**
1428
     * Helper method to show an object as string.
1429
     *
1430
     * @param object $obj
1431
     *
1432
     * @return string
1433
     */
1434
    private static function objToStr($obj)
1435
    {
1436
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
1437
    }
1438
1439
    private function getCommitOrderCalculator()
1440
    {
1441
        return new Utility\CommitOrderCalculator();
1442
    }
1443
1444
    /**
1445
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
1446
     *
1447
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
1448
     * 2. Read Only entities are skipped.
1449
     * 3. Proxies are skipped.
1450
     * 4. Only if entity is properly managed.
1451
     *
1452
     * @param object $entity
1453
     *
1454
     * @return void
1455
     *
1456
     * @throws \InvalidArgumentException
1457
     */
1458
    private function computeSingleEntityChangeSet($entity)
1459
    {
1460
        $state = $this->getEntityState($entity);
1461
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
1462
            throw new \InvalidArgumentException(
1463
                "Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)
1464
            );
1465
        }
1466
        $class = $this->manager->getClassMetadata(get_class($entity));
1467
        // Compute changes for INSERTed entities first. This must always happen even in this case.
1468
        $this->computeScheduleInsertsChangeSets();
1469
        if ($class->isReadOnly()) {
1470
            return;
1471
        }
1472
        // Ignore uninitialized proxy objects
1473
        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...
1474
            return;
1475
        }
1476
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1477
        $oid = spl_object_hash($entity);
1478 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...
1479
            !isset($this->entityDeletions[$oid]) &&
1480
            isset($this->entityStates[$oid])
1481
        ) {
1482
            $this->computeChangeSet($class, $entity);
1483
        }
1484
    }
1485
1486
    /**
1487
     * Computes the changesets of all entities scheduled for insertion.
1488
     *
1489
     * @return void
1490
     */
1491
    private function computeScheduleInsertsChangeSets()
1492
    {
1493
        foreach ($this->entityInsertions as $entity) {
1494
            $class = $this->manager->getClassMetadata(get_class($entity));
1495
            $this->computeChangeSet($class, $entity);
1496
        }
1497
    }
1498
1499
    /**
1500
     * Computes the changes of an association.
1501
     *
1502
     * @param array $assoc The association mapping.
1503
     * @param mixed $value The value of the association.
1504
     *
1505
     * @throws \InvalidArgumentException
1506
     * @throws \UnexpectedValueException
1507
     *
1508
     * @return void
1509
     */
1510
    private function computeAssociationChanges($assoc, $value)
1511
    {
1512
        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...
1513
            return;
1514
        }
1515
        if ($value instanceof ApiCollection && $value->isDirty()) {
1516
            $coid                            = spl_object_hash($value);
1517
            $this->collectionUpdates[$coid]  = $value;
1518
            $this->visitedCollections[$coid] = $value;
1519
        }
1520
        // Look through the entities, and in any of their associations,
1521
        // 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...
1522
        // Unwrap. Uninitialized collections will simply be empty.
1523
        $unwrappedValue  = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap();
1524
        $targetClass     = $this->manager->getClassMetadata($assoc['target']);
1525
        $targetClassName = $targetClass->getName();
1526
        foreach ($unwrappedValue as $key => $entry) {
1527
            if (!($entry instanceof $targetClassName)) {
1528
                throw new \InvalidArgumentException('Invalid association');
1529
            }
1530
            $state = $this->getEntityState($entry, self::STATE_NEW);
1531
            if (!($entry instanceof $assoc['target'])) {
1532
                throw new \UnexpectedValueException('Unexpected association');
1533
            }
1534
            switch ($state) {
1535
                case self::STATE_NEW:
1536
                    if (!$assoc['isCascadePersist']) {
1537
                        throw new \InvalidArgumentException('New entity through relationship');
1538
                    }
1539
                    $this->persistNew($targetClass, $entry);
1540
                    $this->computeChangeSet($targetClass, $entry);
1541
                    break;
1542
                case self::STATE_REMOVED:
1543
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1544
                    // and remove the element from Collection.
1545
                    if ($assoc['type'] & ApiMetadata::TO_MANY) {
1546
                        unset($value[$key]);
1547
                    }
1548
                    break;
1549
                case self::STATE_DETACHED:
1550
                    // Can actually not happen right now as we assume STATE_NEW,
1551
                    // so the exception will be raised from the DBAL layer (constraint violation).
1552
                    throw new \InvalidArgumentException('Detached entity through relationship');
1553
                    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...
1554
                default:
1555
                    // MANAGED associated entities are already taken into account
1556
                    // during changeset calculation anyway, since they are in the identity map.
1557
            }
1558
        }
1559
    }
1560
1561
    private function executeInserts(ApiMetadata $class)
1562
    {
1563
        $className = $class->getName();
1564
        $persister = $this->getEntityPersister($className);
1565
        foreach ($this->entityInsertions as $oid => $entity) {
1566
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1567
                continue;
1568
            }
1569
            $persister->pushNewEntity($entity);
1570
            unset($this->entityInsertions[$oid]);
1571
        }
1572
        $postInsertIds = $persister->flushNewEntities();
1573
        if ($postInsertIds) {
1574
            // Persister returned post-insert IDs
1575
            foreach ($postInsertIds as $postInsertId) {
1576
                $id      = $postInsertId['generatedId'];
1577
                $entity  = $postInsertId['entity'];
1578
                $oid     = spl_object_hash($entity);
1579
                $idField = $class->getIdentifierFieldNames()[0];
1580
                $class->getReflectionProperty($idField)->setValue($entity, $id);
1581
                $this->entityIdentifiers[$oid]            = [$idField => $id];
1582
                $this->entityStates[$oid]                 = self::STATE_MANAGED;
1583
                $this->originalEntityData[$oid][$idField] = $id;
1584
                $this->addToIdentityMap($entity);
1585
            }
1586
        }
1587
    }
1588
1589
    private function executeUpdates($class)
1590
    {
1591
        $className = $class->name;
1592
        $persister = $this->getEntityPersister($className);
1593
        foreach ($this->entityUpdates as $oid => $entity) {
1594
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1595
                continue;
1596
            }
1597
            $this->recomputeSingleEntityChangeSet($class, $entity);
1598
1599
            if (!empty($this->entityChangeSets[$oid])) {
1600
                $persister->update($entity);
1601
            }
1602
            unset($this->entityUpdates[$oid]);
1603
        }
1604
    }
1605
1606
    /**
1607
     * Executes a detach operation on the given entity.
1608
     *
1609
     * @param object  $entity
1610
     * @param array   $visited
1611
     * @param boolean $noCascade if true, don't cascade detach operation.
1612
     *
1613
     * @return void
1614
     */
1615
    private function doDetach($entity, array &$visited, $noCascade = false)
1616
    {
1617
        $oid = spl_object_hash($entity);
1618
        if (isset($visited[$oid])) {
1619
            return; // Prevent infinite recursion
1620
        }
1621
        $visited[$oid] = $entity; // mark visited
1622
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
1623
            case self::STATE_MANAGED:
1624
                if ($this->isInIdentityMap($entity)) {
1625
                    $this->removeFromIdentityMap($entity);
1626
                }
1627
                unset(
1628
                    $this->entityInsertions[$oid],
1629
                    $this->entityUpdates[$oid],
1630
                    $this->entityDeletions[$oid],
1631
                    $this->entityIdentifiers[$oid],
1632
                    $this->entityStates[$oid],
1633
                    $this->originalEntityData[$oid]
1634
                );
1635
                break;
1636
            case self::STATE_NEW:
1637
            case self::STATE_DETACHED:
1638
                return;
1639
        }
1640
        if (!$noCascade) {
1641
            $this->cascadeDetach($entity, $visited);
1642
        }
1643
    }
1644
1645
    /**
1646
     * Executes a refresh operation on an entity.
1647
     *
1648
     * @param object $entity  The entity to refresh.
1649
     * @param array  $visited The already visited entities during cascades.
1650
     *
1651
     * @return void
1652
     *
1653
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
1654
     */
1655
    private function doRefresh($entity, array &$visited)
1656
    {
1657
        $oid = spl_object_hash($entity);
1658
        if (isset($visited[$oid])) {
1659
            return; // Prevent infinite recursion
1660
        }
1661
        $visited[$oid] = $entity; // mark visited
1662
        $class         = $this->em->getClassMetadata(get_class($entity));
0 ignored issues
show
Bug introduced by
The property em does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1663
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1664
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1665
        }
1666
        $this->getEntityPersister($class->name)->refresh(
1667
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1668
            $entity
1669
        );
1670
        $this->cascadeRefresh($entity, $visited);
1671
    }
1672
1673
    /**
1674
     * Cascades a refresh operation to associated entities.
1675
     *
1676
     * @param object $entity
1677
     * @param array  $visited
1678
     *
1679
     * @return void
1680
     */
1681 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...
1682
    {
1683
        $class               = $this->em->getClassMetadata(get_class($entity));
1684
        $associationMappings = array_filter(
1685
            $class->associationMappings,
1686
            function ($assoc) {
1687
                return $assoc['isCascadeRefresh'];
1688
            }
1689
        );
1690
        foreach ($associationMappings as $assoc) {
1691
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1692
            switch (true) {
1693
                case ($relatedEntities instanceof PersistentCollection):
0 ignored issues
show
Bug introduced by
The class Bankiru\Api\Doctrine\PersistentCollection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1694
                    // Unwrap so that foreach() does not initialize
1695
                    $relatedEntities = $relatedEntities->unwrap();
1696
                // break; is commented intentionally!
1697
                case ($relatedEntities instanceof Collection):
1698
                case (is_array($relatedEntities)):
1699
                    foreach ($relatedEntities as $relatedEntity) {
1700
                        $this->doRefresh($relatedEntity, $visited);
1701
                    }
1702
                    break;
1703
                case ($relatedEntities !== null):
1704
                    $this->doRefresh($relatedEntities, $visited);
1705
                    break;
1706
                default:
1707
                    // Do nothing
1708
            }
1709
        }
1710
    }
1711
1712
    /**
1713
     * Cascades a detach operation to associated entities.
1714
     *
1715
     * @param object $entity
1716
     * @param array  $visited
1717
     *
1718
     * @return void
1719
     */
1720 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...
1721
    {
1722
        $class               = $this->em->getClassMetadata(get_class($entity));
1723
        $associationMappings = array_filter(
1724
            $class->associationMappings,
1725
            function ($assoc) {
1726
                return $assoc['isCascadeDetach'];
1727
            }
1728
        );
1729
        foreach ($associationMappings as $assoc) {
1730
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1731
            switch (true) {
1732
                case ($relatedEntities instanceof PersistentCollection):
0 ignored issues
show
Bug introduced by
The class Bankiru\Api\Doctrine\PersistentCollection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1733
                    // Unwrap so that foreach() does not initialize
1734
                    $relatedEntities = $relatedEntities->unwrap();
1735
                // break; is commented intentionally!
1736
                case ($relatedEntities instanceof Collection):
1737
                case (is_array($relatedEntities)):
1738
                    foreach ($relatedEntities as $relatedEntity) {
1739
                        $this->doDetach($relatedEntity, $visited);
1740
                    }
1741
                    break;
1742
                case ($relatedEntities !== null):
1743
                    $this->doDetach($relatedEntities, $visited);
1744
                    break;
1745
                default:
1746
                    // Do nothing
1747
            }
1748
        }
1749
    }
1750
1751
    /**
1752
     * Cascades a merge operation to associated entities.
1753
     *
1754
     * @param object $entity
1755
     * @param object $managedCopy
1756
     * @param array  $visited
1757
     *
1758
     * @return void
1759
     */
1760
    private function cascadeMerge($entity, $managedCopy, array &$visited)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
1761
    {
1762
        $class               = $this->manager->getClassMetadata(get_class($entity));
1763
        $associationMappings = array_filter(
1764
            $class->getAssociationMappings(),
1765
            function ($assoc) {
1766
                return $assoc['isCascadeMerge'];
1767
            }
1768
        );
1769
        foreach ($associationMappings as $assoc) {
1770
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1771
            if ($relatedEntities instanceof Collection) {
1772
                if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) {
1773
                    continue;
1774
                }
1775
                if ($relatedEntities instanceof ApiCollection) {
1776
                    // Unwrap so that foreach() does not initialize
1777
                    $relatedEntities = $relatedEntities->unwrap();
1778
                }
1779
                foreach ($relatedEntities as $relatedEntity) {
1780
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
0 ignored issues
show
Bug introduced by
The method doMerge() 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...
1781
                }
1782
            } else {
1783
                if ($relatedEntities !== null) {
1784
                    $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
0 ignored issues
show
Bug introduced by
The method doMerge() 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...
1785
                }
1786
            }
1787
        }
1788
    }
1789
1790
    /**
1791
     * Cascades the delete operation to associated entities.
1792
     *
1793
     * @param object $entity
1794
     * @param array  $visited
1795
     *
1796
     * @return void
1797
     */
1798
    private function cascadeRemove($entity, array &$visited)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
1799
    {
1800
        $class               = $this->em->getClassMetadata(get_class($entity));
1801
        $associationMappings = array_filter(
1802
            $class->associationMappings,
1803
            function ($assoc) {
1804
                return $assoc['isCascadeRemove'];
1805
            }
1806
        );
1807
        $entitiesToCascade   = [];
1808
        foreach ($associationMappings as $assoc) {
1809
            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...
1810
                $entity->__load();
1811
            }
1812
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1813
            switch (true) {
1814
                case ($relatedEntities instanceof Collection):
1815
                case (is_array($relatedEntities)):
1816
                    // If its a PersistentCollection initialization is intended! No unwrap!
1817
                    foreach ($relatedEntities as $relatedEntity) {
1818
                        $entitiesToCascade[] = $relatedEntity;
1819
                    }
1820
                    break;
1821
                case ($relatedEntities !== null):
1822
                    $entitiesToCascade[] = $relatedEntities;
1823
                    break;
1824
                default:
1825
                    // Do nothing
1826
            }
1827
        }
1828
        foreach ($entitiesToCascade as $relatedEntity) {
1829
            $this->doRemove($relatedEntity, $visited);
0 ignored issues
show
Bug introduced by
The method doRemove() 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...
1830
        }
1831
    }
1832
1833
    /**
1834
     * Executes any extra updates that have been scheduled.
1835
     */
1836
    private function executeExtraUpdates()
1837
    {
1838
        foreach ($this->extraUpdates as $oid => $update) {
1839
            list ($entity, $changeset) = $update;
1840
            $this->entityChangeSets[$oid] = $changeset;
1841
            $this->getEntityPersister(get_class($entity))->update($entity);
1842
        }
1843
        $this->extraUpdates = [];
1844
    }
1845
1846
    private function executeDeletions(ApiMetadata $class)
1847
    {
1848
        $className = $class->getName();
1849
        $persister = $this->getEntityPersister($className);
1850
        foreach ($this->entityDeletions as $oid => $entity) {
1851
            if ($this->manager->getClassMetadata(get_class($entity))->name !== $className) {
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...
1852
                continue;
1853
            }
1854
            $persister->delete($entity);
1855
            unset(
1856
                $this->entityDeletions[$oid],
1857
                $this->entityIdentifiers[$oid],
1858
                $this->originalEntityData[$oid],
1859
                $this->entityStates[$oid]
1860
            );
1861
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1862
            // is obtained by a new entity because the old one went out of scope.
1863
            //$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...
1864
            //            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...
1865
            //                $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...
1866
            //            }
1867
        }
1868
    }
1869
1870
    /**
1871
     * @param object $entity
1872
     * @param object $managedCopy
1873
     *
1874
     * @throws ORMException
1875
     * @throws OptimisticLockException
1876
     * @throws TransactionRequiredException
1877
     */
1878
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
1879
    {
1880
        $class = $this->em->getClassMetadata(get_class($entity));
1881
        foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
0 ignored issues
show
Bug introduced by
The property reflectionPropertiesGetter does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1882
            $name = $prop->name;
1883
            $prop->setAccessible(true);
1884
            if (!isset($class->associationMappings[$name])) {
1885
                if (!$class->isIdentifier($name)) {
1886
                    $prop->setValue($managedCopy, $prop->getValue($entity));
1887
                }
1888
            } else {
1889
                $assoc2 = $class->associationMappings[$name];
1890
                if ($assoc2['type'] & ClassMetadata::TO_ONE) {
1891
                    $other = $prop->getValue($entity);
1892
                    if ($other === null) {
1893
                        $prop->setValue($managedCopy, null);
1894
                    } else {
1895
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
1896
                            // do not merge fields marked lazy that have not been fetched.
1897
                            continue;
1898
                        }
1899
                        if (!$assoc2['isCascadeMerge']) {
1900
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
1901
                                $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
1902
                                $relatedId   = $targetClass->getIdentifierValues($other);
1903
                                if ($targetClass->subClasses) {
1904
                                    $other = $this->em->find($targetClass->name, $relatedId);
1905
                                } else {
1906
                                    $other = $this->em->getProxyFactory()->getProxy(
1907
                                        $assoc2['targetEntity'],
1908
                                        $relatedId
1909
                                    );
1910
                                    $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...
1911
                                }
1912
                            }
1913
                            $prop->setValue($managedCopy, $other);
1914
                        }
1915
                    }
1916
                } else {
1917
                    $mergeCol = $prop->getValue($entity);
1918
                    if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) {
0 ignored issues
show
Bug introduced by
The class Bankiru\Api\Doctrine\PersistentCollection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1919
                        // do not merge fields marked lazy that have not been fetched.
1920
                        // keep the lazy persistent collection of the managed copy.
1921
                        continue;
1922
                    }
1923
                    $managedCol = $prop->getValue($managedCopy);
1924
                    if (!$managedCol) {
1925
                        $managedCol = new PersistentCollection(
1926
                            $this->em,
1927
                            $this->em->getClassMetadata($assoc2['targetEntity']),
1928
                            new ArrayCollection
1929
                        );
1930
                        $managedCol->setOwner($managedCopy, $assoc2);
1931
                        $prop->setValue($managedCopy, $managedCol);
1932
                    }
1933
                    if ($assoc2['isCascadeMerge']) {
1934
                        $managedCol->initialize();
1935
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
1936
                        if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
1937
                            $managedCol->unwrap()->clear();
1938
                            $managedCol->setDirty(true);
1939
                            if ($assoc2['isOwningSide']
1940
                                && $assoc2['type'] == ClassMetadata::MANY_TO_MANY
1941
                                && $class->isChangeTrackingNotify()
1942
                            ) {
1943
                                $this->scheduleForDirtyCheck($managedCopy);
1944
                            }
1945
                        }
1946
                    }
1947
                }
1948
            }
1949
            if ($class->isChangeTrackingNotify()) {
1950
                // Just treat all properties as changed, there is no other choice.
1951
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1952
            }
1953
        }
1954
    }
1955
1956
}
1957