Passed
Pull Request — master (#14)
by Pavel
05:12
created

UnitOfWork::createEntityCache()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

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

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

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

Loading history...
502
            if (isset($this->entityChangeSets[$oid])) {
503
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
504
            } else {
505
                if (!isset($this->entityInsertions[$oid])) {
506
                    $this->entityChangeSets[$oid] = $changeSet;
507
                    $this->entityUpdates[$oid]    = $entity;
508
                }
509
            }
510
            $this->originalEntityData[$oid] = (object)$actualData;
511
        }
512 2
    }
513
514
    /**
515
     * Schedules an entity for insertion into the database.
516
     * If the entity already has an identifier, it will be added to the identity map.
517
     *
518
     * @param object $entity The entity to schedule for insertion.
519
     *
520
     * @return void
521
     *
522
     * @throws \InvalidArgumentException
523
     */
524 6
    public function scheduleForInsert($entity)
525
    {
526 6
        $oid = spl_object_hash($entity);
527 6
        if (isset($this->entityUpdates[$oid])) {
528
            throw new \InvalidArgumentException('Dirty entity cannot be scheduled for insertion');
529
        }
530 6
        if (isset($this->entityDeletions[$oid])) {
531
            throw new \InvalidArgumentException('Removed entity scheduled for insertion');
532
        }
533 6
        if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) {
534
            throw new \InvalidArgumentException('Managed entity scheduled for insertion');
535
        }
536 6
        if (isset($this->entityInsertions[$oid])) {
537
            throw new \InvalidArgumentException('Entity scheduled for insertion twice');
538
        }
539 6
        $this->entityInsertions[$oid] = $entity;
540 6
        if (isset($this->entityIdentifiers[$oid])) {
541
            $this->addToIdentityMap($entity);
542
        }
543 6
        if ($entity instanceof NotifyPropertyChanged) {
544
            $entity->addPropertyChangedListener($this);
545
        }
546 6
    }
547
548
    /**
549
     * Checks whether an entity is scheduled for insertion.
550
     *
551
     * @param object $entity
552
     *
553
     * @return boolean
554
     */
555 1
    public function isScheduledForInsert($entity)
556
    {
557 1
        return isset($this->entityInsertions[spl_object_hash($entity)]);
558
    }
559
560
    /**
561
     * Schedules an entity for being updated.
562
     *
563
     * @param object $entity The entity to schedule for being updated.
564
     *
565
     * @return void
566
     *
567
     * @throws \InvalidArgumentException
568
     */
569
    public function scheduleForUpdate($entity)
570
    {
571
        $oid = spl_object_hash($entity);
572
        if (!isset($this->entityIdentifiers[$oid])) {
573
            throw new \InvalidArgumentException('Entity has no identity');
574
        }
575
        if (isset($this->entityDeletions[$oid])) {
576
            throw new \InvalidArgumentException('Entity is removed');
577
        }
578
        if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) {
579
            $this->entityUpdates[$oid] = $entity;
580
        }
581
    }
582
583
    /**
584
     * Checks whether an entity is registered as dirty in the unit of work.
585
     * Note: Is not very useful currently as dirty entities are only registered
586
     * at commit time.
587
     *
588
     * @param object $entity
589
     *
590
     * @return boolean
591
     */
592
    public function isScheduledForUpdate($entity)
593
    {
594
        return isset($this->entityUpdates[spl_object_hash($entity)]);
595
    }
596
597
    /**
598
     * Checks whether an entity is registered to be checked in the unit of work.
599
     *
600
     * @param object $entity
601
     *
602
     * @return boolean
603
     */
604
    public function isScheduledForDirtyCheck($entity)
605
    {
606
        $rootEntityName = $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
607
608
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
609
    }
610
611
    /**
612
     * INTERNAL:
613
     * Schedules an entity for deletion.
614
     *
615
     * @param object $entity
616
     *
617
     * @return void
618
     */
619
    public function scheduleForDelete($entity)
620
    {
621
        $oid = spl_object_hash($entity);
622
        if (isset($this->entityInsertions[$oid])) {
623
            if ($this->isInIdentityMap($entity)) {
624
                $this->removeFromIdentityMap($entity);
625
            }
626
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
627
628
            return; // entity has not been persisted yet, so nothing more to do.
629
        }
630
        if (!$this->isInIdentityMap($entity)) {
631
            return;
632
        }
633
        $this->removeFromIdentityMap($entity);
634
        unset($this->entityUpdates[$oid]);
635
        if (!isset($this->entityDeletions[$oid])) {
636
            $this->entityDeletions[$oid] = $entity;
637
            $this->entityStates[$oid]    = self::STATE_REMOVED;
638
        }
639
    }
640
641
    /**
642
     * Checks whether an entity is registered as removed/deleted with the unit
643
     * of work.
644
     *
645
     * @param object $entity
646
     *
647
     * @return boolean
648
     */
649
    public function isScheduledForDelete($entity)
650
    {
651
        return isset($this->entityDeletions[spl_object_hash($entity)]);
652
    }
653
654
    /**
655
     * Checks whether an entity is scheduled for insertion, update or deletion.
656
     *
657
     * @param object $entity
658
     *
659
     * @return boolean
660
     */
661
    public function isEntityScheduled($entity)
662
    {
663
        $oid = spl_object_hash($entity);
664
665
        return isset($this->entityInsertions[$oid])
666
               || isset($this->entityUpdates[$oid])
667
               || isset($this->entityDeletions[$oid]);
668
    }
669
670
    /**
671
     * INTERNAL:
672
     * Removes an entity from the identity map. This effectively detaches the
673
     * entity from the persistence management of Doctrine.
674
     *
675
     * @ignore
676
     *
677
     * @param object $entity
678
     *
679
     * @return boolean
680
     *
681
     * @throws \InvalidArgumentException
682
     */
683
    public function removeFromIdentityMap($entity)
684
    {
685
        $oid           = spl_object_hash($entity);
686
        $classMetadata = $this->manager->getClassMetadata(get_class($entity));
687
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
688
        if ($idHash === '') {
689
            throw new \InvalidArgumentException('Entity has no identity');
690
        }
691
        $className = $classMetadata->getRootEntityName();
692
        if (isset($this->identityMap[$className][$idHash])) {
693
            unset($this->identityMap[$className][$idHash]);
694
            unset($this->readOnlyObjects[$oid]);
695
696
            //$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...
697
            return true;
698
        }
699
700
        return false;
701
    }
702
703
    /**
704
     * Commits the UnitOfWork, executing all operations that have been postponed
705
     * up to this point. The state of all managed entities will be synchronized with
706
     * the database.
707
     *
708
     * The operations are executed in the following order:
709
     *
710
     * 1) All entity insertions
711
     * 2) All entity updates
712
     * 3) All collection deletions
713
     * 4) All collection updates
714
     * 5) All entity deletions
715
     *
716
     * @param null|object|array $entity
717
     *
718
     * @return void
719
     *
720
     * @throws \Exception
721
     */
722 6
    public function commit($entity = null)
723
    {
724
        // Compute changes done since last commit.
725 6
        if ($entity === null) {
726 6
            $this->computeChangeSets();
727 6
        } elseif (is_object($entity)) {
728
            $this->computeSingleEntityChangeSet($entity);
729
        } elseif (is_array($entity)) {
730
            foreach ((array)$entity as $object) {
731
                $this->computeSingleEntityChangeSet($object);
732
            }
733
        }
734 6
        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...
735 2
              $this->entityDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
740 6
        ) {
741 1
            return; // Nothing to do.
742
        }
743 6
        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...
744
            foreach ($this->orphanRemovals as $orphan) {
745
                $this->remove($orphan);
746
            }
747
        }
748
        // Now we need a commit order to maintain referential integrity
749 6
        $commitOrder = $this->getCommitOrder();
750
751
        // Collection deletions (deletions of complete collections)
752
        // 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...
753
        //       //fixme: collection mutations
754
        //       $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...
755
        // }
756 6
        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...
757 6
            foreach ($commitOrder as $class) {
758 6
                $this->executeInserts($class);
759 6
            }
760 6
        }
761 6
        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...
762 2
            foreach ($commitOrder as $class) {
763 2
                $this->executeUpdates($class);
764 2
            }
765 2
        }
766
        // Extra updates that were requested by persisters.
767 6
        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...
768
            $this->executeExtraUpdates();
769
        }
770
        // Collection updates (deleteRows, updateRows, insertRows)
771 6
        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...
772
            //fixme: decide what to do with collection mutation if API does not support this
773
            //$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...
774 6
        }
775
        // Entity deletions come last and need to be in reverse commit order
776 6
        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...
777
            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...
778
                $this->executeDeletions($commitOrder[$i]);
779
            }
780
        }
781
782
        // Take new snapshots from visited collections
783 6
        foreach ($this->visitedCollections as $coll) {
784 2
            $coll->takeSnapshot();
785 6
        }
786
787
        // Clear up
788 6
        $this->entityInsertions =
789 6
        $this->entityUpdates =
790 6
        $this->entityDeletions =
791 6
        $this->extraUpdates =
792 6
        $this->entityChangeSets =
793 6
        $this->collectionUpdates =
794 6
        $this->collectionDeletions =
795 6
        $this->visitedCollections =
796 6
        $this->scheduledForSynchronization =
797 6
        $this->orphanRemovals = [];
798 6
    }
799
800
    /**
801
     * Gets the changeset for an entity.
802
     *
803
     * @param object $entity
804
     *
805
     * @return array
806
     */
807 2
    public function & getEntityChangeSet($entity)
808
    {
809 2
        $oid  = spl_object_hash($entity);
810 2
        $data = [];
811 2
        if (!isset($this->entityChangeSets[$oid])) {
812
            return $data;
813
        }
814
815 2
        return $this->entityChangeSets[$oid];
816
    }
817
818
    /**
819
     * Computes the changes that happened to a single entity.
820
     *
821
     * Modifies/populates the following properties:
822
     *
823
     * {@link _originalEntityData}
824
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
825
     * then it was not fetched from the database and therefore we have no original
826
     * entity data yet. All of the current entity data is stored as the original entity data.
827
     *
828
     * {@link _entityChangeSets}
829
     * The changes detected on all properties of the entity are stored there.
830
     * A change is a tuple array where the first entry is the old value and the second
831
     * entry is the new value of the property. Changesets are used by persisters
832
     * to INSERT/UPDATE the persistent entity state.
833
     *
834
     * {@link _entityUpdates}
835
     * If the entity is already fully MANAGED (has been fetched from the database before)
836
     * and any changes to its properties are detected, then a reference to the entity is stored
837
     * there to mark it for an update.
838
     *
839
     * {@link _collectionDeletions}
840
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
841
     * then this collection is marked for deletion.
842
     *
843
     * @ignore
844
     *
845
     * @internal Don't call from the outside.
846
     *
847
     * @param ApiMetadata $class  The class descriptor of the entity.
848
     * @param object      $entity The entity for which to compute the changes.
849
     *
850
     * @return void
851
     */
852 6
    public function computeChangeSet(ApiMetadata $class, $entity)
853
    {
854 6
        $oid = spl_object_hash($entity);
855 6
        if (isset($this->readOnlyObjects[$oid])) {
856
            return;
857
        }
858
859 6
        $actualData = [];
860 6
        foreach ($class->getReflectionProperties() as $name => $refProp) {
861 6
            $value = $refProp->getValue($entity);
862 6
            if (null !== $value && $class->isCollectionValuedAssociation($name)) {
863 2
                if ($value instanceof ApiCollection) {
864 1
                    if ($value->getOwner() === $entity) {
865 1
                        continue;
866
                    }
867
                    $value = new ArrayCollection($value->getValues());
868
                }
869
                // If $value is not a Collection then use an ArrayCollection.
870 2
                if (!$value instanceof Collection) {
871
                    $value = new ArrayCollection($value);
872
                }
873 2
                $assoc = $class->getAssociationMapping($name);
874
                // Inject PersistentCollection
875 2
                $value = new ApiCollection(
876 2
                    $this->manager,
877 2
                    $this->manager->getClassMetadata($assoc['target']),
878
                    $value
879 2
                );
880 2
                $value->setOwner($entity, $assoc);
881 2
                $value->setDirty(!$value->isEmpty());
882 2
                $class->getReflectionProperty($name)->setValue($entity, $value);
883 2
                $actualData[$name] = $value;
884 2
                continue;
885
            }
886 6
            if (!$class->isIdentifier($name)) {
887 6
                $actualData[$name] = $value;
888 6
            }
889 6
        }
890 6
        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 6
            $this->originalEntityData[$oid] = (object)$actualData;
894 6
            $changeSet                      = [];
895 6
            foreach ($actualData as $propName => $actualValue) {
896 6 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 6
                    $changeSet[$propName] = [null, $actualValue];
898 6
                    continue;
899
                }
900 5
                $assoc = $class->getAssociationMapping($propName);
901 5
                if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) {
902 5
                    $changeSet[$propName] = [null, $actualValue];
903 5
                }
904 6
            }
905 6
            $this->entityChangeSets[$oid] = $changeSet;
906 6
        } else {
907
908
            // Entity is "fully" MANAGED: it was already fully persisted before
909
            // and we have a copy of the original data
910 3
            $originalData           = $this->originalEntityData[$oid];
911 3
            $isChangeTrackingNotify = false;
912 3
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
913 3
                ? $this->entityChangeSets[$oid]
914 3
                : [];
915
916 3
            foreach ($actualData as $propName => $actualValue) {
917
                // skip field, its a partially omitted one!
918 3
                if (!property_exists($originalData, $propName)) {
919
                    continue;
920
                }
921 3
                $orgValue = $originalData->$propName;
922
                // skip if value haven't changed
923 3
                if ($orgValue === $actualValue) {
924
925 3
                    continue;
926
                }
927
                // if regular field
928 2 View Code Duplication
                if (!$class->hasAssociation($propName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
1037 3
                    !isset($this->entityDeletions[$oid]) &&
1038 3
                    isset($this->entityStates[$oid])
1039 3
                ) {
1040 3
                    $this->computeChangeSet($class, $entity);
1041 3
                }
1042 3
            }
1043 6
        }
1044 6
    }
1045
1046
    /**
1047
     * INTERNAL:
1048
     * Schedules an orphaned entity for removal. The remove() operation will be
1049
     * invoked on that entity at the beginning of the next commit of this
1050
     * UnitOfWork.
1051
     *
1052
     * @ignore
1053
     *
1054
     * @param object $entity
1055
     *
1056
     * @return void
1057
     */
1058
    public function scheduleOrphanRemoval($entity)
1059
    {
1060
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
1061
    }
1062
1063 2
    public function loadCollection(ApiCollection $collection)
1064
    {
1065 2
        $assoc     = $collection->getMapping();
1066 2
        $persister = $this->getEntityPersister($assoc['target']);
1067 2
        switch ($assoc['type']) {
1068 2
            case ApiMetadata::ONE_TO_MANY:
1069 2
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
1070 2
                break;
1071 2
        }
1072 2
        $collection->setInitialized(true);
1073 2
    }
1074
1075
    public function getCollectionPersister($association)
1076
    {
1077
        $role = isset($association['cache'])
1078
            ? $association['sourceEntity'] . '::' . $association['fieldName']
1079
            : $association['type'];
1080
        if (array_key_exists($role, $this->collectionPersisters)) {
1081
            return $this->collectionPersisters[$role];
1082
        }
1083
        $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...
1084
1085
        return $this->collectionPersisters[$role];
1086
    }
1087
1088
    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...
1089
    {
1090
    }
1091
1092 2
    public function cancelOrphanRemoval($value)
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

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

Loading history...
1093
    {
1094 2
    }
1095
1096
    /**
1097
     * INTERNAL:
1098
     * Sets a property value of the original data array of an entity.
1099
     *
1100
     * @ignore
1101
     *
1102
     * @param string $oid
1103
     * @param string $property
1104
     * @param mixed  $value
1105
     *
1106
     * @return void
1107
     */
1108 10
    public function setOriginalEntityProperty($oid, $property, $value)
1109
    {
1110 10
        if (!array_key_exists($oid, $this->originalEntityData)) {
1111 10
            $this->originalEntityData[$oid] = new \stdClass();
1112 10
        }
1113
1114 10
        $this->originalEntityData[$oid]->$property = $value;
1115 10
    }
1116
1117
    public function scheduleExtraUpdate($entity, $changeset)
1118
    {
1119
        $oid         = spl_object_hash($entity);
1120
        $extraUpdate = [$entity, $changeset];
1121
        if (isset($this->extraUpdates[$oid])) {
1122
            list(, $changeset2) = $this->extraUpdates[$oid];
1123
            $extraUpdate = [$entity, $changeset + $changeset2];
1124
        }
1125
        $this->extraUpdates[$oid] = $extraUpdate;
1126
    }
1127
1128
    /**
1129
     * Refreshes the state of the given entity from the database, overwriting
1130
     * any local, unpersisted changes.
1131
     *
1132
     * @param object $entity The entity to refresh.
1133
     *
1134
     * @return void
1135
     *
1136
     * @throws InvalidArgumentException If the entity is not MANAGED.
1137
     */
1138
    public function refresh($entity)
1139
    {
1140
        $visited = [];
1141
        $this->doRefresh($entity, $visited);
1142
    }
1143
1144
    /**
1145
     * Clears the UnitOfWork.
1146
     *
1147
     * @param string|null $entityName if given, only entities of this type will get detached.
1148
     *
1149
     * @return void
1150
     */
1151
    public function clear($entityName = null)
1152
    {
1153
        if ($entityName === null) {
1154
            $this->identityMap =
1155
            $this->entityIdentifiers =
1156
            $this->originalEntityData =
1157
            $this->entityChangeSets =
1158
            $this->entityStates =
1159
            $this->scheduledForSynchronization =
1160
            $this->entityInsertions =
1161
            $this->entityUpdates =
1162
            $this->entityDeletions =
1163
            $this->collectionDeletions =
1164
            $this->collectionUpdates =
1165
            $this->extraUpdates =
1166
            $this->readOnlyObjects =
1167
            $this->visitedCollections =
1168
            $this->orphanRemovals = [];
1169
        } else {
1170
            $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...
1171
            $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...
1172
        }
1173
    }
1174
1175
    /**
1176
     * @param PersistentCollection $coll
1177
     *
1178
     * @return bool
1179
     */
1180
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
1181
    {
1182
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
1183
    }
1184
1185
    /**
1186
     * Schedules an entity for dirty-checking at commit-time.
1187
     *
1188
     * @param object $entity The entity to schedule for dirty-checking.
1189
     *
1190
     * @return void
1191
     *
1192
     * @todo Rename: scheduleForSynchronization
1193
     */
1194
    public function scheduleForDirtyCheck($entity)
1195
    {
1196
        $rootClassName                                                               =
1197
            $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
1198
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
1199
    }
1200
1201
    /**
1202
     * Deletes an entity as part of the current unit of work.
1203
     *
1204
     * @param object $entity The entity to remove.
1205
     *
1206
     * @return void
1207
     */
1208
    public function remove($entity)
1209
    {
1210
        $visited = [];
1211
        $this->doRemove($entity, $visited);
1212
    }
1213
1214
    /**
1215
     * Merges the state of the given detached entity into this UnitOfWork.
1216
     *
1217
     * @param object $entity
1218
     *
1219
     * @return object The managed copy of the entity.
1220
     */
1221
    public function merge($entity)
1222
    {
1223
        $visited = [];
1224
1225
        return $this->doMerge($entity, $visited);
1226
    }
1227
1228
    /**
1229
     * Detaches an entity from the persistence management. It's persistence will
1230
     * no longer be managed by Doctrine.
1231
     *
1232
     * @param object $entity The entity to detach.
1233
     *
1234
     * @return void
1235
     */
1236
    public function detach($entity)
1237
    {
1238
        $visited = [];
1239
        $this->doDetach($entity, $visited);
1240
    }
1241
1242
    /**
1243
     * Resolve metadata against source data and root class
1244
     *
1245
     * @param \stdClass $data
1246
     * @param string    $class
1247
     *
1248
     * @return ApiMetadata
1249
     * @throws MappingException
1250
     */
1251 15
    private function resolveSourceMetadataForClass(\stdClass $data, $class)
1252
    {
1253 15
        $metadata           = $this->manager->getClassMetadata($class);
1254 15
        $discriminatorValue = $metadata->getDiscriminatorValue();
1255 15
        if ($metadata->getDiscriminatorField()) {
1256 2
            $property = $metadata->getDiscriminatorField()['fieldName'];
1257 2
            if (isset($data->$property)) {
1258 2
                $discriminatorValue = $data->$property;
1259 2
            }
1260 2
        }
1261
1262 15
        $map = $metadata->getDiscriminatorMap();
1263
1264 15
        if (!array_key_exists($discriminatorValue, $map)) {
1265
            throw MappingException::unknownDiscriminatorValue($discriminatorValue, $class);
1266
        }
1267
1268 15
        $realClass = $map[$discriminatorValue];
1269
1270 15
        return $this->manager->getClassMetadata($realClass);
1271
    }
1272
1273
    /**
1274
     * Helper method to show an object as string.
1275
     *
1276
     * @param object $obj
1277
     *
1278
     * @return string
1279
     */
1280
    private static function objToStr($obj)
1281
    {
1282
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
1283
    }
1284
1285
    /**
1286
     * @param ApiMetadata $class
1287
     *
1288
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
1289
     */
1290 14
    private function newInstance(ApiMetadata $class)
1291
    {
1292 14
        $entity = $class->newInstance();
1293
1294 14
        if ($entity instanceof ObjectManagerAware) {
1295
            $entity->injectObjectManager($this->manager, $class);
1296
        }
1297
1298 14
        return $entity;
1299
    }
1300
1301
    /**
1302
     * @param ApiMetadata $classMetadata
1303
     *
1304
     * @return EntityDataCacheInterface
1305
     */
1306 22
    private function createEntityCache(ApiMetadata $classMetadata)
1307
    {
1308 22
        $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName());
1309 22
        $cache         = new VoidEntityCache($classMetadata);
1310 22
        if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) {
1311
            $cache =
1312 1
                new LoggingCache(
1313 1
                    new ApiEntityCache(
1314 1
                        $this->manager->getConfiguration()->getApiCache(),
1315 1
                        $classMetadata,
1316
                        $configuration
1317 1
                    ),
1318 1
                    $this->manager->getConfiguration()->getApiCacheLogger()
1319 1
                );
1320
1321 1
            return $cache;
1322
        }
1323
1324 21
        return $cache;
1325
    }
1326
1327
    /**
1328
     * @param ApiMetadata $classMetadata
1329
     *
1330
     * @return CrudsApiInterface
1331
     */
1332 22
    private function createApi(ApiMetadata $classMetadata)
1333
    {
1334 22
        $client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName());
1335
1336 22
        $api = $this->manager
1337 22
            ->getConfiguration()
1338 22
            ->getFactoryRegistry()
1339 22
            ->create(
1340 22
                $classMetadata->getApiFactory(),
1341 22
                $client,
1342
                $classMetadata
1343 22
            );
1344
1345 22
        return $api;
1346
    }
1347
1348 6
    private function doPersist($entity, $visited)
1349
    {
1350 6
        $oid = spl_object_hash($entity);
1351 6
        if (isset($visited[$oid])) {
1352
            return; // Prevent infinite recursion
1353
        }
1354 6
        $visited[$oid] = $entity; // Mark visited
1355 6
        $class         = $this->manager->getClassMetadata(get_class($entity));
1356
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1357
        // If we would detect DETACHED here we would throw an exception anyway with the same
1358
        // consequences (not recoverable/programming error), so just assuming NEW here
1359
        // lets us avoid some database lookups for entities with natural identifiers.
1360 6
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1361
        switch ($entityState) {
1362 6
            case self::STATE_MANAGED:
1363
                $this->scheduleForDirtyCheck($entity);
1364
                break;
1365 6
            case self::STATE_NEW:
1366 6
                $this->persistNew($class, $entity);
1367 6
                break;
1368
            case self::STATE_REMOVED:
1369
                // Entity becomes managed again
1370
                unset($this->entityDeletions[$oid]);
1371
                $this->addToIdentityMap($entity);
1372
                $this->entityStates[$oid] = self::STATE_MANAGED;
1373
                break;
1374
            case self::STATE_DETACHED:
1375
                // Can actually not happen right now since we assume STATE_NEW.
1376
                throw new \InvalidArgumentException('Detached entity cannot be persisted');
1377
            default:
1378
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1379
        }
1380 6
        $this->cascadePersist($entity, $visited);
1381 6
    }
1382
1383
    /**
1384
     * Cascades the save operation to associated entities.
1385
     *
1386
     * @param object $entity
1387
     * @param array  $visited
1388
     *
1389
     * @return void
1390
     * @throws \InvalidArgumentException
1391
     * @throws MappingException
1392
     */
1393 6
    private function cascadePersist($entity, array &$visited)
1394
    {
1395 6
        $class               = $this->manager->getClassMetadata(get_class($entity));
1396 6
        $associationMappings = [];
1397 6
        foreach ($class->getAssociationNames() as $name) {
1398 5
            $assoc = $class->getAssociationMapping($name);
1399 5
            if ($assoc['isCascadePersist']) {
1400
                $associationMappings[$name] = $assoc;
1401
            }
1402 6
        }
1403 6
        foreach ($associationMappings as $assoc) {
1404
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1405
            switch (true) {
1406
                case ($relatedEntities instanceof ApiCollection):
1407
                    // Unwrap so that foreach() does not initialize
1408
                    $relatedEntities = $relatedEntities->unwrap();
1409
                // break; is commented intentionally!
1410
                case ($relatedEntities instanceof Collection):
1411
                case (is_array($relatedEntities)):
1412
                    if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) {
1413
                        throw new \InvalidArgumentException('Invalid association for cascade');
1414
                    }
1415
                    foreach ($relatedEntities as $relatedEntity) {
1416
                        $this->doPersist($relatedEntity, $visited);
1417
                    }
1418
                    break;
1419
                case ($relatedEntities !== null):
1420
                    if (!$relatedEntities instanceof $assoc['target']) {
1421
                        throw new \InvalidArgumentException('Invalid association for cascade');
1422
                    }
1423
                    $this->doPersist($relatedEntities, $visited);
1424
                    break;
1425
                default:
1426
                    // Do nothing
1427
            }
1428 6
        }
1429 6
    }
1430
1431
    /**
1432
     * @param ApiMetadata $class
1433
     * @param object      $entity
1434
     *
1435
     * @return void
1436
     */
1437 6
    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...
1438
    {
1439 6
        $oid = spl_object_hash($entity);
1440
        //        $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...
1441
        //        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...
1442
        //            $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...
1443
        //        }
1444
        //        $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...
1445
        //        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...
1446
        //            $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...
1447
        //            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...
1448
        //                $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...
1449
        //                $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...
1450
        //            }
1451
        //            $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...
1452
        //        }
1453 6
        $this->entityStates[$oid] = self::STATE_MANAGED;
1454 6
        $this->scheduleForInsert($entity);
1455 6
    }
1456
1457
    /**
1458
     * Gets the commit order.
1459
     *
1460
     * @param array|null $entityChangeSet
1461
     *
1462
     * @return array
1463
     */
1464 6
    private function getCommitOrder(array $entityChangeSet = null)
1465
    {
1466 6
        if ($entityChangeSet === null) {
1467 6
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1468 6
        }
1469 6
        $calc = $this->getCommitOrderCalculator();
1470
        // See if there are any new classes in the changeset, that are not in the
1471
        // commit order graph yet (don't have a node).
1472
        // We have to inspect changeSet to be able to correctly build dependencies.
1473
        // It is not possible to use IdentityMap here because post inserted ids
1474
        // are not yet available.
1475
        /** @var ApiMetadata[] $newNodes */
1476 6
        $newNodes = [];
1477 6
        foreach ((array)$entityChangeSet as $entity) {
1478 6
            $class = $this->manager->getClassMetadata(get_class($entity));
1479 6
            if ($calc->hasNode($class->getName())) {
1480
                continue;
1481
            }
1482 6
            $calc->addNode($class->getName(), $class);
1483 6
            $newNodes[] = $class;
1484 6
        }
1485
        // Calculate dependencies for new nodes
1486 6
        while ($class = array_pop($newNodes)) {
1487 6
            foreach ($class->getAssociationMappings() as $assoc) {
1488 5
                if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) {
1489 5
                    continue;
1490
                }
1491 5
                $targetClass = $this->manager->getClassMetadata($assoc['target']);
1492 5
                if (!$calc->hasNode($targetClass->getName())) {
1493 3
                    $calc->addNode($targetClass->getName(), $targetClass);
1494 3
                    $newNodes[] = $targetClass;
1495 3
                }
1496 5
                $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable']));
1497
                // If the target class has mapped subclasses, these share the same dependency.
1498 5
                if (!$targetClass->getSubclasses()) {
1499
                    continue;
1500
                }
1501 5
                foreach ($targetClass->getSubclasses() as $subClassName) {
1502 5
                    $targetSubClass = $this->manager->getClassMetadata($subClassName);
1503 5
                    if (!$calc->hasNode($subClassName)) {
1504 5
                        $calc->addNode($targetSubClass->name, $targetSubClass);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Bankiru\Api\Doctrine\Mapping\ApiMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1508 5
                }
1509 6
            }
1510 6
        }
1511
1512 6
        return $calc->sort();
1513
    }
1514
1515 6
    private function getCommitOrderCalculator()
1516
    {
1517 6
        return new Utility\CommitOrderCalculator();
1518
    }
1519
1520
    /**
1521
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
1522
     *
1523
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
1524
     * 2. Read Only entities are skipped.
1525
     * 3. Proxies are skipped.
1526
     * 4. Only if entity is properly managed.
1527
     *
1528
     * @param object $entity
1529
     *
1530
     * @return void
1531
     *
1532
     * @throws \InvalidArgumentException
1533
     */
1534
    private function computeSingleEntityChangeSet($entity)
1535
    {
1536
        $state = $this->getEntityState($entity);
1537
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
1538
            throw new \InvalidArgumentException(
1539
                "Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)
1540
            );
1541
        }
1542
        $class = $this->manager->getClassMetadata(get_class($entity));
1543
        // Compute changes for INSERTed entities first. This must always happen even in this case.
1544
        $this->computeScheduleInsertsChangeSets();
1545
        if ($class->isReadOnly()) {
1546
            return;
1547
        }
1548
        // Ignore uninitialized proxy objects
1549
        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...
1550
            return;
1551
        }
1552
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1553
        $oid = spl_object_hash($entity);
1554 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...
1555
            !isset($this->entityDeletions[$oid]) &&
1556
            isset($this->entityStates[$oid])
1557
        ) {
1558
            $this->computeChangeSet($class, $entity);
1559
        }
1560
    }
1561
1562
    /**
1563
     * Computes the changesets of all entities scheduled for insertion.
1564
     *
1565
     * @return void
1566
     */
1567 6
    private function computeScheduleInsertsChangeSets()
1568
    {
1569 6
        foreach ($this->entityInsertions as $entity) {
1570 6
            $class = $this->manager->getClassMetadata(get_class($entity));
1571 6
            $this->computeChangeSet($class, $entity);
1572 6
        }
1573 6
    }
1574
1575
    /**
1576
     * Computes the changes of an association.
1577
     *
1578
     * @param array $assoc The association mapping.
1579
     * @param mixed $value The value of the association.
1580
     *
1581
     * @throws \InvalidArgumentException
1582
     * @throws \UnexpectedValueException
1583
     *
1584
     * @return void
1585
     */
1586 2
    private function computeAssociationChanges($assoc, $value)
1587
    {
1588 2
        if ($value instanceof Proxy && !$value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1589
            return;
1590
        }
1591 2
        if ($value instanceof ApiCollection && $value->isDirty()) {
1592 2
            $coid                            = spl_object_hash($value);
1593 2
            $this->collectionUpdates[$coid]  = $value;
1594 2
            $this->visitedCollections[$coid] = $value;
1595 2
        }
1596
        // Look through the entities, and in any of their associations,
1597
        // 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...
1598
        // Unwrap. Uninitialized collections will simply be empty.
1599 2
        $unwrappedValue  = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap();
1600 2
        $targetClass     = $this->manager->getClassMetadata($assoc['target']);
1601 2
        $targetClassName = $targetClass->getName();
1602 2
        foreach ($unwrappedValue as $key => $entry) {
1603 2
            if (!($entry instanceof $targetClassName)) {
1604
                throw new \InvalidArgumentException('Invalid association');
1605
            }
1606 2
            $state = $this->getEntityState($entry, self::STATE_NEW);
1607 2
            if (!($entry instanceof $assoc['target'])) {
1608
                throw new \UnexpectedValueException('Unexpected association');
1609
            }
1610
            switch ($state) {
1611 2
                case self::STATE_NEW:
1612
                    if (!$assoc['isCascadePersist']) {
1613
                        throw new \InvalidArgumentException('New entity through relationship');
1614
                    }
1615
                    $this->persistNew($targetClass, $entry);
1616
                    $this->computeChangeSet($targetClass, $entry);
1617
                    break;
1618 2
                case self::STATE_REMOVED:
1619
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1620
                    // and remove the element from Collection.
1621
                    if ($assoc['type'] & ApiMetadata::TO_MANY) {
1622
                        unset($value[$key]);
1623
                    }
1624
                    break;
1625 2
                case self::STATE_DETACHED:
1626
                    // Can actually not happen right now as we assume STATE_NEW,
1627
                    // so the exception will be raised from the DBAL layer (constraint violation).
1628
                    throw new \InvalidArgumentException('Detached entity through relationship');
1629
                    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...
1630 2
                default:
1631
                    // MANAGED associated entities are already taken into account
1632
                    // during changeset calculation anyway, since they are in the identity map.
1633 2
            }
1634 2
        }
1635 2
    }
1636
1637 6
    private function executeInserts(ApiMetadata $class)
1638
    {
1639 6
        $className = $class->getName();
1640 6
        $persister = $this->getEntityPersister($className);
1641 6
        foreach ($this->entityInsertions as $oid => $entity) {
1642 6
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1643 5
                continue;
1644
            }
1645 6
            $persister->pushNewEntity($entity);
1646 6
            unset($this->entityInsertions[$oid]);
1647 6
        }
1648 6
        $postInsertIds = $persister->flushNewEntities();
1649 6
        if ($postInsertIds) {
1650
            // Persister returned post-insert IDs
1651 6
            foreach ($postInsertIds as $postInsertId) {
1652 6
                $id     = $postInsertId['generatedId'];
1653 6
                $entity = $postInsertId['entity'];
1654 6
                $oid    = spl_object_hash($entity);
1655
1656 6
                if ($id instanceof \stdClass) {
1657 4
                    $id = (array)$id;
1658 4
                }
1659 6
                if (!is_array($id)) {
1660 2
                    $id = [$class->getApiFieldName($class->getIdentifierFieldNames()[0]) => $id];
1661 2
                }
1662
1663 6
                if (!array_key_exists($oid, $this->originalEntityData)) {
1664
                    $this->originalEntityData[$oid] = new \stdClass();
1665
                }
1666
1667 6
                $idValues = [];
1668 6
                foreach ((array)$id as $apiIdField => $idValue) {
1669 6
                    $idName   = $class->getFieldName($apiIdField);
1670 6
                    $typeName = $class->getTypeOfField($idName);
1671 6
                    $type     = $this->manager->getConfiguration()->getTypeRegistry()->get($typeName);
1672 6
                    $idValue  = $type->toApiValue($idValue);
1673 6
                    $class->getReflectionProperty($idName)->setValue($entity, $idValue);
1674 6
                    $idValues[$idName]                       = $idValue;
1675 6
                    $this->originalEntityData[$oid]->$idName = $idValue;
1676 6
                }
1677
1678 6
                $this->entityIdentifiers[$oid] = $idValues;
1679 6
                $this->entityStates[$oid]      = self::STATE_MANAGED;
1680 6
                $this->addToIdentityMap($entity);
1681 6
            }
1682 6
        }
1683 6
    }
1684
1685 2
    private function executeUpdates($class)
1686
    {
1687 2
        $className = $class->name;
1688 2
        $persister = $this->getEntityPersister($className);
1689 2
        foreach ($this->entityUpdates as $oid => $entity) {
1690 2
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1691 1
                continue;
1692
            }
1693 2
            $this->recomputeSingleEntityChangeSet($class, $entity);
1694
1695 2
            if (!empty($this->entityChangeSets[$oid])) {
1696 2
                $persister->update($entity);
1697 2
            }
1698 2
            unset($this->entityUpdates[$oid]);
1699 2
        }
1700 2
    }
1701
1702
    /**
1703
     * Executes a refresh operation on an entity.
1704
     *
1705
     * @param object $entity  The entity to refresh.
1706
     * @param array  $visited The already visited entities during cascades.
1707
     *
1708
     * @return void
1709
     *
1710
     * @throws \InvalidArgumentException If the entity is not MANAGED.
1711
     */
1712
    private function doRefresh($entity, array &$visited)
1713
    {
1714
        $oid = spl_object_hash($entity);
1715
        if (isset($visited[$oid])) {
1716
            return; // Prevent infinite recursion
1717
        }
1718
        $visited[$oid] = $entity; // mark visited
1719
        $class         = $this->manager->getClassMetadata(get_class($entity));
1720
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1721
            throw new \InvalidArgumentException('Entity not managed');
1722
        }
1723
        $this->getEntityPersister($class->getName())->refresh(
1724
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1725
            $entity
1726
        );
1727
        $this->cascadeRefresh($entity, $visited);
1728
    }
1729
1730
    /**
1731
     * Cascades a refresh operation to associated entities.
1732
     *
1733
     * @param object $entity
1734
     * @param array  $visited
1735
     *
1736
     * @return void
1737
     */
1738 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...
1739
    {
1740
        $class               = $this->manager->getClassMetadata(get_class($entity));
1741
        $associationMappings = array_filter(
1742
            $class->getAssociationMappings(),
1743
            function ($assoc) {
1744
                return $assoc['isCascadeRefresh'];
1745
            }
1746
        );
1747
        foreach ($associationMappings as $assoc) {
1748
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1749
            switch (true) {
1750
                case ($relatedEntities instanceof ApiCollection):
1751
                    // Unwrap so that foreach() does not initialize
1752
                    $relatedEntities = $relatedEntities->unwrap();
1753
                // break; is commented intentionally!
1754
                case ($relatedEntities instanceof Collection):
1755
                case (is_array($relatedEntities)):
1756
                    foreach ($relatedEntities as $relatedEntity) {
1757
                        $this->doRefresh($relatedEntity, $visited);
1758
                    }
1759
                    break;
1760
                case ($relatedEntities !== null):
1761
                    $this->doRefresh($relatedEntities, $visited);
1762
                    break;
1763
                default:
1764
                    // Do nothing
1765
            }
1766
        }
1767
    }
1768
1769
    /**
1770
     * Cascades a detach operation to associated entities.
1771
     *
1772
     * @param object $entity
1773
     * @param array  $visited
1774
     *
1775
     * @return void
1776
     */
1777 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...
1778
    {
1779
        $class               = $this->manager->getClassMetadata(get_class($entity));
1780
        $associationMappings = array_filter(
1781
            $class->getAssociationMappings(),
1782
            function ($assoc) {
1783
                return $assoc['isCascadeDetach'];
1784
            }
1785
        );
1786
        foreach ($associationMappings as $assoc) {
1787
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1788
            switch (true) {
1789
                case ($relatedEntities instanceof ApiCollection):
1790
                    // Unwrap so that foreach() does not initialize
1791
                    $relatedEntities = $relatedEntities->unwrap();
1792
                // break; is commented intentionally!
1793
                case ($relatedEntities instanceof Collection):
1794
                case (is_array($relatedEntities)):
1795
                    foreach ($relatedEntities as $relatedEntity) {
1796
                        $this->doDetach($relatedEntity, $visited);
1797
                    }
1798
                    break;
1799
                case ($relatedEntities !== null):
1800
                    $this->doDetach($relatedEntities, $visited);
1801
                    break;
1802
                default:
1803
                    // Do nothing
1804
            }
1805
        }
1806
    }
1807
1808
    /**
1809
     * Cascades a merge operation to associated entities.
1810
     *
1811
     * @param object $entity
1812
     * @param object $managedCopy
1813
     * @param array  $visited
1814
     *
1815
     * @return void
1816
     */
1817
    private function cascadeMerge($entity, $managedCopy, array &$visited)
1818
    {
1819
        $class               = $this->manager->getClassMetadata(get_class($entity));
1820
        $associationMappings = array_filter(
1821
            $class->getAssociationMappings(),
1822
            function ($assoc) {
1823
                return $assoc['isCascadeMerge'];
1824
            }
1825
        );
1826
        foreach ($associationMappings as $assoc) {
1827
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1828
            if ($relatedEntities instanceof Collection) {
1829
                if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) {
1830
                    continue;
1831
                }
1832
                if ($relatedEntities instanceof ApiCollection) {
1833
                    // Unwrap so that foreach() does not initialize
1834
                    $relatedEntities = $relatedEntities->unwrap();
1835
                }
1836
                foreach ($relatedEntities as $relatedEntity) {
1837
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
1838
                }
1839
            } else {
1840
                if ($relatedEntities !== null) {
1841
                    $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
1842
                }
1843
            }
1844
        }
1845
    }
1846
1847
    /**
1848
     * Cascades the delete operation to associated entities.
1849
     *
1850
     * @param object $entity
1851
     * @param array  $visited
1852
     *
1853
     * @return void
1854
     */
1855
    private function cascadeRemove($entity, array &$visited)
1856
    {
1857
        $class               = $this->manager->getClassMetadata(get_class($entity));
1858
        $associationMappings = array_filter(
1859
            $class->getAssociationMappings(),
1860
            function ($assoc) {
1861
                return $assoc['isCascadeRemove'];
1862
            }
1863
        );
1864
        $entitiesToCascade   = [];
1865
        foreach ($associationMappings as $assoc) {
1866
            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...
1867
                $entity->__load();
1868
            }
1869
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1870
            switch (true) {
1871
                case ($relatedEntities instanceof Collection):
1872
                case (is_array($relatedEntities)):
1873
                    // If its a PersistentCollection initialization is intended! No unwrap!
1874
                    foreach ($relatedEntities as $relatedEntity) {
1875
                        $entitiesToCascade[] = $relatedEntity;
1876
                    }
1877
                    break;
1878
                case ($relatedEntities !== null):
1879
                    $entitiesToCascade[] = $relatedEntities;
1880
                    break;
1881
                default:
1882
                    // Do nothing
1883
            }
1884
        }
1885
        foreach ($entitiesToCascade as $relatedEntity) {
1886
            $this->doRemove($relatedEntity, $visited);
1887
        }
1888
    }
1889
1890
    /**
1891
     * Executes any extra updates that have been scheduled.
1892
     */
1893
    private function executeExtraUpdates()
1894
    {
1895
        foreach ($this->extraUpdates as $oid => $update) {
1896
            list ($entity, $changeset) = $update;
1897
            $this->entityChangeSets[$oid] = $changeset;
1898
            $this->getEntityPersister(get_class($entity))->update($entity);
1899
        }
1900
        $this->extraUpdates = [];
1901
    }
1902
1903
    private function executeDeletions(ApiMetadata $class)
1904
    {
1905
        $className = $class->getName();
1906
        $persister = $this->getEntityPersister($className);
1907
        foreach ($this->entityDeletions as $oid => $entity) {
1908
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1909
                continue;
1910
            }
1911
            $persister->delete($entity);
1912
            unset(
1913
                $this->entityDeletions[$oid],
1914
                $this->entityIdentifiers[$oid],
1915
                $this->originalEntityData[$oid],
1916
                $this->entityStates[$oid]
1917
            );
1918
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1919
            // is obtained by a new entity because the old one went out of scope.
1920
            //$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...
1921
            //            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...
1922
            //                $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...
1923
            //            }
1924
        }
1925
    }
1926
1927
    /**
1928
     * @param object $entity
1929
     * @param object $managedCopy
1930
     */
1931
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
1932
    {
1933
        $class = $this->manager->getClassMetadata(get_class($entity));
1934
        foreach ($this->reflectionPropertiesGetter->getProperties($class->getName()) as $prop) {
1935
            $name = $prop->name;
1936
            $prop->setAccessible(true);
1937
            if ($class->hasAssociation($name)) {
1938
                if (!$class->isIdentifier($name)) {
1939
                    $prop->setValue($managedCopy, $prop->getValue($entity));
1940
                }
1941
            } else {
1942
                $assoc2 = $class->getAssociationMapping($name);
1943
                if ($assoc2['type'] & ApiMetadata::TO_ONE) {
1944
                    $other = $prop->getValue($entity);
1945
                    if ($other === null) {
1946
                        $prop->setValue($managedCopy, null);
1947
                    } else {
1948
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
1949
                            // do not merge fields marked lazy that have not been fetched.
1950
                            continue;
1951
                        }
1952
                        if (!$assoc2['isCascadeMerge']) {
1953
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
1954
                                $targetClass = $this->manager->getClassMetadata($assoc2['targetEntity']);
1955
                                $relatedId   = $targetClass->getIdentifierValues($other);
1956
                                if ($targetClass->getSubclasses()) {
1957
                                    $other = $this->manager->find($targetClass->getName(), $relatedId);
1958
                                } else {
1959
                                    $other = $this->manager->getProxyFactory()->getProxy(
1960
                                        $assoc2['targetEntity'],
1961
                                        $relatedId
1962
                                    );
1963
                                    $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...
1964
                                }
1965
                            }
1966
                            $prop->setValue($managedCopy, $other);
1967
                        }
1968
                    }
1969
                } else {
1970
                    $mergeCol = $prop->getValue($entity);
1971
                    if ($mergeCol instanceof ApiCollection && !$mergeCol->isInitialized()) {
1972
                        // do not merge fields marked lazy that have not been fetched.
1973
                        // keep the lazy persistent collection of the managed copy.
1974
                        continue;
1975
                    }
1976
                    $managedCol = $prop->getValue($managedCopy);
1977
                    if (!$managedCol) {
1978
                        $managedCol = new ApiCollection(
1979
                            $this->manager,
1980
                            $this->manager->getClassMetadata($assoc2['target']),
1981
                            new ArrayCollection
1982
                        );
1983
                        $managedCol->setOwner($managedCopy, $assoc2);
1984
                        $prop->setValue($managedCopy, $managedCol);
1985
                    }
1986
                    if ($assoc2['isCascadeMerge']) {
1987
                        $managedCol->initialize();
1988
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
1989
                        if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
1990
                            $managedCol->unwrap()->clear();
1991
                            $managedCol->setDirty(true);
1992
                            if ($assoc2['isOwningSide']
1993
                                && $assoc2['type'] == ApiMetadata::MANY_TO_MANY
1994
                                && $class->isChangeTrackingNotify()
1995
                            ) {
1996
                                $this->scheduleForDirtyCheck($managedCopy);
1997
                            }
1998
                        }
1999
                    }
2000
                }
2001
            }
2002
            if ($class->isChangeTrackingNotify()) {
2003
                // Just treat all properties as changed, there is no other choice.
2004
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2005
            }
2006
        }
2007
    }
2008
2009
    /**
2010
     * Deletes an entity as part of the current unit of work.
2011
     *
2012
     * This method is internally called during delete() cascades as it tracks
2013
     * the already visited entities to prevent infinite recursions.
2014
     *
2015
     * @param object $entity  The entity to delete.
2016
     * @param array  $visited The map of the already visited entities.
2017
     *
2018
     * @return void
2019
     *
2020
     * @throws \InvalidArgumentException If the instance is a detached entity.
2021
     * @throws \UnexpectedValueException
2022
     */
2023
    private function doRemove($entity, array &$visited)
2024
    {
2025
        $oid = spl_object_hash($entity);
2026
        if (isset($visited[$oid])) {
2027
            return; // Prevent infinite recursion
2028
        }
2029
        $visited[$oid] = $entity; // mark visited
2030
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
2031
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
2032
        $this->cascadeRemove($entity, $visited);
2033
        $class       = $this->manager->getClassMetadata(get_class($entity));
0 ignored issues
show
Unused Code introduced by
$class is not used, you could remove the assignment.

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

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

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

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

Loading history...
2034
        $entityState = $this->getEntityState($entity);
2035
        switch ($entityState) {
2036
            case self::STATE_NEW:
2037
            case self::STATE_REMOVED:
2038
                // nothing to do
2039
                break;
2040
            case self::STATE_MANAGED:
2041
                $this->scheduleForDelete($entity);
2042
                break;
2043
            case self::STATE_DETACHED:
2044
                throw new \InvalidArgumentException('Detached entity cannot be removed');
2045
            default:
2046
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
2047
        }
2048
    }
2049
2050
    /**
2051
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2052
     *
2053
     * @param object $entity
2054
     *
2055
     * @return bool
2056
     */
2057
    private function isLoaded($entity)
2058
    {
2059
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2060
    }
2061
2062
    /**
2063
     * Sets/adds associated managed copies into the previous entity's association field
2064
     *
2065
     * @param object $entity
2066
     * @param array  $association
2067
     * @param object $previousManagedCopy
2068
     * @param object $managedCopy
2069
     *
2070
     * @return void
2071
     */
2072
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2073
    {
2074
        $assocField = $association['fieldName'];
2075
        $prevClass  = $this->manager->getClassMetadata(get_class($previousManagedCopy));
2076
        if ($association['type'] & ApiMetadata::TO_ONE) {
2077
            $prevClass->getReflectionProperty($assocField)->setValue($previousManagedCopy, $managedCopy);
2078
2079
            return;
2080
        }
2081
        /** @var array $value */
2082
        $value   = $prevClass->getReflectionProperty($assocField)->getValue($previousManagedCopy);
2083
        $value[] = $managedCopy;
2084
        if ($association['type'] == ApiMetadata::ONE_TO_MANY) {
2085
            $class = $this->manager->getClassMetadata(get_class($entity));
2086
            $class->getReflectionProperty($association['mappedBy'])->setValue($managedCopy, $previousManagedCopy);
2087
        }
2088
    }
2089
2090
    /**
2091
     * Executes a merge operation on an entity.
2092
     *
2093
     * @param object      $entity
2094
     * @param array       $visited
2095
     * @param object|null $prevManagedCopy
2096
     * @param array|null  $assoc
2097
     *
2098
     * @return object The managed copy of the entity.
2099
     *
2100
     * @throws \InvalidArgumentException If the entity instance is NEW.
2101
     * @throws \OutOfBoundsException
2102
     */
2103
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
2104
    {
2105
        $oid = spl_object_hash($entity);
2106
        if (isset($visited[$oid])) {
2107
            $managedCopy = $visited[$oid];
2108
            if ($prevManagedCopy !== null) {
2109
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2110
            }
2111
2112
            return $managedCopy;
2113
        }
2114
        $class = $this->manager->getClassMetadata(get_class($entity));
2115
        // First we assume DETACHED, although it can still be NEW but we can avoid
2116
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
2117
        // we need to fetch it from the db anyway in order to merge.
2118
        // MANAGED entities are ignored by the merge operation.
2119
        $managedCopy = $entity;
2120
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2121
            // Try to look the entity up in the identity map.
2122
            $id = $class->getIdentifierValues($entity);
2123
            // If there is no ID, it is actually NEW.
2124
            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...
2125
                $managedCopy = $this->newInstance($class);
2126
                $this->persistNew($class, $managedCopy);
2127
            } else {
2128
                $flatId      = ($class->containsForeignIdentifier())
2129
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
2130
                    : $id;
2131
                $managedCopy = $this->tryGetById($flatId, $class->getRootEntityName());
2132
                if ($managedCopy) {
2133
                    // We have the entity in-memory already, just make sure its not removed.
2134
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
2135
                        throw new \InvalidArgumentException('Removed entity cannot be merged');
2136
                    }
2137
                } else {
2138
                    // We need to fetch the managed copy in order to merge.
2139
                    $managedCopy = $this->manager->find($class->getName(), $flatId);
2140
                }
2141
                if ($managedCopy === null) {
2142
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
2143
                    // since the managed entity was not found.
2144
                    if (!$class->isIdentifierNatural()) {
2145
                        throw new \OutOfBoundsException('Entity not found');
2146
                    }
2147
                    $managedCopy = $this->newInstance($class);
2148
                    $class->setIdentifierValues($managedCopy, $id);
2149
                    $this->persistNew($class, $managedCopy);
2150
                }
2151
            }
2152
2153
            $visited[$oid] = $managedCopy; // mark visited
2154
            if ($this->isLoaded($entity)) {
2155
                if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized()) {
2156
                    $managedCopy->__load();
2157
                }
2158
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
2159
            }
2160
            if ($class->isChangeTrackingDeferredExplicit()) {
2161
                $this->scheduleForDirtyCheck($entity);
2162
            }
2163
        }
2164
        if ($prevManagedCopy !== null) {
2165
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2166
        }
2167
        // Mark the managed copy visited as well
2168
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
2169
        $this->cascadeMerge($entity, $managedCopy, $visited);
2170
2171
        return $managedCopy;
2172
    }
2173
2174
    /**
2175
     * Executes a detach operation on the given entity.
2176
     *
2177
     * @param object  $entity
2178
     * @param array   $visited
2179
     * @param boolean $noCascade if true, don't cascade detach operation.
2180
     *
2181
     * @return void
2182
     */
2183
    private function doDetach($entity, array &$visited, $noCascade = false)
2184
    {
2185
        $oid = spl_object_hash($entity);
2186
        if (isset($visited[$oid])) {
2187
            return; // Prevent infinite recursion
2188
        }
2189
        $visited[$oid] = $entity; // mark visited
2190
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2191
            case self::STATE_MANAGED:
2192
                if ($this->isInIdentityMap($entity)) {
2193
                    $this->removeFromIdentityMap($entity);
2194
                }
2195
                unset(
2196
                    $this->entityInsertions[$oid],
2197
                    $this->entityUpdates[$oid],
2198
                    $this->entityDeletions[$oid],
2199
                    $this->entityIdentifiers[$oid],
2200
                    $this->entityStates[$oid],
2201
                    $this->originalEntityData[$oid]
2202
                );
2203
                break;
2204
            case self::STATE_NEW:
2205
            case self::STATE_DETACHED:
2206
                return;
2207
        }
2208
        if (!$noCascade) {
2209
            $this->cascadeDetach($entity, $visited);
2210
        }
2211
    }
2212
2213
    /**
2214
     * @param ApiMetadata $class
2215
     *
2216
     * @return EntityHydrator
2217
     */
2218 15
    private function getHydratorForClass(ApiMetadata $class)
2219
    {
2220 15
        if (!array_key_exists($class->getName(), $this->hydrators)) {
2221 15
            $this->hydrators[$class->getName()] = new EntityHydrator($this->manager, $class);
2222 15
        }
2223
2224 15
        return $this->hydrators[$class->getName()];
2225
    }
2226
}
2227