Failed Conditions
Pull Request — 2.8.x (#7946)
by
unknown
09:22 queued 12s
created

UnitOfWork   F

Complexity

Total Complexity 517

Size/Duplication

Total Lines 3569
Duplicated Lines 0 %

Test Coverage

Coverage 93.4%

Importance

Changes 14
Bugs 1 Features 0
Metric Value
wmc 517
eloc 1213
c 14
b 1
f 0
dl 0
loc 3569
ccs 1202
cts 1287
cp 0.934
rs 0.8

99 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getCollectionPersister() 0 24 6
A markReadOnly() 0 7 3
A getCommitOrderCalculator() 0 3 1
A getScheduledCollectionUpdates() 0 3 1
A setOriginalEntityData() 0 3 1
A isReadOnly() 0 7 2
A assertThatThereAreNoUnintentionallyNonPersistedAssociations() 0 9 2
A scheduleCollectionDeletion() 0 9 1
A getOriginalEntityData() 0 7 2
A clear() 0 27 3
A getScheduledEntityDeletions() 0 3 1
A loadCollection() 0 16 3
A clearEntityChangeSet() 0 3 1
A cancelOrphanRemoval() 0 3 1
A getScheduledEntityInsertions() 0 3 1
A convertSingleFieldIdentifierToPHPValue() 0 5 1
A getIdentityMap() 0 3 1
A getScheduledEntityUpdates() 0 3 1
A dispatchOnFlushEvent() 0 4 2
A tryGetById() 0 7 2
A afterTransactionComplete() 0 4 1
A initializeObject() 0 10 3
F createEntity() 0 268 59
A getSingleIdentifierValue() 0 13 4
C lock() 0 50 13
A hasPendingInsertions() 0 3 1
A setOriginalEntityProperty() 0 3 1
A isIdentifierEquals() 0 23 6
A objToStr() 0 3 2
A registerManaged() 0 12 4
A isCollectionScheduledForDeletion() 0 3 1
A afterTransactionRolledBack() 0 4 1
A scheduleOrphanRemoval() 0 3 1
A getEntityIdentifier() 0 3 1
A newInstance() 0 9 2
A size() 0 5 1
A propertyChanged() 0 16 4
A dispatchPostFlushEvent() 0 4 2
A scheduleForDirtyCheck() 0 5 1
A getScheduledCollectionDeletions() 0 3 1
A hydrationComplete() 0 3 1
B getEntityPersister() 0 35 7
A triggerEagerLoads() 0 19 4
A performCallbackOnCachedPersister() 0 9 4
F commit() 0 122 32
A hasMissingIdsWhichAreForeignKeys() 0 9 4
A remove() 0 5 1
A scheduleForUpdate() 0 14 5
A updateAssociationWithMergedEntity() 0 18 3
C recomputeSingleEntityChangeSet() 0 50 16
A isInIdentityMap() 0 12 2
B cascadeDetach() 0 30 7
B cascadeRemove() 0 38 9
A containsIdHash() 0 3 1
A isScheduledForDelete() 0 3 1
A refresh() 0 5 1
A isEntityScheduled() 0 7 3
A addToIdentityMap() 0 19 4
B executeInserts() 0 57 9
A ensureVersionMatch() 0 16 5
B doDetach() 0 32 7
A merge() 0 5 1
B cascadePersist() 0 47 9
A scheduleExtraUpdate() 0 12 2
A executeDeletions() 0 29 5
B computeChangeSets() 0 41 11
A computeScheduleInsertsChangeSets() 0 6 2
C getEntityState() 0 67 13
A getByIdHash() 0 3 1
C computeAssociationChanges() 0 64 14
A isScheduledForDirtyCheck() 0 5 1
A clearEntityInsertionsForEntityName() 0 6 4
F computeChangeSet() 0 180 44
A getEntityChangeSet() 0 10 2
A scheduleForDelete() 0 25 5
A detach() 0 5 1
B scheduleForInsert() 0 27 8
A persist() 0 5 1
A clearIdentityMapForEntityName() 0 10 3
A isScheduledForInsert() 0 3 1
B cascadeMerge() 0 27 7
C getCommitOrder() 0 66 12
B cascadeRefresh() 0 30 7
A executeExtraUpdates() 0 10 2
B doPersist() 0 47 7
C doMerge() 0 87 12
A addToEntityIdentifiersAndEntityMap() 0 19 3
A isLoaded() 0 3 2
B computeSingleEntityChangeSet() 0 31 11
A removeFromIdentityMap() 0 22 3
A isScheduledForUpdate() 0 3 1
A executeUpdates() 0 26 6
D mergeEntityStateIntoManagedCopy() 0 96 23
B doRemove() 0 37 7
A tryGetByIdHash() 0 7 2
A persistNew() 0 30 5
A postCommitCleanup() 0 28 4
A doRefresh() 0 22 3

How to fix   Complexity   

Complex Class

Complex classes like UnitOfWork often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UnitOfWork, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\NotifyPropertyChanged;
25
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
26
use Doctrine\Common\Persistence\ObjectManagerAware;
27
use Doctrine\Common\PropertyChangedListener;
28
use Doctrine\DBAL\LockMode;
29
use Doctrine\ORM\Cache\Persister\CachedPersister;
30
use Doctrine\ORM\Event\LifecycleEventArgs;
31
use Doctrine\ORM\Event\ListenersInvoker;
32
use Doctrine\ORM\Event\OnFlushEventArgs;
33
use Doctrine\ORM\Event\PostFlushEventArgs;
34
use Doctrine\ORM\Event\PreFlushEventArgs;
35
use Doctrine\ORM\Event\PreUpdateEventArgs;
36
use Doctrine\ORM\Internal\HydrationCompleteHandler;
37
use Doctrine\ORM\Mapping\ClassMetadata;
38
use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
39
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
40
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
41
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
42
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
43
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
44
use Doctrine\ORM\Proxy\Proxy;
45
use Doctrine\ORM\Utility\IdentifierFlattener;
46
use InvalidArgumentException;
47
use Throwable;
48
use UnexpectedValueException;
49
use function get_class;
50
use function is_object;
51
52
/**
53
 * The UnitOfWork is responsible for tracking changes to objects during an
54
 * "object-level" transaction and for writing out changes to the database
55
 * in the correct order.
56
 *
57
 * Internal note: This class contains highly performance-sensitive code.
58
 *
59
 * @since       2.0
60
 * @author      Benjamin Eberlei <[email protected]>
61
 * @author      Guilherme Blanco <[email protected]>
62
 * @author      Jonathan Wage <[email protected]>
63
 * @author      Roman Borschel <[email protected]>
64
 * @author      Rob Caiger <[email protected]>
65
 */
66
class UnitOfWork implements PropertyChangedListener
0 ignored issues
show
Deprecated Code introduced by
The interface Doctrine\Common\PropertyChangedListener has been deprecated: 1.3 Use Doctrine\Persistence\PropertyChangedListener ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

66
class UnitOfWork implements /** @scrutinizer ignore-deprecated */ PropertyChangedListener

This interface has been deprecated. The supplier of the interface has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.

Loading history...
67
{
68
    /**
69
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
70
     */
71
    const STATE_MANAGED = 1;
72
73
    /**
74
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
75
     * and is not (yet) managed by an EntityManager.
76
     */
77
    const STATE_NEW = 2;
78
79
    /**
80
     * A detached entity is an instance with persistent state and identity that is not
81
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
82
     */
83
    const STATE_DETACHED = 3;
84
85
    /**
86
     * A removed entity instance is an instance with a persistent identity,
87
     * associated with an EntityManager, whose persistent state will be deleted
88
     * on commit.
89
     */
90
    const STATE_REMOVED = 4;
91
92
    /**
93
     * Hint used to collect all primary keys of associated entities during hydration
94
     * and execute it in a dedicated query afterwards
95
     * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
96
     */
97
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
98
99
    /**
100
     * The identity map that holds references to all managed entities that have
101
     * an identity. The entities are grouped by their class name.
102
     * Since all classes in a hierarchy must share the same identifier set,
103
     * we always take the root class name of the hierarchy.
104
     *
105
     * @var array
106
     */
107
    private $identityMap = [];
108
109
    /**
110
     * Map of all identifiers of managed entities.
111
     * Keys are object ids (spl_object_hash).
112
     *
113
     * @var array
114
     */
115
    private $entityIdentifiers = [];
116
117
    /**
118
     * Map of the original entity data of managed entities.
119
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
120
     * at commit time.
121
     *
122
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
123
     *                A value will only really be copied if the value in the entity is modified
124
     *                by the user.
125
     *
126
     * @var array
127
     */
128
    private $originalEntityData = [];
129
130
    /**
131
     * Map of entity changes. Keys are object ids (spl_object_hash).
132
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
133
     *
134
     * @var array
135
     */
136
    private $entityChangeSets = [];
137
138
    /**
139
     * The (cached) states of any known entities.
140
     * Keys are object ids (spl_object_hash).
141
     *
142
     * @var array
143
     */
144
    private $entityStates = [];
145
146
    /**
147
     * Map of entities that are scheduled for dirty checking at commit time.
148
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
149
     * Keys are object ids (spl_object_hash).
150
     *
151
     * @var array
152
     */
153
    private $scheduledForSynchronization = [];
154
155
    /**
156
     * A list of all pending entity insertions.
157
     *
158
     * @var array
159
     */
160
    private $entityInsertions = [];
161
162
    /**
163
     * A list of all pending entity updates.
164
     *
165
     * @var array
166
     */
167
    private $entityUpdates = [];
168
169
    /**
170
     * Any pending extra updates that have been scheduled by persisters.
171
     *
172
     * @var array
173
     */
174
    private $extraUpdates = [];
175
176
    /**
177
     * A list of all pending entity deletions.
178
     *
179
     * @var array
180
     */
181
    private $entityDeletions = [];
182
183
    /**
184
     * New entities that were discovered through relationships that were not
185
     * marked as cascade-persist. During flush, this array is populated and
186
     * then pruned of any entities that were discovered through a valid
187
     * cascade-persist path. (Leftovers cause an error.)
188
     *
189
     * Keys are OIDs, payload is a two-item array describing the association
190
     * and the entity.
191
     *
192
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
193
     */
194
    private $nonCascadedNewDetectedEntities = [];
195
196
    /**
197
     * All pending collection deletions.
198
     *
199
     * @var array
200
     */
201
    private $collectionDeletions = [];
202
203
    /**
204
     * All pending collection updates.
205
     *
206
     * @var array
207
     */
208
    private $collectionUpdates = [];
209
210
    /**
211
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
212
     * At the end of the UnitOfWork all these collections will make new snapshots
213
     * of their data.
214
     *
215
     * @var array
216
     */
217
    private $visitedCollections = [];
218
219
    /**
220
     * The EntityManager that "owns" this UnitOfWork instance.
221
     *
222
     * @var EntityManagerInterface
223
     */
224
    private $em;
225
226
    /**
227
     * The entity persister instances used to persist entity instances.
228
     *
229
     * @var array
230
     */
231
    private $persisters = [];
232
233
    /**
234
     * The collection persister instances used to persist collections.
235
     *
236
     * @var array
237
     */
238
    private $collectionPersisters = [];
239
240
    /**
241
     * The EventManager used for dispatching events.
242
     *
243
     * @var \Doctrine\Common\EventManager
244
     */
245
    private $evm;
246
247
    /**
248
     * The ListenersInvoker used for dispatching events.
249
     *
250
     * @var \Doctrine\ORM\Event\ListenersInvoker
251
     */
252
    private $listenersInvoker;
253
254
    /**
255
     * The IdentifierFlattener used for manipulating identifiers
256
     *
257
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
258
     */
259
    private $identifierFlattener;
260
261
    /**
262
     * Orphaned entities that are scheduled for removal.
263
     *
264
     * @var array
265
     */
266
    private $orphanRemovals = [];
267
268
    /**
269
     * Read-Only objects are never evaluated
270
     *
271
     * @var array
272
     */
273
    private $readOnlyObjects = [];
274
275
    /**
276
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
277
     *
278
     * @var array
279
     */
280
    private $eagerLoadingEntities = [];
281
282
    /**
283
     * @var boolean
284
     */
285
    protected $hasCache = false;
286
287
    /**
288
     * Helper for handling completion of hydration
289
     *
290
     * @var HydrationCompleteHandler
291
     */
292
    private $hydrationCompleteHandler;
293
294
    /**
295
     * @var ReflectionPropertiesGetter
296
     */
297
    private $reflectionPropertiesGetter;
298
299
    /**
300
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
301
     *
302
     * @param EntityManagerInterface $em
303
     */
304 2514
    public function __construct(EntityManagerInterface $em)
305
    {
306 2514
        $this->em                         = $em;
307 2514
        $this->evm                        = $em->getEventManager();
308 2514
        $this->listenersInvoker           = new ListenersInvoker($em);
309 2514
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
310 2514
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
311 2514
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
312 2514
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
0 ignored issues
show
Deprecated Code introduced by
The class Doctrine\Common\Persiste...untimeReflectionService has been deprecated: 1.3 Use Doctrine\Persistence\Mapping\RuntimeReflectionService ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

312
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(/** @scrutinizer ignore-deprecated */ new RuntimeReflectionService());
Loading history...
Bug introduced by
new Doctrine\Common\Pers...timeReflectionService() of type Doctrine\Common\Persiste...untimeReflectionService is incompatible with the type Doctrine\Common\Persiste...pping\ReflectionService expected by parameter $reflectionService of Doctrine\ORM\Mapping\Ref...esGetter::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

312
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(/** @scrutinizer ignore-type */ new RuntimeReflectionService());
Loading history...
313 2514
    }
314
315
    /**
316
     * Commits the UnitOfWork, executing all operations that have been postponed
317
     * up to this point. The state of all managed entities will be synchronized with
318
     * the database.
319
     *
320
     * The operations are executed in the following order:
321
     *
322
     * 1) All entity insertions
323
     * 2) All entity updates
324
     * 3) All collection deletions
325
     * 4) All collection updates
326
     * 5) All entity deletions
327
     *
328
     * @param null|object|array $entity
329
     *
330
     * @return void
331
     *
332
     * @throws \Exception
333
     */
334 1106
    public function commit($entity = null)
335
    {
336
        // Raise preFlush
337 1106
        if ($this->evm->hasListeners(Events::preFlush)) {
338 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
339
        }
340
341
        // Compute changes done since last commit.
342 1106
        if (null === $entity) {
343 1099
            $this->computeChangeSets();
344 14
        } elseif (is_object($entity)) {
345 12
            $this->computeSingleEntityChangeSet($entity);
346 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
347 2
            foreach ($entity as $object) {
348 2
                $this->computeSingleEntityChangeSet($object);
349
            }
350
        }
351
352 1103
        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...
353 178
                $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...
354 140
                $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...
355 44
                $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionUpdates 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...
356 40
                $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions 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...
357 1103
                $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...
358 27
            $this->dispatchOnFlushEvent();
359 27
            $this->dispatchPostFlushEvent();
360
361 27
            $this->postCommitCleanup($entity);
362
363 27
            return; // Nothing to do.
364
        }
365
366 1099
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
367
368 1097
        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...
369 16
            foreach ($this->orphanRemovals as $orphan) {
370 16
                $this->remove($orphan);
371
            }
372
        }
373
374 1097
        $this->dispatchOnFlushEvent();
375
376
        // Now we need a commit order to maintain referential integrity
377 1097
        $commitOrder = $this->getCommitOrder();
378
379 1097
        $conn = $this->em->getConnection();
380 1097
        $conn->beginTransaction();
381
382
        try {
383
            // Collection deletions (deletions of complete collections)
384 1097
            foreach ($this->collectionDeletions as $collectionToDelete) {
385 21
                if (! $collectionToDelete instanceof PersistentCollection) {
386
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
387
388
                    continue;
389
                }
390
391
                // Deferred explicit tracked collections can be removed only when owning relation was persisted
392 21
                $owner = $collectionToDelete->getOwner();
393
394 21
                if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
0 ignored issues
show
Bug introduced by
The method isChangeTrackingDeferredImplicit() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

394
                if ($this->em->getClassMetadata(get_class($owner))->/** @scrutinizer ignore-call */ isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
Loading history...
395 20
                    $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
396
                }
397
            }
398
399 1097
            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...
400 1093
                foreach ($commitOrder as $class) {
401 1093
                    $this->executeInserts($class);
402
                }
403
            }
404
405 1096
            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...
406 123
                foreach ($commitOrder as $class) {
407 123
                    $this->executeUpdates($class);
408
                }
409
            }
410
411
            // Extra updates that were requested by persisters.
412 1092
            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...
413 44
                $this->executeExtraUpdates();
414
            }
415
416
            // Collection updates (deleteRows, updateRows, insertRows)
417 1092
            foreach ($this->collectionUpdates as $collectionToUpdate) {
418 552
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
419
            }
420
421
            // Entity deletions come last and need to be in reverse commit order
422 1092
            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...
423 65
                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...
424 65
                    $this->executeDeletions($commitOrder[$i]);
425
                }
426
            }
427
428
            // Commit failed silently
429 1092
            if ($conn->commit() === false) {
430 1
                $object = is_object($entity) ? $entity : null;
431
432 1092
                throw new OptimisticLockException('Commit failed', $object);
433
            }
434 12
        } catch (Throwable $e) {
435 12
            $this->em->close();
436
437 12
            if ($conn->isTransactionActive()) {
438 12
                $conn->rollBack();
439
            }
440
441 12
            $this->afterTransactionRolledBack();
442
443 12
            throw $e;
444
        }
445
446 1091
        $this->afterTransactionComplete();
447
448
        // Take new snapshots from visited collections
449 1091
        foreach ($this->visitedCollections as $coll) {
450 551
            $coll->takeSnapshot();
451
        }
452
453 1091
        $this->dispatchPostFlushEvent();
454
455 1090
        $this->postCommitCleanup($entity);
456 1090
    }
457
458
    /**
459
     * @param null|object|object[] $entity
460
     */
461 1094
    private function postCommitCleanup($entity) : void
462
    {
463 1094
        $this->entityInsertions =
464 1094
        $this->entityUpdates =
465 1094
        $this->entityDeletions =
466 1094
        $this->extraUpdates =
467 1094
        $this->collectionUpdates =
468 1094
        $this->nonCascadedNewDetectedEntities =
469 1094
        $this->collectionDeletions =
470 1094
        $this->visitedCollections =
471 1094
        $this->orphanRemovals = [];
472
473 1094
        if (null === $entity) {
474 1088
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
475
476 1088
            return;
477
        }
478
479 12
        $entities = \is_object($entity)
480 10
            ? [$entity]
481 12
            : $entity;
482
483 12
        foreach ($entities as $object) {
484 12
            $oid = \spl_object_hash($object);
485
486 12
            $this->clearEntityChangeSet($oid);
487
488 12
            unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
489
        }
490 12
    }
491
492
    /**
493
     * Computes the changesets of all entities scheduled for insertion.
494
     *
495
     * @return void
496
     */
497 1105
    private function computeScheduleInsertsChangeSets()
498
    {
499 1105
        foreach ($this->entityInsertions as $entity) {
500 1097
            $class = $this->em->getClassMetadata(get_class($entity));
501
502 1097
            $this->computeChangeSet($class, $entity);
503
        }
504 1103
    }
505
506
    /**
507
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
508
     *
509
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
510
     * 2. Read Only entities are skipped.
511
     * 3. Proxies are skipped.
512
     * 4. Only if entity is properly managed.
513
     *
514
     * @param object $entity
515
     *
516
     * @return void
517
     *
518
     * @throws \InvalidArgumentException
519
     */
520 14
    private function computeSingleEntityChangeSet($entity)
521
    {
522 14
        $state = $this->getEntityState($entity);
523
524 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
525 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
526
        }
527
528 13
        $class = $this->em->getClassMetadata(get_class($entity));
529
530 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
531 13
            $this->persist($entity);
532
        }
533
534
        // Compute changes for INSERTed entities first. This must always happen even in this case.
535 13
        $this->computeScheduleInsertsChangeSets();
536
537 13
        if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
538
            return;
539
        }
540
541
        // Ignore uninitialized proxy objects
542 13
        if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
543 1
            return;
544
        }
545
546
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
547 12
        $oid = spl_object_hash($entity);
548
549 12
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
550 7
            $this->computeChangeSet($class, $entity);
551
        }
552 12
    }
553
554
    /**
555
     * Executes any extra updates that have been scheduled.
556
     */
557 44
    private function executeExtraUpdates()
558
    {
559 44
        foreach ($this->extraUpdates as $oid => $update) {
560 44
            list ($entity, $changeset) = $update;
561
562 44
            $this->entityChangeSets[$oid] = $changeset;
563 44
            $this->getEntityPersister(get_class($entity))->update($entity);
564
        }
565
566 44
        $this->extraUpdates = [];
567 44
    }
568
569
    /**
570
     * Gets the changeset for an entity.
571
     *
572
     * @param object $entity
573
     *
574
     * @return array
575
     */
576
    public function & getEntityChangeSet($entity)
577
    {
578 1091
        $oid  = spl_object_hash($entity);
579 1091
        $data = [];
580
581 1091
        if (!isset($this->entityChangeSets[$oid])) {
582 4
            return $data;
583
        }
584
585 1091
        return $this->entityChangeSets[$oid];
586
    }
587
588
    /**
589
     * Computes the changes that happened to a single entity.
590
     *
591
     * Modifies/populates the following properties:
592
     *
593
     * {@link _originalEntityData}
594
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
595
     * then it was not fetched from the database and therefore we have no original
596
     * entity data yet. All of the current entity data is stored as the original entity data.
597
     *
598
     * {@link _entityChangeSets}
599
     * The changes detected on all properties of the entity are stored there.
600
     * A change is a tuple array where the first entry is the old value and the second
601
     * entry is the new value of the property. Changesets are used by persisters
602
     * to INSERT/UPDATE the persistent entity state.
603
     *
604
     * {@link _entityUpdates}
605
     * If the entity is already fully MANAGED (has been fetched from the database before)
606
     * and any changes to its properties are detected, then a reference to the entity is stored
607
     * there to mark it for an update.
608
     *
609
     * {@link _collectionDeletions}
610
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
611
     * then this collection is marked for deletion.
612
     *
613
     * @ignore
614
     *
615
     * @internal Don't call from the outside.
616
     *
617
     * @param ClassMetadata $class  The class descriptor of the entity.
618
     * @param object        $entity The entity for which to compute the changes.
619
     *
620
     * @return void
621
     */
622 1107
    public function computeChangeSet(ClassMetadata $class, $entity)
623
    {
624 1107
        $oid = spl_object_hash($entity);
625
626 1107
        if (isset($this->readOnlyObjects[$oid])) {
627 2
            return;
628
        }
629
630 1107
        if ( ! $class->isInheritanceTypeNone()) {
631 339
            $class = $this->em->getClassMetadata(get_class($entity));
632
        }
633
634 1107
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
635
636 1107
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
637 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
638
        }
639
640 1107
        $actualData = [];
641
642 1107
        foreach ($class->reflFields as $name => $refProp) {
643 1107
            $value = $refProp->getValue($entity);
644
645 1107
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
646 824
                if ($value instanceof PersistentCollection) {
647 207
                    if ($value->getOwner() === $entity) {
648 207
                        continue;
649
                    }
650
651 5
                    $value = new ArrayCollection($value->getValues());
652
                }
653
654
                // If $value is not a Collection then use an ArrayCollection.
655 819
                if ( ! $value instanceof Collection) {
656 249
                    $value = new ArrayCollection($value);
657
                }
658
659 819
                $assoc = $class->associationMappings[$name];
660
661
                // Inject PersistentCollection
662 819
                $value = new PersistentCollection(
663 819
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
664
                );
665 819
                $value->setOwner($entity, $assoc);
666 819
                $value->setDirty( ! $value->isEmpty());
667
668 819
                $class->reflFields[$name]->setValue($entity, $value);
669
670 819
                $actualData[$name] = $value;
671
672 819
                continue;
673
            }
674
675 1107
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
0 ignored issues
show
Bug introduced by
The method isIdGeneratorIdentity() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

675
            if (( ! $class->isIdentifier($name) || ! $class->/** @scrutinizer ignore-call */ isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
Loading history...
676 1066
                $actualData[$name] = $value;
677
            }
678
        }
679
680 1107
        if ( ! isset($this->originalEntityData[$oid])) {
681
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
682
            // These result in an INSERT.
683 1103
            $this->originalEntityData[$oid] = $actualData;
684 1103
            $changeSet = [];
685
686 1103
            foreach ($actualData as $propName => $actualValue) {
687 1079
                if ( ! isset($class->associationMappings[$propName])) {
688 1020
                    $changeSet[$propName] = [null, $actualValue];
689
690 1020
                    continue;
691
                }
692
693 957
                $assoc = $class->associationMappings[$propName];
694
695 957
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
696 902
                    $changeSet[$propName] = [null, $actualValue];
697
                }
698
            }
699
700 1103
            $this->entityChangeSets[$oid] = $changeSet;
701
        } else {
702
            // Entity is "fully" MANAGED: it was already fully persisted before
703
            // and we have a copy of the original data
704 280
            $originalData           = $this->originalEntityData[$oid];
705 280
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
0 ignored issues
show
Bug introduced by
The method isChangeTrackingNotify() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

705
            /** @scrutinizer ignore-call */ 
706
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
Loading history...
706 280
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
707
                ? $this->entityChangeSets[$oid]
708 280
                : [];
709
710 280
            foreach ($actualData as $propName => $actualValue) {
711
                // skip field, its a partially omitted one!
712 262
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
713 8
                    continue;
714
                }
715
716 262
                $orgValue = $originalData[$propName];
717
718
                // skip if value haven't changed
719 262
                if ($orgValue === $actualValue) {
720 245
                    continue;
721
                }
722
723
                // if regular field
724 119
                if ( ! isset($class->associationMappings[$propName])) {
725 64
                    if ($isChangeTrackingNotify) {
726
                        continue;
727
                    }
728
729 64
                    $changeSet[$propName] = [$orgValue, $actualValue];
730
731 64
                    continue;
732
                }
733
734 59
                $assoc = $class->associationMappings[$propName];
735
736
                // Persistent collection was exchanged with the "originally"
737
                // created one. This can only mean it was cloned and replaced
738
                // on another entity.
739 59
                if ($actualValue instanceof PersistentCollection) {
740 8
                    $owner = $actualValue->getOwner();
741 8
                    if ($owner === null) { // cloned
742
                        $actualValue->setOwner($entity, $assoc);
743 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
744
                        if (!$actualValue->isInitialized()) {
745
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
746
                        }
747
                        $newValue = clone $actualValue;
748
                        $newValue->setOwner($entity, $assoc);
749
                        $class->reflFields[$propName]->setValue($entity, $newValue);
750
                    }
751
                }
752
753 59
                if ($orgValue instanceof PersistentCollection) {
754
                    // A PersistentCollection was de-referenced, so delete it.
755 8
                    $coid = spl_object_hash($orgValue);
756
757 8
                    if (isset($this->collectionDeletions[$coid])) {
758
                        continue;
759
                    }
760
761 8
                    $this->collectionDeletions[$coid] = $orgValue;
762 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
763
764 8
                    continue;
765
                }
766
767 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
768 50
                    if ($assoc['isOwningSide']) {
769 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
770
                    }
771
772 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
773 4
                        $this->scheduleOrphanRemoval($orgValue);
774
                    }
775
                }
776
            }
777
778 280
            if ($changeSet) {
779 92
                $this->entityChangeSets[$oid]   = $changeSet;
780 92
                $this->originalEntityData[$oid] = $actualData;
781 92
                $this->entityUpdates[$oid]      = $entity;
782
            }
783
        }
784
785
        // Look for changes in associations of the entity
786 1107
        foreach ($class->associationMappings as $field => $assoc) {
787 957
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
788 667
                continue;
789
            }
790
791 925
            $this->computeAssociationChanges($assoc, $val);
792
793 917
            if ( ! isset($this->entityChangeSets[$oid]) &&
794 917
                $assoc['isOwningSide'] &&
795 917
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
796 917
                $val instanceof PersistentCollection &&
797 917
                $val->isDirty()) {
798
799 35
                $this->entityChangeSets[$oid]   = [];
800 35
                $this->originalEntityData[$oid] = $actualData;
801 35
                $this->entityUpdates[$oid]      = $entity;
802
            }
803
        }
804 1099
    }
805
806
    /**
807
     * Computes all the changes that have been done to entities and collections
808
     * since the last commit and stores these changes in the _entityChangeSet map
809
     * temporarily for access by the persisters, until the UoW commit is finished.
810
     *
811
     * @return void
812
     */
813 1099
    public function computeChangeSets()
814
    {
815
        // Compute changes for INSERTed entities first. This must always happen.
816 1099
        $this->computeScheduleInsertsChangeSets();
817
818
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
819 1097
        foreach ($this->identityMap as $className => $entities) {
820 482
            $class = $this->em->getClassMetadata($className);
821
822
            // Skip class if instances are read-only
823 482
            if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
824 1
                continue;
825
            }
826
827
            // If change tracking is explicit or happens through notification, then only compute
828
            // changes on entities of that type that are explicitly marked for synchronization.
829
            switch (true) {
830 481
                case ($class->isChangeTrackingDeferredImplicit()):
831 476
                    $entitiesToProcess = $entities;
832 476
                    break;
833
834 6
                case (isset($this->scheduledForSynchronization[$className])):
835 5
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
836 5
                    break;
837
838
                default:
839 2
                    $entitiesToProcess = [];
840
841
            }
842
843 481
            foreach ($entitiesToProcess as $entity) {
844
                // Ignore uninitialized proxy objects
845 460
                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
846 38
                    continue;
847
                }
848
849
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
850 458
                $oid = spl_object_hash($entity);
851
852 458
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
853 273
                    $this->computeChangeSet($class, $entity);
854
                }
855
            }
856
        }
857 1097
    }
858
859
    /**
860
     * Computes the changes of an association.
861
     *
862
     * @param array $assoc The association mapping.
863
     * @param mixed $value The value of the association.
864
     *
865
     * @throws ORMInvalidArgumentException
866
     * @throws ORMException
867
     *
868
     * @return void
869
     */
870 925
    private function computeAssociationChanges($assoc, $value)
871
    {
872 925
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
873 30
            return;
874
        }
875
876 924
        if ($value instanceof PersistentCollection && $value->isDirty()) {
877 556
            $coid = spl_object_hash($value);
878
879 556
            $this->collectionUpdates[$coid] = $value;
880 556
            $this->visitedCollections[$coid] = $value;
881
        }
882
883
        // Look through the entities, and in any of their associations,
884
        // for transient (new) entities, recursively. ("Persistence by reachability")
885
        // Unwrap. Uninitialized collections will simply be empty.
886 924
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
887 924
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
888
889 924
        foreach ($unwrappedValue as $key => $entry) {
890 764
            if (! ($entry instanceof $targetClass->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
891 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
892
            }
893
894 756
            $state = $this->getEntityState($entry, self::STATE_NEW);
895
896 756
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
897
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
898
            }
899
900
            switch ($state) {
901 756
                case self::STATE_NEW:
902 42
                    if ( ! $assoc['isCascadePersist']) {
903
                        /*
904
                         * For now just record the details, because this may
905
                         * not be an issue if we later discover another pathway
906
                         * through the object-graph where cascade-persistence
907
                         * is enabled for this object.
908
                         */
909 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
910
911 6
                        break;
912
                    }
913
914 37
                    $this->persistNew($targetClass, $entry);
915 37
                    $this->computeChangeSet($targetClass, $entry);
916
917 37
                    break;
918
919 748
                case self::STATE_REMOVED:
920
                    // Consume the $value as array (it's either an array or an ArrayAccess)
921
                    // and remove the element from Collection.
922 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
923 3
                        unset($value[$key]);
924
                    }
925 4
                    break;
926
927 748
                case self::STATE_DETACHED:
928
                    // Can actually not happen right now as we assume STATE_NEW,
929
                    // so the exception will be raised from the DBAL layer (constraint violation).
930
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
931
                    break;
932
933
                default:
934
                    // MANAGED associated entities are already taken into account
935
                    // during changeset calculation anyway, since they are in the identity map.
936
            }
937
        }
938 916
    }
939
940
    /**
941
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
942
     * @param object                              $entity
943
     *
944
     * @return void
945
     */
946 1130
    private function persistNew($class, $entity)
947
    {
948 1130
        $oid    = spl_object_hash($entity);
949 1130
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
950
951 1130
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
952 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
953
        }
954
955 1130
        $idGen = $class->idGenerator;
956
957 1130
        if ( ! $idGen->isPostInsertGenerator()) {
958 300
            $idValue = $idGen->generate($this->em, $entity);
959
960 300
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
961 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
962
963 2
                $class->setIdentifierValues($entity, $idValue);
964
            }
965
966
            // Some identifiers may be foreign keys to new entities.
967
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
968 300
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
969 297
                $this->entityIdentifiers[$oid] = $idValue;
970
            }
971
        }
972
973 1130
        $this->entityStates[$oid] = self::STATE_MANAGED;
974
975 1130
        $this->scheduleForInsert($entity);
976 1130
    }
977
978
    /**
979
     * @param mixed[] $idValue
980
     */
981 300
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
982
    {
983 300
        foreach ($idValue as $idField => $idFieldValue) {
984 300
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
985 3
                return true;
986
            }
987
        }
988
989 297
        return false;
990
    }
991
992
    /**
993
     * INTERNAL:
994
     * Computes the changeset of an individual entity, independently of the
995
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
996
     *
997
     * The passed entity must be a managed entity. If the entity already has a change set
998
     * because this method is invoked during a commit cycle then the change sets are added.
999
     * whereby changes detected in this method prevail.
1000
     *
1001
     * @ignore
1002
     *
1003
     * @param ClassMetadata $class  The class descriptor of the entity.
1004
     * @param object        $entity The entity for which to (re)calculate the change set.
1005
     *
1006
     * @return void
1007
     *
1008
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
1009
     */
1010 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
1011
    {
1012 16
        $oid = spl_object_hash($entity);
1013
1014 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
1015
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1016
        }
1017
1018
        // skip if change tracking is "NOTIFY"
1019 16
        if ($class->isChangeTrackingNotify()) {
1020
            return;
1021
        }
1022
1023 16
        if ( ! $class->isInheritanceTypeNone()) {
1024 3
            $class = $this->em->getClassMetadata(get_class($entity));
1025
        }
1026
1027 16
        $actualData = [];
1028
1029 16
        foreach ($class->reflFields as $name => $refProp) {
1030 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1031 16
                && ($name !== $class->versionField)
1032 16
                && ! $class->isCollectionValuedAssociation($name)) {
1033 16
                $actualData[$name] = $refProp->getValue($entity);
1034
            }
1035
        }
1036
1037 16
        if ( ! isset($this->originalEntityData[$oid])) {
1038
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1039
        }
1040
1041 16
        $originalData = $this->originalEntityData[$oid];
1042 16
        $changeSet = [];
1043
1044 16
        foreach ($actualData as $propName => $actualValue) {
1045 16
            $orgValue = $originalData[$propName] ?? null;
1046
1047 16
            if ($orgValue !== $actualValue) {
1048 7
                $changeSet[$propName] = [$orgValue, $actualValue];
1049
            }
1050
        }
1051
1052 16
        if ($changeSet) {
1053 7
            if (isset($this->entityChangeSets[$oid])) {
1054 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1055 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1056 1
                $this->entityChangeSets[$oid] = $changeSet;
1057 1
                $this->entityUpdates[$oid]    = $entity;
1058
            }
1059 7
            $this->originalEntityData[$oid] = $actualData;
1060
        }
1061 16
    }
1062
1063
    /**
1064
     * Executes all entity insertions for entities of the specified type.
1065
     *
1066
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1067
     *
1068
     * @return void
1069
     */
1070 1093
    private function executeInserts($class)
1071
    {
1072 1093
        $entities   = [];
1073 1093
        $className  = $class->name;
1074 1093
        $persister  = $this->getEntityPersister($className);
1075 1093
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1076
1077 1093
        $insertionsForClass = [];
1078
1079 1093
        foreach ($this->entityInsertions as $oid => $entity) {
1080
1081 1093
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1082 920
                continue;
1083
            }
1084
1085 1093
            $insertionsForClass[$oid] = $entity;
1086
1087 1093
            $persister->addInsert($entity);
1088
1089 1093
            unset($this->entityInsertions[$oid]);
1090
1091 1093
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1092 136
                $entities[] = $entity;
1093
            }
1094
        }
1095
1096 1093
        $postInsertIds = $persister->executeInserts();
1097
1098 1093
        if ($postInsertIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postInsertIds 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...
1099
            // Persister returned post-insert IDs
1100 987
            foreach ($postInsertIds as $postInsertId) {
1101 987
                $idField = $class->getSingleIdentifierFieldName();
1102 987
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1103
1104 987
                $entity  = $postInsertId['entity'];
1105 987
                $oid     = spl_object_hash($entity);
1106
1107 987
                $class->reflFields[$idField]->setValue($entity, $idValue);
1108
1109 987
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1110 987
                $this->entityStates[$oid] = self::STATE_MANAGED;
1111 987
                $this->originalEntityData[$oid][$idField] = $idValue;
1112
1113 987
                $this->addToIdentityMap($entity);
1114
            }
1115
        } else {
1116 824
            foreach ($insertionsForClass as $oid => $entity) {
1117 283
                if (! isset($this->entityIdentifiers[$oid])) {
1118
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1119
                    //add it now
1120 3
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1121
                }
1122
            }
1123
        }
1124
1125 1093
        foreach ($entities as $entity) {
1126 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1127
        }
1128 1093
    }
1129
1130
    /**
1131
     * @param object $entity
1132
     */
1133 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1134
    {
1135 3
        $identifier = [];
1136
1137 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1138 3
            $value = $class->getFieldValue($entity, $idField);
1139
1140 3
            if (isset($class->associationMappings[$idField])) {
1141
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1142 3
                $value = $this->getSingleIdentifierValue($value);
1143
            }
1144
1145 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1146
        }
1147
1148 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1149 3
        $this->entityIdentifiers[$oid] = $identifier;
1150
1151 3
        $this->addToIdentityMap($entity);
1152 3
    }
1153
1154
    /**
1155
     * Executes all entity updates for entities of the specified type.
1156
     *
1157
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1158
     *
1159
     * @return void
1160
     */
1161 123
    private function executeUpdates($class)
1162
    {
1163 123
        $className          = $class->name;
1164 123
        $persister          = $this->getEntityPersister($className);
1165 123
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1166 123
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1167
1168 123
        foreach ($this->entityUpdates as $oid => $entity) {
1169 123
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1170 79
                continue;
1171
            }
1172
1173 123
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1174 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1175
1176 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1177
            }
1178
1179 123
            if ( ! empty($this->entityChangeSets[$oid])) {
1180 89
                $persister->update($entity);
1181
            }
1182
1183 119
            unset($this->entityUpdates[$oid]);
1184
1185 119
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1186 11
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1187
            }
1188
        }
1189 119
    }
1190
1191
    /**
1192
     * Executes all entity deletions for entities of the specified type.
1193
     *
1194
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1195
     *
1196
     * @return void
1197
     */
1198 65
    private function executeDeletions($class)
1199
    {
1200 65
        $className  = $class->name;
1201 65
        $persister  = $this->getEntityPersister($className);
1202 65
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1203
1204 65
        foreach ($this->entityDeletions as $oid => $entity) {
1205 65
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1206 26
                continue;
1207
            }
1208
1209 65
            $persister->delete($entity);
1210
1211
            unset(
1212 65
                $this->entityDeletions[$oid],
1213 65
                $this->entityIdentifiers[$oid],
1214 65
                $this->originalEntityData[$oid],
1215 65
                $this->entityStates[$oid]
1216
            );
1217
1218
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1219
            // is obtained by a new entity because the old one went out of scope.
1220
            //$this->entityStates[$oid] = self::STATE_NEW;
1221 65
            if ( ! $class->isIdentifierNatural()) {
1222 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1223
            }
1224
1225 65
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1226 9
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1227
            }
1228
        }
1229 64
    }
1230
1231
    /**
1232
     * Gets the commit order.
1233
     *
1234
     * @param array|null $entityChangeSet
1235
     *
1236
     * @return array
1237
     */
1238 1097
    private function getCommitOrder(array $entityChangeSet = null)
1239
    {
1240 1097
        if ($entityChangeSet === null) {
1241 1097
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1242
        }
1243
1244 1097
        $calc = $this->getCommitOrderCalculator();
1245
1246
        // See if there are any new classes in the changeset, that are not in the
1247
        // commit order graph yet (don't have a node).
1248
        // We have to inspect changeSet to be able to correctly build dependencies.
1249
        // It is not possible to use IdentityMap here because post inserted ids
1250
        // are not yet available.
1251 1097
        $newNodes = [];
1252
1253 1097
        foreach ($entityChangeSet as $entity) {
1254 1097
            $class = $this->em->getClassMetadata(get_class($entity));
1255
1256 1097
            if ($calc->hasNode($class->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1257 668
                continue;
1258
            }
1259
1260 1097
            $calc->addNode($class->name, $class);
1261
1262 1097
            $newNodes[] = $class;
1263
        }
1264
1265
        // Calculate dependencies for new nodes
1266 1097
        while ($class = array_pop($newNodes)) {
1267 1097
            foreach ($class->associationMappings as $assoc) {
1268 946
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1269 897
                    continue;
1270
                }
1271
1272 893
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1273
1274 893
                if ( ! $calc->hasNode($targetClass->name)) {
1275 685
                    $calc->addNode($targetClass->name, $targetClass);
1276
1277 685
                    $newNodes[] = $targetClass;
1278
                }
1279
1280 893
                $joinColumns = reset($assoc['joinColumns']);
1281
1282 893
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1283
1284
                // If the target class has mapped subclasses, these share the same dependency.
1285 893
                if ( ! $targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1286 886
                    continue;
1287
                }
1288
1289 239
                foreach ($targetClass->subClasses as $subClassName) {
1290 239
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1291
1292 239
                    if ( ! $calc->hasNode($subClassName)) {
1293 209
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1294
1295 209
                        $newNodes[] = $targetSubClass;
1296
                    }
1297
1298 239
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1299
                }
1300
            }
1301
        }
1302
1303 1097
        return $calc->sort();
1304
    }
1305
1306
    /**
1307
     * Schedules an entity for insertion into the database.
1308
     * If the entity already has an identifier, it will be added to the identity map.
1309
     *
1310
     * @param object $entity The entity to schedule for insertion.
1311
     *
1312
     * @return void
1313
     *
1314
     * @throws ORMInvalidArgumentException
1315
     * @throws \InvalidArgumentException
1316
     */
1317 1131
    public function scheduleForInsert($entity)
1318
    {
1319 1131
        $oid = spl_object_hash($entity);
1320
1321 1131
        if (isset($this->entityUpdates[$oid])) {
1322
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1323
        }
1324
1325 1131
        if (isset($this->entityDeletions[$oid])) {
1326 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1327
        }
1328 1131
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1329 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1330
        }
1331
1332 1131
        if (isset($this->entityInsertions[$oid])) {
1333 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1334
        }
1335
1336 1131
        $this->entityInsertions[$oid] = $entity;
1337
1338 1131
        if (isset($this->entityIdentifiers[$oid])) {
1339 297
            $this->addToIdentityMap($entity);
1340
        }
1341
1342 1131
        if ($entity instanceof NotifyPropertyChanged) {
1343 8
            $entity->addPropertyChangedListener($this);
1344
        }
1345 1131
    }
1346
1347
    /**
1348
     * Checks whether an entity is scheduled for insertion.
1349
     *
1350
     * @param object $entity
1351
     *
1352
     * @return boolean
1353
     */
1354 662
    public function isScheduledForInsert($entity)
1355
    {
1356 662
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1357
    }
1358
1359
    /**
1360
     * Schedules an entity for being updated.
1361
     *
1362
     * @param object $entity The entity to schedule for being updated.
1363
     *
1364
     * @return void
1365
     *
1366
     * @throws ORMInvalidArgumentException
1367
     */
1368 1
    public function scheduleForUpdate($entity)
1369
    {
1370 1
        $oid = spl_object_hash($entity);
1371
1372 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1373
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1374
        }
1375
1376 1
        if (isset($this->entityDeletions[$oid])) {
1377
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1378
        }
1379
1380 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1381 1
            $this->entityUpdates[$oid] = $entity;
1382
        }
1383 1
    }
1384
1385
    /**
1386
     * INTERNAL:
1387
     * Schedules an extra update that will be executed immediately after the
1388
     * regular entity updates within the currently running commit cycle.
1389
     *
1390
     * Extra updates for entities are stored as (entity, changeset) tuples.
1391
     *
1392
     * @ignore
1393
     *
1394
     * @param object $entity    The entity for which to schedule an extra update.
1395
     * @param array  $changeset The changeset of the entity (what to update).
1396
     *
1397
     * @return void
1398
     */
1399 44
    public function scheduleExtraUpdate($entity, array $changeset)
1400
    {
1401 44
        $oid         = spl_object_hash($entity);
1402 44
        $extraUpdate = [$entity, $changeset];
1403
1404 44
        if (isset($this->extraUpdates[$oid])) {
1405 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1406
1407 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1408
        }
1409
1410 44
        $this->extraUpdates[$oid] = $extraUpdate;
1411 44
    }
1412
1413
    /**
1414
     * Checks whether an entity is registered as dirty in the unit of work.
1415
     * Note: Is not very useful currently as dirty entities are only registered
1416
     * at commit time.
1417
     *
1418
     * @param object $entity
1419
     *
1420
     * @return boolean
1421
     */
1422
    public function isScheduledForUpdate($entity)
1423
    {
1424
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1425
    }
1426
1427
    /**
1428
     * Checks whether an entity is registered to be checked in the unit of work.
1429
     *
1430
     * @param object $entity
1431
     *
1432
     * @return boolean
1433
     */
1434 5
    public function isScheduledForDirtyCheck($entity)
1435
    {
1436 5
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1437
1438 5
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1439
    }
1440
1441
    /**
1442
     * INTERNAL:
1443
     * Schedules an entity for deletion.
1444
     *
1445
     * @param object $entity
1446
     *
1447
     * @return void
1448
     */
1449 68
    public function scheduleForDelete($entity)
1450
    {
1451 68
        $oid = spl_object_hash($entity);
1452
1453 68
        if (isset($this->entityInsertions[$oid])) {
1454 1
            if ($this->isInIdentityMap($entity)) {
1455
                $this->removeFromIdentityMap($entity);
1456
            }
1457
1458 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1459
1460 1
            return; // entity has not been persisted yet, so nothing more to do.
1461
        }
1462
1463 68
        if ( ! $this->isInIdentityMap($entity)) {
1464 1
            return;
1465
        }
1466
1467 67
        $this->removeFromIdentityMap($entity);
1468
1469 67
        unset($this->entityUpdates[$oid]);
1470
1471 67
        if ( ! isset($this->entityDeletions[$oid])) {
1472 67
            $this->entityDeletions[$oid] = $entity;
1473 67
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1474
        }
1475 67
    }
1476
1477
    /**
1478
     * Checks whether an entity is registered as removed/deleted with the unit
1479
     * of work.
1480
     *
1481
     * @param object $entity
1482
     *
1483
     * @return boolean
1484
     */
1485 17
    public function isScheduledForDelete($entity)
1486
    {
1487 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1488
    }
1489
1490
    /**
1491
     * Checks whether an entity is scheduled for insertion, update or deletion.
1492
     *
1493
     * @param object $entity
1494
     *
1495
     * @return boolean
1496
     */
1497
    public function isEntityScheduled($entity)
1498
    {
1499
        $oid = spl_object_hash($entity);
1500
1501
        return isset($this->entityInsertions[$oid])
1502
            || isset($this->entityUpdates[$oid])
1503
            || isset($this->entityDeletions[$oid]);
1504
    }
1505
1506
    /**
1507
     * INTERNAL:
1508
     * Registers an entity in the identity map.
1509
     * Note that entities in a hierarchy are registered with the class name of
1510
     * the root entity.
1511
     *
1512
     * @ignore
1513
     *
1514
     * @param object $entity The entity to register.
1515
     *
1516
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1517
     *                 the entity in question is already managed.
1518
     *
1519
     * @throws ORMInvalidArgumentException
1520
     */
1521 1195
    public function addToIdentityMap($entity)
1522
    {
1523 1195
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1524 1195
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1525
1526 1195
        if (empty($identifier) || in_array(null, $identifier, true)) {
1527 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1528
        }
1529
1530 1189
        $idHash    = implode(' ', $identifier);
1531 1189
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1532
1533 1189
        if (isset($this->identityMap[$className][$idHash])) {
1534 87
            return false;
1535
        }
1536
1537 1189
        $this->identityMap[$className][$idHash] = $entity;
1538
1539 1189
        return true;
1540
    }
1541
1542
    /**
1543
     * Gets the state of an entity with regard to the current unit of work.
1544
     *
1545
     * @param object   $entity
1546
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1547
     *                         This parameter can be set to improve performance of entity state detection
1548
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1549
     *                         is either known or does not matter for the caller of the method.
1550
     *
1551
     * @return int The entity state.
1552
     */
1553 1145
    public function getEntityState($entity, $assume = null)
1554
    {
1555 1145
        $oid = spl_object_hash($entity);
1556
1557 1145
        if (isset($this->entityStates[$oid])) {
1558 830
            return $this->entityStates[$oid];
1559
        }
1560
1561 1139
        if ($assume !== null) {
1562 1135
            return $assume;
1563
        }
1564
1565
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1566
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1567
        // the UoW does not hold references to such objects and the object hash can be reused.
1568
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1569 13
        $class = $this->em->getClassMetadata(get_class($entity));
1570 13
        $id    = $class->getIdentifierValues($entity);
1571
1572 13
        if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array<mixed,mixed> 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...
1573 5
            return self::STATE_NEW;
1574
        }
1575
1576 10
        if ($class->containsForeignIdentifier) {
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1577 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1578
        }
1579
1580
        switch (true) {
1581 10
            case ($class->isIdentifierNatural()):
0 ignored issues
show
Bug introduced by
The method isIdentifierNatural() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1581
            case ($class->/** @scrutinizer ignore-call */ isIdentifierNatural()):

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...
1582
                // Check for a version field, if available, to avoid a db lookup.
1583 5
                if ($class->isVersioned) {
0 ignored issues
show
Bug introduced by
Accessing isVersioned on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1584 1
                    return ($class->getFieldValue($entity, $class->versionField))
0 ignored issues
show
Bug introduced by
Accessing versionField on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
The method getFieldValue() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. Did you maybe mean getFieldNames()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1584
                    return ($class->/** @scrutinizer ignore-call */ getFieldValue($entity, $class->versionField))

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...
1585
                        ? self::STATE_DETACHED
1586 1
                        : self::STATE_NEW;
1587
                }
1588
1589
                // Last try before db lookup: check the identity map.
1590 4
                if ($this->tryGetById($id, $class->rootEntityName)) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1591 1
                    return self::STATE_DETACHED;
1592
                }
1593
1594
                // db lookup
1595 4
                if ($this->getEntityPersister($class->name)->exists($entity)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1596
                    return self::STATE_DETACHED;
1597
                }
1598
1599 4
                return self::STATE_NEW;
1600
1601 5
            case ( ! $class->idGenerator->isPostInsertGenerator()):
0 ignored issues
show
Bug introduced by
Accessing idGenerator on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1602
                // if we have a pre insert generator we can't be sure that having an id
1603
                // really means that the entity exists. We have to verify this through
1604
                // the last resort: a db lookup
1605
1606
                // Last try before db lookup: check the identity map.
1607
                if ($this->tryGetById($id, $class->rootEntityName)) {
1608
                    return self::STATE_DETACHED;
1609
                }
1610
1611
                // db lookup
1612
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1613
                    return self::STATE_DETACHED;
1614
                }
1615
1616
                return self::STATE_NEW;
1617
1618
            default:
1619 5
                return self::STATE_DETACHED;
1620
        }
1621
    }
1622
1623
    /**
1624
     * INTERNAL:
1625
     * Removes an entity from the identity map. This effectively detaches the
1626
     * entity from the persistence management of Doctrine.
1627
     *
1628
     * @ignore
1629
     *
1630
     * @param object $entity
1631
     *
1632
     * @return boolean
1633
     *
1634
     * @throws ORMInvalidArgumentException
1635
     */
1636 82
    public function removeFromIdentityMap($entity)
1637
    {
1638 82
        $oid           = spl_object_hash($entity);
1639 82
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1640 82
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1641
1642 82
        if ($idHash === '') {
1643
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1644
        }
1645
1646 82
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1647
1648 82
        if (isset($this->identityMap[$className][$idHash])) {
1649 82
            unset($this->identityMap[$className][$idHash]);
1650 82
            unset($this->readOnlyObjects[$oid]);
1651
1652
            //$this->entityStates[$oid] = self::STATE_DETACHED;
1653
1654 82
            return true;
1655
        }
1656
1657
        return false;
1658
    }
1659
1660
    /**
1661
     * INTERNAL:
1662
     * Gets an entity in the identity map by its identifier hash.
1663
     *
1664
     * @ignore
1665
     *
1666
     * @param string $idHash
1667
     * @param string $rootClassName
1668
     *
1669
     * @return object
1670
     */
1671 6
    public function getByIdHash($idHash, $rootClassName)
1672
    {
1673 6
        return $this->identityMap[$rootClassName][$idHash];
1674
    }
1675
1676
    /**
1677
     * INTERNAL:
1678
     * Tries to get an entity by its identifier hash. If no entity is found for
1679
     * the given hash, FALSE is returned.
1680
     *
1681
     * @ignore
1682
     *
1683
     * @param mixed  $idHash        (must be possible to cast it to string)
1684
     * @param string $rootClassName
1685
     *
1686
     * @return object|bool The found entity or FALSE.
1687
     */
1688 35
    public function tryGetByIdHash($idHash, $rootClassName)
1689
    {
1690 35
        $stringIdHash = (string) $idHash;
1691
1692 35
        return isset($this->identityMap[$rootClassName][$stringIdHash])
1693 35
            ? $this->identityMap[$rootClassName][$stringIdHash]
1694 35
            : false;
1695
    }
1696
1697
    /**
1698
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1699
     *
1700
     * @param object $entity
1701
     *
1702
     * @return boolean
1703
     */
1704 234
    public function isInIdentityMap($entity)
1705
    {
1706 234
        $oid = spl_object_hash($entity);
1707
1708 234
        if (empty($this->entityIdentifiers[$oid])) {
1709 37
            return false;
1710
        }
1711
1712 217
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1713 217
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1714
1715 217
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1716
    }
1717
1718
    /**
1719
     * INTERNAL:
1720
     * Checks whether an identifier hash exists in the identity map.
1721
     *
1722
     * @ignore
1723
     *
1724
     * @param string $idHash
1725
     * @param string $rootClassName
1726
     *
1727
     * @return boolean
1728
     */
1729
    public function containsIdHash($idHash, $rootClassName)
1730
    {
1731
        return isset($this->identityMap[$rootClassName][$idHash]);
1732
    }
1733
1734
    /**
1735
     * Persists an entity as part of the current unit of work.
1736
     *
1737
     * @param object $entity The entity to persist.
1738
     *
1739
     * @return void
1740
     */
1741 1126
    public function persist($entity)
1742
    {
1743 1126
        $visited = [];
1744
1745 1126
        $this->doPersist($entity, $visited);
1746 1119
    }
1747
1748
    /**
1749
     * Persists an entity as part of the current unit of work.
1750
     *
1751
     * This method is internally called during persist() cascades as it tracks
1752
     * the already visited entities to prevent infinite recursions.
1753
     *
1754
     * @param object $entity  The entity to persist.
1755
     * @param array  $visited The already visited entities.
1756
     *
1757
     * @return void
1758
     *
1759
     * @throws ORMInvalidArgumentException
1760
     * @throws UnexpectedValueException
1761
     */
1762 1126
    private function doPersist($entity, array &$visited)
1763
    {
1764 1126
        $oid = spl_object_hash($entity);
1765
1766 1126
        if (isset($visited[$oid])) {
1767 111
            return; // Prevent infinite recursion
1768
        }
1769
1770 1126
        $visited[$oid] = $entity; // Mark visited
1771
1772 1126
        $class = $this->em->getClassMetadata(get_class($entity));
1773
1774
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1775
        // If we would detect DETACHED here we would throw an exception anyway with the same
1776
        // consequences (not recoverable/programming error), so just assuming NEW here
1777
        // lets us avoid some database lookups for entities with natural identifiers.
1778 1126
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1779
1780
        switch ($entityState) {
1781 1126
            case self::STATE_MANAGED:
1782
                // Nothing to do, except if policy is "deferred explicit"
1783 240
                if ($class->isChangeTrackingDeferredExplicit()) {
0 ignored issues
show
Bug introduced by
The method isChangeTrackingDeferredExplicit() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1783
                if ($class->/** @scrutinizer ignore-call */ isChangeTrackingDeferredExplicit()) {
Loading history...
1784 5
                    $this->scheduleForDirtyCheck($entity);
1785
                }
1786 240
                break;
1787
1788 1126
            case self::STATE_NEW:
1789 1125
                $this->persistNew($class, $entity);
1790 1125
                break;
1791
1792 1
            case self::STATE_REMOVED:
1793
                // Entity becomes managed again
1794 1
                unset($this->entityDeletions[$oid]);
1795 1
                $this->addToIdentityMap($entity);
1796
1797 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1798 1
                break;
1799
1800
            case self::STATE_DETACHED:
1801
                // Can actually not happen right now since we assume STATE_NEW.
1802
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1803
1804
            default:
1805
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1806
        }
1807
1808 1126
        $this->cascadePersist($entity, $visited);
1809 1119
    }
1810
1811
    /**
1812
     * Deletes an entity as part of the current unit of work.
1813
     *
1814
     * @param object $entity The entity to remove.
1815
     *
1816
     * @return void
1817
     */
1818 67
    public function remove($entity)
1819
    {
1820 67
        $visited = [];
1821
1822 67
        $this->doRemove($entity, $visited);
1823 67
    }
1824
1825
    /**
1826
     * Deletes an entity as part of the current unit of work.
1827
     *
1828
     * This method is internally called during delete() cascades as it tracks
1829
     * the already visited entities to prevent infinite recursions.
1830
     *
1831
     * @param object $entity  The entity to delete.
1832
     * @param array  $visited The map of the already visited entities.
1833
     *
1834
     * @return void
1835
     *
1836
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1837
     * @throws UnexpectedValueException
1838
     */
1839 67
    private function doRemove($entity, array &$visited)
1840
    {
1841 67
        $oid = spl_object_hash($entity);
1842
1843 67
        if (isset($visited[$oid])) {
1844 1
            return; // Prevent infinite recursion
1845
        }
1846
1847 67
        $visited[$oid] = $entity; // mark visited
1848
1849
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1850
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1851 67
        $this->cascadeRemove($entity, $visited);
1852
1853 67
        $class       = $this->em->getClassMetadata(get_class($entity));
1854 67
        $entityState = $this->getEntityState($entity);
1855
1856
        switch ($entityState) {
1857 67
            case self::STATE_NEW:
1858 67
            case self::STATE_REMOVED:
1859
                // nothing to do
1860 2
                break;
1861
1862 67
            case self::STATE_MANAGED:
1863 67
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1864
1865 67
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1866 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1867
                }
1868
1869 67
                $this->scheduleForDelete($entity);
1870 67
                break;
1871
1872
            case self::STATE_DETACHED:
1873
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1874
            default:
1875
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1876
        }
1877
1878 67
    }
1879
1880
    /**
1881
     * Merges the state of the given detached entity into this UnitOfWork.
1882
     *
1883
     * @param object $entity
1884
     *
1885
     * @return object The managed copy of the entity.
1886
     *
1887
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1888
     *         attribute and the version check against the managed copy fails.
1889
     *
1890
     * @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
1891
     */
1892 44
    public function merge($entity)
1893
    {
1894 44
        $visited = [];
1895
1896 44
        return $this->doMerge($entity, $visited);
1897
    }
1898
1899
    /**
1900
     * Executes a merge operation on an entity.
1901
     *
1902
     * @param object      $entity
1903
     * @param array       $visited
1904
     * @param object|null $prevManagedCopy
1905
     * @param array|null  $assoc
1906
     *
1907
     * @return object The managed copy of the entity.
1908
     *
1909
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1910
     *         attribute and the version check against the managed copy fails.
1911
     * @throws ORMInvalidArgumentException If the entity instance is NEW.
1912
     * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided
1913
     */
1914 44
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
1915
    {
1916 44
        $oid = spl_object_hash($entity);
1917
1918 44
        if (isset($visited[$oid])) {
1919 4
            $managedCopy = $visited[$oid];
1920
1921 4
            if ($prevManagedCopy !== null) {
1922 4
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1923
            }
1924
1925 4
            return $managedCopy;
1926
        }
1927
1928 44
        $class = $this->em->getClassMetadata(get_class($entity));
1929
1930
        // First we assume DETACHED, although it can still be NEW but we can avoid
1931
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1932
        // we need to fetch it from the db anyway in order to merge.
1933
        // MANAGED entities are ignored by the merge operation.
1934 44
        $managedCopy = $entity;
1935
1936 44
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1937
            // Try to look the entity up in the identity map.
1938 42
            $id = $class->getIdentifierValues($entity);
1939
1940
            // If there is no ID, it is actually NEW.
1941 42
            if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array<mixed,mixed> 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...
1942 6
                $managedCopy = $this->newInstance($class);
1943
1944 6
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1945 6
                $this->persistNew($class, $managedCopy);
1946
            } else {
1947 37
                $flatId = ($class->containsForeignIdentifier)
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1948 3
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1949 37
                    : $id;
1950
1951 37
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1952
1953 37
                if ($managedCopy) {
1954
                    // We have the entity in-memory already, just make sure its not removed.
1955 15
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $entity of Doctrine\ORM\UnitOfWork::getEntityState() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1955
                    if ($this->getEntityState(/** @scrutinizer ignore-type */ $managedCopy) == self::STATE_REMOVED) {
Loading history...
1956 15
                        throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge");
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $entity of Doctrine\ORM\ORMInvalidA...tion::entityIsRemoved() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1956
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, "merge");
Loading history...
1957
                    }
1958
                } else {
1959
                    // We need to fetch the managed copy in order to merge.
1960 25
                    $managedCopy = $this->em->find($class->name, $flatId);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1961
                }
1962
1963 37
                if ($managedCopy === null) {
1964
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1965
                    // since the managed entity was not found.
1966 3
                    if ( ! $class->isIdentifierNatural()) {
1967 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1968 1
                            $class->getName(),
1969 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1970
                        );
1971
                    }
1972
1973 2
                    $managedCopy = $this->newInstance($class);
1974 2
                    $class->setIdentifierValues($managedCopy, $id);
0 ignored issues
show
Bug introduced by
The method setIdentifierValues() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1974
                    $class->/** @scrutinizer ignore-call */ 
1975
                            setIdentifierValues($managedCopy, $id);
Loading history...
1975
1976 2
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1977 2
                    $this->persistNew($class, $managedCopy);
1978
                } else {
1979 34
                    $this->ensureVersionMatch($class, $entity, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork::ensureVersionMatch() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1979
                    $this->ensureVersionMatch($class, $entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1980 33
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork:...yStateIntoManagedCopy() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1980
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1981
                }
1982
            }
1983
1984 40
            $visited[$oid] = $managedCopy; // mark visited
1985
1986 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1987
                $this->scheduleForDirtyCheck($entity);
1988
            }
1989
        }
1990
1991 42
        if ($prevManagedCopy !== null) {
1992 6
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1992
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1993
        }
1994
1995
        // Mark the managed copy visited as well
1996 42
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $obj of spl_object_hash() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1996
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
1997
1998 42
        $this->cascadeMerge($entity, $managedCopy, $visited);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork::cascadeMerge() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1998
        $this->cascadeMerge($entity, /** @scrutinizer ignore-type */ $managedCopy, $visited);
Loading history...
1999
2000 42
        return $managedCopy;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $managedCopy also could return the type true which is incompatible with the documented return type object.
Loading history...
2001
    }
2002
2003
    /**
2004
     * @param ClassMetadata $class
2005
     * @param object        $entity
2006
     * @param object        $managedCopy
2007
     *
2008
     * @return void
2009
     *
2010
     * @throws OptimisticLockException
2011
     */
2012 34
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
2013
    {
2014 34
        if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
2015 31
            return;
2016
        }
2017
2018 4
        $reflField          = $class->reflFields[$class->versionField];
2019 4
        $managedCopyVersion = $reflField->getValue($managedCopy);
2020 4
        $entityVersion      = $reflField->getValue($entity);
2021
2022
        // Throw exception if versions don't match.
2023 4
        if ($managedCopyVersion == $entityVersion) {
2024 3
            return;
2025
        }
2026
2027 1
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
2028
    }
2029
2030
    /**
2031
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2032
     *
2033
     * @param object $entity
2034
     *
2035
     * @return bool
2036
     */
2037 41
    private function isLoaded($entity)
2038
    {
2039 41
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2040
    }
2041
2042
    /**
2043
     * Sets/adds associated managed copies into the previous entity's association field
2044
     *
2045
     * @param object $entity
2046
     * @param array  $association
2047
     * @param object $previousManagedCopy
2048
     * @param object $managedCopy
2049
     *
2050
     * @return void
2051
     */
2052 6
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2053
    {
2054 6
        $assocField = $association['fieldName'];
2055 6
        $prevClass  = $this->em->getClassMetadata(get_class($previousManagedCopy));
2056
2057 6
        if ($association['type'] & ClassMetadata::TO_ONE) {
2058 6
            $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2059
2060 6
            return;
2061
        }
2062
2063 1
        $value   = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
2064 1
        $value[] = $managedCopy;
2065
2066 1
        if ($association['type'] == ClassMetadata::ONE_TO_MANY) {
2067 1
            $class = $this->em->getClassMetadata(get_class($entity));
2068
2069 1
            $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
2070
        }
2071 1
    }
2072
2073
    /**
2074
     * Detaches an entity from the persistence management. It's persistence will
2075
     * no longer be managed by Doctrine.
2076
     *
2077
     * @param object $entity The entity to detach.
2078
     *
2079
     * @return void
2080
     *
2081
     * @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
2082
     */
2083 9
    public function detach($entity)
2084
    {
2085 9
        $visited = [];
2086
2087 9
        $this->doDetach($entity, $visited);
2088 9
    }
2089
2090
    /**
2091
     * Executes a detach operation on the given entity.
2092
     *
2093
     * @param object  $entity
2094
     * @param array   $visited
2095
     * @param boolean $noCascade if true, don't cascade detach operation.
2096
     *
2097
     * @return void
2098
     */
2099 18
    private function doDetach($entity, array &$visited, $noCascade = false)
2100
    {
2101 18
        $oid = spl_object_hash($entity);
2102
2103 18
        if (isset($visited[$oid])) {
2104
            return; // Prevent infinite recursion
2105
        }
2106
2107 18
        $visited[$oid] = $entity; // mark visited
2108
2109 18
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2110 18
            case self::STATE_MANAGED:
2111 16
                if ($this->isInIdentityMap($entity)) {
2112 15
                    $this->removeFromIdentityMap($entity);
2113
                }
2114
2115
                unset(
2116 16
                    $this->entityInsertions[$oid],
2117 16
                    $this->entityUpdates[$oid],
2118 16
                    $this->entityDeletions[$oid],
2119 16
                    $this->entityIdentifiers[$oid],
2120 16
                    $this->entityStates[$oid],
2121 16
                    $this->originalEntityData[$oid]
2122
                );
2123 16
                break;
2124 2
            case self::STATE_NEW:
2125 2
            case self::STATE_DETACHED:
2126 2
                return;
2127
        }
2128
2129 16
        if ( ! $noCascade) {
2130 16
            $this->cascadeDetach($entity, $visited);
2131
        }
2132 16
    }
2133
2134
    /**
2135
     * Refreshes the state of the given entity from the database, overwriting
2136
     * any local, unpersisted changes.
2137
     *
2138
     * @param object $entity The entity to refresh.
2139
     *
2140
     * @return void
2141
     *
2142
     * @throws InvalidArgumentException If the entity is not MANAGED.
2143
     */
2144 17
    public function refresh($entity)
2145
    {
2146 17
        $visited = [];
2147
2148 17
        $this->doRefresh($entity, $visited);
2149 17
    }
2150
2151
    /**
2152
     * Executes a refresh operation on an entity.
2153
     *
2154
     * @param object $entity  The entity to refresh.
2155
     * @param array  $visited The already visited entities during cascades.
2156
     *
2157
     * @return void
2158
     *
2159
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
2160
     */
2161 17
    private function doRefresh($entity, array &$visited)
2162
    {
2163 17
        $oid = spl_object_hash($entity);
2164
2165 17
        if (isset($visited[$oid])) {
2166
            return; // Prevent infinite recursion
2167
        }
2168
2169 17
        $visited[$oid] = $entity; // mark visited
2170
2171 17
        $class = $this->em->getClassMetadata(get_class($entity));
2172
2173 17
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
2174
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2175
        }
2176
2177 17
        $this->getEntityPersister($class->name)->refresh(
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2178 17
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->ge...ntityIdentifiers[$oid]) can also be of type false; however, parameter $id of Doctrine\ORM\Persisters\...ityPersister::refresh() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2178
            /** @scrutinizer ignore-type */ array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
Loading history...
2179 17
            $entity
2180
        );
2181
2182 17
        $this->cascadeRefresh($entity, $visited);
2183 17
    }
2184
2185
    /**
2186
     * Cascades a refresh operation to associated entities.
2187
     *
2188
     * @param object $entity
2189
     * @param array  $visited
2190
     *
2191
     * @return void
2192
     */
2193 17
    private function cascadeRefresh($entity, array &$visited)
2194
    {
2195 17
        $class = $this->em->getClassMetadata(get_class($entity));
2196
2197 17
        $associationMappings = array_filter(
2198 17
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2199
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2200
        );
2201
2202 17
        foreach ($associationMappings as $assoc) {
2203 5
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2204
2205
            switch (true) {
2206 5
                case ($relatedEntities instanceof PersistentCollection):
2207
                    // Unwrap so that foreach() does not initialize
2208 5
                    $relatedEntities = $relatedEntities->unwrap();
2209
                    // break; is commented intentionally!
2210
2211
                case ($relatedEntities instanceof Collection):
2212
                case (is_array($relatedEntities)):
2213 5
                    foreach ($relatedEntities as $relatedEntity) {
2214
                        $this->doRefresh($relatedEntity, $visited);
2215
                    }
2216 5
                    break;
2217
2218
                case ($relatedEntities !== null):
2219
                    $this->doRefresh($relatedEntities, $visited);
2220
                    break;
2221
2222
                default:
2223
                    // Do nothing
2224
            }
2225
        }
2226 17
    }
2227
2228
    /**
2229
     * Cascades a detach operation to associated entities.
2230
     *
2231
     * @param object $entity
2232
     * @param array  $visited
2233
     *
2234
     * @return void
2235
     */
2236 16
    private function cascadeDetach($entity, array &$visited)
2237
    {
2238 16
        $class = $this->em->getClassMetadata(get_class($entity));
2239
2240 16
        $associationMappings = array_filter(
2241 16
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2242
            function ($assoc) { return $assoc['isCascadeDetach']; }
2243
        );
2244
2245 16
        foreach ($associationMappings as $assoc) {
2246 4
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2247
2248
            switch (true) {
2249 4
                case ($relatedEntities instanceof PersistentCollection):
2250
                    // Unwrap so that foreach() does not initialize
2251 2
                    $relatedEntities = $relatedEntities->unwrap();
2252
                    // break; is commented intentionally!
2253
2254 2
                case ($relatedEntities instanceof Collection):
2255 1
                case (is_array($relatedEntities)):
2256 3
                    foreach ($relatedEntities as $relatedEntity) {
2257 1
                        $this->doDetach($relatedEntity, $visited);
2258
                    }
2259 3
                    break;
2260
2261 1
                case ($relatedEntities !== null):
2262
                    $this->doDetach($relatedEntities, $visited);
2263
                    break;
2264
2265
                default:
2266
                    // Do nothing
2267
            }
2268
        }
2269 16
    }
2270
2271
    /**
2272
     * Cascades a merge operation to associated entities.
2273
     *
2274
     * @param object $entity
2275
     * @param object $managedCopy
2276
     * @param array  $visited
2277
     *
2278
     * @return void
2279
     */
2280 42
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2281
    {
2282 42
        $class = $this->em->getClassMetadata(get_class($entity));
2283
2284 42
        $associationMappings = array_filter(
2285 42
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2286
            function ($assoc) { return $assoc['isCascadeMerge']; }
2287
        );
2288
2289 42
        foreach ($associationMappings as $assoc) {
2290 16
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2291
2292 16
            if ($relatedEntities instanceof Collection) {
2293 10
                if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2294 1
                    continue;
2295
                }
2296
2297 9
                if ($relatedEntities instanceof PersistentCollection) {
2298
                    // Unwrap so that foreach() does not initialize
2299 5
                    $relatedEntities = $relatedEntities->unwrap();
2300
                }
2301
2302 9
                foreach ($relatedEntities as $relatedEntity) {
2303 1
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2304
                }
2305 7
            } else if ($relatedEntities !== null) {
2306 6
                $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2307
            }
2308
        }
2309 42
    }
2310
2311
    /**
2312
     * Cascades the save operation to associated entities.
2313
     *
2314
     * @param object $entity
2315
     * @param array  $visited
2316
     *
2317
     * @return void
2318
     */
2319 1126
    private function cascadePersist($entity, array &$visited)
2320
    {
2321 1126
        $class = $this->em->getClassMetadata(get_class($entity));
2322
2323 1126
        $associationMappings = array_filter(
2324 1126
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2325
            function ($assoc) { return $assoc['isCascadePersist']; }
2326
        );
2327
2328 1126
        foreach ($associationMappings as $assoc) {
2329 695
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2330
2331
            switch (true) {
2332 695
                case ($relatedEntities instanceof PersistentCollection):
2333
                    // Unwrap so that foreach() does not initialize
2334 22
                    $relatedEntities = $relatedEntities->unwrap();
2335
                    // break; is commented intentionally!
2336
2337 695
                case ($relatedEntities instanceof Collection):
2338 629
                case (is_array($relatedEntities)):
2339 584
                    if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
2340 3
                        throw ORMInvalidArgumentException::invalidAssociation(
2341 3
                            $this->em->getClassMetadata($assoc['targetEntity']),
2342 3
                            $assoc,
2343 3
                            $relatedEntities
2344
                        );
2345
                    }
2346
2347 581
                    foreach ($relatedEntities as $relatedEntity) {
2348 301
                        $this->doPersist($relatedEntity, $visited);
2349
                    }
2350
2351 581
                    break;
2352
2353 613
                case ($relatedEntities !== null):
2354 256
                    if (! $relatedEntities instanceof $assoc['targetEntity']) {
2355 4
                        throw ORMInvalidArgumentException::invalidAssociation(
2356 4
                            $this->em->getClassMetadata($assoc['targetEntity']),
2357 4
                            $assoc,
2358 4
                            $relatedEntities
2359
                        );
2360
                    }
2361
2362 252
                    $this->doPersist($relatedEntities, $visited);
2363 252
                    break;
2364
2365
                default:
2366
                    // Do nothing
2367
            }
2368
        }
2369 1119
    }
2370
2371
    /**
2372
     * Cascades the delete operation to associated entities.
2373
     *
2374
     * @param object $entity
2375
     * @param array  $visited
2376
     *
2377
     * @return void
2378
     */
2379 67
    private function cascadeRemove($entity, array &$visited)
2380
    {
2381 67
        $class = $this->em->getClassMetadata(get_class($entity));
2382
2383 67
        $associationMappings = array_filter(
2384 67
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2385
            function ($assoc) { return $assoc['isCascadeRemove']; }
2386
        );
2387
2388 67
        $entitiesToCascade = [];
2389
2390 67
        foreach ($associationMappings as $assoc) {
2391 26
            if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2392 6
                $entity->__load();
2393
            }
2394
2395 26
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2396
2397
            switch (true) {
2398 26
                case ($relatedEntities instanceof Collection):
2399 19
                case (is_array($relatedEntities)):
2400
                    // If its a PersistentCollection initialization is intended! No unwrap!
2401 20
                    foreach ($relatedEntities as $relatedEntity) {
2402 10
                        $entitiesToCascade[] = $relatedEntity;
2403
                    }
2404 20
                    break;
2405
2406 19
                case ($relatedEntities !== null):
2407 7
                    $entitiesToCascade[] = $relatedEntities;
2408 7
                    break;
2409
2410
                default:
2411
                    // Do nothing
2412
            }
2413
        }
2414
2415 67
        foreach ($entitiesToCascade as $relatedEntity) {
2416 16
            $this->doRemove($relatedEntity, $visited);
2417
        }
2418 67
    }
2419
2420
    /**
2421
     * Acquire a lock on the given entity.
2422
     *
2423
     * @param object $entity
2424
     * @param int    $lockMode
2425
     * @param int    $lockVersion
2426
     *
2427
     * @return void
2428
     *
2429
     * @throws ORMInvalidArgumentException
2430
     * @throws TransactionRequiredException
2431
     * @throws OptimisticLockException
2432
     */
2433 10
    public function lock($entity, $lockMode, $lockVersion = null)
2434
    {
2435 10
        if ($entity === null) {
2436 1
            throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock().");
2437
        }
2438
2439 9
        if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2440 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2441
        }
2442
2443 8
        $class = $this->em->getClassMetadata(get_class($entity));
2444
2445
        switch (true) {
2446 8
            case LockMode::OPTIMISTIC === $lockMode:
2447 6
                if ( ! $class->isVersioned) {
0 ignored issues
show
Bug introduced by
Accessing isVersioned on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2448 1
                    throw OptimisticLockException::notVersioned($class->name);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2449
                }
2450
2451 5
                if ($lockVersion === null) {
2452 1
                    return;
2453
                }
2454
2455 4
                if ($entity instanceof Proxy && !$entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2456 1
                    $entity->__load();
2457
                }
2458
2459 4
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing versionField on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2460
2461 4
                if ($entityVersion != $lockVersion) {
2462 2
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2463
                }
2464
2465 2
                break;
2466
2467 2
            case LockMode::NONE === $lockMode:
2468 2
            case LockMode::PESSIMISTIC_READ === $lockMode:
2469 1
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2470 2
                if (!$this->em->getConnection()->isTransactionActive()) {
2471 2
                    throw TransactionRequiredException::transactionRequired();
2472
                }
2473
2474
                $oid = spl_object_hash($entity);
2475
2476
                $this->getEntityPersister($class->name)->lock(
2477
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->ge...ntityIdentifiers[$oid]) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...EntityPersister::lock() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2477
                    /** @scrutinizer ignore-type */ array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
Loading history...
2478
                    $lockMode
2479
                );
2480
                break;
2481
2482
            default:
2483
                // Do nothing
2484
        }
2485 2
    }
2486
2487
    /**
2488
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
2489
     *
2490
     * @return \Doctrine\ORM\Internal\CommitOrderCalculator
2491
     */
2492 1097
    public function getCommitOrderCalculator()
2493
    {
2494 1097
        return new Internal\CommitOrderCalculator();
2495
    }
2496
2497
    /**
2498
     * Clears the UnitOfWork.
2499
     *
2500
     * @param string|null $entityName if given, only entities of this type will get detached.
2501
     *
2502
     * @return void
2503
     *
2504
     * @throws ORMInvalidArgumentException if an invalid entity name is given
2505
     */
2506 1335
    public function clear($entityName = null)
2507
    {
2508 1335
        if ($entityName === null) {
2509 1332
            $this->identityMap                    =
2510 1332
            $this->entityIdentifiers              =
2511 1332
            $this->originalEntityData             =
2512 1332
            $this->entityChangeSets               =
2513 1332
            $this->entityStates                   =
2514 1332
            $this->scheduledForSynchronization    =
2515 1332
            $this->entityInsertions               =
2516 1332
            $this->entityUpdates                  =
2517 1332
            $this->entityDeletions                =
2518 1332
            $this->nonCascadedNewDetectedEntities =
2519 1332
            $this->collectionDeletions            =
2520 1332
            $this->collectionUpdates              =
2521 1332
            $this->extraUpdates                   =
2522 1332
            $this->readOnlyObjects                =
2523 1332
            $this->visitedCollections             =
2524 1332
            $this->eagerLoadingEntities           =
2525 1332
            $this->orphanRemovals                 = [];
2526
        } else {
2527 10
            $this->clearIdentityMapForEntityName($entityName);
2528 10
            $this->clearEntityInsertionsForEntityName($entityName);
2529
        }
2530
2531 1335
        if ($this->evm->hasListeners(Events::onClear)) {
2532 9
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
2533
        }
2534 1335
    }
2535
2536
    /**
2537
     * INTERNAL:
2538
     * Schedules an orphaned entity for removal. The remove() operation will be
2539
     * invoked on that entity at the beginning of the next commit of this
2540
     * UnitOfWork.
2541
     *
2542
     * @ignore
2543
     *
2544
     * @param object $entity
2545
     *
2546
     * @return void
2547
     */
2548 17
    public function scheduleOrphanRemoval($entity)
2549
    {
2550 17
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2551 17
    }
2552
2553
    /**
2554
     * INTERNAL:
2555
     * Cancels a previously scheduled orphan removal.
2556
     *
2557
     * @ignore
2558
     *
2559
     * @param object $entity
2560
     *
2561
     * @return void
2562
     */
2563 117
    public function cancelOrphanRemoval($entity)
2564
    {
2565 117
        unset($this->orphanRemovals[spl_object_hash($entity)]);
2566 117
    }
2567
2568
    /**
2569
     * INTERNAL:
2570
     * Schedules a complete collection for removal when this UnitOfWork commits.
2571
     *
2572
     * @param PersistentCollection $coll
2573
     *
2574
     * @return void
2575
     */
2576 16
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2577
    {
2578 16
        $coid = spl_object_hash($coll);
2579
2580
        // TODO: if $coll is already scheduled for recreation ... what to do?
2581
        // Just remove $coll from the scheduled recreations?
2582 16
        unset($this->collectionUpdates[$coid]);
2583
2584 16
        $this->collectionDeletions[$coid] = $coll;
2585 16
    }
2586
2587
    /**
2588
     * @param PersistentCollection $coll
2589
     *
2590
     * @return bool
2591
     */
2592
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2593
    {
2594
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2595
    }
2596
2597
    /**
2598
     * @param ClassMetadata $class
2599
     *
2600
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
2601
     */
2602 728
    private function newInstance($class)
2603
    {
2604 728
        $entity = $class->newInstance();
2605
2606 728
        if ($entity instanceof \Doctrine\Common\Persistence\ObjectManagerAware) {
2607 4
            $entity->injectObjectManager($this->em, $class);
2608
        }
2609
2610 728
        return $entity;
2611
    }
2612
2613
    /**
2614
     * INTERNAL:
2615
     * Creates an entity. Used for reconstitution of persistent entities.
2616
     *
2617
     * Internal note: Highly performance-sensitive method.
2618
     *
2619
     * @ignore
2620
     *
2621
     * @param string $className The name of the entity class.
2622
     * @param array  $data      The data for the entity.
2623
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the entity.
2624
     *
2625
     * @return object The managed entity instance.
2626
     *
2627
     * @todo Rename: getOrCreateEntity
2628
     */
2629 873
    public function createEntity($className, array $data, &$hints = [])
2630
    {
2631 873
        $class = $this->em->getClassMetadata($className);
2632
2633 873
        $id = $this->identifierFlattener->flattenIdentifier($class, $data);
2634 873
        $idHash = implode(' ', $id);
2635
2636 873
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2637 329
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
2638 329
            $oid = spl_object_hash($entity);
2639
2640
            if (
2641 329
                isset($hints[Query::HINT_REFRESH])
2642 329
                && isset($hints[Query::HINT_REFRESH_ENTITY])
2643 329
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
2644 329
                && $unmanagedProxy instanceof Proxy
2645 329
                && $this->isIdentifierEquals($unmanagedProxy, $entity)
2646
            ) {
2647
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
2648
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
2649
                // refreshed object may be anything
2650
2651 2
                foreach ($class->identifier as $fieldName) {
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2652 2
                    $class->reflFields[$fieldName]->setValue($unmanagedProxy, null);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2653
                }
2654
2655 2
                return $unmanagedProxy;
2656
            }
2657
2658 327
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2659 23
                $entity->__setInitialized(true);
2660
2661 23
                if ($entity instanceof NotifyPropertyChanged) {
2662 23
                    $entity->addPropertyChangedListener($this);
2663
                }
2664
            } else {
2665 306
                if ( ! isset($hints[Query::HINT_REFRESH])
2666 306
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2667 234
                    return $entity;
2668
                }
2669
            }
2670
2671
            // inject ObjectManager upon refresh.
2672 116
            if ($entity instanceof ObjectManagerAware) {
2673 3
                $entity->injectObjectManager($this->em, $class);
2674
            }
2675
2676 116
            $this->originalEntityData[$oid] = $data;
2677
        } else {
2678 723
            $entity = $this->newInstance($class);
2679 723
            $oid    = spl_object_hash($entity);
2680
2681 723
            $this->entityIdentifiers[$oid]  = $id;
2682 723
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2683 723
            $this->originalEntityData[$oid] = $data;
2684
2685 723
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
2686
2687 723
            if ($entity instanceof NotifyPropertyChanged) {
2688 2
                $entity->addPropertyChangedListener($this);
2689
            }
2690
        }
2691
2692 761
        foreach ($data as $field => $value) {
2693 761
            if (isset($class->fieldMappings[$field])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2694 760
                $class->reflFields[$field]->setValue($entity, $value);
2695
            }
2696
        }
2697
2698
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2699 761
        unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2700
2701 761
        if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) {
2702
            unset($this->eagerLoadingEntities[$class->rootEntityName]);
2703
        }
2704
2705
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2706 761
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2707 34
            return $entity;
2708
        }
2709
2710 727
        foreach ($class->associationMappings as $field => $assoc) {
2711
            // Check if the association is not among the fetch-joined associations already.
2712 623
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2713 261
                continue;
2714
            }
2715
2716 599
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2717
2718
            switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $assoc['type'] & Doctrin...g\ClassMetadata::TO_ONE of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
2719 599
                case ($assoc['type'] & ClassMetadata::TO_ONE):
2720 516
                    if ( ! $assoc['isOwningSide']) {
2721
2722
                        // use the given entity association
2723 67
                        if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2724
2725 3
                            $this->originalEntityData[$oid][$field] = $data[$field];
2726
2727 3
                            $class->reflFields[$field]->setValue($entity, $data[$field]);
2728 3
                            $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
2729
2730 3
                            continue 2;
2731
                        }
2732
2733
                        // Inverse side of x-to-one can never be lazy
2734 64
                        $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
2735
2736 64
                        continue 2;
2737
                    }
2738
2739
                    // use the entity association
2740 516
                    if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2741 39
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2742 39
                        $this->originalEntityData[$oid][$field] = $data[$field];
2743
2744 39
                        break;
2745
                    }
2746
2747 508
                    $associatedId = [];
2748
2749
                    // TODO: Is this even computed right in all cases of composite keys?
2750 508
                    foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
2751 508
                        $joinColumnValue = $data[$srcColumn] ?? null;
2752
2753 508
                        if ($joinColumnValue !== null) {
2754 306
                            if ($targetClass->containsForeignIdentifier) {
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2755 12
                                $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
0 ignored issues
show
Bug introduced by
The method getFieldForColumn() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2755
                                $associatedId[$targetClass->/** @scrutinizer ignore-call */ getFieldForColumn($targetColumn)] = $joinColumnValue;
Loading history...
2756
                            } else {
2757 306
                                $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2758
                            }
2759 296
                        } elseif ($targetClass->containsForeignIdentifier
2760 296
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2761
                        ) {
2762
                            // the missing key is part of target's entity primary key
2763 7
                            $associatedId = [];
2764 7
                            break;
2765
                        }
2766
                    }
2767
2768 508
                    if ( ! $associatedId) {
2769
                        // Foreign key is NULL
2770 296
                        $class->reflFields[$field]->setValue($entity, null);
2771 296
                        $this->originalEntityData[$oid][$field] = null;
2772
2773 296
                        break;
2774
                    }
2775
2776 306
                    if ( ! isset($hints['fetchMode'][$class->name][$field])) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2777 302
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2778
                    }
2779
2780
                    // Foreign key is set
2781
                    // Check identity map first
2782
                    // FIXME: Can break easily with composite keys if join column values are in
2783
                    //        wrong order. The correct order is the one in ClassMetadata#identifier.
2784 306
                    $relatedIdHash = implode(' ', $associatedId);
2785
2786
                    switch (true) {
2787 306
                        case (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])):
2788 178
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2789
2790
                            // If this is an uninitialized proxy, we are deferring eager loads,
2791
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2792
                            // then we can append this entity for eager loading!
2793 178
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2794 178
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2795 178
                                !$targetClass->isIdentifierComposite &&
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2796 178
                                $newValue instanceof Proxy &&
2797 178
                                $newValue->__isInitialized__ === false) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2798
2799
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2800
                            }
2801
2802 178
                            break;
2803
2804 205
                        case ($targetClass->subClasses):
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2805
                            // If it might be a subtype, it can not be lazy. There isn't even
2806
                            // a way to solve this with deferred eager loading, which means putting
2807
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2808 32
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2809 32
                            break;
2810
2811
                        default:
2812
                            switch (true) {
2813
                                // We are negating the condition here. Other cases will assume it is valid!
2814 175
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2815 167
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2816 167
                                    break;
2817
2818
                                // Deferred eager load only works for single identifier classes
2819 8
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
2820
                                    // TODO: Is there a faster approach?
2821 8
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2822
2823 8
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2824 8
                                    break;
2825
2826
                                default:
2827
                                    // TODO: This is very imperformant, ignore it?
2828
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2829
                                    break;
2830
                            }
2831
2832
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2833 175
                            $newValueOid = spl_object_hash($newValue);
2834 175
                            $this->entityIdentifiers[$newValueOid] = $associatedId;
2835 175
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2836
2837
                            if (
2838 175
                                $newValue instanceof NotifyPropertyChanged &&
2839 175
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
2840
                            ) {
2841
                                $newValue->addPropertyChangedListener($this);
2842
                            }
2843 175
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2844
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2845 175
                            break;
2846
                    }
2847
2848 306
                    $this->originalEntityData[$oid][$field] = $newValue;
2849 306
                    $class->reflFields[$field]->setValue($entity, $newValue);
2850
2851 306
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2852 60
                        $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2853 60
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2854
                    }
2855
2856 306
                    break;
2857
2858
                default:
2859
                    // Ignore if its a cached collection
2860 507
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2861
                        break;
2862
                    }
2863
2864
                    // use the given collection
2865 507
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2866
2867 3
                        $data[$field]->setOwner($entity, $assoc);
2868
2869 3
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2870 3
                        $this->originalEntityData[$oid][$field] = $data[$field];
2871
2872 3
                        break;
2873
                    }
2874
2875
                    // Inject collection
2876 507
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2877 507
                    $pColl->setOwner($entity, $assoc);
2878 507
                    $pColl->setInitialized(false);
2879
2880 507
                    $reflField = $class->reflFields[$field];
2881 507
                    $reflField->setValue($entity, $pColl);
2882
2883 507
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2884 4
                        $this->loadCollection($pColl);
2885 4
                        $pColl->takeSnapshot();
2886
                    }
2887
2888 507
                    $this->originalEntityData[$oid][$field] = $pColl;
2889 507
                    break;
2890
            }
2891
        }
2892
2893
        // defer invoking of postLoad event to hydration complete step
2894 727
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2895
2896 727
        return $entity;
2897
    }
2898
2899
    /**
2900
     * @return void
2901
     */
2902 946
    public function triggerEagerLoads()
2903
    {
2904 946
        if ( ! $this->eagerLoadingEntities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->eagerLoadingEntities 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...
2905 946
            return;
2906
        }
2907
2908
        // avoid infinite recursion
2909 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2910 7
        $this->eagerLoadingEntities = [];
2911
2912 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2913 7
            if ( ! $ids) {
2914
                continue;
2915
            }
2916
2917 7
            $class = $this->em->getClassMetadata($entityName);
2918
2919 7
            $this->getEntityPersister($entityName)->loadAll(
2920 7
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->id...ay(array_values($ids))) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...ityPersister::loadAll() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2920
                /** @scrutinizer ignore-type */ array_combine($class->identifier, [array_values($ids)])
Loading history...
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2921
            );
2922
        }
2923 7
    }
2924
2925
    /**
2926
     * Initializes (loads) an uninitialized persistent collection of an entity.
2927
     *
2928
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2929
     *
2930
     * @return void
2931
     *
2932
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2933
     */
2934 150
    public function loadCollection(PersistentCollection $collection)
2935
    {
2936 150
        $assoc     = $collection->getMapping();
2937 150
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2938
2939 150
        switch ($assoc['type']) {
2940 150
            case ClassMetadata::ONE_TO_MANY:
2941 78
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2942 78
                break;
2943
2944 86
            case ClassMetadata::MANY_TO_MANY:
2945 86
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2946 86
                break;
2947
        }
2948
2949 150
        $collection->setInitialized(true);
2950 150
    }
2951
2952
    /**
2953
     * Gets the identity map of the UnitOfWork.
2954
     *
2955
     * @return array
2956
     */
2957 2
    public function getIdentityMap()
2958
    {
2959 2
        return $this->identityMap;
2960
    }
2961
2962
    /**
2963
     * Gets the original data of an entity. The original data is the data that was
2964
     * present at the time the entity was reconstituted from the database.
2965
     *
2966
     * @param object $entity
2967
     *
2968
     * @return array
2969
     */
2970 123
    public function getOriginalEntityData($entity)
2971
    {
2972 123
        $oid = spl_object_hash($entity);
2973
2974 123
        return isset($this->originalEntityData[$oid])
2975 119
            ? $this->originalEntityData[$oid]
2976 123
            : [];
2977
    }
2978
2979
    /**
2980
     * @ignore
2981
     *
2982
     * @param object $entity
2983
     * @param array  $data
2984
     *
2985
     * @return void
2986
     */
2987
    public function setOriginalEntityData($entity, array $data)
2988
    {
2989
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2990
    }
2991
2992
    /**
2993
     * INTERNAL:
2994
     * Sets a property value of the original data array of an entity.
2995
     *
2996
     * @ignore
2997
     *
2998
     * @param string $oid
2999
     * @param string $property
3000
     * @param mixed  $value
3001
     *
3002
     * @return void
3003
     */
3004 315
    public function setOriginalEntityProperty($oid, $property, $value)
3005
    {
3006 315
        $this->originalEntityData[$oid][$property] = $value;
3007 315
    }
3008
3009
    /**
3010
     * Gets the identifier of an entity.
3011
     * The returned value is always an array of identifier values. If the entity
3012
     * has a composite identifier then the identifier values are in the same
3013
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
3014
     *
3015
     * @param object $entity
3016
     *
3017
     * @return array The identifier values.
3018
     */
3019 887
    public function getEntityIdentifier($entity)
3020
    {
3021 887
        return $this->entityIdentifiers[spl_object_hash($entity)];
3022
    }
3023
3024
    /**
3025
     * Processes an entity instance to extract their identifier values.
3026
     *
3027
     * @param object $entity The entity instance.
3028
     *
3029
     * @return mixed A scalar value.
3030
     *
3031
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
3032
     */
3033 142
    public function getSingleIdentifierValue($entity)
3034
    {
3035 142
        $class = $this->em->getClassMetadata(get_class($entity));
3036
3037 140
        if ($class->isIdentifierComposite) {
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3038
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
3039
        }
3040
3041 140
        $values = $this->isInIdentityMap($entity)
3042 126
            ? $this->getEntityIdentifier($entity)
3043 140
            : $class->getIdentifierValues($entity);
3044
3045 140
        return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null;
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3046
    }
3047
3048
    /**
3049
     * Tries to find an entity with the given identifier in the identity map of
3050
     * this UnitOfWork.
3051
     *
3052
     * @param mixed  $id            The entity identifier to look for.
3053
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
3054
     *
3055
     * @return object|bool Returns the entity with the specified identifier if it exists in
3056
     *                     this UnitOfWork, FALSE otherwise.
3057
     */
3058 569
    public function tryGetById($id, $rootClassName)
3059
    {
3060 569
        $idHash = implode(' ', (array) $id);
3061
3062 569
        return isset($this->identityMap[$rootClassName][$idHash])
3063 90
            ? $this->identityMap[$rootClassName][$idHash]
3064 569
            : false;
3065
    }
3066
3067
    /**
3068
     * Schedules an entity for dirty-checking at commit-time.
3069
     *
3070
     * @param object $entity The entity to schedule for dirty-checking.
3071
     *
3072
     * @return void
3073
     *
3074
     * @todo Rename: scheduleForSynchronization
3075
     */
3076 9
    public function scheduleForDirtyCheck($entity)
3077
    {
3078 9
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3079
3080 9
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
3081 9
    }
3082
3083
    /**
3084
     * Checks whether the UnitOfWork has any pending insertions.
3085
     *
3086
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
3087
     */
3088
    public function hasPendingInsertions()
3089
    {
3090
        return ! empty($this->entityInsertions);
3091
    }
3092
3093
    /**
3094
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
3095
     * number of entities in the identity map.
3096
     *
3097
     * @return integer
3098
     */
3099 1
    public function size()
3100
    {
3101 1
        $countArray = array_map('count', $this->identityMap);
3102
3103 1
        return array_sum($countArray);
3104
    }
3105
3106
    /**
3107
     * Gets the EntityPersister for an Entity.
3108
     *
3109
     * @param string $entityName The name of the Entity.
3110
     *
3111
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
3112
     */
3113 1160
    public function getEntityPersister($entityName)
3114
    {
3115 1160
        if (isset($this->persisters[$entityName])) {
3116 910
            return $this->persisters[$entityName];
3117
        }
3118
3119 1160
        $class = $this->em->getClassMetadata($entityName);
3120
3121
        switch (true) {
3122 1160
            case ($class->isInheritanceTypeNone()):
0 ignored issues
show
Bug introduced by
The method isInheritanceTypeNone() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3122
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeNone()):
Loading history...
3123 1110
                $persister = new BasicEntityPersister($this->em, $class);
3124 1110
                break;
3125
3126 396
            case ($class->isInheritanceTypeSingleTable()):
0 ignored issues
show
Bug introduced by
The method isInheritanceTypeSingleTable() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3126
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeSingleTable()):
Loading history...
3127 227
                $persister = new SingleTablePersister($this->em, $class);
3128 227
                break;
3129
3130 363
            case ($class->isInheritanceTypeJoined()):
0 ignored issues
show
Bug introduced by
The method isInheritanceTypeJoined() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3130
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeJoined()):
Loading history...
3131 363
                $persister = new JoinedSubclassPersister($this->em, $class);
3132 363
                break;
3133
3134
            default:
3135
                throw new \RuntimeException('No persister found for entity.');
3136
        }
3137
3138 1160
        if ($this->hasCache && $class->cache !== null) {
0 ignored issues
show
Bug introduced by
Accessing cache on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3139 128
            $persister = $this->em->getConfiguration()
3140 128
                ->getSecondLevelCacheConfiguration()
3141 128
                ->getCacheFactory()
3142 128
                ->buildCachedEntityPersister($this->em, $persister, $class);
3143
        }
3144
3145 1160
        $this->persisters[$entityName] = $persister;
3146
3147 1160
        return $this->persisters[$entityName];
3148
    }
3149
3150
    /**
3151
     * Gets a collection persister for a collection-valued association.
3152
     *
3153
     * @param array $association
3154
     *
3155
     * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister
3156
     */
3157 591
    public function getCollectionPersister(array $association)
3158
    {
3159 591
        $role = isset($association['cache'])
3160 78
            ? $association['sourceEntity'] . '::' . $association['fieldName']
3161 591
            : $association['type'];
3162
3163 591
        if (isset($this->collectionPersisters[$role])) {
3164 459
            return $this->collectionPersisters[$role];
3165
        }
3166
3167 591
        $persister = ClassMetadata::ONE_TO_MANY === $association['type']
3168 417
            ? new OneToManyPersister($this->em)
3169 591
            : new ManyToManyPersister($this->em);
3170
3171 591
        if ($this->hasCache && isset($association['cache'])) {
3172 77
            $persister = $this->em->getConfiguration()
3173 77
                ->getSecondLevelCacheConfiguration()
3174 77
                ->getCacheFactory()
3175 77
                ->buildCachedCollectionPersister($this->em, $persister, $association);
3176
        }
3177
3178 591
        $this->collectionPersisters[$role] = $persister;
3179
3180 591
        return $this->collectionPersisters[$role];
3181
    }
3182
3183
    /**
3184
     * INTERNAL:
3185
     * Registers an entity as managed.
3186
     *
3187
     * @param object $entity The entity.
3188
     * @param array  $id     The identifier values.
3189
     * @param array  $data   The original entity data.
3190
     *
3191
     * @return void
3192
     */
3193 211
    public function registerManaged($entity, array $id, array $data)
3194
    {
3195 211
        $oid = spl_object_hash($entity);
3196
3197 211
        $this->entityIdentifiers[$oid]  = $id;
3198 211
        $this->entityStates[$oid]       = self::STATE_MANAGED;
3199 211
        $this->originalEntityData[$oid] = $data;
3200
3201 211
        $this->addToIdentityMap($entity);
3202
3203 205
        if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) {
3204 2
            $entity->addPropertyChangedListener($this);
3205
        }
3206 205
    }
3207
3208
    /**
3209
     * INTERNAL:
3210
     * Clears the property changeset of the entity with the given OID.
3211
     *
3212
     * @param string $oid The entity's OID.
3213
     *
3214
     * @return void
3215
     */
3216 12
    public function clearEntityChangeSet($oid)
3217
    {
3218 12
        unset($this->entityChangeSets[$oid]);
3219 12
    }
3220
3221
    /* PropertyChangedListener implementation */
3222
3223
    /**
3224
     * Notifies this UnitOfWork of a property change in an entity.
3225
     *
3226
     * @param object $entity       The entity that owns the property.
3227
     * @param string $propertyName The name of the property that changed.
3228
     * @param mixed  $oldValue     The old value of the property.
3229
     * @param mixed  $newValue     The new value of the property.
3230
     *
3231
     * @return void
3232
     */
3233 4
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
3234
    {
3235 4
        $oid   = spl_object_hash($entity);
3236 4
        $class = $this->em->getClassMetadata(get_class($entity));
3237
3238 4
        $isAssocField = isset($class->associationMappings[$propertyName]);
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3239
3240 4
        if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3241 1
            return; // ignore non-persistent fields
3242
        }
3243
3244
        // Update changeset and mark entity for synchronization
3245 4
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
3246
3247 4
        if ( ! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3248 4
            $this->scheduleForDirtyCheck($entity);
3249
        }
3250 4
    }
3251
3252
    /**
3253
     * Gets the currently scheduled entity insertions in this UnitOfWork.
3254
     *
3255
     * @return array
3256
     */
3257 2
    public function getScheduledEntityInsertions()
3258
    {
3259 2
        return $this->entityInsertions;
3260
    }
3261
3262
    /**
3263
     * Gets the currently scheduled entity updates in this UnitOfWork.
3264
     *
3265
     * @return array
3266
     */
3267 3
    public function getScheduledEntityUpdates()
3268
    {
3269 3
        return $this->entityUpdates;
3270
    }
3271
3272
    /**
3273
     * Gets the currently scheduled entity deletions in this UnitOfWork.
3274
     *
3275
     * @return array
3276
     */
3277 1
    public function getScheduledEntityDeletions()
3278
    {
3279 1
        return $this->entityDeletions;
3280
    }
3281
3282
    /**
3283
     * Gets the currently scheduled complete collection deletions
3284
     *
3285
     * @return array
3286
     */
3287 1
    public function getScheduledCollectionDeletions()
3288
    {
3289 1
        return $this->collectionDeletions;
3290
    }
3291
3292
    /**
3293
     * Gets the currently scheduled collection inserts, updates and deletes.
3294
     *
3295
     * @return array
3296
     */
3297
    public function getScheduledCollectionUpdates()
3298
    {
3299
        return $this->collectionUpdates;
3300
    }
3301
3302
    /**
3303
     * Helper method to initialize a lazy loading proxy or persistent collection.
3304
     *
3305
     * @param object $obj
3306
     *
3307
     * @return void
3308
     */
3309 2
    public function initializeObject($obj)
3310
    {
3311 2
        if ($obj instanceof Proxy) {
3312 1
            $obj->__load();
3313
3314 1
            return;
3315
        }
3316
3317 1
        if ($obj instanceof PersistentCollection) {
3318 1
            $obj->initialize();
3319
        }
3320 1
    }
3321
3322
    /**
3323
     * Helper method to show an object as string.
3324
     *
3325
     * @param object $obj
3326
     *
3327
     * @return string
3328
     */
3329 1
    private static function objToStr($obj)
3330
    {
3331 1
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj).'@'.spl_object_hash($obj);
3332
    }
3333
3334
    /**
3335
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3336
     *
3337
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3338
     * on this object that might be necessary to perform a correct update.
3339
     *
3340
     * @param object $object
3341
     *
3342
     * @return void
3343
     *
3344
     * @throws ORMInvalidArgumentException
3345
     */
3346 6
    public function markReadOnly($object)
3347
    {
3348 6
        if ( ! is_object($object) || ! $this->isInIdentityMap($object)) {
3349 1
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3350
        }
3351
3352 5
        $this->readOnlyObjects[spl_object_hash($object)] = true;
3353 5
    }
3354
3355
    /**
3356
     * Is this entity read only?
3357
     *
3358
     * @param object $object
3359
     *
3360
     * @return bool
3361
     *
3362
     * @throws ORMInvalidArgumentException
3363
     */
3364 3
    public function isReadOnly($object)
3365
    {
3366 3
        if ( ! is_object($object)) {
3367
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3368
        }
3369
3370 3
        return isset($this->readOnlyObjects[spl_object_hash($object)]);
3371
    }
3372
3373
    /**
3374
     * Perform whatever processing is encapsulated here after completion of the transaction.
3375
     */
3376 1091
    private function afterTransactionComplete()
3377
    {
3378
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3379 96
            $persister->afterTransactionComplete();
3380 1091
        });
3381 1091
    }
3382
3383
    /**
3384
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
3385
     */
3386 12
    private function afterTransactionRolledBack()
3387
    {
3388
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3389 3
            $persister->afterTransactionRolledBack();
3390 12
        });
3391 12
    }
3392
3393
    /**
3394
     * Performs an action after the transaction.
3395
     *
3396
     * @param callable $callback
3397
     */
3398 1097
    private function performCallbackOnCachedPersister(callable $callback)
3399
    {
3400 1097
        if ( ! $this->hasCache) {
3401 1001
            return;
3402
        }
3403
3404 96
        foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) {
3405 96
            if ($persister instanceof CachedPersister) {
3406 96
                $callback($persister);
3407
            }
3408
        }
3409 96
    }
3410
3411 1101
    private function dispatchOnFlushEvent()
3412
    {
3413 1101
        if ($this->evm->hasListeners(Events::onFlush)) {
3414 4
            $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3415
        }
3416 1101
    }
3417
3418 1095
    private function dispatchPostFlushEvent()
3419
    {
3420 1095
        if ($this->evm->hasListeners(Events::postFlush)) {
3421 5
            $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3422
        }
3423 1094
    }
3424
3425
    /**
3426
     * Verifies if two given entities actually are the same based on identifier comparison
3427
     *
3428
     * @param object $entity1
3429
     * @param object $entity2
3430
     *
3431
     * @return bool
3432
     */
3433 14
    private function isIdentifierEquals($entity1, $entity2)
3434
    {
3435 14
        if ($entity1 === $entity2) {
3436
            return true;
3437
        }
3438
3439 14
        $class = $this->em->getClassMetadata(get_class($entity1));
3440
3441 14
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
3442 11
            return false;
3443
        }
3444
3445 3
        $oid1 = spl_object_hash($entity1);
3446 3
        $oid2 = spl_object_hash($entity2);
3447
3448 3
        $id1 = isset($this->entityIdentifiers[$oid1])
3449 3
            ? $this->entityIdentifiers[$oid1]
3450 3
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
3451 3
        $id2 = isset($this->entityIdentifiers[$oid2])
3452 3
            ? $this->entityIdentifiers[$oid2]
3453 3
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
3454
3455 3
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
3456
    }
3457
3458
    /**
3459
     * @throws ORMInvalidArgumentException
3460
     */
3461 1099
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
3462
    {
3463 1099
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
3464
3465 1099
        $this->nonCascadedNewDetectedEntities = [];
3466
3467 1099
        if ($entitiesNeedingCascadePersist) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entitiesNeedingCascadePersist 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...
3468 5
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
3469 5
                \array_values($entitiesNeedingCascadePersist)
3470
            );
3471
        }
3472 1097
    }
3473
3474
    /**
3475
     * @param object $entity
3476
     * @param object $managedCopy
3477
     *
3478
     * @throws ORMException
3479
     * @throws OptimisticLockException
3480
     * @throws TransactionRequiredException
3481
     */
3482 40
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
3483
    {
3484 40
        if (! $this->isLoaded($entity)) {
3485 7
            return;
3486
        }
3487
3488 33
        if (! $this->isLoaded($managedCopy)) {
3489 4
            $managedCopy->__load();
3490
        }
3491
3492 33
        $class = $this->em->getClassMetadata(get_class($entity));
3493
3494 33
        foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
3495 33
            $name = $prop->name;
3496
3497 33
            $prop->setAccessible(true);
3498
3499 33
            if ( ! isset($class->associationMappings[$name])) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3500 33
                if ( ! $class->isIdentifier($name)) {
3501 33
                    $prop->setValue($managedCopy, $prop->getValue($entity));
3502
                }
3503
            } else {
3504 29
                $assoc2 = $class->associationMappings[$name];
3505
3506 29
                if ($assoc2['type'] & ClassMetadata::TO_ONE) {
3507 25
                    $other = $prop->getValue($entity);
3508 25
                    if ($other === null) {
3509 12
                        $prop->setValue($managedCopy, null);
3510
                    } else {
3511 16
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
3512
                            // do not merge fields marked lazy that have not been fetched.
3513 4
                            continue;
3514
                        }
3515
3516 12
                        if ( ! $assoc2['isCascadeMerge']) {
3517 6
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
3518 3
                                $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
3519 3
                                $relatedId   = $targetClass->getIdentifierValues($other);
3520
3521 3
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3522 2
                                    $other = $this->em->find($targetClass->name, $relatedId);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3523
                                } else {
3524 1
                                    $other = $this->em->getProxyFactory()->getProxy(
3525 1
                                        $assoc2['targetEntity'],
3526 1
                                        $relatedId
3527
                                    );
3528 1
                                    $this->registerManaged($other, $relatedId, []);
3529
                                }
3530
                            }
3531
3532 21
                            $prop->setValue($managedCopy, $other);
3533
                        }
3534
                    }
3535
                } else {
3536 17
                    $mergeCol = $prop->getValue($entity);
3537
3538 17
                    if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
3539
                        // do not merge fields marked lazy that have not been fetched.
3540
                        // keep the lazy persistent collection of the managed copy.
3541 5
                        continue;
3542
                    }
3543
3544 14
                    $managedCol = $prop->getValue($managedCopy);
3545
3546 14
                    if ( ! $managedCol) {
3547 4
                        $managedCol = new PersistentCollection(
3548 4
                            $this->em,
3549 4
                            $this->em->getClassMetadata($assoc2['targetEntity']),
3550 4
                            new ArrayCollection
3551
                        );
3552 4
                        $managedCol->setOwner($managedCopy, $assoc2);
3553 4
                        $prop->setValue($managedCopy, $managedCol);
3554
                    }
3555
3556 14
                    if ($assoc2['isCascadeMerge']) {
3557 9
                        $managedCol->initialize();
3558
3559
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
3560 9
                        if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
3561 1
                            $managedCol->unwrap()->clear();
3562 1
                            $managedCol->setDirty(true);
3563
3564 1
                            if ($assoc2['isOwningSide']
3565 1
                                && $assoc2['type'] == ClassMetadata::MANY_TO_MANY
3566 1
                                && $class->isChangeTrackingNotify()
3567
                            ) {
3568
                                $this->scheduleForDirtyCheck($managedCopy);
3569
                            }
3570
                        }
3571
                    }
3572
                }
3573
            }
3574
3575 33
            if ($class->isChangeTrackingNotify()) {
3576
                // Just treat all properties as changed, there is no other choice.
3577
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
3578
            }
3579
        }
3580 33
    }
3581
3582
    /**
3583
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
3584
     * Unit of work able to fire deferred events, related to loading events here.
3585
     *
3586
     * @internal should be called internally from object hydrators
3587
     */
3588 960
    public function hydrationComplete()
3589
    {
3590 960
        $this->hydrationCompleteHandler->hydrationComplete();
3591 960
    }
3592
3593
    /**
3594
     * @param string $entityName
3595
     */
3596 10
    private function clearIdentityMapForEntityName($entityName)
3597
    {
3598 10
        if (! isset($this->identityMap[$entityName])) {
3599 1
            return;
3600
        }
3601
3602 9
        $visited = [];
3603
3604 9
        foreach ($this->identityMap[$entityName] as $entity) {
3605 9
            $this->doDetach($entity, $visited, false);
3606
        }
3607 9
    }
3608
3609
    /**
3610
     * @param string $entityName
3611
     */
3612 10
    private function clearEntityInsertionsForEntityName($entityName)
3613
    {
3614 10
        foreach ($this->entityInsertions as $hash => $entity) {
3615
            // note: performance optimization - `instanceof` is much faster than a function call
3616 1
            if ($entity instanceof $entityName && get_class($entity) === $entityName) {
3617
                unset($this->entityInsertions[$hash]);
3618
            }
3619
        }
3620 10
    }
3621
3622
    /**
3623
     * @param ClassMetadata $class
3624
     * @param mixed         $identifierValue
3625
     *
3626
     * @return mixed the identifier after type conversion
3627
     *
3628
     * @throws \Doctrine\ORM\Mapping\MappingException if the entity has more than a single identifier
3629
     */
3630 989
    private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue)
3631
    {
3632 989
        return $this->em->getConnection()->convertToPHPValue(
3633 989
            $identifierValue,
3634 989
            $class->getTypeOfField($class->getSingleIdentifierFieldName())
3635
        );
3636
    }
3637
}
3638