Passed
Push — master ( 966fd5...e75d59 )
by Pavel
03:58
created

UnitOfWork::loadCollection()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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

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

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

Loading history...
738 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...
739 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...
740 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...
741 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...
742 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...
743 7
        ) {
744 1
            return; // Nothing to do.
745
        }
746 7
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
747
            foreach ($this->orphanRemovals as $orphan) {
748
                $this->remove($orphan);
749
            }
750
        }
751
        // Now we need a commit order to maintain referential integrity
752 7
        $commitOrder = $this->getCommitOrder();
753
754
        // Collection deletions (deletions of complete collections)
755
        // 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...
756
        //       //fixme: collection mutations
757
        //       $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...
758
        // }
759 7
        if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

Consider removing the loop.

Loading history...
775
            //fixme: decide what to do with collection mutation if API does not support this
776
            //$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...
777 7
        }
778
        // Entity deletions come last and need to be in reverse commit order
779 7
        if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
780
            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...
781
                $this->executeDeletions($commitOrder[$i]);
782
            }
783
        }
784
785
        // Take new snapshots from visited collections
786 7
        foreach ($this->visitedCollections as $coll) {
787 2
            $coll->takeSnapshot();
788 7
        }
789
790
        // Clear up
791 7
        $this->entityInsertions =
792 7
        $this->entityUpdates =
793 7
        $this->entityDeletions =
794 7
        $this->extraUpdates =
795 7
        $this->entityChangeSets =
796 7
        $this->collectionUpdates =
797 7
        $this->collectionDeletions =
798 7
        $this->visitedCollections =
799 7
        $this->scheduledForSynchronization =
800 7
        $this->orphanRemovals = [];
801 7
    }
802
803
    /**
804
     * Gets the changeset for an entity.
805
     *
806
     * @param object $entity
807
     *
808
     * @return array
809
     */
810 2
    public function & getEntityChangeSet($entity)
811
    {
812 2
        $oid  = spl_object_hash($entity);
813 2
        $data = [];
814 2
        if (!isset($this->entityChangeSets[$oid])) {
815
            return $data;
816
        }
817
818 2
        return $this->entityChangeSets[$oid];
819
    }
820
821
    /**
822
     * Computes the changes that happened to a single entity.
823
     *
824
     * Modifies/populates the following properties:
825
     *
826
     * {@link _originalEntityData}
827
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
828
     * then it was not fetched from the database and therefore we have no original
829
     * entity data yet. All of the current entity data is stored as the original entity data.
830
     *
831
     * {@link _entityChangeSets}
832
     * The changes detected on all properties of the entity are stored there.
833
     * A change is a tuple array where the first entry is the old value and the second
834
     * entry is the new value of the property. Changesets are used by persisters
835
     * to INSERT/UPDATE the persistent entity state.
836
     *
837
     * {@link _entityUpdates}
838
     * If the entity is already fully MANAGED (has been fetched from the database before)
839
     * and any changes to its properties are detected, then a reference to the entity is stored
840
     * there to mark it for an update.
841
     *
842
     * {@link _collectionDeletions}
843
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
844
     * then this collection is marked for deletion.
845
     *
846
     * @ignore
847
     *
848
     * @internal Don't call from the outside.
849
     *
850
     * @param ApiMetadata $class  The class descriptor of the entity.
851
     * @param object      $entity The entity for which to compute the changes.
852
     *
853
     * @return void
854
     */
855 7
    public function computeChangeSet(ApiMetadata $class, $entity)
856
    {
857 7
        $oid = spl_object_hash($entity);
858 7
        if (isset($this->readOnlyObjects[$oid])) {
859
            return;
860
        }
861
862 7
        $actualData = [];
863 7
        foreach ($class->getReflectionProperties() as $name => $refProp) {
864 7
            $value = $refProp->getValue($entity);
865 7
            if (null !== $value && $class->isCollectionValuedAssociation($name)) {
866 2
                if ($value instanceof ApiCollection) {
867 1
                    if ($value->getOwner() === $entity) {
868 1
                        continue;
869
                    }
870
                    $value = new ArrayCollection($value->getValues());
871
                }
872
                // If $value is not a Collection then use an ArrayCollection.
873 2
                if (!$value instanceof Collection) {
874
                    $value = new ArrayCollection($value);
875
                }
876 2
                $assoc = $class->getAssociationMapping($name);
877
                // Inject PersistentCollection
878 2
                $value = new ApiCollection(
879 2
                    $this->manager,
880 2
                    $this->manager->getClassMetadata($assoc['target']),
881
                    $value
882 2
                );
883 2
                $value->setOwner($entity, $assoc);
884 2
                $value->setDirty(!$value->isEmpty());
885 2
                $class->getReflectionProperty($name)->setValue($entity, $value);
886 2
                $actualData[$name] = $value;
887 2
                continue;
888
            }
889 7
            if (!$class->isIdentifier($name)) {
890 7
                $actualData[$name] = $value;
891 7
            }
892 7
        }
893 7
        if (!isset($this->originalEntityData[$oid])) {
894
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
895
            // These result in an INSERT.
896 7
            $this->originalEntityData[$oid] = (object)$actualData;
897 7
            $changeSet                      = [];
898 7
            foreach ($actualData as $propName => $actualValue) {
899 7 View Code Duplication
                if (!$class->hasAssociation($propName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
900 7
                    $changeSet[$propName] = [null, $actualValue];
901 7
                    continue;
902
                }
903 5
                $assoc = $class->getAssociationMapping($propName);
904 5
                if ($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE) {
905 5
                    $changeSet[$propName] = [null, $actualValue];
906 5
                }
907 7
            }
908 7
            $this->entityChangeSets[$oid] = $changeSet;
909 7
        } else {
910
911
            // Entity is "fully" MANAGED: it was already fully persisted before
912
            // and we have a copy of the original data
913 3
            $originalData           = $this->originalEntityData[$oid];
914 3
            $isChangeTrackingNotify = false;
915 3
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
916 3
                ? $this->entityChangeSets[$oid]
917 3
                : [];
918
919 3
            foreach ($actualData as $propName => $actualValue) {
920
                // skip field, its a partially omitted one!
921 3
                if (!property_exists($originalData, $propName)) {
922
                    continue;
923
                }
924 3
                $orgValue = $originalData->$propName;
925
                // skip if value haven't changed
926 3
                if ($orgValue === $actualValue) {
927
928 3
                    continue;
929
                }
930
                // if regular field
931 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...
932 1
                    if ($isChangeTrackingNotify) {
933
                        continue;
934
                    }
935 1
                    $changeSet[$propName] = [$orgValue, $actualValue];
936 1
                    continue;
937
                }
938
939 1
                $assoc = $class->getAssociationMapping($propName);
940
                // Persistent collection was exchanged with the "originally"
941
                // created one. This can only mean it was cloned and replaced
942
                // on another entity.
943 1
                if ($actualValue instanceof ApiCollection) {
944
                    $owner = $actualValue->getOwner();
945
                    if ($owner === null) { // cloned
946
                        $actualValue->setOwner($entity, $assoc);
947
                    } else {
948
                        if ($owner !== $entity) { // no clone, we have to fix
949
                            if (!$actualValue->isInitialized()) {
950
                                $actualValue->initialize(); // we have to do this otherwise the cols share state
951
                            }
952
                            $newValue = clone $actualValue;
953
                            $newValue->setOwner($entity, $assoc);
954
                            $class->getReflectionProperty($propName)->setValue($entity, $newValue);
955
                        }
956
                    }
957
                }
958 1
                if ($orgValue instanceof ApiCollection) {
959
                    // A PersistentCollection was de-referenced, so delete it.
960
                    $coid = spl_object_hash($orgValue);
961
                    if (isset($this->collectionDeletions[$coid])) {
962
                        continue;
963
                    }
964
                    $this->collectionDeletions[$coid] = $orgValue;
965
                    $changeSet[$propName]             = $orgValue; // Signal changeset, to-many assocs will be ignored.
966
                    continue;
967
                }
968 1
                if ($assoc['type'] & ApiMetadata::TO_ONE) {
969 1
                    if ($assoc['isOwningSide']) {
970 1
                        $changeSet[$propName] = [$orgValue, $actualValue];
971 1
                    }
972 1
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
973
                        $this->scheduleOrphanRemoval($orgValue);
974
                    }
975 1
                }
976 3
            }
977 3
            if ($changeSet) {
978 2
                $this->entityChangeSets[$oid]   = $changeSet;
979 2
                $this->originalEntityData[$oid] = (object)$actualData;
980 2
                $this->entityUpdates[$oid]      = $entity;
981 2
            }
982
        }
983
        // Look for changes in associations of the entity
984 7
        foreach ($class->getAssociationMappings() as $field => $assoc) {
985 5
            if (($val = $class->getReflectionProperty($field)->getValue($entity)) === null) {
986 5
                continue;
987
            }
988 2
            $this->computeAssociationChanges($assoc, $val);
989 2
            if (!isset($this->entityChangeSets[$oid]) &&
990 2
                $assoc['isOwningSide'] &&
991 2
                $assoc['type'] == ApiMetadata::MANY_TO_MANY &&
992 2
                $val instanceof ApiCollection &&
993
                $val->isDirty()
994 2
            ) {
995
                $this->entityChangeSets[$oid]   = [];
996
                $this->originalEntityData[$oid] = (object)$actualData;
997
                $this->entityUpdates[$oid]      = $entity;
998
            }
999 7
        }
1000 7
    }
1001
1002
    /**
1003
     * Computes all the changes that have been done to entities and collections
1004
     * since the last commit and stores these changes in the _entityChangeSet map
1005
     * temporarily for access by the persisters, until the UoW commit is finished.
1006
     *
1007
     * @return void
1008
     */
1009 7
    public function computeChangeSets()
1010
    {
1011
        // Compute changes for INSERTed entities first. This must always happen.
1012 7
        $this->computeScheduleInsertsChangeSets();
1013
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
1014 7
        foreach ($this->identityMap as $className => $entities) {
1015 3
            $class = $this->manager->getClassMetadata($className);
1016
            // Skip class if instances are read-only
1017 3
            if ($class->isReadOnly()) {
1018
                continue;
1019
            }
1020
            // If change tracking is explicit or happens through notification, then only compute
1021
            // changes on entities of that type that are explicitly marked for synchronization.
1022 3
            switch (true) {
1023 3
                case ($class->isChangeTrackingDeferredImplicit()):
1024 3
                    $entitiesToProcess = $entities;
1025 3
                    break;
1026
                case (isset($this->scheduledForSynchronization[$className])):
1027
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
1028
                    break;
1029
                default:
1030
                    $entitiesToProcess = [];
1031
            }
1032 3
            foreach ($entitiesToProcess as $entity) {
1033
                // Ignore uninitialized proxy objects
1034 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...
1035
                    continue;
1036
                }
1037
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1038 3
                $oid = spl_object_hash($entity);
1039 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...
1040 3
                    !isset($this->entityDeletions[$oid]) &&
1041 3
                    isset($this->entityStates[$oid])
1042 3
                ) {
1043 3
                    $this->computeChangeSet($class, $entity);
1044 3
                }
1045 3
            }
1046 7
        }
1047 7
    }
1048
1049
    /**
1050
     * INTERNAL:
1051
     * Schedules an orphaned entity for removal. The remove() operation will be
1052
     * invoked on that entity at the beginning of the next commit of this
1053
     * UnitOfWork.
1054
     *
1055
     * @ignore
1056
     *
1057
     * @param object $entity
1058
     *
1059
     * @return void
1060
     */
1061
    public function scheduleOrphanRemoval($entity)
1062
    {
1063
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
1064
    }
1065
1066 2
    public function loadCollection(ApiCollection $collection)
1067
    {
1068 2
        $assoc     = $collection->getMapping();
1069 2
        $persister = $this->getEntityPersister($assoc['target']);
1070 2
        switch ($assoc['type']) {
1071 2
            case ApiMetadata::ONE_TO_MANY:
1072 2
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
1073 2
                break;
1074 2
        }
1075 2
        $collection->setInitialized(true);
1076 2
    }
1077
1078 1
    public function getCollectionPersister($association)
1079
    {
1080 1
        $targetMetadata = $this->manager->getClassMetadata($association['target']);
1081 1
        $role           = $association['sourceEntity'] . '::' . $association['field'];
1082
1083 1
        if (!array_key_exists($role, $this->collectionPersisters)) {
1084 1
            $this->collectionPersisters[$role] = new CollectionPersister(
1085 1
                $this->manager,
1086 1
                $targetMetadata,
1087 1
                new CollectionMatcher($this->manager, $this->getCrudsApi($targetMetadata)),
1088
                $association
1089 1
            );
1090 1
        }
1091
1092 1
        return $this->collectionPersisters[$role];
1093
    }
1094
1095
    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...
1096
    {
1097
    }
1098
1099 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...
1100
    {
1101 2
    }
1102
1103
    /**
1104
     * INTERNAL:
1105
     * Sets a property value of the original data array of an entity.
1106
     *
1107
     * @ignore
1108
     *
1109
     * @param string $oid
1110
     * @param string $property
1111
     * @param mixed  $value
1112
     *
1113
     * @return void
1114
     */
1115 11
    public function setOriginalEntityProperty($oid, $property, $value)
1116
    {
1117 11
        if (!array_key_exists($oid, $this->originalEntityData)) {
1118 11
            $this->originalEntityData[$oid] = new \stdClass();
1119 11
        }
1120
1121 11
        $this->originalEntityData[$oid]->$property = $value;
1122 11
    }
1123
1124
    public function scheduleExtraUpdate($entity, $changeset)
1125
    {
1126
        $oid         = spl_object_hash($entity);
1127
        $extraUpdate = [$entity, $changeset];
1128
        if (isset($this->extraUpdates[$oid])) {
1129
            list(, $changeset2) = $this->extraUpdates[$oid];
1130
            $extraUpdate = [$entity, $changeset + $changeset2];
1131
        }
1132
        $this->extraUpdates[$oid] = $extraUpdate;
1133
    }
1134
1135
    /**
1136
     * Refreshes the state of the given entity from the database, overwriting
1137
     * any local, unpersisted changes.
1138
     *
1139
     * @param object $entity The entity to refresh.
1140
     *
1141
     * @return void
1142
     *
1143
     * @throws InvalidArgumentException If the entity is not MANAGED.
1144
     */
1145
    public function refresh($entity)
1146
    {
1147
        $visited = [];
1148
        $this->doRefresh($entity, $visited);
1149
    }
1150
1151
    /**
1152
     * Clears the UnitOfWork.
1153
     *
1154
     * @param string|null $entityName if given, only entities of this type will get detached.
1155
     *
1156
     * @return void
1157
     */
1158
    public function clear($entityName = null)
1159
    {
1160
        if ($entityName === null) {
1161
            $this->identityMap =
1162
            $this->entityIdentifiers =
1163
            $this->originalEntityData =
1164
            $this->entityChangeSets =
1165
            $this->entityStates =
1166
            $this->scheduledForSynchronization =
1167
            $this->entityInsertions =
1168
            $this->entityUpdates =
1169
            $this->entityDeletions =
1170
            $this->collectionDeletions =
1171
            $this->collectionUpdates =
1172
            $this->extraUpdates =
1173
            $this->readOnlyObjects =
1174
            $this->visitedCollections =
1175
            $this->orphanRemovals = [];
1176
        } else {
1177
            $this->clearIdentityMapForEntityName($entityName);
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...
1178
            $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...
1179
        }
1180
    }
1181
1182
    /**
1183
     * @param PersistentCollection $coll
1184
     *
1185
     * @return bool
1186
     */
1187
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
1188
    {
1189
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
1190
    }
1191
1192
    /**
1193
     * Schedules an entity for dirty-checking at commit-time.
1194
     *
1195
     * @param object $entity The entity to schedule for dirty-checking.
1196
     *
1197
     * @return void
1198
     *
1199
     * @todo Rename: scheduleForSynchronization
1200
     */
1201
    public function scheduleForDirtyCheck($entity)
1202
    {
1203
        $rootClassName                                                               =
1204
            $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
1205
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
1206
    }
1207
1208
    /**
1209
     * Deletes an entity as part of the current unit of work.
1210
     *
1211
     * @param object $entity The entity to remove.
1212
     *
1213
     * @return void
1214
     */
1215
    public function remove($entity)
1216
    {
1217
        $visited = [];
1218
        $this->doRemove($entity, $visited);
1219
    }
1220
1221
    /**
1222
     * Merges the state of the given detached entity into this UnitOfWork.
1223
     *
1224
     * @param object $entity
1225
     *
1226
     * @return object The managed copy of the entity.
1227
     */
1228
    public function merge($entity)
1229
    {
1230
        $visited = [];
1231
1232
        return $this->doMerge($entity, $visited);
1233
    }
1234
1235
    /**
1236
     * Detaches an entity from the persistence management. It's persistence will
1237
     * no longer be managed by Doctrine.
1238
     *
1239
     * @param object $entity The entity to detach.
1240
     *
1241
     * @return void
1242
     */
1243
    public function detach($entity)
1244
    {
1245
        $visited = [];
1246
        $this->doDetach($entity, $visited);
1247
    }
1248
1249
    /**
1250
     * Resolve metadata against source data and root class
1251
     *
1252
     * @param \stdClass $data
1253
     * @param string    $class
1254
     *
1255
     * @return ApiMetadata
1256
     * @throws MappingException
1257
     */
1258 18
    private function resolveSourceMetadataForClass(\stdClass $data, $class)
1259
    {
1260 18
        $metadata           = $this->manager->getClassMetadata($class);
1261 18
        $discriminatorValue = $metadata->getDiscriminatorValue();
1262 18
        if ($metadata->getDiscriminatorField()) {
1263 2
            $property = $metadata->getDiscriminatorField()['fieldName'];
1264 2
            if (isset($data->$property)) {
1265 2
                $discriminatorValue = $data->$property;
1266 2
            }
1267 2
        }
1268
1269 18
        $map = $metadata->getDiscriminatorMap();
1270
1271 18
        if (!array_key_exists($discriminatorValue, $map)) {
1272
            throw MappingException::unknownDiscriminatorValue($discriminatorValue, $class);
1273
        }
1274
1275 18
        $realClass = $map[$discriminatorValue];
1276
1277 18
        return $this->manager->getClassMetadata($realClass);
1278
    }
1279
1280
    /**
1281
     * Helper method to show an object as string.
1282
     *
1283
     * @param object $obj
1284
     *
1285
     * @return string
1286
     */
1287
    private static function objToStr($obj)
1288
    {
1289
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
1290
    }
1291
1292
    /**
1293
     * @param ApiMetadata $class
1294
     *
1295
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
1296
     */
1297 17
    private function newInstance(ApiMetadata $class)
1298
    {
1299 17
        $entity = $class->newInstance();
1300
1301 17
        if ($entity instanceof ObjectManagerAware) {
1302
            $entity->injectObjectManager($this->manager, $class);
1303
        }
1304
1305 17
        return $entity;
1306
    }
1307
1308
    /**
1309
     * @param ApiMetadata $classMetadata
1310
     *
1311
     * @return EntityDataCacheInterface
1312
     */
1313 26
    private function createEntityCache(ApiMetadata $classMetadata)
1314
    {
1315 26
        $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName());
1316 26
        $cache         = new VoidEntityCache($classMetadata);
1317 26
        if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) {
1318
            $cache =
1319 1
                new LoggingCache(
1320 1
                    new ApiEntityCache(
1321 1
                        $this->manager->getConfiguration()->getApiCache(),
1322 1
                        $classMetadata,
1323
                        $configuration
1324 1
                    ),
1325 1
                    $this->manager->getConfiguration()->getApiCacheLogger()
1326 1
                );
1327
1328 1
            return $cache;
1329
        }
1330
1331 25
        return $cache;
1332
    }
1333
1334
    /**
1335
     * @param ApiMetadata $classMetadata
1336
     *
1337
     * @return CrudsApiInterface
1338
     */
1339 26
    private function getCrudsApi(ApiMetadata $classMetadata)
1340
    {
1341 26
        if (!array_key_exists($classMetadata->getName(), $this->apis)) {
1342 26
            $client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName());
1343
1344 26
            $api = $this->manager
1345 26
                ->getConfiguration()
1346 26
                ->getFactoryRegistry()
1347 26
                ->create(
1348 26
                    $classMetadata->getApiFactory(),
1349 26
                    $client,
1350
                    $classMetadata
1351 26
                );
1352
1353 26
            $this->apis[$classMetadata->getName()] = $api;
1354 26
        }
1355
1356 26
        return $this->apis[$classMetadata->getName()];
1357
    }
1358
1359 7
    private function doPersist($entity, $visited)
1360
    {
1361 7
        $oid = spl_object_hash($entity);
1362 7
        if (isset($visited[$oid])) {
1363
            return; // Prevent infinite recursion
1364
        }
1365 7
        $visited[$oid] = $entity; // Mark visited
1366 7
        $class         = $this->manager->getClassMetadata(get_class($entity));
1367
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1368
        // If we would detect DETACHED here we would throw an exception anyway with the same
1369
        // consequences (not recoverable/programming error), so just assuming NEW here
1370
        // lets us avoid some database lookups for entities with natural identifiers.
1371 7
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1372
        switch ($entityState) {
1373 7
            case self::STATE_MANAGED:
1374
                $this->scheduleForDirtyCheck($entity);
1375
                break;
1376 7
            case self::STATE_NEW:
1377 7
                $this->persistNew($class, $entity);
1378 7
                break;
1379
            case self::STATE_REMOVED:
1380
                // Entity becomes managed again
1381
                unset($this->entityDeletions[$oid]);
1382
                $this->addToIdentityMap($entity);
1383
                $this->entityStates[$oid] = self::STATE_MANAGED;
1384
                break;
1385
            case self::STATE_DETACHED:
1386
                // Can actually not happen right now since we assume STATE_NEW.
1387
                throw new \InvalidArgumentException('Detached entity cannot be persisted');
1388
            default:
1389
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1390
        }
1391 7
        $this->cascadePersist($entity, $visited);
1392 7
    }
1393
1394
    /**
1395
     * Cascades the save operation to associated entities.
1396
     *
1397
     * @param object $entity
1398
     * @param array  $visited
1399
     *
1400
     * @return void
1401
     * @throws \InvalidArgumentException
1402
     * @throws MappingException
1403
     */
1404 7
    private function cascadePersist($entity, array &$visited)
1405
    {
1406 7
        $class               = $this->manager->getClassMetadata(get_class($entity));
1407 7
        $associationMappings = [];
1408 7
        foreach ($class->getAssociationNames() as $name) {
1409 5
            $assoc = $class->getAssociationMapping($name);
1410 5
            if ($assoc['isCascadePersist']) {
1411
                $associationMappings[$name] = $assoc;
1412
            }
1413 7
        }
1414 7
        foreach ($associationMappings as $assoc) {
1415
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1416
            switch (true) {
1417
                case ($relatedEntities instanceof ApiCollection):
1418
                    // Unwrap so that foreach() does not initialize
1419
                    $relatedEntities = $relatedEntities->unwrap();
1420
                // break; is commented intentionally!
1421
                case ($relatedEntities instanceof Collection):
1422
                case (is_array($relatedEntities)):
1423
                    if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) {
1424
                        throw new \InvalidArgumentException('Invalid association for cascade');
1425
                    }
1426
                    foreach ($relatedEntities as $relatedEntity) {
1427
                        $this->doPersist($relatedEntity, $visited);
1428
                    }
1429
                    break;
1430
                case ($relatedEntities !== null):
1431
                    if (!$relatedEntities instanceof $assoc['target']) {
1432
                        throw new \InvalidArgumentException('Invalid association for cascade');
1433
                    }
1434
                    $this->doPersist($relatedEntities, $visited);
1435
                    break;
1436
                default:
1437
                    // Do nothing
1438
            }
1439 7
        }
1440 7
    }
1441
1442
    /**
1443
     * @param ApiMetadata $class
1444
     * @param object      $entity
1445
     *
1446
     * @return void
1447
     */
1448 7
    private function persistNew($class, $entity)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

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

Loading history...
1449
    {
1450 7
        $oid = spl_object_hash($entity);
1451
        //        $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...
1452
        //        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...
1453
        //            $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...
1454
        //        }
1455
        //        $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...
1456
        //        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...
1457
        //            $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...
1458
        //            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...
1459
        //                $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...
1460
        //                $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...
1461
        //            }
1462
        //            $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...
1463
        //        }
1464 7
        $this->entityStates[$oid] = self::STATE_MANAGED;
1465 7
        $this->scheduleForInsert($entity);
1466 7
    }
1467
1468
    /**
1469
     * Gets the commit order.
1470
     *
1471
     * @param array|null $entityChangeSet
1472
     *
1473
     * @return array
1474
     */
1475 7
    private function getCommitOrder(array $entityChangeSet = null)
1476
    {
1477 7
        if ($entityChangeSet === null) {
1478 7
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1479 7
        }
1480 7
        $calc = $this->getCommitOrderCalculator();
1481
        // See if there are any new classes in the changeset, that are not in the
1482
        // commit order graph yet (don't have a node).
1483
        // We have to inspect changeSet to be able to correctly build dependencies.
1484
        // It is not possible to use IdentityMap here because post inserted ids
1485
        // are not yet available.
1486
        /** @var ApiMetadata[] $newNodes */
1487 7
        $newNodes = [];
1488 7
        foreach ((array)$entityChangeSet as $entity) {
1489 7
            $class = $this->manager->getClassMetadata(get_class($entity));
1490 7
            if ($calc->hasNode($class->getName())) {
1491
                continue;
1492
            }
1493 7
            $calc->addNode($class->getName(), $class);
1494 7
            $newNodes[] = $class;
1495 7
        }
1496
        // Calculate dependencies for new nodes
1497 7
        while ($class = array_pop($newNodes)) {
1498 7
            foreach ($class->getAssociationMappings() as $assoc) {
1499 5
                if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) {
1500 5
                    continue;
1501
                }
1502 5
                $targetClass = $this->manager->getClassMetadata($assoc['target']);
1503 5
                if (!$calc->hasNode($targetClass->getName())) {
1504 3
                    $calc->addNode($targetClass->getName(), $targetClass);
1505 3
                    $newNodes[] = $targetClass;
1506 3
                }
1507 5
                $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable']));
1508
                // If the target class has mapped subclasses, these share the same dependency.
1509 5
                if (!$targetClass->getSubclasses()) {
1510
                    continue;
1511
                }
1512 5
                foreach ($targetClass->getSubclasses() as $subClassName) {
1513 5
                    $targetSubClass = $this->manager->getClassMetadata($subClassName);
1514 5
                    if (!$calc->hasNode($subClassName)) {
1515 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...
1516 5
                        $newNodes[] = $targetSubClass;
1517 5
                    }
1518 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...
1519 5
                }
1520 7
            }
1521 7
        }
1522
1523 7
        return $calc->sort();
1524
    }
1525
1526 7
    private function getCommitOrderCalculator()
1527
    {
1528 7
        return new Utility\CommitOrderCalculator();
1529
    }
1530
1531
    /**
1532
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
1533
     *
1534
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
1535
     * 2. Read Only entities are skipped.
1536
     * 3. Proxies are skipped.
1537
     * 4. Only if entity is properly managed.
1538
     *
1539
     * @param object $entity
1540
     *
1541
     * @return void
1542
     *
1543
     * @throws \InvalidArgumentException
1544
     */
1545
    private function computeSingleEntityChangeSet($entity)
1546
    {
1547
        $state = $this->getEntityState($entity);
1548
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
1549
            throw new \InvalidArgumentException(
1550
                "Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)
1551
            );
1552
        }
1553
        $class = $this->manager->getClassMetadata(get_class($entity));
1554
        // Compute changes for INSERTed entities first. This must always happen even in this case.
1555
        $this->computeScheduleInsertsChangeSets();
1556
        if ($class->isReadOnly()) {
1557
            return;
1558
        }
1559
        // Ignore uninitialized proxy objects
1560
        if ($entity instanceof Proxy && !$entity->__isInitialized__) {
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...
1561
            return;
1562
        }
1563
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1564
        $oid = spl_object_hash($entity);
1565 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...
1566
            !isset($this->entityDeletions[$oid]) &&
1567
            isset($this->entityStates[$oid])
1568
        ) {
1569
            $this->computeChangeSet($class, $entity);
1570
        }
1571
    }
1572
1573
    /**
1574
     * Computes the changesets of all entities scheduled for insertion.
1575
     *
1576
     * @return void
1577
     */
1578 7
    private function computeScheduleInsertsChangeSets()
1579
    {
1580 7
        foreach ($this->entityInsertions as $entity) {
1581 7
            $class = $this->manager->getClassMetadata(get_class($entity));
1582 7
            $this->computeChangeSet($class, $entity);
1583 7
        }
1584 7
    }
1585
1586
    /**
1587
     * Computes the changes of an association.
1588
     *
1589
     * @param array $assoc The association mapping.
1590
     * @param mixed $value The value of the association.
1591
     *
1592
     * @throws \InvalidArgumentException
1593
     * @throws \UnexpectedValueException
1594
     *
1595
     * @return void
1596
     */
1597 2
    private function computeAssociationChanges($assoc, $value)
1598
    {
1599 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...
1600
            return;
1601
        }
1602 2
        if ($value instanceof ApiCollection && $value->isDirty()) {
1603 2
            $coid                            = spl_object_hash($value);
1604 2
            $this->collectionUpdates[$coid]  = $value;
1605 2
            $this->visitedCollections[$coid] = $value;
1606 2
        }
1607
        // Look through the entities, and in any of their associations,
1608
        // 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...
1609
        // Unwrap. Uninitialized collections will simply be empty.
1610 2
        $unwrappedValue  = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap();
1611 2
        $targetClass     = $this->manager->getClassMetadata($assoc['target']);
1612 2
        $targetClassName = $targetClass->getName();
1613 2
        foreach ($unwrappedValue as $key => $entry) {
1614 2
            if (!($entry instanceof $targetClassName)) {
1615
                throw new \InvalidArgumentException('Invalid association');
1616
            }
1617 2
            $state = $this->getEntityState($entry, self::STATE_NEW);
1618 2
            if (!($entry instanceof $assoc['target'])) {
1619
                throw new \UnexpectedValueException('Unexpected association');
1620
            }
1621
            switch ($state) {
1622 2
                case self::STATE_NEW:
1623
                    if (!$assoc['isCascadePersist']) {
1624
                        throw new \InvalidArgumentException('New entity through relationship');
1625
                    }
1626
                    $this->persistNew($targetClass, $entry);
1627
                    $this->computeChangeSet($targetClass, $entry);
1628
                    break;
1629 2
                case self::STATE_REMOVED:
1630
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1631
                    // and remove the element from Collection.
1632
                    if ($assoc['type'] & ApiMetadata::TO_MANY) {
1633
                        unset($value[$key]);
1634
                    }
1635
                    break;
1636 2
                case self::STATE_DETACHED:
1637
                    // Can actually not happen right now as we assume STATE_NEW,
1638
                    // so the exception will be raised from the DBAL layer (constraint violation).
1639
                    throw new \InvalidArgumentException('Detached entity through relationship');
1640
                    break;
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...
1641 2
                default:
1642
                    // MANAGED associated entities are already taken into account
1643
                    // during changeset calculation anyway, since they are in the identity map.
1644 2
            }
1645 2
        }
1646 2
    }
1647
1648 7
    private function executeInserts(ApiMetadata $class)
1649
    {
1650 7
        $className = $class->getName();
1651 7
        $persister = $this->getEntityPersister($className);
1652 7
        foreach ($this->entityInsertions as $oid => $entity) {
1653 7
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1654 5
                continue;
1655
            }
1656 7
            $persister->pushNewEntity($entity);
1657 7
            unset($this->entityInsertions[$oid]);
1658 7
        }
1659 7
        $postInsertIds = $persister->flushNewEntities();
1660 7
        if ($postInsertIds) {
1661
            // Persister returned post-insert IDs
1662 7
            foreach ($postInsertIds as $postInsertId) {
1663 7
                $id     = $postInsertId['generatedId'];
1664 7
                $entity = $postInsertId['entity'];
1665 7
                $oid    = spl_object_hash($entity);
1666
1667 7
                if ($id instanceof \stdClass) {
1668 5
                    $id = (array)$id;
1669 5
                }
1670 7
                if (!is_array($id)) {
1671 2
                    $id = [$class->getApiFieldName($class->getIdentifierFieldNames()[0]) => $id];
1672 2
                }
1673
1674 7
                if (!array_key_exists($oid, $this->originalEntityData)) {
1675
                    $this->originalEntityData[$oid] = new \stdClass();
1676
                }
1677
1678 7
                $idValues = [];
1679 7
                foreach ((array)$id as $apiIdField => $idValue) {
1680 7
                    $idName   = $class->getFieldName($apiIdField);
1681 7
                    $typeName = $class->getTypeOfField($idName);
1682 7
                    $type     = $this->manager->getConfiguration()->getTypeRegistry()->get($typeName);
1683 7
                    $idValue  = $type->toApiValue($idValue, $class->getFieldOptions($idName));
1684 7
                    $class->getReflectionProperty($idName)->setValue($entity, $idValue);
1685 7
                    $idValues[$idName]                       = $idValue;
1686 7
                    $this->originalEntityData[$oid]->$idName = $idValue;
1687 7
                }
1688
1689 7
                $this->entityIdentifiers[$oid] = $idValues;
1690 7
                $this->entityStates[$oid]      = self::STATE_MANAGED;
1691 7
                $this->addToIdentityMap($entity);
1692 7
            }
1693 7
        }
1694 7
    }
1695
1696 2
    private function executeUpdates($class)
1697
    {
1698 2
        $className = $class->name;
1699 2
        $persister = $this->getEntityPersister($className);
1700 2
        foreach ($this->entityUpdates as $oid => $entity) {
1701 2
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1702 1
                continue;
1703
            }
1704 2
            $this->recomputeSingleEntityChangeSet($class, $entity);
1705
1706 2
            if (!empty($this->entityChangeSets[$oid])) {
1707 2
                $persister->update($entity);
1708 2
            }
1709 2
            unset($this->entityUpdates[$oid]);
1710 2
        }
1711 2
    }
1712
1713
    /**
1714
     * Executes a refresh operation on an entity.
1715
     *
1716
     * @param object $entity  The entity to refresh.
1717
     * @param array  $visited The already visited entities during cascades.
1718
     *
1719
     * @return void
1720
     *
1721
     * @throws \InvalidArgumentException If the entity is not MANAGED.
1722
     */
1723
    private function doRefresh($entity, array &$visited)
1724
    {
1725
        $oid = spl_object_hash($entity);
1726
        if (isset($visited[$oid])) {
1727
            return; // Prevent infinite recursion
1728
        }
1729
        $visited[$oid] = $entity; // mark visited
1730
        $class         = $this->manager->getClassMetadata(get_class($entity));
1731
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1732
            throw new \InvalidArgumentException('Entity not managed');
1733
        }
1734
        $this->getEntityPersister($class->getName())->refresh(
1735
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1736
            $entity
1737
        );
1738
        $this->cascadeRefresh($entity, $visited);
1739
    }
1740
1741
    /**
1742
     * Cascades a refresh operation to associated entities.
1743
     *
1744
     * @param object $entity
1745
     * @param array  $visited
1746
     *
1747
     * @return void
1748
     */
1749 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...
1750
    {
1751
        $class               = $this->manager->getClassMetadata(get_class($entity));
1752
        $associationMappings = array_filter(
1753
            $class->getAssociationMappings(),
1754
            function ($assoc) {
1755
                return $assoc['isCascadeRefresh'];
1756
            }
1757
        );
1758
        foreach ($associationMappings as $assoc) {
1759
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1760
            switch (true) {
1761
                case ($relatedEntities instanceof ApiCollection):
1762
                    // Unwrap so that foreach() does not initialize
1763
                    $relatedEntities = $relatedEntities->unwrap();
1764
                // break; is commented intentionally!
1765
                case ($relatedEntities instanceof Collection):
1766
                case (is_array($relatedEntities)):
1767
                    foreach ($relatedEntities as $relatedEntity) {
1768
                        $this->doRefresh($relatedEntity, $visited);
1769
                    }
1770
                    break;
1771
                case ($relatedEntities !== null):
1772
                    $this->doRefresh($relatedEntities, $visited);
1773
                    break;
1774
                default:
1775
                    // Do nothing
1776
            }
1777
        }
1778
    }
1779
1780
    /**
1781
     * Cascades a detach operation to associated entities.
1782
     *
1783
     * @param object $entity
1784
     * @param array  $visited
1785
     *
1786
     * @return void
1787
     */
1788 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...
1789
    {
1790
        $class               = $this->manager->getClassMetadata(get_class($entity));
1791
        $associationMappings = array_filter(
1792
            $class->getAssociationMappings(),
1793
            function ($assoc) {
1794
                return $assoc['isCascadeDetach'];
1795
            }
1796
        );
1797
        foreach ($associationMappings as $assoc) {
1798
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1799
            switch (true) {
1800
                case ($relatedEntities instanceof ApiCollection):
1801
                    // Unwrap so that foreach() does not initialize
1802
                    $relatedEntities = $relatedEntities->unwrap();
1803
                // break; is commented intentionally!
1804
                case ($relatedEntities instanceof Collection):
1805
                case (is_array($relatedEntities)):
1806
                    foreach ($relatedEntities as $relatedEntity) {
1807
                        $this->doDetach($relatedEntity, $visited);
1808
                    }
1809
                    break;
1810
                case ($relatedEntities !== null):
1811
                    $this->doDetach($relatedEntities, $visited);
1812
                    break;
1813
                default:
1814
                    // Do nothing
1815
            }
1816
        }
1817
    }
1818
1819
    /**
1820
     * Cascades a merge operation to associated entities.
1821
     *
1822
     * @param object $entity
1823
     * @param object $managedCopy
1824
     * @param array  $visited
1825
     *
1826
     * @return void
1827
     */
1828
    private function cascadeMerge($entity, $managedCopy, array &$visited)
1829
    {
1830
        $class               = $this->manager->getClassMetadata(get_class($entity));
1831
        $associationMappings = array_filter(
1832
            $class->getAssociationMappings(),
1833
            function ($assoc) {
1834
                return $assoc['isCascadeMerge'];
1835
            }
1836
        );
1837
        foreach ($associationMappings as $assoc) {
1838
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1839
            if ($relatedEntities instanceof Collection) {
1840
                if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) {
1841
                    continue;
1842
                }
1843
                if ($relatedEntities instanceof ApiCollection) {
1844
                    // Unwrap so that foreach() does not initialize
1845
                    $relatedEntities = $relatedEntities->unwrap();
1846
                }
1847
                foreach ($relatedEntities as $relatedEntity) {
1848
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
1849
                }
1850
            } else {
1851
                if ($relatedEntities !== null) {
1852
                    $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
1853
                }
1854
            }
1855
        }
1856
    }
1857
1858
    /**
1859
     * Cascades the delete operation to associated entities.
1860
     *
1861
     * @param object $entity
1862
     * @param array  $visited
1863
     *
1864
     * @return void
1865
     */
1866
    private function cascadeRemove($entity, array &$visited)
1867
    {
1868
        $class               = $this->manager->getClassMetadata(get_class($entity));
1869
        $associationMappings = array_filter(
1870
            $class->getAssociationMappings(),
1871
            function ($assoc) {
1872
                return $assoc['isCascadeRemove'];
1873
            }
1874
        );
1875
        $entitiesToCascade   = [];
1876
        foreach ($associationMappings as $assoc) {
1877
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
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...
1878
                $entity->__load();
1879
            }
1880
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1881
            switch (true) {
1882
                case ($relatedEntities instanceof Collection):
1883
                case (is_array($relatedEntities)):
1884
                    // If its a PersistentCollection initialization is intended! No unwrap!
1885
                    foreach ($relatedEntities as $relatedEntity) {
1886
                        $entitiesToCascade[] = $relatedEntity;
1887
                    }
1888
                    break;
1889
                case ($relatedEntities !== null):
1890
                    $entitiesToCascade[] = $relatedEntities;
1891
                    break;
1892
                default:
1893
                    // Do nothing
1894
            }
1895
        }
1896
        foreach ($entitiesToCascade as $relatedEntity) {
1897
            $this->doRemove($relatedEntity, $visited);
1898
        }
1899
    }
1900
1901
    /**
1902
     * Executes any extra updates that have been scheduled.
1903
     */
1904
    private function executeExtraUpdates()
1905
    {
1906
        foreach ($this->extraUpdates as $oid => $update) {
1907
            list ($entity, $changeset) = $update;
1908
            $this->entityChangeSets[$oid] = $changeset;
1909
            $this->getEntityPersister(get_class($entity))->update($entity);
1910
        }
1911
        $this->extraUpdates = [];
1912
    }
1913
1914
    private function executeDeletions(ApiMetadata $class)
1915
    {
1916
        $className = $class->getName();
1917
        $persister = $this->getEntityPersister($className);
1918
        foreach ($this->entityDeletions as $oid => $entity) {
1919
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1920
                continue;
1921
            }
1922
            $persister->delete($entity);
1923
            unset(
1924
                $this->entityDeletions[$oid],
1925
                $this->entityIdentifiers[$oid],
1926
                $this->originalEntityData[$oid],
1927
                $this->entityStates[$oid]
1928
            );
1929
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1930
            // is obtained by a new entity because the old one went out of scope.
1931
            //$this->entityStates[$oid] = self::STATE_NEW;
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...
1932
            //            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...
1933
            //                $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...
1934
            //            }
1935
        }
1936
    }
1937
1938
    /**
1939
     * @param object $entity
1940
     * @param object $managedCopy
1941
     */
1942
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
1943
    {
1944
        $class = $this->manager->getClassMetadata(get_class($entity));
1945
        foreach ($this->reflectionPropertiesGetter->getProperties($class->getName()) as $prop) {
1946
            $name = $prop->name;
1947
            $prop->setAccessible(true);
1948
            if ($class->hasAssociation($name)) {
1949
                if (!$class->isIdentifier($name)) {
1950
                    $prop->setValue($managedCopy, $prop->getValue($entity));
1951
                }
1952
            } else {
1953
                $assoc2 = $class->getAssociationMapping($name);
1954
                if ($assoc2['type'] & ApiMetadata::TO_ONE) {
1955
                    $other = $prop->getValue($entity);
1956
                    if ($other === null) {
1957
                        $prop->setValue($managedCopy, null);
1958
                    } else {
1959
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
1960
                            // do not merge fields marked lazy that have not been fetched.
1961
                            continue;
1962
                        }
1963
                        if (!$assoc2['isCascadeMerge']) {
1964
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
1965
                                $targetClass = $this->manager->getClassMetadata($assoc2['targetEntity']);
1966
                                $relatedId   = $targetClass->getIdentifierValues($other);
1967
                                if ($targetClass->getSubclasses()) {
1968
                                    $other = $this->manager->find($targetClass->getName(), $relatedId);
1969
                                } else {
1970
                                    $other = $this->manager->getProxyFactory()->getProxy(
1971
                                        $assoc2['targetEntity'],
1972
                                        $relatedId
1973
                                    );
1974
                                    $this->registerManaged($other, $relatedId, []);
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...
1975
                                }
1976
                            }
1977
                            $prop->setValue($managedCopy, $other);
1978
                        }
1979
                    }
1980
                } else {
1981
                    $mergeCol = $prop->getValue($entity);
1982
                    if ($mergeCol instanceof ApiCollection && !$mergeCol->isInitialized()) {
1983
                        // do not merge fields marked lazy that have not been fetched.
1984
                        // keep the lazy persistent collection of the managed copy.
1985
                        continue;
1986
                    }
1987
                    $managedCol = $prop->getValue($managedCopy);
1988
                    if (!$managedCol) {
1989
                        $managedCol = new ApiCollection(
1990
                            $this->manager,
1991
                            $this->manager->getClassMetadata($assoc2['target']),
1992
                            new ArrayCollection
1993
                        );
1994
                        $managedCol->setOwner($managedCopy, $assoc2);
1995
                        $prop->setValue($managedCopy, $managedCol);
1996
                    }
1997
                    if ($assoc2['isCascadeMerge']) {
1998
                        $managedCol->initialize();
1999
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
2000
                        if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
2001
                            $managedCol->unwrap()->clear();
2002
                            $managedCol->setDirty(true);
2003
                            if ($assoc2['isOwningSide']
2004
                                && $assoc2['type'] == ApiMetadata::MANY_TO_MANY
2005
                                && $class->isChangeTrackingNotify()
2006
                            ) {
2007
                                $this->scheduleForDirtyCheck($managedCopy);
2008
                            }
2009
                        }
2010
                    }
2011
                }
2012
            }
2013
            if ($class->isChangeTrackingNotify()) {
2014
                // Just treat all properties as changed, there is no other choice.
2015
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2016
            }
2017
        }
2018
    }
2019
2020
    /**
2021
     * Deletes an entity as part of the current unit of work.
2022
     *
2023
     * This method is internally called during delete() cascades as it tracks
2024
     * the already visited entities to prevent infinite recursions.
2025
     *
2026
     * @param object $entity  The entity to delete.
2027
     * @param array  $visited The map of the already visited entities.
2028
     *
2029
     * @return void
2030
     *
2031
     * @throws \InvalidArgumentException If the instance is a detached entity.
2032
     * @throws \UnexpectedValueException
2033
     */
2034
    private function doRemove($entity, array &$visited)
2035
    {
2036
        $oid = spl_object_hash($entity);
2037
        if (isset($visited[$oid])) {
2038
            return; // Prevent infinite recursion
2039
        }
2040
        $visited[$oid] = $entity; // mark visited
2041
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
2042
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
2043
        $this->cascadeRemove($entity, $visited);
2044
        $class       = $this->manager->getClassMetadata(get_class($entity));
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...
2045
        $entityState = $this->getEntityState($entity);
2046
        switch ($entityState) {
2047
            case self::STATE_NEW:
2048
            case self::STATE_REMOVED:
2049
                // nothing to do
2050
                break;
2051
            case self::STATE_MANAGED:
2052
                $this->scheduleForDelete($entity);
2053
                break;
2054
            case self::STATE_DETACHED:
2055
                throw new \InvalidArgumentException('Detached entity cannot be removed');
2056
            default:
2057
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
2058
        }
2059
    }
2060
2061
    /**
2062
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2063
     *
2064
     * @param object $entity
2065
     *
2066
     * @return bool
2067
     */
2068
    private function isLoaded($entity)
2069
    {
2070
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2071
    }
2072
2073
    /**
2074
     * Sets/adds associated managed copies into the previous entity's association field
2075
     *
2076
     * @param object $entity
2077
     * @param array  $association
2078
     * @param object $previousManagedCopy
2079
     * @param object $managedCopy
2080
     *
2081
     * @return void
2082
     */
2083
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2084
    {
2085
        $assocField = $association['fieldName'];
2086
        $prevClass  = $this->manager->getClassMetadata(get_class($previousManagedCopy));
2087
        if ($association['type'] & ApiMetadata::TO_ONE) {
2088
            $prevClass->getReflectionProperty($assocField)->setValue($previousManagedCopy, $managedCopy);
2089
2090
            return;
2091
        }
2092
        /** @var array $value */
2093
        $value   = $prevClass->getReflectionProperty($assocField)->getValue($previousManagedCopy);
2094
        $value[] = $managedCopy;
2095
        if ($association['type'] == ApiMetadata::ONE_TO_MANY) {
2096
            $class = $this->manager->getClassMetadata(get_class($entity));
2097
            $class->getReflectionProperty($association['mappedBy'])->setValue($managedCopy, $previousManagedCopy);
2098
        }
2099
    }
2100
2101
    /**
2102
     * Executes a merge operation on an entity.
2103
     *
2104
     * @param object      $entity
2105
     * @param array       $visited
2106
     * @param object|null $prevManagedCopy
2107
     * @param array|null  $assoc
2108
     *
2109
     * @return object The managed copy of the entity.
2110
     *
2111
     * @throws \InvalidArgumentException If the entity instance is NEW.
2112
     * @throws \OutOfBoundsException
2113
     */
2114
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
2115
    {
2116
        $oid = spl_object_hash($entity);
2117
        if (isset($visited[$oid])) {
2118
            $managedCopy = $visited[$oid];
2119
            if ($prevManagedCopy !== null) {
2120
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2121
            }
2122
2123
            return $managedCopy;
2124
        }
2125
        $class = $this->manager->getClassMetadata(get_class($entity));
2126
        // First we assume DETACHED, although it can still be NEW but we can avoid
2127
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
2128
        // we need to fetch it from the db anyway in order to merge.
2129
        // MANAGED entities are ignored by the merge operation.
2130
        $managedCopy = $entity;
2131
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2132
            // Try to look the entity up in the identity map.
2133
            $id = $class->getIdentifierValues($entity);
2134
            // If there is no ID, it is actually NEW.
2135
            if (!$id) {
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...
2136
                $managedCopy = $this->newInstance($class);
2137
                $this->persistNew($class, $managedCopy);
2138
            } else {
2139
                $flatId      = ($class->containsForeignIdentifier())
2140
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
2141
                    : $id;
2142
                $managedCopy = $this->tryGetById($flatId, $class->getRootEntityName());
2143
                if ($managedCopy) {
2144
                    // We have the entity in-memory already, just make sure its not removed.
2145
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
2146
                        throw new \InvalidArgumentException('Removed entity cannot be merged');
2147
                    }
2148
                } else {
2149
                    // We need to fetch the managed copy in order to merge.
2150
                    $managedCopy = $this->manager->find($class->getName(), $flatId);
2151
                }
2152
                if ($managedCopy === null) {
2153
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
2154
                    // since the managed entity was not found.
2155
                    if (!$class->isIdentifierNatural()) {
2156
                        throw new \OutOfBoundsException('Entity not found');
2157
                    }
2158
                    $managedCopy = $this->newInstance($class);
2159
                    $class->setIdentifierValues($managedCopy, $id);
2160
                    $this->persistNew($class, $managedCopy);
2161
                }
2162
            }
2163
2164
            $visited[$oid] = $managedCopy; // mark visited
2165
            if ($this->isLoaded($entity)) {
2166
                if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized()) {
2167
                    $managedCopy->__load();
2168
                }
2169
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
2170
            }
2171
            if ($class->isChangeTrackingDeferredExplicit()) {
2172
                $this->scheduleForDirtyCheck($entity);
2173
            }
2174
        }
2175
        if ($prevManagedCopy !== null) {
2176
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2177
        }
2178
        // Mark the managed copy visited as well
2179
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
2180
        $this->cascadeMerge($entity, $managedCopy, $visited);
2181
2182
        return $managedCopy;
2183
    }
2184
2185
    /**
2186
     * Executes a detach operation on the given entity.
2187
     *
2188
     * @param object  $entity
2189
     * @param array   $visited
2190
     * @param boolean $noCascade if true, don't cascade detach operation.
2191
     *
2192
     * @return void
2193
     */
2194
    private function doDetach($entity, array &$visited, $noCascade = false)
2195
    {
2196
        $oid = spl_object_hash($entity);
2197
        if (isset($visited[$oid])) {
2198
            return; // Prevent infinite recursion
2199
        }
2200
        $visited[$oid] = $entity; // mark visited
2201
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2202
            case self::STATE_MANAGED:
2203
                if ($this->isInIdentityMap($entity)) {
2204
                    $this->removeFromIdentityMap($entity);
2205
                }
2206
                unset(
2207
                    $this->entityInsertions[$oid],
2208
                    $this->entityUpdates[$oid],
2209
                    $this->entityDeletions[$oid],
2210
                    $this->entityIdentifiers[$oid],
2211
                    $this->entityStates[$oid],
2212
                    $this->originalEntityData[$oid]
2213
                );
2214
                break;
2215
            case self::STATE_NEW:
2216
            case self::STATE_DETACHED:
2217
                return;
2218
        }
2219
        if (!$noCascade) {
2220
            $this->cascadeDetach($entity, $visited);
2221
        }
2222
    }
2223
2224
    /**
2225
     * @param ApiMetadata $class
2226
     *
2227
     * @return EntityHydrator
2228
     */
2229 18
    private function getHydratorForClass(ApiMetadata $class)
2230
    {
2231 18
        if (!array_key_exists($class->getName(), $this->hydrators)) {
2232 18
            $this->hydrators[$class->getName()] = new EntityHydrator($this->manager, $class);
2233 18
        }
2234
2235 18
        return $this->hydrators[$class->getName()];
2236
    }
2237
}
2238