Completed
Pull Request — master (#14)
by Pavel
04:42
created

UnitOfWork::propertyChanged()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

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

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

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

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

Loading history...
1081
1082
        return $this->collectionPersisters[$role];
1083
    }
1084
1085
    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...
1086
    {
1087
    }
1088
1089 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...
1090
    {
1091 2
    }
1092
1093
    /**
1094
     * INTERNAL:
1095
     * Sets a property value of the original data array of an entity.
1096
     *
1097
     * @ignore
1098
     *
1099
     * @param string $oid
1100
     * @param string $property
1101
     * @param mixed  $value
1102
     *
1103
     * @return void
1104
     */
1105 10
    public function setOriginalEntityProperty($oid, $property, $value)
1106
    {
1107 10
        if (!array_key_exists($oid, $this->originalEntityData)) {
1108 10
            $this->originalEntityData[$oid] = new \stdClass();
1109 10
        }
1110
1111 10
        $this->originalEntityData[$oid]->$property = $value;
1112 10
    }
1113
1114
    public function scheduleExtraUpdate($entity, $changeset)
1115
    {
1116
        $oid         = spl_object_hash($entity);
1117
        $extraUpdate = [$entity, $changeset];
1118
        if (isset($this->extraUpdates[$oid])) {
1119
            list(, $changeset2) = $this->extraUpdates[$oid];
1120
            $extraUpdate = [$entity, $changeset + $changeset2];
1121
        }
1122
        $this->extraUpdates[$oid] = $extraUpdate;
1123
    }
1124
1125
    /**
1126
     * Refreshes the state of the given entity from the database, overwriting
1127
     * any local, unpersisted changes.
1128
     *
1129
     * @param object $entity The entity to refresh.
1130
     *
1131
     * @return void
1132
     *
1133
     * @throws InvalidArgumentException If the entity is not MANAGED.
1134
     */
1135
    public function refresh($entity)
1136
    {
1137
        $visited = [];
1138
        $this->doRefresh($entity, $visited);
1139
    }
1140
1141
    /**
1142
     * Clears the UnitOfWork.
1143
     *
1144
     * @param string|null $entityName if given, only entities of this type will get detached.
1145
     *
1146
     * @return void
1147
     */
1148
    public function clear($entityName = null)
1149
    {
1150
        if ($entityName === null) {
1151
            $this->identityMap =
1152
            $this->entityIdentifiers =
1153
            $this->originalEntityData =
1154
            $this->entityChangeSets =
1155
            $this->entityStates =
1156
            $this->scheduledForSynchronization =
1157
            $this->entityInsertions =
1158
            $this->entityUpdates =
1159
            $this->entityDeletions =
1160
            $this->collectionDeletions =
1161
            $this->collectionUpdates =
1162
            $this->extraUpdates =
1163
            $this->readOnlyObjects =
1164
            $this->visitedCollections =
1165
            $this->orphanRemovals = [];
1166
        } else {
1167
            $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...
1168
            $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...
1169
        }
1170
    }
1171
1172
    /**
1173
     * @param PersistentCollection $coll
1174
     *
1175
     * @return bool
1176
     */
1177
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
1178
    {
1179
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
1180
    }
1181
1182
    /**
1183
     * Schedules an entity for dirty-checking at commit-time.
1184
     *
1185
     * @param object $entity The entity to schedule for dirty-checking.
1186
     *
1187
     * @return void
1188
     *
1189
     * @todo Rename: scheduleForSynchronization
1190
     */
1191
    public function scheduleForDirtyCheck($entity)
1192
    {
1193
        $rootClassName                                                               =
1194
            $this->manager->getClassMetadata(get_class($entity))->getRootEntityName();
1195
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
1196
    }
1197
1198
    /**
1199
     * Deletes an entity as part of the current unit of work.
1200
     *
1201
     * @param object $entity The entity to remove.
1202
     *
1203
     * @return void
1204
     */
1205
    public function remove($entity)
1206
    {
1207
        $visited = [];
1208
        $this->doRemove($entity, $visited);
1209
    }
1210
1211
    /**
1212
     * Merges the state of the given detached entity into this UnitOfWork.
1213
     *
1214
     * @param object $entity
1215
     *
1216
     * @return object The managed copy of the entity.
1217
     */
1218
    public function merge($entity)
1219
    {
1220
        $visited = [];
1221
1222
        return $this->doMerge($entity, $visited);
1223
    }
1224
1225
    /**
1226
     * Detaches an entity from the persistence management. It's persistence will
1227
     * no longer be managed by Doctrine.
1228
     *
1229
     * @param object $entity The entity to detach.
1230
     *
1231
     * @return void
1232
     */
1233
    public function detach($entity)
1234
    {
1235
        $visited = [];
1236
        $this->doDetach($entity, $visited);
1237
    }
1238
1239
    /**
1240
     * Helper method to show an object as string.
1241
     *
1242
     * @param object $obj
1243
     *
1244
     * @return string
1245
     */
1246
    private static function objToStr($obj)
1247
    {
1248
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
1249
    }
1250
1251
    /**
1252
     * @param ApiMetadata $class
1253
     *
1254
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
1255
     */
1256 12
    private function newInstance(ApiMetadata $class)
1257
    {
1258 12
        $entity = $class->newInstance();
1259
1260 12
        if ($entity instanceof ObjectManagerAware) {
1261
            $entity->injectObjectManager($this->manager, $class);
1262
        }
1263
1264 12
        return $entity;
1265
    }
1266
1267
    /**
1268
     * @param ApiMetadata $classMetadata
1269
     *
1270
     * @return EntityDataCacheInterface
1271
     */
1272 19
    private function createEntityCache(ApiMetadata $classMetadata)
1273
    {
1274 19
        $configuration = $this->manager->getConfiguration()->getCacheConfiguration($classMetadata->getName());
1275 19
        $cache         = new VoidEntityCache($classMetadata);
1276 19
        if ($configuration->isEnabled() && $this->manager->getConfiguration()->getApiCache()) {
1277
            $cache =
1278 1
                new LoggingCache(
1279 1
                    new ApiEntityCache(
1280 1
                        $this->manager->getConfiguration()->getApiCache(),
1281 1
                        $classMetadata,
1282
                        $configuration
1283 1
                    ),
1284 1
                    $this->manager->getConfiguration()->getApiCacheLogger()
1285 1
                );
1286
1287 1
            return $cache;
1288
        }
1289
1290 18
        return $cache;
1291
    }
1292
1293
    /**
1294
     * @param ApiMetadata $classMetadata
1295
     *
1296
     * @return CrudsApiInterface
1297
     */
1298 19
    private function createApi(ApiMetadata $classMetadata)
1299
    {
1300 19
        $client = $this->manager->getConfiguration()->getClientRegistry()->get($classMetadata->getClientName());
1301
1302 19
        $api = $this->manager
1303 19
            ->getConfiguration()
1304 19
            ->getFactoryRegistry()
1305 19
            ->create(
1306 19
                $classMetadata->getApiFactory(),
1307 19
                $client,
1308
                $classMetadata
1309 19
            );
1310
1311 19
        return $api;
1312
    }
1313
1314 5
    private function doPersist($entity, $visited)
1315
    {
1316 5
        $oid = spl_object_hash($entity);
1317 5
        if (isset($visited[$oid])) {
1318
            return; // Prevent infinite recursion
1319
        }
1320 5
        $visited[$oid] = $entity; // Mark visited
1321 5
        $class         = $this->manager->getClassMetadata(get_class($entity));
1322
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1323
        // If we would detect DETACHED here we would throw an exception anyway with the same
1324
        // consequences (not recoverable/programming error), so just assuming NEW here
1325
        // lets us avoid some database lookups for entities with natural identifiers.
1326 5
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1327
        switch ($entityState) {
1328 5
            case self::STATE_MANAGED:
1329
                $this->scheduleForDirtyCheck($entity);
1330
                break;
1331 5
            case self::STATE_NEW:
1332 5
                $this->persistNew($class, $entity);
1333 5
                break;
1334
            case self::STATE_REMOVED:
1335
                // Entity becomes managed again
1336
                unset($this->entityDeletions[$oid]);
1337
                $this->addToIdentityMap($entity);
1338
                $this->entityStates[$oid] = self::STATE_MANAGED;
1339
                break;
1340
            case self::STATE_DETACHED:
1341
                // Can actually not happen right now since we assume STATE_NEW.
1342
                throw new \InvalidArgumentException('Detached entity cannot be persisted');
1343
            default:
1344
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1345
        }
1346 5
        $this->cascadePersist($entity, $visited);
1347 5
    }
1348
1349
    /**
1350
     * Cascades the save operation to associated entities.
1351
     *
1352
     * @param object $entity
1353
     * @param array  $visited
1354
     *
1355
     * @return void
1356
     * @throws \InvalidArgumentException
1357
     * @throws MappingException
1358
     */
1359 5
    private function cascadePersist($entity, array &$visited)
1360
    {
1361 5
        $class               = $this->manager->getClassMetadata(get_class($entity));
1362 5
        $associationMappings = [];
1363 5
        foreach ($class->getAssociationNames() as $name) {
1364 5
            $assoc = $class->getAssociationMapping($name);
1365 5
            if ($assoc['isCascadePersist']) {
1366
                $associationMappings[$name] = $assoc;
1367
            }
1368 5
        }
1369 5
        foreach ($associationMappings as $assoc) {
1370
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1371
            switch (true) {
1372
                case ($relatedEntities instanceof ApiCollection):
1373
                    // Unwrap so that foreach() does not initialize
1374
                    $relatedEntities = $relatedEntities->unwrap();
1375
                // break; is commented intentionally!
1376
                case ($relatedEntities instanceof Collection):
1377
                case (is_array($relatedEntities)):
1378
                    if (($assoc['type'] & ApiMetadata::TO_MANY) <= 0) {
1379
                        throw new \InvalidArgumentException('Invalid association for cascade');
1380
                    }
1381
                    foreach ($relatedEntities as $relatedEntity) {
1382
                        $this->doPersist($relatedEntity, $visited);
1383
                    }
1384
                    break;
1385
                case ($relatedEntities !== null):
1386
                    if (!$relatedEntities instanceof $assoc['target']) {
1387
                        throw new \InvalidArgumentException('Invalid association for cascade');
1388
                    }
1389
                    $this->doPersist($relatedEntities, $visited);
1390
                    break;
1391
                default:
1392
                    // Do nothing
1393
            }
1394 5
        }
1395 5
    }
1396
1397
    /**
1398
     * @param ApiMetadata $class
1399
     * @param object      $entity
1400
     *
1401
     * @return void
1402
     */
1403 5
    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...
1404
    {
1405 5
        $oid = spl_object_hash($entity);
1406
        //        $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...
1407
        //        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...
1408
        //            $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...
1409
        //        }
1410
        //        $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...
1411
        //        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...
1412
        //            $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...
1413
        //            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...
1414
        //                $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...
1415
        //                $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...
1416
        //            }
1417
        //            $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...
1418
        //        }
1419 5
        $this->entityStates[$oid] = self::STATE_MANAGED;
1420 5
        $this->scheduleForInsert($entity);
1421 5
    }
1422
1423
    /**
1424
     * Gets the commit order.
1425
     *
1426
     * @param array|null $entityChangeSet
1427
     *
1428
     * @return array
1429
     */
1430 5
    private function getCommitOrder(array $entityChangeSet = null)
1431
    {
1432 5
        if ($entityChangeSet === null) {
1433 5
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1434 5
        }
1435 5
        $calc = $this->getCommitOrderCalculator();
1436
        // See if there are any new classes in the changeset, that are not in the
1437
        // commit order graph yet (don't have a node).
1438
        // We have to inspect changeSet to be able to correctly build dependencies.
1439
        // It is not possible to use IdentityMap here because post inserted ids
1440
        // are not yet available.
1441
        /** @var ApiMetadata[] $newNodes */
1442 5
        $newNodes = [];
1443 5
        foreach ((array)$entityChangeSet as $entity) {
1444 5
            $class = $this->manager->getClassMetadata(get_class($entity));
1445 5
            if ($calc->hasNode($class->getName())) {
1446
                continue;
1447
            }
1448 5
            $calc->addNode($class->getName(), $class);
1449 5
            $newNodes[] = $class;
1450 5
        }
1451
        // Calculate dependencies for new nodes
1452 5
        while ($class = array_pop($newNodes)) {
1453 5
            foreach ($class->getAssociationMappings() as $assoc) {
1454 5
                if (!($assoc['isOwningSide'] && $assoc['type'] & ApiMetadata::TO_ONE)) {
1455 5
                    continue;
1456
                }
1457 5
                $targetClass = $this->manager->getClassMetadata($assoc['target']);
1458 5
                if (!$calc->hasNode($targetClass->getName())) {
1459 3
                    $calc->addNode($targetClass->getName(), $targetClass);
1460 3
                    $newNodes[] = $targetClass;
1461 3
                }
1462 5
                $calc->addDependency($targetClass->getName(), $class->name, (int)empty($assoc['nullable']));
1463
                // If the target class has mapped subclasses, these share the same dependency.
1464 5
                if (!$targetClass->getSubclasses()) {
1465 5
                    continue;
1466
                }
1467
                foreach ($targetClass->getSubclasses() as $subClassName) {
1468
                    $targetSubClass = $this->manager->getClassMetadata($subClassName);
1469
                    if (!$calc->hasNode($subClassName)) {
1470
                        $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...
1471
                        $newNodes[] = $targetSubClass;
1472
                    }
1473
                    $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...
1474
                }
1475 5
            }
1476 5
        }
1477
1478 5
        return $calc->sort();
1479
    }
1480
1481 5
    private function getCommitOrderCalculator()
1482
    {
1483 5
        return new Utility\CommitOrderCalculator();
1484
    }
1485
1486
    /**
1487
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
1488
     *
1489
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
1490
     * 2. Read Only entities are skipped.
1491
     * 3. Proxies are skipped.
1492
     * 4. Only if entity is properly managed.
1493
     *
1494
     * @param object $entity
1495
     *
1496
     * @return void
1497
     *
1498
     * @throws \InvalidArgumentException
1499
     */
1500
    private function computeSingleEntityChangeSet($entity)
1501
    {
1502
        $state = $this->getEntityState($entity);
1503
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
1504
            throw new \InvalidArgumentException(
1505
                "Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)
1506
            );
1507
        }
1508
        $class = $this->manager->getClassMetadata(get_class($entity));
1509
        // Compute changes for INSERTed entities first. This must always happen even in this case.
1510
        $this->computeScheduleInsertsChangeSets();
1511
        if ($class->isReadOnly()) {
1512
            return;
1513
        }
1514
        // Ignore uninitialized proxy objects
1515
        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...
1516
            return;
1517
        }
1518
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
1519
        $oid = spl_object_hash($entity);
1520 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...
1521
            !isset($this->entityDeletions[$oid]) &&
1522
            isset($this->entityStates[$oid])
1523
        ) {
1524
            $this->computeChangeSet($class, $entity);
1525
        }
1526
    }
1527
1528
    /**
1529
     * Computes the changesets of all entities scheduled for insertion.
1530
     *
1531
     * @return void
1532
     */
1533 5
    private function computeScheduleInsertsChangeSets()
1534
    {
1535 5
        foreach ($this->entityInsertions as $entity) {
1536 5
            $class = $this->manager->getClassMetadata(get_class($entity));
1537 5
            $this->computeChangeSet($class, $entity);
1538 5
        }
1539 5
    }
1540
1541
    /**
1542
     * Computes the changes of an association.
1543
     *
1544
     * @param array $assoc The association mapping.
1545
     * @param mixed $value The value of the association.
1546
     *
1547
     * @throws \InvalidArgumentException
1548
     * @throws \UnexpectedValueException
1549
     *
1550
     * @return void
1551
     */
1552 2
    private function computeAssociationChanges($assoc, $value)
1553
    {
1554 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...
1555
            return;
1556
        }
1557 2
        if ($value instanceof ApiCollection && $value->isDirty()) {
1558 2
            $coid                            = spl_object_hash($value);
1559 2
            $this->collectionUpdates[$coid]  = $value;
1560 2
            $this->visitedCollections[$coid] = $value;
1561 2
        }
1562
        // Look through the entities, and in any of their associations,
1563
        // 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...
1564
        // Unwrap. Uninitialized collections will simply be empty.
1565 2
        $unwrappedValue  = ($assoc['type'] & ApiMetadata::TO_ONE) ? [$value] : $value->unwrap();
1566 2
        $targetClass     = $this->manager->getClassMetadata($assoc['target']);
1567 2
        $targetClassName = $targetClass->getName();
1568 2
        foreach ($unwrappedValue as $key => $entry) {
1569 2
            if (!($entry instanceof $targetClassName)) {
1570
                throw new \InvalidArgumentException('Invalid association');
1571
            }
1572 2
            $state = $this->getEntityState($entry, self::STATE_NEW);
1573 2
            if (!($entry instanceof $assoc['target'])) {
1574
                throw new \UnexpectedValueException('Unexpected association');
1575
            }
1576
            switch ($state) {
1577 2
                case self::STATE_NEW:
1578
                    if (!$assoc['isCascadePersist']) {
1579
                        throw new \InvalidArgumentException('New entity through relationship');
1580
                    }
1581
                    $this->persistNew($targetClass, $entry);
1582
                    $this->computeChangeSet($targetClass, $entry);
1583
                    break;
1584 2
                case self::STATE_REMOVED:
1585
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1586
                    // and remove the element from Collection.
1587
                    if ($assoc['type'] & ApiMetadata::TO_MANY) {
1588
                        unset($value[$key]);
1589
                    }
1590
                    break;
1591 2
                case self::STATE_DETACHED:
1592
                    // Can actually not happen right now as we assume STATE_NEW,
1593
                    // so the exception will be raised from the DBAL layer (constraint violation).
1594
                    throw new \InvalidArgumentException('Detached entity through relationship');
1595
                    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...
1596 2
                default:
1597
                    // MANAGED associated entities are already taken into account
1598
                    // during changeset calculation anyway, since they are in the identity map.
1599 2
            }
1600 2
        }
1601 2
    }
1602
1603 5
    private function executeInserts(ApiMetadata $class)
1604
    {
1605 5
        $className = $class->getName();
1606 5
        $persister = $this->getEntityPersister($className);
1607 5
        foreach ($this->entityInsertions as $oid => $entity) {
1608 5
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1609 5
                continue;
1610
            }
1611 5
            $persister->pushNewEntity($entity);
1612 5
            unset($this->entityInsertions[$oid]);
1613 5
        }
1614 5
        $postInsertIds = $persister->flushNewEntities();
1615 5
        if ($postInsertIds) {
1616
            // Persister returned post-insert IDs
1617 5
            foreach ($postInsertIds as $postInsertId) {
1618 5
                $id     = $postInsertId['generatedId'];
1619 5
                $entity = $postInsertId['entity'];
1620 5
                $oid    = spl_object_hash($entity);
1621
1622 5
                if ($id instanceof \stdClass) {
1623 3
                    $id = (array)$id;
1624 3
                }
1625 5
                if (!is_array($id)) {
1626 2
                    $id = [$class->getApiFieldName($class->getIdentifierFieldNames()[0]) => $id];
1627 2
                }
1628
1629 5
                if (!array_key_exists($oid, $this->originalEntityData)) {
1630
                    $this->originalEntityData[$oid] = new \stdClass();
1631
                }
1632
1633 5
                $idValues = [];
1634 5
                foreach ((array)$id as $apiIdField => $idValue) {
1635 5
                    $idName   = $class->getFieldName($apiIdField);
1636 5
                    $typeName = $class->getTypeOfField($idName);
1637 5
                    $type     = $this->manager->getConfiguration()->getTypeRegistry()->get($typeName);
1638 5
                    $idValue  = $type->toApiValue($idValue);
1639 5
                    $class->getReflectionProperty($idName)->setValue($entity, $idValue);
1640 5
                    $idValues[$idName] =  $idValue;
1641 5
                    $this->originalEntityData[$oid]->$idName = $idValue;
1642 5
                }
1643
1644 5
                $this->entityIdentifiers[$oid]  = $idValues;
1645 5
                $this->entityStates[$oid]       = self::STATE_MANAGED;
1646 5
                $this->addToIdentityMap($entity);
1647 5
            }
1648 5
        }
1649 5
    }
1650
1651 1
    private function executeUpdates($class)
1652
    {
1653 1
        $className = $class->name;
1654 1
        $persister = $this->getEntityPersister($className);
1655 1
        foreach ($this->entityUpdates as $oid => $entity) {
1656 1
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1657 1
                continue;
1658
            }
1659 1
            $this->recomputeSingleEntityChangeSet($class, $entity);
1660
1661 1
            if (!empty($this->entityChangeSets[$oid])) {
1662 1
                $persister->update($entity);
1663 1
            }
1664 1
            unset($this->entityUpdates[$oid]);
1665 1
        }
1666 1
    }
1667
1668
    /**
1669
     * Executes a refresh operation on an entity.
1670
     *
1671
     * @param object $entity  The entity to refresh.
1672
     * @param array  $visited The already visited entities during cascades.
1673
     *
1674
     * @return void
1675
     *
1676
     * @throws \InvalidArgumentException If the entity is not MANAGED.
1677
     */
1678
    private function doRefresh($entity, array &$visited)
1679
    {
1680
        $oid = spl_object_hash($entity);
1681
        if (isset($visited[$oid])) {
1682
            return; // Prevent infinite recursion
1683
        }
1684
        $visited[$oid] = $entity; // mark visited
1685
        $class         = $this->manager->getClassMetadata(get_class($entity));
1686
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1687
            throw new \InvalidArgumentException('Entity not managed');
1688
        }
1689
        $this->getEntityPersister($class->getName())->refresh(
1690
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1691
            $entity
1692
        );
1693
        $this->cascadeRefresh($entity, $visited);
1694
    }
1695
1696
    /**
1697
     * Cascades a refresh operation to associated entities.
1698
     *
1699
     * @param object $entity
1700
     * @param array  $visited
1701
     *
1702
     * @return void
1703
     */
1704 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...
1705
    {
1706
        $class               = $this->manager->getClassMetadata(get_class($entity));
1707
        $associationMappings = array_filter(
1708
            $class->getAssociationMappings(),
1709
            function ($assoc) {
1710
                return $assoc['isCascadeRefresh'];
1711
            }
1712
        );
1713
        foreach ($associationMappings as $assoc) {
1714
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1715
            switch (true) {
1716
                case ($relatedEntities instanceof ApiCollection):
1717
                    // Unwrap so that foreach() does not initialize
1718
                    $relatedEntities = $relatedEntities->unwrap();
1719
                // break; is commented intentionally!
1720
                case ($relatedEntities instanceof Collection):
1721
                case (is_array($relatedEntities)):
1722
                    foreach ($relatedEntities as $relatedEntity) {
1723
                        $this->doRefresh($relatedEntity, $visited);
1724
                    }
1725
                    break;
1726
                case ($relatedEntities !== null):
1727
                    $this->doRefresh($relatedEntities, $visited);
1728
                    break;
1729
                default:
1730
                    // Do nothing
1731
            }
1732
        }
1733
    }
1734
1735
    /**
1736
     * Cascades a detach operation to associated entities.
1737
     *
1738
     * @param object $entity
1739
     * @param array  $visited
1740
     *
1741
     * @return void
1742
     */
1743 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...
1744
    {
1745
        $class               = $this->manager->getClassMetadata(get_class($entity));
1746
        $associationMappings = array_filter(
1747
            $class->getAssociationMappings(),
1748
            function ($assoc) {
1749
                return $assoc['isCascadeDetach'];
1750
            }
1751
        );
1752
        foreach ($associationMappings as $assoc) {
1753
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1754
            switch (true) {
1755
                case ($relatedEntities instanceof ApiCollection):
1756
                    // Unwrap so that foreach() does not initialize
1757
                    $relatedEntities = $relatedEntities->unwrap();
1758
                // break; is commented intentionally!
1759
                case ($relatedEntities instanceof Collection):
1760
                case (is_array($relatedEntities)):
1761
                    foreach ($relatedEntities as $relatedEntity) {
1762
                        $this->doDetach($relatedEntity, $visited);
1763
                    }
1764
                    break;
1765
                case ($relatedEntities !== null):
1766
                    $this->doDetach($relatedEntities, $visited);
1767
                    break;
1768
                default:
1769
                    // Do nothing
1770
            }
1771
        }
1772
    }
1773
1774
    /**
1775
     * Cascades a merge operation to associated entities.
1776
     *
1777
     * @param object $entity
1778
     * @param object $managedCopy
1779
     * @param array  $visited
1780
     *
1781
     * @return void
1782
     */
1783
    private function cascadeMerge($entity, $managedCopy, array &$visited)
1784
    {
1785
        $class               = $this->manager->getClassMetadata(get_class($entity));
1786
        $associationMappings = array_filter(
1787
            $class->getAssociationMappings(),
1788
            function ($assoc) {
1789
                return $assoc['isCascadeMerge'];
1790
            }
1791
        );
1792
        foreach ($associationMappings as $assoc) {
1793
            $relatedEntities = $class->getReflectionProperty($assoc['field'])->getValue($entity);
1794
            if ($relatedEntities instanceof Collection) {
1795
                if ($relatedEntities === $class->getReflectionProperty($assoc['field'])->getValue($managedCopy)) {
1796
                    continue;
1797
                }
1798
                if ($relatedEntities instanceof ApiCollection) {
1799
                    // Unwrap so that foreach() does not initialize
1800
                    $relatedEntities = $relatedEntities->unwrap();
1801
                }
1802
                foreach ($relatedEntities as $relatedEntity) {
1803
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
1804
                }
1805
            } else {
1806
                if ($relatedEntities !== null) {
1807
                    $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
1808
                }
1809
            }
1810
        }
1811
    }
1812
1813
    /**
1814
     * Cascades the delete operation to associated entities.
1815
     *
1816
     * @param object $entity
1817
     * @param array  $visited
1818
     *
1819
     * @return void
1820
     */
1821
    private function cascadeRemove($entity, array &$visited)
1822
    {
1823
        $class               = $this->manager->getClassMetadata(get_class($entity));
1824
        $associationMappings = array_filter(
1825
            $class->getAssociationMappings(),
1826
            function ($assoc) {
1827
                return $assoc['isCascadeRemove'];
1828
            }
1829
        );
1830
        $entitiesToCascade   = [];
1831
        foreach ($associationMappings as $assoc) {
1832
            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...
1833
                $entity->__load();
1834
            }
1835
            $relatedEntities = $class->getReflectionProperty($assoc['fieldName'])->getValue($entity);
1836
            switch (true) {
1837
                case ($relatedEntities instanceof Collection):
1838
                case (is_array($relatedEntities)):
1839
                    // If its a PersistentCollection initialization is intended! No unwrap!
1840
                    foreach ($relatedEntities as $relatedEntity) {
1841
                        $entitiesToCascade[] = $relatedEntity;
1842
                    }
1843
                    break;
1844
                case ($relatedEntities !== null):
1845
                    $entitiesToCascade[] = $relatedEntities;
1846
                    break;
1847
                default:
1848
                    // Do nothing
1849
            }
1850
        }
1851
        foreach ($entitiesToCascade as $relatedEntity) {
1852
            $this->doRemove($relatedEntity, $visited);
1853
        }
1854
    }
1855
1856
    /**
1857
     * Executes any extra updates that have been scheduled.
1858
     */
1859
    private function executeExtraUpdates()
1860
    {
1861
        foreach ($this->extraUpdates as $oid => $update) {
1862
            list ($entity, $changeset) = $update;
1863
            $this->entityChangeSets[$oid] = $changeset;
1864
            $this->getEntityPersister(get_class($entity))->update($entity);
1865
        }
1866
        $this->extraUpdates = [];
1867
    }
1868
1869
    private function executeDeletions(ApiMetadata $class)
1870
    {
1871
        $className = $class->getName();
1872
        $persister = $this->getEntityPersister($className);
1873
        foreach ($this->entityDeletions as $oid => $entity) {
1874
            if ($this->manager->getClassMetadata(get_class($entity))->getName() !== $className) {
1875
                continue;
1876
            }
1877
            $persister->delete($entity);
1878
            unset(
1879
                $this->entityDeletions[$oid],
1880
                $this->entityIdentifiers[$oid],
1881
                $this->originalEntityData[$oid],
1882
                $this->entityStates[$oid]
1883
            );
1884
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1885
            // is obtained by a new entity because the old one went out of scope.
1886
            //$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...
1887
            //            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...
1888
            //                $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...
1889
            //            }
1890
        }
1891
    }
1892
1893
    /**
1894
     * @param object $entity
1895
     * @param object $managedCopy
1896
     */
1897
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
1898
    {
1899
        $class = $this->manager->getClassMetadata(get_class($entity));
1900
        foreach ($this->reflectionPropertiesGetter->getProperties($class->getName()) as $prop) {
1901
            $name = $prop->name;
1902
            $prop->setAccessible(true);
1903
            if ($class->hasAssociation($name)) {
1904
                if (!$class->isIdentifier($name)) {
1905
                    $prop->setValue($managedCopy, $prop->getValue($entity));
1906
                }
1907
            } else {
1908
                $assoc2 = $class->getAssociationMapping($name);
1909
                if ($assoc2['type'] & ApiMetadata::TO_ONE) {
1910
                    $other = $prop->getValue($entity);
1911
                    if ($other === null) {
1912
                        $prop->setValue($managedCopy, null);
1913
                    } else {
1914
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
1915
                            // do not merge fields marked lazy that have not been fetched.
1916
                            continue;
1917
                        }
1918
                        if (!$assoc2['isCascadeMerge']) {
1919
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
1920
                                $targetClass = $this->manager->getClassMetadata($assoc2['targetEntity']);
1921
                                $relatedId   = $targetClass->getIdentifierValues($other);
1922
                                if ($targetClass->getSubclasses()) {
1923
                                    $other = $this->manager->find($targetClass->getName(), $relatedId);
1924
                                } else {
1925
                                    $other = $this->manager->getProxyFactory()->getProxy(
1926
                                        $assoc2['targetEntity'],
1927
                                        $relatedId
1928
                                    );
1929
                                    $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...
1930
                                }
1931
                            }
1932
                            $prop->setValue($managedCopy, $other);
1933
                        }
1934
                    }
1935
                } else {
1936
                    $mergeCol = $prop->getValue($entity);
1937
                    if ($mergeCol instanceof ApiCollection && !$mergeCol->isInitialized()) {
1938
                        // do not merge fields marked lazy that have not been fetched.
1939
                        // keep the lazy persistent collection of the managed copy.
1940
                        continue;
1941
                    }
1942
                    $managedCol = $prop->getValue($managedCopy);
1943
                    if (!$managedCol) {
1944
                        $managedCol = new ApiCollection(
1945
                            $this->manager,
1946
                            $this->manager->getClassMetadata($assoc2['target']),
1947
                            new ArrayCollection
1948
                        );
1949
                        $managedCol->setOwner($managedCopy, $assoc2);
1950
                        $prop->setValue($managedCopy, $managedCol);
1951
                    }
1952
                    if ($assoc2['isCascadeMerge']) {
1953
                        $managedCol->initialize();
1954
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
1955
                        if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
1956
                            $managedCol->unwrap()->clear();
1957
                            $managedCol->setDirty(true);
1958
                            if ($assoc2['isOwningSide']
1959
                                && $assoc2['type'] == ApiMetadata::MANY_TO_MANY
1960
                                && $class->isChangeTrackingNotify()
1961
                            ) {
1962
                                $this->scheduleForDirtyCheck($managedCopy);
1963
                            }
1964
                        }
1965
                    }
1966
                }
1967
            }
1968
            if ($class->isChangeTrackingNotify()) {
1969
                // Just treat all properties as changed, there is no other choice.
1970
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1971
            }
1972
        }
1973
    }
1974
1975
    /**
1976
     * Deletes an entity as part of the current unit of work.
1977
     *
1978
     * This method is internally called during delete() cascades as it tracks
1979
     * the already visited entities to prevent infinite recursions.
1980
     *
1981
     * @param object $entity  The entity to delete.
1982
     * @param array  $visited The map of the already visited entities.
1983
     *
1984
     * @return void
1985
     *
1986
     * @throws \InvalidArgumentException If the instance is a detached entity.
1987
     * @throws \UnexpectedValueException
1988
     */
1989
    private function doRemove($entity, array &$visited)
1990
    {
1991
        $oid = spl_object_hash($entity);
1992
        if (isset($visited[$oid])) {
1993
            return; // Prevent infinite recursion
1994
        }
1995
        $visited[$oid] = $entity; // mark visited
1996
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1997
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1998
        $this->cascadeRemove($entity, $visited);
1999
        $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...
2000
        $entityState = $this->getEntityState($entity);
2001
        switch ($entityState) {
2002
            case self::STATE_NEW:
2003
            case self::STATE_REMOVED:
2004
                // nothing to do
2005
                break;
2006
            case self::STATE_MANAGED:
2007
                $this->scheduleForDelete($entity);
2008
                break;
2009
            case self::STATE_DETACHED:
2010
                throw new \InvalidArgumentException('Detached entity cannot be removed');
2011
            default:
2012
                throw new \UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
2013
        }
2014
    }
2015
2016
    /**
2017
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2018
     *
2019
     * @param object $entity
2020
     *
2021
     * @return bool
2022
     */
2023
    private function isLoaded($entity)
2024
    {
2025
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2026
    }
2027
2028
    /**
2029
     * Sets/adds associated managed copies into the previous entity's association field
2030
     *
2031
     * @param object $entity
2032
     * @param array  $association
2033
     * @param object $previousManagedCopy
2034
     * @param object $managedCopy
2035
     *
2036
     * @return void
2037
     */
2038
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2039
    {
2040
        $assocField = $association['fieldName'];
2041
        $prevClass  = $this->manager->getClassMetadata(get_class($previousManagedCopy));
2042
        if ($association['type'] & ApiMetadata::TO_ONE) {
2043
            $prevClass->getReflectionProperty($assocField)->setValue($previousManagedCopy, $managedCopy);
2044
2045
            return;
2046
        }
2047
        /** @var array $value */
2048
        $value   = $prevClass->getReflectionProperty($assocField)->getValue($previousManagedCopy);
2049
        $value[] = $managedCopy;
2050
        if ($association['type'] == ApiMetadata::ONE_TO_MANY) {
2051
            $class = $this->manager->getClassMetadata(get_class($entity));
2052
            $class->getReflectionProperty($association['mappedBy'])->setValue($managedCopy, $previousManagedCopy);
2053
        }
2054
    }
2055
2056
    /**
2057
     * Executes a merge operation on an entity.
2058
     *
2059
     * @param object      $entity
2060
     * @param array       $visited
2061
     * @param object|null $prevManagedCopy
2062
     * @param array|null  $assoc
2063
     *
2064
     * @return object The managed copy of the entity.
2065
     *
2066
     * @throws \InvalidArgumentException If the entity instance is NEW.
2067
     * @throws \OutOfBoundsException
2068
     */
2069
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
2070
    {
2071
        $oid = spl_object_hash($entity);
2072
        if (isset($visited[$oid])) {
2073
            $managedCopy = $visited[$oid];
2074
            if ($prevManagedCopy !== null) {
2075
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2076
            }
2077
2078
            return $managedCopy;
2079
        }
2080
        $class = $this->manager->getClassMetadata(get_class($entity));
2081
        // First we assume DETACHED, although it can still be NEW but we can avoid
2082
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
2083
        // we need to fetch it from the db anyway in order to merge.
2084
        // MANAGED entities are ignored by the merge operation.
2085
        $managedCopy = $entity;
2086
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2087
            // Try to look the entity up in the identity map.
2088
            $id = $class->getIdentifierValues($entity);
2089
            // If there is no ID, it is actually NEW.
2090
            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...
2091
                $managedCopy = $this->newInstance($class);
2092
                $this->persistNew($class, $managedCopy);
2093
            } else {
2094
                $flatId      = ($class->containsForeignIdentifier())
2095
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
2096
                    : $id;
2097
                $managedCopy = $this->tryGetById($flatId, $class->getRootEntityName());
2098
                if ($managedCopy) {
2099
                    // We have the entity in-memory already, just make sure its not removed.
2100
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
2101
                        throw new \InvalidArgumentException('Removed entity cannot be merged');
2102
                    }
2103
                } else {
2104
                    // We need to fetch the managed copy in order to merge.
2105
                    $managedCopy = $this->manager->find($class->getName(), $flatId);
2106
                }
2107
                if ($managedCopy === null) {
2108
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
2109
                    // since the managed entity was not found.
2110
                    if (!$class->isIdentifierNatural()) {
2111
                        throw new \OutOfBoundsException('Entity not found');
2112
                    }
2113
                    $managedCopy = $this->newInstance($class);
2114
                    $class->setIdentifierValues($managedCopy, $id);
2115
                    $this->persistNew($class, $managedCopy);
2116
                }
2117
            }
2118
2119
            $visited[$oid] = $managedCopy; // mark visited
2120
            if ($this->isLoaded($entity)) {
2121
                if ($managedCopy instanceof Proxy && !$managedCopy->__isInitialized()) {
2122
                    $managedCopy->__load();
2123
                }
2124
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
2125
            }
2126
            if ($class->isChangeTrackingDeferredExplicit()) {
2127
                $this->scheduleForDirtyCheck($entity);
2128
            }
2129
        }
2130
        if ($prevManagedCopy !== null) {
2131
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
2132
        }
2133
        // Mark the managed copy visited as well
2134
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
2135
        $this->cascadeMerge($entity, $managedCopy, $visited);
2136
2137
        return $managedCopy;
2138
    }
2139
2140
    /**
2141
     * Executes a detach operation on the given entity.
2142
     *
2143
     * @param object  $entity
2144
     * @param array   $visited
2145
     * @param boolean $noCascade if true, don't cascade detach operation.
2146
     *
2147
     * @return void
2148
     */
2149
    private function doDetach($entity, array &$visited, $noCascade = false)
2150
    {
2151
        $oid = spl_object_hash($entity);
2152
        if (isset($visited[$oid])) {
2153
            return; // Prevent infinite recursion
2154
        }
2155
        $visited[$oid] = $entity; // mark visited
2156
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2157
            case self::STATE_MANAGED:
2158
                if ($this->isInIdentityMap($entity)) {
2159
                    $this->removeFromIdentityMap($entity);
2160
                }
2161
                unset(
2162
                    $this->entityInsertions[$oid],
2163
                    $this->entityUpdates[$oid],
2164
                    $this->entityDeletions[$oid],
2165
                    $this->entityIdentifiers[$oid],
2166
                    $this->entityStates[$oid],
2167
                    $this->originalEntityData[$oid]
2168
                );
2169
                break;
2170
            case self::STATE_NEW:
2171
            case self::STATE_DETACHED:
2172
                return;
2173
        }
2174
        if (!$noCascade) {
2175
            $this->cascadeDetach($entity, $visited);
2176
        }
2177
    }
2178
}
2179