Passed
Pull Request — 2.6 (#7318)
by
unknown
09:54
created

UnitOfWork   F

Complexity

Total Complexity 517

Size/Duplication

Total Lines 3579
Duplicated Lines 0 %

Test Coverage

Coverage 93.43%

Importance

Changes 0
Metric Value
eloc 1215
dl 0
loc 3579
ccs 1209
cts 1294
cp 0.9343
rs 0.8
c 0
b 0
f 0
wmc 517

100 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A computeScheduleInsertsChangeSets() 0 6 2
A getEntityChangeSet() 0 10 2
A executeExtraUpdates() 0 10 2
B computeSingleEntityChangeSet() 0 31 11
F commit() 0 101 26
A postCommitCleanup() 0 28 4
A hasMissingIdsWhichAreForeignKeys() 0 9 4
A remove() 0 5 1
A getCollectionPersister() 0 24 6
A scheduleForUpdate() 0 14 5
A updateAssociationWithMergedEntity() 0 18 3
A markReadOnly() 0 7 3
A getCommitOrderCalculator() 0 3 1
D recomputeSingleEntityChangeSet() 0 57 18
A getScheduledCollectionUpdates() 0 3 1
A isInIdentityMap() 0 12 2
A setOriginalEntityData() 0 3 1
B cascadeDetach() 0 30 7
A isReadOnly() 0 7 2
A assertThatThereAreNoUnintentionallyNonPersistedAssociations() 0 9 2
A scheduleCollectionDeletion() 0 9 1
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 getOriginalEntityData() 0 7 2
A addToIdentityMap() 0 19 4
B executeInserts() 0 57 9
A ensureVersionMatch() 0 16 5
A clear() 0 26 3
B doDetach() 0 32 7
A getScheduledEntityDeletions() 0 3 1
A merge() 0 5 1
A loadCollection() 0 16 3
A clearEntityChangeSet() 0 3 1
B cascadePersist() 0 47 9
A cancelOrphanRemoval() 0 3 1
A scheduleExtraUpdate() 0 12 2
A executeDeletions() 0 29 5
B computeChangeSets() 0 41 11
A getScheduledEntityInsertions() 0 3 1
A convertSingleFieldIdentifierToPHPValue() 0 5 1
C getEntityState() 0 67 13
A getIdentityMap() 0 3 1
A getByIdHash() 0 3 1
A getScheduledEntityUpdates() 0 3 1
C computeAssociationChanges() 0 64 14
A isScheduledForDirtyCheck() 0 5 1
A clearEntityInsertionsForEntityName() 0 6 4
F computeChangeSet() 0 181 44
A dispatchOnFlushEvent() 0 4 2
A tryGetById() 0 7 2
A afterTransactionComplete() 0 4 1
A scheduleForDelete() 0 25 5
A initializeObject() 0 10 3
F createEntity() 0 268 59
A detach() 0 5 1
A getSingleIdentifierValue() 0 13 4
B scheduleForInsert() 0 27 8
A persist() 0 5 1
A clearIdentityMapForEntityName() 0 10 3
C lock() 0 50 13
A hasPendingInsertions() 0 3 1
A isScheduledForInsert() 0 3 1
A setOriginalEntityProperty() 0 3 1
A isIdentifierEquals() 0 23 6
A objToStr() 0 3 2
B cascadeMerge() 0 27 7
A registerManaged() 0 12 4
C getCommitOrder() 0 66 12
A isCollectionScheduledForDeletion() 0 3 1
B cascadeRefresh() 0 30 7
A afterTransactionRolledBack() 0 4 1
B doPersist() 0 47 7
A scheduleOrphanRemoval() 0 3 1
A getEntityIdentifier() 0 3 1
C doMerge() 0 87 12
A newInstance() 0 9 2
A addToEntityIdentifiersAndEntityMap() 0 19 3
A isLoaded() 0 3 2
A size() 0 5 1
A propertyChanged() 0 16 4
A updateOriginalEntityData() 0 11 4
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 dispatchPostFlushEvent() 0 4 2
A scheduleForDirtyCheck() 0 5 1
A tryGetByIdHash() 0 7 2
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
A persistNew() 0 30 5
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
50
/**
51
 * The UnitOfWork is responsible for tracking changes to objects during an
52
 * "object-level" transaction and for writing out changes to the database
53
 * in the correct order.
54
 *
55
 * Internal note: This class contains highly performance-sensitive code.
56
 *
57
 * @since       2.0
58
 * @author      Benjamin Eberlei <[email protected]>
59
 * @author      Guilherme Blanco <[email protected]>
60
 * @author      Jonathan Wage <[email protected]>
61
 * @author      Roman Borschel <[email protected]>
62
 * @author      Rob Caiger <[email protected]>
63
 */
64
class UnitOfWork implements PropertyChangedListener
65
{
66
    /**
67
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
68
     */
69
    const STATE_MANAGED = 1;
70
71
    /**
72
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
73
     * and is not (yet) managed by an EntityManager.
74
     */
75
    const STATE_NEW = 2;
76
77
    /**
78
     * A detached entity is an instance with persistent state and identity that is not
79
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
80
     */
81
    const STATE_DETACHED = 3;
82
83
    /**
84
     * A removed entity instance is an instance with a persistent identity,
85
     * associated with an EntityManager, whose persistent state will be deleted
86
     * on commit.
87
     */
88
    const STATE_REMOVED = 4;
89
90
    /**
91
     * Hint used to collect all primary keys of associated entities during hydration
92
     * and execute it in a dedicated query afterwards
93
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
94
     */
95
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
96
97
    /**
98
     * The identity map that holds references to all managed entities that have
99
     * an identity. The entities are grouped by their class name.
100
     * Since all classes in a hierarchy must share the same identifier set,
101
     * we always take the root class name of the hierarchy.
102
     *
103
     * @var array
104
     */
105
    private $identityMap = [];
106
107
    /**
108
     * Map of all identifiers of managed entities.
109
     * Keys are object ids (spl_object_hash).
110
     *
111
     * @var array
112
     */
113
    private $entityIdentifiers = [];
114
115
    /**
116
     * Map of the original entity data of managed entities.
117
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
118
     * at commit time.
119
     *
120
     * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
121
     *                A value will only really be copied if the value in the entity is modified
122
     *                by the user.
123
     *
124
     * @var array
125
     */
126
    private $originalEntityData = [];
127
128
    /**
129
     * Map of entity changes. Keys are object ids (spl_object_hash).
130
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
131
     *
132
     * @var array
133
     */
134
    private $entityChangeSets = [];
135
136
    /**
137
     * The (cached) states of any known entities.
138
     * Keys are object ids (spl_object_hash).
139
     *
140
     * @var array
141
     */
142
    private $entityStates = [];
143
144
    /**
145
     * Map of entities that are scheduled for dirty checking at commit time.
146
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
147
     * Keys are object ids (spl_object_hash).
148
     *
149
     * @var array
150
     */
151
    private $scheduledForSynchronization = [];
152
153
    /**
154
     * A list of all pending entity insertions.
155
     *
156
     * @var array
157
     */
158
    private $entityInsertions = [];
159
160
    /**
161
     * A list of all pending entity updates.
162
     *
163
     * @var array
164
     */
165
    private $entityUpdates = [];
166
167
    /**
168
     * Any pending extra updates that have been scheduled by persisters.
169
     *
170
     * @var array
171
     */
172
    private $extraUpdates = [];
173
174
    /**
175
     * A list of all pending entity deletions.
176
     *
177
     * @var array
178
     */
179
    private $entityDeletions = [];
180
181
    /**
182
     * New entities that were discovered through relationships that were not
183
     * marked as cascade-persist. During flush, this array is populated and
184
     * then pruned of any entities that were discovered through a valid
185
     * cascade-persist path. (Leftovers cause an error.)
186
     *
187
     * Keys are OIDs, payload is a two-item array describing the association
188
     * and the entity.
189
     *
190
     * @var object[][]|array[][] indexed by respective object spl_object_hash()
191
     */
192
    private $nonCascadedNewDetectedEntities = [];
193
194
    /**
195
     * All pending collection deletions.
196
     *
197
     * @var array
198
     */
199
    private $collectionDeletions = [];
200
201
    /**
202
     * All pending collection updates.
203
     *
204
     * @var array
205
     */
206
    private $collectionUpdates = [];
207
208
    /**
209
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
210
     * At the end of the UnitOfWork all these collections will make new snapshots
211
     * of their data.
212
     *
213
     * @var array
214
     */
215
    private $visitedCollections = [];
216
217
    /**
218
     * The EntityManager that "owns" this UnitOfWork instance.
219
     *
220
     * @var EntityManagerInterface
221
     */
222
    private $em;
223
224
    /**
225
     * The entity persister instances used to persist entity instances.
226
     *
227
     * @var array
228
     */
229
    private $persisters = [];
230
231
    /**
232
     * The collection persister instances used to persist collections.
233
     *
234
     * @var array
235
     */
236
    private $collectionPersisters = [];
237
238
    /**
239
     * The EventManager used for dispatching events.
240
     *
241
     * @var \Doctrine\Common\EventManager
242
     */
243
    private $evm;
244
245
    /**
246
     * The ListenersInvoker used for dispatching events.
247
     *
248
     * @var \Doctrine\ORM\Event\ListenersInvoker
249
     */
250
    private $listenersInvoker;
251
252
    /**
253
     * The IdentifierFlattener used for manipulating identifiers
254
     *
255
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
256
     */
257
    private $identifierFlattener;
258
259
    /**
260
     * Orphaned entities that are scheduled for removal.
261
     *
262
     * @var array
263
     */
264
    private $orphanRemovals = [];
265
266
    /**
267
     * Read-Only objects are never evaluated
268
     *
269
     * @var array
270
     */
271
    private $readOnlyObjects = [];
272
273
    /**
274
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
275
     *
276
     * @var array
277
     */
278
    private $eagerLoadingEntities = [];
279
280
    /**
281
     * @var boolean
282
     */
283
    protected $hasCache = false;
284
285
    /**
286
     * Helper for handling completion of hydration
287
     *
288
     * @var HydrationCompleteHandler
289
     */
290
    private $hydrationCompleteHandler;
291
292
    /**
293
     * @var ReflectionPropertiesGetter
294
     */
295
    private $reflectionPropertiesGetter;
296
297
    /**
298
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
299
     *
300
     * @param EntityManagerInterface $em
301
     */
302 2470
    public function __construct(EntityManagerInterface $em)
303
    {
304 2470
        $this->em                         = $em;
305 2470
        $this->evm                        = $em->getEventManager();
306 2470
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2470
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2470
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2470
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2470
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2470
    }
312
313
    /**
314
     * Commits the UnitOfWork, executing all operations that have been postponed
315
     * up to this point. The state of all managed entities will be synchronized with
316
     * the database.
317
     *
318
     * The operations are executed in the following order:
319
     *
320
     * 1) All entity insertions
321
     * 2) All entity updates
322
     * 3) All collection deletions
323
     * 4) All collection updates
324
     * 5) All entity deletions
325
     *
326
     * @param null|object|array $entity
327
     *
328
     * @return void
329
     *
330
     * @throws \Exception
331
     */
332 1087
    public function commit($entity = null)
333
    {
334
        // Raise preFlush
335 1087
        if ($this->evm->hasListeners(Events::preFlush)) {
336 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
337
        }
338
339
        // Compute changes done since last commit.
340 1087
        if (null === $entity) {
341 1077
            $this->computeChangeSets();
342 19
        } elseif (is_object($entity)) {
343 17
            $this->computeSingleEntityChangeSet($entity);
344 2
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
345 2
            foreach ($entity as $object) {
346 2
                $this->computeSingleEntityChangeSet($object);
347
            }
348
        }
349
350 1084
        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...
351 177
                $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...
352 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...
353 42
                $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...
354 38
                $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...
355 1084
                $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...
356 26
            $this->dispatchOnFlushEvent();
357 26
            $this->dispatchPostFlushEvent();
358
359 26
            return; // Nothing to do.
360
        }
361
362 1080
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
363
364 1078
        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...
365 16
            foreach ($this->orphanRemovals as $orphan) {
366 16
                $this->remove($orphan);
367
            }
368
        }
369
370 1078
        $this->dispatchOnFlushEvent();
371
372
        // Now we need a commit order to maintain referential integrity
373 1078
        $commitOrder = $this->getCommitOrder();
374
375 1078
        $conn = $this->em->getConnection();
376 1078
        $conn->beginTransaction();
377
378
        try {
379
            // Collection deletions (deletions of complete collections)
380 1078
            foreach ($this->collectionDeletions as $collectionToDelete) {
381 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
382
            }
383
384 1078
            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...
385 1074
                foreach ($commitOrder as $class) {
386 1074
                    $this->executeInserts($class);
387
                }
388
            }
389
390 1077
            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...
391 125
                foreach ($commitOrder as $class) {
392 125
                    $this->executeUpdates($class);
393
                }
394
            }
395
396
            // Extra updates that were requested by persisters.
397 1073
            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...
398 44
                $this->executeExtraUpdates();
399
            }
400
401
            // Collection updates (deleteRows, updateRows, insertRows)
402 1073
            foreach ($this->collectionUpdates as $collectionToUpdate) {
403 544
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
404
            }
405
406
            // Entity deletions come last and need to be in reverse commit order
407 1073
            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...
408 64
                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...
409 64
                    $this->executeDeletions($commitOrder[$i]);
410
                }
411
            }
412
413 1073
            $conn->commit();
414 11
        } catch (Throwable $e) {
415 11
            $this->em->close();
416 11
            $conn->rollBack();
417
418 11
            $this->afterTransactionRolledBack();
419
420 11
            throw $e;
421
        }
422
423 1073
        $this->afterTransactionComplete();
424
425
        // Take new snapshots from visited collections
426 1073
        foreach ($this->visitedCollections as $coll) {
427 543
            $coll->takeSnapshot();
428
        }
429
430 1073
        $this->dispatchPostFlushEvent();
431
432 1072
        $this->postCommitCleanup($entity);
433 1072
    }
434
435
    /**
436
     * @param null|object|object[] $entity
437
     */
438 1072
    private function postCommitCleanup($entity) : void
439
    {
440 1072
        $this->entityInsertions =
441 1072
        $this->entityUpdates =
442 1072
        $this->entityDeletions =
443 1072
        $this->extraUpdates =
444 1072
        $this->collectionUpdates =
445 1072
        $this->nonCascadedNewDetectedEntities =
446 1072
        $this->collectionDeletions =
447 1072
        $this->visitedCollections =
448 1072
        $this->orphanRemovals = [];
449
450 1072
        if (null === $entity) {
451 1062
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
452
453 1062
            return;
454
        }
455
456 16
        $entities = \is_object($entity)
457 14
            ? [$entity]
458 16
            : $entity;
459
460 16
        foreach ($entities as $object) {
461 16
            $oid = \spl_object_hash($object);
462
463 16
            $this->clearEntityChangeSet($oid);
464
465 16
            unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
466
        }
467 16
    }
468
469
    /**
470
     * Computes the changesets of all entities scheduled for insertion.
471
     *
472
     * @return void
473
     */
474 1086
    private function computeScheduleInsertsChangeSets()
475
    {
476 1086
        foreach ($this->entityInsertions as $entity) {
477 1078
            $class = $this->em->getClassMetadata(get_class($entity));
478
479 1078
            $this->computeChangeSet($class, $entity);
480
        }
481 1084
    }
482
483
    /**
484
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
485
     *
486
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
487
     * 2. Read Only entities are skipped.
488
     * 3. Proxies are skipped.
489
     * 4. Only if entity is properly managed.
490
     *
491
     * @param object $entity
492
     *
493
     * @return void
494
     *
495
     * @throws \InvalidArgumentException
496
     */
497 19
    private function computeSingleEntityChangeSet($entity)
498
    {
499 19
        $state = $this->getEntityState($entity);
500
501 19
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
502 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
503
        }
504
505 18
        $class = $this->em->getClassMetadata(get_class($entity));
506
507 18
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
508 17
            $this->persist($entity);
509
        }
510
511
        // Compute changes for INSERTed entities first. This must always happen even in this case.
512 18
        $this->computeScheduleInsertsChangeSets();
513
514 18
        if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
515
            return;
516
        }
517
518
        // Ignore uninitialized proxy objects
519 18
        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...
520 2
            return;
521
        }
522
523
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
524 16
        $oid = spl_object_hash($entity);
525
526 16
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
527 7
            $this->computeChangeSet($class, $entity);
528
        }
529 16
    }
530
531
    /**
532
     * Executes any extra updates that have been scheduled.
533
     */
534 44
    private function executeExtraUpdates()
535
    {
536 44
        foreach ($this->extraUpdates as $oid => $update) {
537 44
            list ($entity, $changeset) = $update;
538
539 44
            $this->entityChangeSets[$oid] = $changeset;
540 44
            $this->getEntityPersister(get_class($entity))->update($entity);
541
        }
542
543 44
        $this->extraUpdates = [];
544 44
    }
545
546
    /**
547
     * Gets the changeset for an entity.
548
     *
549
     * @param object $entity
550
     *
551
     * @return array
552
     */
553 1071
    public function & getEntityChangeSet($entity)
554
    {
555 1071
        $oid  = spl_object_hash($entity);
556 1071
        $data = [];
557
558 1071
        if (!isset($this->entityChangeSets[$oid])) {
559 4
            return $data;
560
        }
561
562 1071
        return $this->entityChangeSets[$oid];
563
    }
564
565
    /**
566
     * Computes the changes that happened to a single entity.
567
     *
568
     * Modifies/populates the following properties:
569
     *
570
     * {@link _originalEntityData}
571
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
572
     * then it was not fetched from the database and therefore we have no original
573
     * entity data yet. All of the current entity data is stored as the original entity data.
574
     *
575
     * {@link _entityChangeSets}
576
     * The changes detected on all properties of the entity are stored there.
577
     * A change is a tuple array where the first entry is the old value and the second
578
     * entry is the new value of the property. Changesets are used by persisters
579
     * to INSERT/UPDATE the persistent entity state.
580
     *
581
     * {@link _entityUpdates}
582
     * If the entity is already fully MANAGED (has been fetched from the database before)
583
     * and any changes to its properties are detected, then a reference to the entity is stored
584
     * there to mark it for an update.
585
     *
586
     * {@link _collectionDeletions}
587
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
588
     * then this collection is marked for deletion.
589
     *
590
     * @ignore
591
     *
592
     * @internal Don't call from the outside.
593
     *
594
     * @param ClassMetadata $class  The class descriptor of the entity.
595
     * @param object        $entity The entity for which to compute the changes.
596
     *
597
     * @return void
598
     */
599 1088
    public function computeChangeSet(ClassMetadata $class, $entity)
600
    {
601 1088
        $oid = spl_object_hash($entity);
602
603 1088
        if (isset($this->readOnlyObjects[$oid])) {
604 2
            return;
605
        }
606
607 1088
        if ( ! $class->isInheritanceTypeNone()) {
608 337
            $class = $this->em->getClassMetadata(get_class($entity));
609
        }
610
611 1088
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
612
613 1088
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
614 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
615
        }
616
617 1088
        $actualData = [];
618
619 1088
        foreach ($class->reflFields as $name => $refProp) {
620 1088
            $value = $refProp->getValue($entity);
621
622 1088
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
623 816
                if ($value instanceof PersistentCollection) {
624 206
                    if ($value->getOwner() === $entity) {
625 206
                        $actualData[$name] = $value;
626 206
                        continue;
627
                    }
628
629 5
                    $value = new ArrayCollection($value->getValues());
630
                }
631
632
                // If $value is not a Collection then use an ArrayCollection.
633 811
                if ( ! $value instanceof Collection) {
634 243
                    $value = new ArrayCollection($value);
635
                }
636
637 811
                $assoc = $class->associationMappings[$name];
638
639
                // Inject PersistentCollection
640 811
                $value = new PersistentCollection(
641 811
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
642
                );
643 811
                $value->setOwner($entity, $assoc);
644 811
                $value->setDirty( ! $value->isEmpty());
645
646 811
                $class->reflFields[$name]->setValue($entity, $value);
647
648 811
                $actualData[$name] = $value;
649
650 811
                continue;
651
            }
652
653 1088
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
654 1088
                $actualData[$name] = $value;
655
            }
656
        }
657
658 1088
        if ( ! isset($this->originalEntityData[$oid])) {
659
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
660
            // These result in an INSERT.
661 1084
            $this->originalEntityData[$oid] = $actualData;
662 1084
            $changeSet = [];
663
664 1084
            foreach ($actualData as $propName => $actualValue) {
665 1061
                if ( ! isset($class->associationMappings[$propName])) {
666 1006
                    $changeSet[$propName] = [null, $actualValue];
667
668 1006
                    continue;
669
                }
670
671 943
                $assoc = $class->associationMappings[$propName];
672
673 943
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
674 943
                    $changeSet[$propName] = [null, $actualValue];
675
                }
676
            }
677
678 1084
            $this->entityChangeSets[$oid] = $changeSet;
679
        } else {
680
            // Entity is "fully" MANAGED: it was already fully persisted before
681
            // and we have a copy of the original data
682 277
            $originalData           = $this->originalEntityData[$oid];
683 277
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
684 277
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
685
                ? $this->entityChangeSets[$oid]
686 277
                : [];
687
688 277
            foreach ($actualData as $propName => $actualValue) {
689
                // skip field, its a partially omitted one!
690 274
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
691 8
                    continue;
692
                }
693
694 274
                $orgValue = $originalData[$propName];
695
696
                // skip if value haven't changed
697 274
                if ($orgValue === $actualValue) {
698 257
                    continue;
699
                }
700
701
                // if regular field
702 121
                if ( ! isset($class->associationMappings[$propName])) {
703 66
                    if ($isChangeTrackingNotify) {
704
                        continue;
705
                    }
706
707 66
                    $changeSet[$propName] = [$orgValue, $actualValue];
708
709 66
                    continue;
710
                }
711
712 59
                $assoc = $class->associationMappings[$propName];
713
714
                // Persistent collection was exchanged with the "originally"
715
                // created one. This can only mean it was cloned and replaced
716
                // on another entity.
717 59
                if ($actualValue instanceof PersistentCollection) {
718 8
                    $owner = $actualValue->getOwner();
719 8
                    if ($owner === null) { // cloned
720
                        $actualValue->setOwner($entity, $assoc);
721 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
722
                        if (!$actualValue->isInitialized()) {
723
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
724
                        }
725
                        $newValue = clone $actualValue;
726
                        $newValue->setOwner($entity, $assoc);
727
                        $class->reflFields[$propName]->setValue($entity, $newValue);
728
                    }
729
                }
730
731 59
                if ($orgValue instanceof PersistentCollection) {
732
                    // A PersistentCollection was de-referenced, so delete it.
733 8
                    $coid = spl_object_hash($orgValue);
734
735 8
                    if (isset($this->collectionDeletions[$coid])) {
736
                        continue;
737
                    }
738
739 8
                    $this->collectionDeletions[$coid] = $orgValue;
740 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
741
742 8
                    continue;
743
                }
744
745 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
746 50
                    if ($assoc['isOwningSide']) {
747 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
748
                    }
749
750 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
751 51
                        $this->scheduleOrphanRemoval($orgValue);
752
                    }
753
                }
754
            }
755
756 277
            if ($changeSet) {
757 94
                $this->entityChangeSets[$oid]   = $changeSet;
758 94
                $this->updateOriginalEntityData($class, $oid, $actualData);
759 94
                $this->entityUpdates[$oid]      = $entity;
760
            }
761
        }
762
763
        // Look for changes in associations of the entity
764 1088
        foreach ($class->associationMappings as $field => $assoc) {
765 943
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
766 661
                continue;
767
            }
768
769 914
            $this->computeAssociationChanges($assoc, $val);
770
771 906
            if ( ! isset($this->entityChangeSets[$oid]) &&
772 906
                $assoc['isOwningSide'] &&
773 906
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
774 906
                $val instanceof PersistentCollection &&
775 906
                $val->isDirty()
776
            ) {
777 35
                $this->entityChangeSets[$oid]   = [];
778 35
                $this->updateOriginalEntityData($class, $oid, $actualData);
779 906
                $this->entityUpdates[$oid]      = $entity;
780
            }
781
        }
782 1080
    }
783
784
    /**
785
     * Updates original entity data with new data.
786
     * Should be called after computing changesets to re-set originalEntityData with actual data.
787
     * Preserves id stored in original entity data, which is messing from actual data.
788
     *
789
     * Should only be called when original data was present before computing change set,
790
     * which means that entity was persisted before and contains valid id.
791
     * In other cases simply `$this->originalEntityData[$oid] = $actualData;` is just fine.
792
     *
793
     * @param ClassMetadata $metadata
794
     * @param string $oid
795
     * @param array $actualData
796
     */
797 130
    private function updateOriginalEntityData(ClassMetadata $metadata, string $oid, array $actualData)
798
    {
799 130
        if ($metadata->isIdGeneratorIdentity()) {
800 111
            $originalData = $this->originalEntityData[$oid];
801 111
            foreach ($metadata->getIdentifier() as $identifier) {
802 111
                if (isset($originalData[$identifier])) {
803 111
                    $actualData[$identifier] = $originalData[$identifier];
804
                }
805
            }
806
        }
807 130
        $this->originalEntityData[$oid] = $actualData;
808 130
    }
809
810
    /**
811
     * Computes all the changes that have been done to entities and collections
812
     * since the last commit and stores these changes in the _entityChangeSet map
813
     * temporarily for access by the persisters, until the UoW commit is finished.
814
     *
815
     * @return void
816
     */
817 1077
    public function computeChangeSets()
818
    {
819
        // Compute changes for INSERTed entities first. This must always happen.
820 1077
        $this->computeScheduleInsertsChangeSets();
821
822
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
823 1075
        foreach ($this->identityMap as $className => $entities) {
824 471
            $class = $this->em->getClassMetadata($className);
825
826
            // Skip class if instances are read-only
827 471
            if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
828 1
                continue;
829
            }
830
831
            // If change tracking is explicit or happens through notification, then only compute
832
            // changes on entities of that type that are explicitly marked for synchronization.
833
            switch (true) {
834 470
                case ($class->isChangeTrackingDeferredImplicit()):
835 468
                    $entitiesToProcess = $entities;
836 468
                    break;
837
838 3
                case (isset($this->scheduledForSynchronization[$className])):
839 3
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
840 3
                    break;
841
842
                default:
843 1
                    $entitiesToProcess = [];
844
845
            }
846
847 470
            foreach ($entitiesToProcess as $entity) {
848
                // Ignore uninitialized proxy objects
849 450
                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...
850 37
                    continue;
851
                }
852
853
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
854 449
                $oid = spl_object_hash($entity);
855
856 449
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
857 470
                    $this->computeChangeSet($class, $entity);
858
                }
859
            }
860
        }
861 1075
    }
862
863
    /**
864
     * Computes the changes of an association.
865
     *
866
     * @param array $assoc The association mapping.
867
     * @param mixed $value The value of the association.
868
     *
869
     * @throws ORMInvalidArgumentException
870
     * @throws ORMException
871
     *
872
     * @return void
873
     */
874 914
    private function computeAssociationChanges($assoc, $value)
875
    {
876 914
        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...
877 30
            return;
878
        }
879
880 913
        if ($value instanceof PersistentCollection && $value->isDirty()) {
881 548
            $coid = spl_object_hash($value);
882
883 548
            $this->collectionUpdates[$coid] = $value;
884 548
            $this->visitedCollections[$coid] = $value;
885
        }
886
887
        // Look through the entities, and in any of their associations,
888
        // for transient (new) entities, recursively. ("Persistence by reachability")
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
889
        // Unwrap. Uninitialized collections will simply be empty.
890 913
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
891 913
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
892
893 913
        foreach ($unwrappedValue as $key => $entry) {
894 753
            if (! ($entry instanceof $targetClass->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
895 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
896
            }
897
898 745
            $state = $this->getEntityState($entry, self::STATE_NEW);
899
900 745
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
901
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
902
            }
903
904
            switch ($state) {
905 745
                case self::STATE_NEW:
906 42
                    if ( ! $assoc['isCascadePersist']) {
907
                        /*
908
                         * For now just record the details, because this may
909
                         * not be an issue if we later discover another pathway
910
                         * through the object-graph where cascade-persistence
911
                         * is enabled for this object.
912
                         */
913 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
914
915 6
                        break;
916
                    }
917
918 37
                    $this->persistNew($targetClass, $entry);
919 37
                    $this->computeChangeSet($targetClass, $entry);
920
921 37
                    break;
922
923 737
                case self::STATE_REMOVED:
924
                    // Consume the $value as array (it's either an array or an ArrayAccess)
925
                    // and remove the element from Collection.
926 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
927 3
                        unset($value[$key]);
928
                    }
929 4
                    break;
930
931 737
                case self::STATE_DETACHED:
932
                    // Can actually not happen right now as we assume STATE_NEW,
933
                    // so the exception will be raised from the DBAL layer (constraint violation).
934
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
935
                    break;
936
937 745
                default:
938
                    // MANAGED associated entities are already taken into account
939
                    // during changeset calculation anyway, since they are in the identity map.
940
            }
941
        }
942 905
    }
943
944
    /**
945
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
946
     * @param object                              $entity
947
     *
948
     * @return void
949
     */
950 1107
    private function persistNew($class, $entity)
951
    {
952 1107
        $oid    = spl_object_hash($entity);
953 1107
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
954
955 1107
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
956 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
957
        }
958
959 1107
        $idGen = $class->idGenerator;
960
961 1107
        if ( ! $idGen->isPostInsertGenerator()) {
962 289
            $idValue = $idGen->generate($this->em, $entity);
963
964 289
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
965 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
966
967 2
                $class->setIdentifierValues($entity, $idValue);
968
            }
969
970
            // Some identifiers may be foreign keys to new entities.
971
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
972 289
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
973 286
                $this->entityIdentifiers[$oid] = $idValue;
974
            }
975
        }
976
977 1107
        $this->entityStates[$oid] = self::STATE_MANAGED;
978
979 1107
        $this->scheduleForInsert($entity);
980 1107
    }
981
982
    /**
983
     * @param mixed[] $idValue
984
     */
985 289
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
986
    {
987 289
        foreach ($idValue as $idField => $idFieldValue) {
988 289
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
989 289
                return true;
990
            }
991
        }
992
993 286
        return false;
994
    }
995
996
    /**
997
     * INTERNAL:
998
     * Computes the changeset of an individual entity, independently of the
999
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1000
     *
1001
     * The passed entity must be a managed entity. If the entity already has a change set
1002
     * because this method is invoked during a commit cycle then the change sets are added.
1003
     * whereby changes detected in this method prevail.
1004
     *
1005
     * @ignore
1006
     *
1007
     * @param ClassMetadata $class  The class descriptor of the entity.
1008
     * @param object        $entity The entity for which to (re)calculate the change set.
1009
     *
1010
     * @return void
1011
     *
1012
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
1013
     */
1014 17
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
1015
    {
1016 17
        $oid = spl_object_hash($entity);
1017
1018 17
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
1019
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1020
        }
1021
1022 17
        if ( ! isset($this->originalEntityData[$oid])) {
1023
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1024
        }
1025
1026
        // skip if change tracking is "NOTIFY"
1027 17
        if ($class->isChangeTrackingNotify()) {
1028
            return;
1029
        }
1030
1031 17
        if ( ! $class->isInheritanceTypeNone()) {
1032 3
            $class = $this->em->getClassMetadata(get_class($entity));
1033
        }
1034
1035 17
        $actualData = [];
1036
1037 17
        foreach ($class->reflFields as $name => $refProp) {
1038 17
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1039 17
                && ($name !== $class->versionField)
1040
                && (
1041 17
                    ! $class->isCollectionValuedAssociation($name)
1042
                    || (
1043 5
                        isset($this->originalEntityData[$oid][$name])
1044 17
                        && $this->originalEntityData[$oid][$name] === $refProp->getValue($entity)
1045
                    )
1046
                )
1047
            ) {
1048 17
                $actualData[$name] = $refProp->getValue($entity);
1049
            }
1050
        }
1051
1052 17
        $originalData = $this->originalEntityData[$oid];
1053 17
        $changeSet = [];
1054
1055 17
        foreach ($actualData as $propName => $actualValue) {
1056 17
            $orgValue = $originalData[$propName] ?? null;
1057
1058 17
            if ($orgValue !== $actualValue) {
1059 17
                $changeSet[$propName] = [$orgValue, $actualValue];
1060
            }
1061
        }
1062
1063 17
        if ($changeSet) {
1064 8
            if (isset($this->entityChangeSets[$oid])) {
1065 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1066 2
            } else if ( ! isset($this->entityInsertions[$oid])) {
1067 2
                $this->entityChangeSets[$oid] = $changeSet;
1068 2
                $this->entityUpdates[$oid]    = $entity;
1069
            }
1070 8
            $this->updateOriginalEntityData($class, $oid, $actualData);
1071
        }
1072 17
    }
1073
1074
    /**
1075
     * Executes all entity insertions for entities of the specified type.
1076
     *
1077
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1078
     *
1079
     * @return void
1080
     */
1081 1074
    private function executeInserts($class)
1082
    {
1083 1074
        $entities   = [];
1084 1074
        $className  = $class->name;
1085 1074
        $persister  = $this->getEntityPersister($className);
1086 1074
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1087
1088 1074
        $insertionsForClass = [];
1089
1090 1074
        foreach ($this->entityInsertions as $oid => $entity) {
1091
1092 1074
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1093 906
                continue;
1094
            }
1095
1096 1074
            $insertionsForClass[$oid] = $entity;
1097
1098 1074
            $persister->addInsert($entity);
1099
1100 1074
            unset($this->entityInsertions[$oid]);
1101
1102 1074
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1103 1074
                $entities[] = $entity;
1104
            }
1105
        }
1106
1107 1074
        $postInsertIds = $persister->executeInserts();
1108
1109 1074
        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...
1110
            // Persister returned post-insert IDs
1111 974
            foreach ($postInsertIds as $postInsertId) {
1112 974
                $idField = $class->getSingleIdentifierFieldName();
1113 974
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1114
1115 974
                $entity  = $postInsertId['entity'];
1116 974
                $oid     = spl_object_hash($entity);
1117
1118 974
                $class->reflFields[$idField]->setValue($entity, $idValue);
1119
1120 974
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1121 974
                $this->entityStates[$oid] = self::STATE_MANAGED;
1122 974
                $this->originalEntityData[$oid][$idField] = $idValue;
1123
1124 974
                $this->addToIdentityMap($entity);
1125
            }
1126
        } else {
1127 812
            foreach ($insertionsForClass as $oid => $entity) {
1128 276
                if (! isset($this->entityIdentifiers[$oid])) {
1129
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1130
                    //add it now
1131 276
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1132
                }
1133
            }
1134
        }
1135
1136 1074
        foreach ($entities as $entity) {
1137 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1138
        }
1139 1074
    }
1140
1141
    /**
1142
     * @param object $entity
1143
     */
1144 3
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1145
    {
1146 3
        $identifier = [];
1147
1148 3
        foreach ($class->getIdentifierFieldNames() as $idField) {
1149 3
            $value = $class->getFieldValue($entity, $idField);
1150
1151 3
            if (isset($class->associationMappings[$idField])) {
1152
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1153 3
                $value = $this->getSingleIdentifierValue($value);
1154
            }
1155
1156 3
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1157
        }
1158
1159 3
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1160 3
        $this->entityIdentifiers[$oid] = $identifier;
1161
1162 3
        $this->addToIdentityMap($entity);
1163 3
    }
1164
1165
    /**
1166
     * Executes all entity updates for entities of the specified type.
1167
     *
1168
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1169
     *
1170
     * @return void
1171
     */
1172 125
    private function executeUpdates($class)
1173
    {
1174 125
        $className          = $class->name;
1175 125
        $persister          = $this->getEntityPersister($className);
1176 125
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1177 125
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1178
1179 125
        foreach ($this->entityUpdates as $oid => $entity) {
1180 125
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1181 79
                continue;
1182
            }
1183
1184 125
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1185 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1186
1187 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1188
            }
1189
1190 125
            if ( ! empty($this->entityChangeSets[$oid])) {
1191 91
                $persister->update($entity);
1192
            }
1193
1194 121
            unset($this->entityUpdates[$oid]);
1195
1196 121
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1197 121
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1198
            }
1199
        }
1200 121
    }
1201
1202
    /**
1203
     * Executes all entity deletions for entities of the specified type.
1204
     *
1205
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1206
     *
1207
     * @return void
1208
     */
1209 64
    private function executeDeletions($class)
1210
    {
1211 64
        $className  = $class->name;
1212 64
        $persister  = $this->getEntityPersister($className);
1213 64
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1214
1215 64
        foreach ($this->entityDeletions as $oid => $entity) {
1216 64
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1217 25
                continue;
1218
            }
1219
1220 64
            $persister->delete($entity);
1221
1222
            unset(
1223 64
                $this->entityDeletions[$oid],
1224 64
                $this->entityIdentifiers[$oid],
1225 64
                $this->originalEntityData[$oid],
1226 64
                $this->entityStates[$oid]
1227
            );
1228
1229
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1230
            // is obtained by a new entity because the old one went out of scope.
1231
            //$this->entityStates[$oid] = self::STATE_NEW;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
1232 64
            if ( ! $class->isIdentifierNatural()) {
1233 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1234
            }
1235
1236 64
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1237 64
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1238
            }
1239
        }
1240 63
    }
1241
1242
    /**
1243
     * Gets the commit order.
1244
     *
1245
     * @param array|null $entityChangeSet
1246
     *
1247
     * @return array
1248
     */
1249 1078
    private function getCommitOrder(array $entityChangeSet = null)
1250
    {
1251 1078
        if ($entityChangeSet === null) {
1252 1078
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1253
        }
1254
1255 1078
        $calc = $this->getCommitOrderCalculator();
1256
1257
        // See if there are any new classes in the changeset, that are not in the
1258
        // commit order graph yet (don't have a node).
1259
        // We have to inspect changeSet to be able to correctly build dependencies.
1260
        // It is not possible to use IdentityMap here because post inserted ids
1261
        // are not yet available.
1262 1078
        $newNodes = [];
1263
1264 1078
        foreach ($entityChangeSet as $entity) {
1265 1078
            $class = $this->em->getClassMetadata(get_class($entity));
1266
1267 1078
            if ($calc->hasNode($class->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1268 658
                continue;
1269
            }
1270
1271 1078
            $calc->addNode($class->name, $class);
1272
1273 1078
            $newNodes[] = $class;
1274
        }
1275
1276
        // Calculate dependencies for new nodes
1277 1078
        while ($class = array_pop($newNodes)) {
1278 1078
            foreach ($class->associationMappings as $assoc) {
1279 932
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1280 886
                    continue;
1281
                }
1282
1283 881
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1284
1285 881
                if ( ! $calc->hasNode($targetClass->name)) {
1286 679
                    $calc->addNode($targetClass->name, $targetClass);
1287
1288 679
                    $newNodes[] = $targetClass;
1289
                }
1290
1291 881
                $joinColumns = reset($assoc['joinColumns']);
1292
1293 881
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1294
1295
                // If the target class has mapped subclasses, these share the same dependency.
1296 881
                if ( ! $targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1297 874
                    continue;
1298
                }
1299
1300 238
                foreach ($targetClass->subClasses as $subClassName) {
1301 238
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1302
1303 238
                    if ( ! $calc->hasNode($subClassName)) {
1304 208
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1305
1306 208
                        $newNodes[] = $targetSubClass;
1307
                    }
1308
1309 238
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1310
                }
1311
            }
1312
        }
1313
1314 1078
        return $calc->sort();
1315
    }
1316
1317
    /**
1318
     * Schedules an entity for insertion into the database.
1319
     * If the entity already has an identifier, it will be added to the identity map.
1320
     *
1321
     * @param object $entity The entity to schedule for insertion.
1322
     *
1323
     * @return void
1324
     *
1325
     * @throws ORMInvalidArgumentException
1326
     * @throws \InvalidArgumentException
1327
     */
1328 1108
    public function scheduleForInsert($entity)
1329
    {
1330 1108
        $oid = spl_object_hash($entity);
1331
1332 1108
        if (isset($this->entityUpdates[$oid])) {
1333
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1334
        }
1335
1336 1108
        if (isset($this->entityDeletions[$oid])) {
1337 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1338
        }
1339 1108
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1340 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1341
        }
1342
1343 1108
        if (isset($this->entityInsertions[$oid])) {
1344 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1345
        }
1346
1347 1108
        $this->entityInsertions[$oid] = $entity;
1348
1349 1108
        if (isset($this->entityIdentifiers[$oid])) {
1350 286
            $this->addToIdentityMap($entity);
1351
        }
1352
1353 1108
        if ($entity instanceof NotifyPropertyChanged) {
1354 8
            $entity->addPropertyChangedListener($this);
1355
        }
1356 1108
    }
1357
1358
    /**
1359
     * Checks whether an entity is scheduled for insertion.
1360
     *
1361
     * @param object $entity
1362
     *
1363
     * @return boolean
1364
     */
1365 653
    public function isScheduledForInsert($entity)
1366
    {
1367 653
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1368
    }
1369
1370
    /**
1371
     * Schedules an entity for being updated.
1372
     *
1373
     * @param object $entity The entity to schedule for being updated.
1374
     *
1375
     * @return void
1376
     *
1377
     * @throws ORMInvalidArgumentException
1378
     */
1379 1
    public function scheduleForUpdate($entity)
1380
    {
1381 1
        $oid = spl_object_hash($entity);
1382
1383 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1384
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1385
        }
1386
1387 1
        if (isset($this->entityDeletions[$oid])) {
1388
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1389
        }
1390
1391 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1392 1
            $this->entityUpdates[$oid] = $entity;
1393
        }
1394 1
    }
1395
1396
    /**
1397
     * INTERNAL:
1398
     * Schedules an extra update that will be executed immediately after the
1399
     * regular entity updates within the currently running commit cycle.
1400
     *
1401
     * Extra updates for entities are stored as (entity, changeset) tuples.
1402
     *
1403
     * @ignore
1404
     *
1405
     * @param object $entity    The entity for which to schedule an extra update.
1406
     * @param array  $changeset The changeset of the entity (what to update).
1407
     *
1408
     * @return void
1409
     */
1410 44
    public function scheduleExtraUpdate($entity, array $changeset)
1411
    {
1412 44
        $oid         = spl_object_hash($entity);
1413 44
        $extraUpdate = [$entity, $changeset];
1414
1415 44
        if (isset($this->extraUpdates[$oid])) {
1416 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1417
1418 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1419
        }
1420
1421 44
        $this->extraUpdates[$oid] = $extraUpdate;
1422 44
    }
1423
1424
    /**
1425
     * Checks whether an entity is registered as dirty in the unit of work.
1426
     * Note: Is not very useful currently as dirty entities are only registered
1427
     * at commit time.
1428
     *
1429
     * @param object $entity
1430
     *
1431
     * @return boolean
1432
     */
1433
    public function isScheduledForUpdate($entity)
1434
    {
1435
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1436
    }
1437
1438
    /**
1439
     * Checks whether an entity is registered to be checked in the unit of work.
1440
     *
1441
     * @param object $entity
1442
     *
1443
     * @return boolean
1444
     */
1445 2
    public function isScheduledForDirtyCheck($entity)
1446
    {
1447 2
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1448
1449 2
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1450
    }
1451
1452
    /**
1453
     * INTERNAL:
1454
     * Schedules an entity for deletion.
1455
     *
1456
     * @param object $entity
1457
     *
1458
     * @return void
1459
     */
1460 67
    public function scheduleForDelete($entity)
1461
    {
1462 67
        $oid = spl_object_hash($entity);
1463
1464 67
        if (isset($this->entityInsertions[$oid])) {
1465 1
            if ($this->isInIdentityMap($entity)) {
1466
                $this->removeFromIdentityMap($entity);
1467
            }
1468
1469 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1470
1471 1
            return; // entity has not been persisted yet, so nothing more to do.
1472
        }
1473
1474 67
        if ( ! $this->isInIdentityMap($entity)) {
1475 1
            return;
1476
        }
1477
1478 66
        $this->removeFromIdentityMap($entity);
1479
1480 66
        unset($this->entityUpdates[$oid]);
1481
1482 66
        if ( ! isset($this->entityDeletions[$oid])) {
1483 66
            $this->entityDeletions[$oid] = $entity;
1484 66
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1485
        }
1486 66
    }
1487
1488
    /**
1489
     * Checks whether an entity is registered as removed/deleted with the unit
1490
     * of work.
1491
     *
1492
     * @param object $entity
1493
     *
1494
     * @return boolean
1495
     */
1496 17
    public function isScheduledForDelete($entity)
1497
    {
1498 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1499
    }
1500
1501
    /**
1502
     * Checks whether an entity is scheduled for insertion, update or deletion.
1503
     *
1504
     * @param object $entity
1505
     *
1506
     * @return boolean
1507
     */
1508
    public function isEntityScheduled($entity)
1509
    {
1510
        $oid = spl_object_hash($entity);
1511
1512
        return isset($this->entityInsertions[$oid])
1513
            || isset($this->entityUpdates[$oid])
1514
            || isset($this->entityDeletions[$oid]);
1515
    }
1516
1517
    /**
1518
     * INTERNAL:
1519
     * Registers an entity in the identity map.
1520
     * Note that entities in a hierarchy are registered with the class name of
1521
     * the root entity.
1522
     *
1523
     * @ignore
1524
     *
1525
     * @param object $entity The entity to register.
1526
     *
1527
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1528
     *                 the entity in question is already managed.
1529
     *
1530
     * @throws ORMInvalidArgumentException
1531
     */
1532 1172
    public function addToIdentityMap($entity)
1533
    {
1534 1172
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1535 1172
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1536
1537 1172
        if (empty($identifier) || in_array(null, $identifier, true)) {
1538 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1539
        }
1540
1541 1166
        $idHash    = implode(' ', $identifier);
1542 1166
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1543
1544 1166
        if (isset($this->identityMap[$className][$idHash])) {
1545 86
            return false;
1546
        }
1547
1548 1166
        $this->identityMap[$className][$idHash] = $entity;
1549
1550 1166
        return true;
1551
    }
1552
1553
    /**
1554
     * Gets the state of an entity with regard to the current unit of work.
1555
     *
1556
     * @param object   $entity
1557
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1558
     *                         This parameter can be set to improve performance of entity state detection
1559
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1560
     *                         is either known or does not matter for the caller of the method.
1561
     *
1562
     * @return int The entity state.
1563
     */
1564 1122
    public function getEntityState($entity, $assume = null)
1565
    {
1566 1122
        $oid = spl_object_hash($entity);
1567
1568 1122
        if (isset($this->entityStates[$oid])) {
1569 815
            return $this->entityStates[$oid];
1570
        }
1571
1572 1116
        if ($assume !== null) {
1573 1112
            return $assume;
1574
        }
1575
1576
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1577
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1578
        // the UoW does not hold references to such objects and the object hash can be reused.
1579
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1580 13
        $class = $this->em->getClassMetadata(get_class($entity));
1581 13
        $id    = $class->getIdentifierValues($entity);
1582
1583 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...
1584 5
            return self::STATE_NEW;
1585
        }
1586
1587 10
        if ($class->containsForeignIdentifier) {
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1588 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1589
        }
1590
1591
        switch (true) {
1592 10
            case ($class->isIdentifierNatural()):
0 ignored issues
show
Bug introduced by
The method isIdentifierNatural() does not exist on Doctrine\Common\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

1592
            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...
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

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

1595
                    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...
Bug introduced by
Accessing versionField on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1596
                        ? self::STATE_DETACHED
1597 1
                        : self::STATE_NEW;
1598
                }
1599
1600
                // Last try before db lookup: check the identity map.
1601 4
                if ($this->tryGetById($id, $class->rootEntityName)) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1602 1
                    return self::STATE_DETACHED;
1603
                }
1604
1605
                // db lookup
1606 4
                if ($this->getEntityPersister($class->name)->exists($entity)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1607
                    return self::STATE_DETACHED;
1608
                }
1609
1610 4
                return self::STATE_NEW;
1611
1612 5
            case ( ! $class->idGenerator->isPostInsertGenerator()):
0 ignored issues
show
Bug introduced by
Accessing idGenerator on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1613
                // if we have a pre insert generator we can't be sure that having an id
1614
                // really means that the entity exists. We have to verify this through
1615
                // the last resort: a db lookup
1616
1617
                // Last try before db lookup: check the identity map.
1618
                if ($this->tryGetById($id, $class->rootEntityName)) {
1619
                    return self::STATE_DETACHED;
1620
                }
1621
1622
                // db lookup
1623
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1624
                    return self::STATE_DETACHED;
1625
                }
1626
1627
                return self::STATE_NEW;
1628
1629
            default:
1630 5
                return self::STATE_DETACHED;
1631
        }
1632
    }
1633
1634
    /**
1635
     * INTERNAL:
1636
     * Removes an entity from the identity map. This effectively detaches the
1637
     * entity from the persistence management of Doctrine.
1638
     *
1639
     * @ignore
1640
     *
1641
     * @param object $entity
1642
     *
1643
     * @return boolean
1644
     *
1645
     * @throws ORMInvalidArgumentException
1646
     */
1647 79
    public function removeFromIdentityMap($entity)
1648
    {
1649 79
        $oid           = spl_object_hash($entity);
1650 79
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1651 79
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1652
1653 79
        if ($idHash === '') {
1654
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1655
        }
1656
1657 79
        $className = $classMetadata->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1658
1659 79
        if (isset($this->identityMap[$className][$idHash])) {
1660 79
            unset($this->identityMap[$className][$idHash]);
1661 79
            unset($this->readOnlyObjects[$oid]);
1662
1663
            //$this->entityStates[$oid] = self::STATE_DETACHED;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
1664
1665 79
            return true;
1666
        }
1667
1668
        return false;
1669
    }
1670
1671
    /**
1672
     * INTERNAL:
1673
     * Gets an entity in the identity map by its identifier hash.
1674
     *
1675
     * @ignore
1676
     *
1677
     * @param string $idHash
1678
     * @param string $rootClassName
1679
     *
1680
     * @return object
1681
     */
1682 6
    public function getByIdHash($idHash, $rootClassName)
1683
    {
1684 6
        return $this->identityMap[$rootClassName][$idHash];
1685
    }
1686
1687
    /**
1688
     * INTERNAL:
1689
     * Tries to get an entity by its identifier hash. If no entity is found for
1690
     * the given hash, FALSE is returned.
1691
     *
1692
     * @ignore
1693
     *
1694
     * @param mixed  $idHash        (must be possible to cast it to string)
1695
     * @param string $rootClassName
1696
     *
1697
     * @return object|bool The found entity or FALSE.
1698
     */
1699 35
    public function tryGetByIdHash($idHash, $rootClassName)
1700
    {
1701 35
        $stringIdHash = (string) $idHash;
1702
1703 35
        return isset($this->identityMap[$rootClassName][$stringIdHash])
1704 35
            ? $this->identityMap[$rootClassName][$stringIdHash]
1705 35
            : false;
1706
    }
1707
1708
    /**
1709
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1710
     *
1711
     * @param object $entity
1712
     *
1713
     * @return boolean
1714
     */
1715 224
    public function isInIdentityMap($entity)
1716
    {
1717 224
        $oid = spl_object_hash($entity);
1718
1719 224
        if (empty($this->entityIdentifiers[$oid])) {
1720 36
            return false;
1721
        }
1722
1723 208
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1724 208
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1725
1726 208
        return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1727
    }
1728
1729
    /**
1730
     * INTERNAL:
1731
     * Checks whether an identifier hash exists in the identity map.
1732
     *
1733
     * @ignore
1734
     *
1735
     * @param string $idHash
1736
     * @param string $rootClassName
1737
     *
1738
     * @return boolean
1739
     */
1740
    public function containsIdHash($idHash, $rootClassName)
1741
    {
1742
        return isset($this->identityMap[$rootClassName][$idHash]);
1743
    }
1744
1745
    /**
1746
     * Persists an entity as part of the current unit of work.
1747
     *
1748
     * @param object $entity The entity to persist.
1749
     *
1750
     * @return void
1751
     */
1752 1103
    public function persist($entity)
1753
    {
1754 1103
        $visited = [];
1755
1756 1103
        $this->doPersist($entity, $visited);
1757 1096
    }
1758
1759
    /**
1760
     * Persists an entity as part of the current unit of work.
1761
     *
1762
     * This method is internally called during persist() cascades as it tracks
1763
     * the already visited entities to prevent infinite recursions.
1764
     *
1765
     * @param object $entity  The entity to persist.
1766
     * @param array  $visited The already visited entities.
1767
     *
1768
     * @return void
1769
     *
1770
     * @throws ORMInvalidArgumentException
1771
     * @throws UnexpectedValueException
1772
     */
1773 1103
    private function doPersist($entity, array &$visited)
1774
    {
1775 1103
        $oid = spl_object_hash($entity);
1776
1777 1103
        if (isset($visited[$oid])) {
1778 110
            return; // Prevent infinite recursion
1779
        }
1780
1781 1103
        $visited[$oid] = $entity; // Mark visited
1782
1783 1103
        $class = $this->em->getClassMetadata(get_class($entity));
1784
1785
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1786
        // If we would detect DETACHED here we would throw an exception anyway with the same
1787
        // consequences (not recoverable/programming error), so just assuming NEW here
1788
        // lets us avoid some database lookups for entities with natural identifiers.
1789 1103
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1790
1791
        switch ($entityState) {
1792 1103
            case self::STATE_MANAGED:
1793
                // Nothing to do, except if policy is "deferred explicit"
1794 239
                if ($class->isChangeTrackingDeferredExplicit()) {
1795 2
                    $this->scheduleForDirtyCheck($entity);
1796
                }
1797 239
                break;
1798
1799 1103
            case self::STATE_NEW:
1800 1102
                $this->persistNew($class, $entity);
1801 1102
                break;
1802
1803 1
            case self::STATE_REMOVED:
1804
                // Entity becomes managed again
1805 1
                unset($this->entityDeletions[$oid]);
1806 1
                $this->addToIdentityMap($entity);
1807
1808 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1809 1
                break;
1810
1811
            case self::STATE_DETACHED:
1812
                // Can actually not happen right now since we assume STATE_NEW.
1813
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1814
1815
            default:
1816
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1817
        }
1818
1819 1103
        $this->cascadePersist($entity, $visited);
1820 1096
    }
1821
1822
    /**
1823
     * Deletes an entity as part of the current unit of work.
1824
     *
1825
     * @param object $entity The entity to remove.
1826
     *
1827
     * @return void
1828
     */
1829 66
    public function remove($entity)
1830
    {
1831 66
        $visited = [];
1832
1833 66
        $this->doRemove($entity, $visited);
1834 66
    }
1835
1836
    /**
1837
     * Deletes an entity as part of the current unit of work.
1838
     *
1839
     * This method is internally called during delete() cascades as it tracks
1840
     * the already visited entities to prevent infinite recursions.
1841
     *
1842
     * @param object $entity  The entity to delete.
1843
     * @param array  $visited The map of the already visited entities.
1844
     *
1845
     * @return void
1846
     *
1847
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1848
     * @throws UnexpectedValueException
1849
     */
1850 66
    private function doRemove($entity, array &$visited)
1851
    {
1852 66
        $oid = spl_object_hash($entity);
1853
1854 66
        if (isset($visited[$oid])) {
1855 1
            return; // Prevent infinite recursion
1856
        }
1857
1858 66
        $visited[$oid] = $entity; // mark visited
1859
1860
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1861
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1862 66
        $this->cascadeRemove($entity, $visited);
1863
1864 66
        $class       = $this->em->getClassMetadata(get_class($entity));
1865 66
        $entityState = $this->getEntityState($entity);
1866
1867
        switch ($entityState) {
1868 66
            case self::STATE_NEW:
1869 66
            case self::STATE_REMOVED:
1870
                // nothing to do
1871 2
                break;
1872
1873 66
            case self::STATE_MANAGED:
1874 66
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1875
1876 66
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1877 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1878
                }
1879
1880 66
                $this->scheduleForDelete($entity);
1881 66
                break;
1882
1883
            case self::STATE_DETACHED:
1884
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1885
            default:
1886
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1887
        }
1888
1889 66
    }
1890
1891
    /**
1892
     * Merges the state of the given detached entity into this UnitOfWork.
1893
     *
1894
     * @param object $entity
1895
     *
1896
     * @return object The managed copy of the entity.
1897
     *
1898
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1899
     *         attribute and the version check against the managed copy fails.
1900
     *
1901
     * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
1902
     */
1903 43
    public function merge($entity)
1904
    {
1905 43
        $visited = [];
1906
1907 43
        return $this->doMerge($entity, $visited);
1908
    }
1909
1910
    /**
1911
     * Executes a merge operation on an entity.
1912
     *
1913
     * @param object      $entity
1914
     * @param array       $visited
1915
     * @param object|null $prevManagedCopy
1916
     * @param array|null  $assoc
1917
     *
1918
     * @return object The managed copy of the entity.
1919
     *
1920
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1921
     *         attribute and the version check against the managed copy fails.
1922
     * @throws ORMInvalidArgumentException If the entity instance is NEW.
1923
     * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided
1924
     */
1925 43
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
1926
    {
1927 43
        $oid = spl_object_hash($entity);
1928
1929 43
        if (isset($visited[$oid])) {
1930 4
            $managedCopy = $visited[$oid];
1931
1932 4
            if ($prevManagedCopy !== null) {
1933 4
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1934
            }
1935
1936 4
            return $managedCopy;
1937
        }
1938
1939 43
        $class = $this->em->getClassMetadata(get_class($entity));
1940
1941
        // First we assume DETACHED, although it can still be NEW but we can avoid
1942
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1943
        // we need to fetch it from the db anyway in order to merge.
1944
        // MANAGED entities are ignored by the merge operation.
1945 43
        $managedCopy = $entity;
1946
1947 43
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1948
            // Try to look the entity up in the identity map.
1949 42
            $id = $class->getIdentifierValues($entity);
1950
1951
            // If there is no ID, it is actually NEW.
1952 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...
1953 6
                $managedCopy = $this->newInstance($class);
1954
1955 6
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1956 6
                $this->persistNew($class, $managedCopy);
1957
            } else {
1958 37
                $flatId = ($class->containsForeignIdentifier)
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1959 3
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1960 37
                    : $id;
1961
1962 37
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1963
1964 37
                if ($managedCopy) {
1965
                    // We have the entity in-memory already, just make sure its not removed.
1966 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

1966
                    if ($this->getEntityState(/** @scrutinizer ignore-type */ $managedCopy) == self::STATE_REMOVED) {
Loading history...
1967 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

1967
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, "merge");
Loading history...
1968
                    }
1969
                } else {
1970
                    // We need to fetch the managed copy in order to merge.
1971 25
                    $managedCopy = $this->em->find($class->name, $flatId);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1972
                }
1973
1974 37
                if ($managedCopy === null) {
1975
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1976
                    // since the managed entity was not found.
1977 3
                    if ( ! $class->isIdentifierNatural()) {
1978 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1979 1
                            $class->getName(),
1980 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1981
                        );
1982
                    }
1983
1984 2
                    $managedCopy = $this->newInstance($class);
1985 2
                    $class->setIdentifierValues($managedCopy, $id);
1986
1987 2
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1988 2
                    $this->persistNew($class, $managedCopy);
1989
                } else {
1990 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

1990
                    $this->ensureVersionMatch($class, $entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1991 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

1991
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1992
                }
1993
            }
1994
1995 40
            $visited[$oid] = $managedCopy; // mark visited
1996
1997 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1998
                $this->scheduleForDirtyCheck($entity);
1999
            }
2000
        }
2001
2002 41
        if ($prevManagedCopy !== null) {
2003 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

2003
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
2004
        }
2005
2006
        // Mark the managed copy visited as well
2007 41
        $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

2007
        $visited[spl_object_hash(/** @scrutinizer ignore-type */ $managedCopy)] = $managedCopy;
Loading history...
2008
2009 41
        $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

2009
        $this->cascadeMerge($entity, /** @scrutinizer ignore-type */ $managedCopy, $visited);
Loading history...
2010
2011 41
        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...
2012
    }
2013
2014
    /**
2015
     * @param ClassMetadata $class
2016
     * @param object        $entity
2017
     * @param object        $managedCopy
2018
     *
2019
     * @return void
2020
     *
2021
     * @throws OptimisticLockException
2022
     */
2023 34
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
2024
    {
2025 34
        if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
2026 31
            return;
2027
        }
2028
2029 4
        $reflField          = $class->reflFields[$class->versionField];
2030 4
        $managedCopyVersion = $reflField->getValue($managedCopy);
2031 4
        $entityVersion      = $reflField->getValue($entity);
2032
2033
        // Throw exception if versions don't match.
2034 4
        if ($managedCopyVersion == $entityVersion) {
2035 3
            return;
2036
        }
2037
2038 1
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
2039
    }
2040
2041
    /**
2042
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
2043
     *
2044
     * @param object $entity
2045
     *
2046
     * @return bool
2047
     */
2048 41
    private function isLoaded($entity)
2049
    {
2050 41
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2051
    }
2052
2053
    /**
2054
     * Sets/adds associated managed copies into the previous entity's association field
2055
     *
2056
     * @param object $entity
2057
     * @param array  $association
2058
     * @param object $previousManagedCopy
2059
     * @param object $managedCopy
2060
     *
2061
     * @return void
2062
     */
2063 6
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2064
    {
2065 6
        $assocField = $association['fieldName'];
2066 6
        $prevClass  = $this->em->getClassMetadata(get_class($previousManagedCopy));
2067
2068 6
        if ($association['type'] & ClassMetadata::TO_ONE) {
2069 6
            $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2070
2071 6
            return;
2072
        }
2073
2074 1
        $value   = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
2075 1
        $value[] = $managedCopy;
2076
2077 1
        if ($association['type'] == ClassMetadata::ONE_TO_MANY) {
2078 1
            $class = $this->em->getClassMetadata(get_class($entity));
2079
2080 1
            $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
2081
        }
2082 1
    }
2083
2084
    /**
2085
     * Detaches an entity from the persistence management. It's persistence will
2086
     * no longer be managed by Doctrine.
2087
     *
2088
     * @param object $entity The entity to detach.
2089
     *
2090
     * @return void
2091
     */
2092 12
    public function detach($entity)
2093
    {
2094 12
        $visited = [];
2095
2096 12
        $this->doDetach($entity, $visited);
2097 12
    }
2098
2099
    /**
2100
     * Executes a detach operation on the given entity.
2101
     *
2102
     * @param object  $entity
2103
     * @param array   $visited
2104
     * @param boolean $noCascade if true, don't cascade detach operation.
2105
     *
2106
     * @return void
2107
     */
2108 16
    private function doDetach($entity, array &$visited, $noCascade = false)
2109
    {
2110 16
        $oid = spl_object_hash($entity);
2111
2112 16
        if (isset($visited[$oid])) {
2113
            return; // Prevent infinite recursion
2114
        }
2115
2116 16
        $visited[$oid] = $entity; // mark visited
2117
2118 16
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2119 16
            case self::STATE_MANAGED:
2120 14
                if ($this->isInIdentityMap($entity)) {
2121 13
                    $this->removeFromIdentityMap($entity);
2122
                }
2123
2124
                unset(
2125 14
                    $this->entityInsertions[$oid],
2126 14
                    $this->entityUpdates[$oid],
2127 14
                    $this->entityDeletions[$oid],
2128 14
                    $this->entityIdentifiers[$oid],
2129 14
                    $this->entityStates[$oid],
2130 14
                    $this->originalEntityData[$oid]
2131
                );
2132 14
                break;
2133 3
            case self::STATE_NEW:
2134 3
            case self::STATE_DETACHED:
2135 3
                return;
2136
        }
2137
2138 14
        if ( ! $noCascade) {
2139 14
            $this->cascadeDetach($entity, $visited);
2140
        }
2141 14
    }
2142
2143
    /**
2144
     * Refreshes the state of the given entity from the database, overwriting
2145
     * any local, unpersisted changes.
2146
     *
2147
     * @param object $entity The entity to refresh.
2148
     *
2149
     * @return void
2150
     *
2151
     * @throws InvalidArgumentException If the entity is not MANAGED.
2152
     */
2153 17
    public function refresh($entity)
2154
    {
2155 17
        $visited = [];
2156
2157 17
        $this->doRefresh($entity, $visited);
2158 17
    }
2159
2160
    /**
2161
     * Executes a refresh operation on an entity.
2162
     *
2163
     * @param object $entity  The entity to refresh.
2164
     * @param array  $visited The already visited entities during cascades.
2165
     *
2166
     * @return void
2167
     *
2168
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
2169
     */
2170 17
    private function doRefresh($entity, array &$visited)
2171
    {
2172 17
        $oid = spl_object_hash($entity);
2173
2174 17
        if (isset($visited[$oid])) {
2175
            return; // Prevent infinite recursion
2176
        }
2177
2178 17
        $visited[$oid] = $entity; // mark visited
2179
2180 17
        $class = $this->em->getClassMetadata(get_class($entity));
2181
2182 17
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
2183
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2184
        }
2185
2186 17
        $this->getEntityPersister($class->name)->refresh(
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2187 17
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2188 17
            $entity
2189
        );
2190
2191 17
        $this->cascadeRefresh($entity, $visited);
2192 17
    }
2193
2194
    /**
2195
     * Cascades a refresh operation to associated entities.
2196
     *
2197
     * @param object $entity
2198
     * @param array  $visited
2199
     *
2200
     * @return void
2201
     */
2202 17
    private function cascadeRefresh($entity, array &$visited)
2203
    {
2204 17
        $class = $this->em->getClassMetadata(get_class($entity));
2205
2206 17
        $associationMappings = array_filter(
2207 17
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2208
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2209
        );
2210
2211 17
        foreach ($associationMappings as $assoc) {
2212 5
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2213
2214
            switch (true) {
2215 5
                case ($relatedEntities instanceof PersistentCollection):
2216
                    // Unwrap so that foreach() does not initialize
2217 5
                    $relatedEntities = $relatedEntities->unwrap();
2218
                    // break; is commented intentionally!
2219
2220
                case ($relatedEntities instanceof Collection):
2221
                case (is_array($relatedEntities)):
2222 5
                    foreach ($relatedEntities as $relatedEntity) {
2223
                        $this->doRefresh($relatedEntity, $visited);
2224
                    }
2225 5
                    break;
2226
2227
                case ($relatedEntities !== null):
2228
                    $this->doRefresh($relatedEntities, $visited);
2229
                    break;
2230
2231 5
                default:
2232
                    // Do nothing
2233
            }
2234
        }
2235 17
    }
2236
2237
    /**
2238
     * Cascades a detach operation to associated entities.
2239
     *
2240
     * @param object $entity
2241
     * @param array  $visited
2242
     *
2243
     * @return void
2244
     */
2245 14
    private function cascadeDetach($entity, array &$visited)
2246
    {
2247 14
        $class = $this->em->getClassMetadata(get_class($entity));
2248
2249 14
        $associationMappings = array_filter(
2250 14
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2251
            function ($assoc) { return $assoc['isCascadeDetach']; }
2252
        );
2253
2254 14
        foreach ($associationMappings as $assoc) {
2255 3
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2256
2257
            switch (true) {
2258 3
                case ($relatedEntities instanceof PersistentCollection):
2259
                    // Unwrap so that foreach() does not initialize
2260 2
                    $relatedEntities = $relatedEntities->unwrap();
2261
                    // break; is commented intentionally!
2262
2263 1
                case ($relatedEntities instanceof Collection):
2264
                case (is_array($relatedEntities)):
2265 3
                    foreach ($relatedEntities as $relatedEntity) {
2266 1
                        $this->doDetach($relatedEntity, $visited);
2267
                    }
2268 3
                    break;
2269
2270
                case ($relatedEntities !== null):
2271
                    $this->doDetach($relatedEntities, $visited);
2272
                    break;
2273
2274 3
                default:
2275
                    // Do nothing
2276
            }
2277
        }
2278 14
    }
2279
2280
    /**
2281
     * Cascades a merge operation to associated entities.
2282
     *
2283
     * @param object $entity
2284
     * @param object $managedCopy
2285
     * @param array  $visited
2286
     *
2287
     * @return void
2288
     */
2289 41
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2290
    {
2291 41
        $class = $this->em->getClassMetadata(get_class($entity));
2292
2293 41
        $associationMappings = array_filter(
2294 41
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2295
            function ($assoc) { return $assoc['isCascadeMerge']; }
2296
        );
2297
2298 41
        foreach ($associationMappings as $assoc) {
2299 16
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2300
2301 16
            if ($relatedEntities instanceof Collection) {
2302 10
                if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2303 1
                    continue;
2304
                }
2305
2306 9
                if ($relatedEntities instanceof PersistentCollection) {
2307
                    // Unwrap so that foreach() does not initialize
2308 5
                    $relatedEntities = $relatedEntities->unwrap();
2309
                }
2310
2311 9
                foreach ($relatedEntities as $relatedEntity) {
2312 9
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2313
                }
2314 7
            } else if ($relatedEntities !== null) {
2315 15
                $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2316
            }
2317
        }
2318 41
    }
2319
2320
    /**
2321
     * Cascades the save operation to associated entities.
2322
     *
2323
     * @param object $entity
2324
     * @param array  $visited
2325
     *
2326
     * @return void
2327
     */
2328 1103
    private function cascadePersist($entity, array &$visited)
2329
    {
2330 1103
        $class = $this->em->getClassMetadata(get_class($entity));
2331
2332 1103
        $associationMappings = array_filter(
2333 1103
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2334
            function ($assoc) { return $assoc['isCascadePersist']; }
2335
        );
2336
2337 1103
        foreach ($associationMappings as $assoc) {
2338 686
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2339
2340
            switch (true) {
2341 686
                case ($relatedEntities instanceof PersistentCollection):
2342
                    // Unwrap so that foreach() does not initialize
2343 21
                    $relatedEntities = $relatedEntities->unwrap();
2344
                    // break; is commented intentionally!
2345
2346 686
                case ($relatedEntities instanceof Collection):
2347 621
                case (is_array($relatedEntities)):
2348 577
                    if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
2349 3
                        throw ORMInvalidArgumentException::invalidAssociation(
2350 3
                            $this->em->getClassMetadata($assoc['targetEntity']),
2351 3
                            $assoc,
2352 3
                            $relatedEntities
2353
                        );
2354
                    }
2355
2356 574
                    foreach ($relatedEntities as $relatedEntity) {
2357 294
                        $this->doPersist($relatedEntity, $visited);
2358
                    }
2359
2360 574
                    break;
2361
2362 610
                case ($relatedEntities !== null):
2363 254
                    if (! $relatedEntities instanceof $assoc['targetEntity']) {
2364 4
                        throw ORMInvalidArgumentException::invalidAssociation(
2365 4
                            $this->em->getClassMetadata($assoc['targetEntity']),
2366 4
                            $assoc,
2367 4
                            $relatedEntities
2368
                        );
2369
                    }
2370
2371 250
                    $this->doPersist($relatedEntities, $visited);
2372 250
                    break;
2373
2374 680
                default:
2375
                    // Do nothing
2376
            }
2377
        }
2378 1096
    }
2379
2380
    /**
2381
     * Cascades the delete operation to associated entities.
2382
     *
2383
     * @param object $entity
2384
     * @param array  $visited
2385
     *
2386
     * @return void
2387
     */
2388 66
    private function cascadeRemove($entity, array &$visited)
2389
    {
2390 66
        $class = $this->em->getClassMetadata(get_class($entity));
2391
2392 66
        $associationMappings = array_filter(
2393 66
            $class->associationMappings,
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2394
            function ($assoc) { return $assoc['isCascadeRemove']; }
2395
        );
2396
2397 66
        $entitiesToCascade = [];
2398
2399 66
        foreach ($associationMappings as $assoc) {
2400 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...
2401 6
                $entity->__load();
2402
            }
2403
2404 26
            $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2405
2406
            switch (true) {
2407 26
                case ($relatedEntities instanceof Collection):
2408 19
                case (is_array($relatedEntities)):
2409
                    // If its a PersistentCollection initialization is intended! No unwrap!
2410 20
                    foreach ($relatedEntities as $relatedEntity) {
2411 10
                        $entitiesToCascade[] = $relatedEntity;
2412
                    }
2413 20
                    break;
2414
2415 19
                case ($relatedEntities !== null):
2416 7
                    $entitiesToCascade[] = $relatedEntities;
2417 7
                    break;
2418
2419 26
                default:
2420
                    // Do nothing
2421
            }
2422
        }
2423
2424 66
        foreach ($entitiesToCascade as $relatedEntity) {
2425 16
            $this->doRemove($relatedEntity, $visited);
2426
        }
2427 66
    }
2428
2429
    /**
2430
     * Acquire a lock on the given entity.
2431
     *
2432
     * @param object $entity
2433
     * @param int    $lockMode
2434
     * @param int    $lockVersion
2435
     *
2436
     * @return void
2437
     *
2438
     * @throws ORMInvalidArgumentException
2439
     * @throws TransactionRequiredException
2440
     * @throws OptimisticLockException
2441
     */
2442 9
    public function lock($entity, $lockMode, $lockVersion = null)
2443
    {
2444 9
        if ($entity === null) {
2445 1
            throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock().");
2446
        }
2447
2448 8
        if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2449 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2450
        }
2451
2452 7
        $class = $this->em->getClassMetadata(get_class($entity));
2453
2454
        switch (true) {
2455 7
            case LockMode::OPTIMISTIC === $lockMode:
2456 5
                if ( ! $class->isVersioned) {
0 ignored issues
show
Bug introduced by
Accessing isVersioned on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2457 1
                    throw OptimisticLockException::notVersioned($class->name);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2458
                }
2459
2460 4
                if ($lockVersion === null) {
2461
                    return;
2462
                }
2463
2464 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...
2465 1
                    $entity->__load();
2466
                }
2467
2468 4
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing versionField on the interface Doctrine\Common\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\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2469
2470 4
                if ($entityVersion != $lockVersion) {
2471 2
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2472
                }
2473
2474 2
                break;
2475
2476 2
            case LockMode::NONE === $lockMode:
2477 2
            case LockMode::PESSIMISTIC_READ === $lockMode:
2478 1
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2479 2
                if (!$this->em->getConnection()->isTransactionActive()) {
2480 2
                    throw TransactionRequiredException::transactionRequired();
2481
                }
2482
2483
                $oid = spl_object_hash($entity);
2484
2485
                $this->getEntityPersister($class->name)->lock(
2486
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2487
                    $lockMode
2488
                );
2489
                break;
2490
2491
            default:
2492
                // Do nothing
2493
        }
2494 2
    }
2495
2496
    /**
2497
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
2498
     *
2499
     * @return \Doctrine\ORM\Internal\CommitOrderCalculator
2500
     */
2501 1078
    public function getCommitOrderCalculator()
2502
    {
2503 1078
        return new Internal\CommitOrderCalculator();
2504
    }
2505
2506
    /**
2507
     * Clears the UnitOfWork.
2508
     *
2509
     * @param string|null $entityName if given, only entities of this type will get detached.
2510
     *
2511
     * @return void
2512
     *
2513
     * @throws ORMInvalidArgumentException if an invalid entity name is given
2514
     */
2515 1302
    public function clear($entityName = null)
2516
    {
2517 1302
        if ($entityName === null) {
2518 1300
            $this->identityMap =
2519 1300
            $this->entityIdentifiers =
2520 1300
            $this->originalEntityData =
2521 1300
            $this->entityChangeSets =
2522 1300
            $this->entityStates =
2523 1300
            $this->scheduledForSynchronization =
2524 1300
            $this->entityInsertions =
2525 1300
            $this->entityUpdates =
2526 1300
            $this->entityDeletions =
2527 1300
            $this->nonCascadedNewDetectedEntities =
2528 1300
            $this->collectionDeletions =
2529 1300
            $this->collectionUpdates =
2530 1300
            $this->extraUpdates =
2531 1300
            $this->readOnlyObjects =
2532 1300
            $this->visitedCollections =
2533 1300
            $this->orphanRemovals = [];
2534
        } else {
2535 4
            $this->clearIdentityMapForEntityName($entityName);
2536 4
            $this->clearEntityInsertionsForEntityName($entityName);
2537
        }
2538
2539 1302
        if ($this->evm->hasListeners(Events::onClear)) {
2540 9
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
2541
        }
2542 1302
    }
2543
2544
    /**
2545
     * INTERNAL:
2546
     * Schedules an orphaned entity for removal. The remove() operation will be
2547
     * invoked on that entity at the beginning of the next commit of this
2548
     * UnitOfWork.
2549
     *
2550
     * @ignore
2551
     *
2552
     * @param object $entity
2553
     *
2554
     * @return void
2555
     */
2556 17
    public function scheduleOrphanRemoval($entity)
2557
    {
2558 17
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2559 17
    }
2560
2561
    /**
2562
     * INTERNAL:
2563
     * Cancels a previously scheduled orphan removal.
2564
     *
2565
     * @ignore
2566
     *
2567
     * @param object $entity
2568
     *
2569
     * @return void
2570
     */
2571 117
    public function cancelOrphanRemoval($entity)
2572
    {
2573 117
        unset($this->orphanRemovals[spl_object_hash($entity)]);
2574 117
    }
2575
2576
    /**
2577
     * INTERNAL:
2578
     * Schedules a complete collection for removal when this UnitOfWork commits.
2579
     *
2580
     * @param PersistentCollection $coll
2581
     *
2582
     * @return void
2583
     */
2584 14
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2585
    {
2586 14
        $coid = spl_object_hash($coll);
2587
2588
        // TODO: if $coll is already scheduled for recreation ... what to do?
2589
        // Just remove $coll from the scheduled recreations?
2590 14
        unset($this->collectionUpdates[$coid]);
2591
2592 14
        $this->collectionDeletions[$coid] = $coll;
2593 14
    }
2594
2595
    /**
2596
     * @param PersistentCollection $coll
2597
     *
2598
     * @return bool
2599
     */
2600
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2601
    {
2602
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2603
    }
2604
2605
    /**
2606
     * @param ClassMetadata $class
2607
     *
2608
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
2609
     */
2610 712
    private function newInstance($class)
2611
    {
2612 712
        $entity = $class->newInstance();
2613
2614 712
        if ($entity instanceof \Doctrine\Common\Persistence\ObjectManagerAware) {
2615 4
            $entity->injectObjectManager($this->em, $class);
2616
        }
2617
2618 712
        return $entity;
2619
    }
2620
2621
    /**
2622
     * INTERNAL:
2623
     * Creates an entity. Used for reconstitution of persistent entities.
2624
     *
2625
     * Internal note: Highly performance-sensitive method.
2626
     *
2627
     * @ignore
2628
     *
2629
     * @param string $className The name of the entity class.
2630
     * @param array  $data      The data for the entity.
2631
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the entity.
2632
     *
2633
     * @return object The managed entity instance.
2634
     *
2635
     * @todo Rename: getOrCreateEntity
2636
     */
2637 854
    public function createEntity($className, array $data, &$hints = [])
2638
    {
2639 854
        $class = $this->em->getClassMetadata($className);
2640
2641 854
        $id = $this->identifierFlattener->flattenIdentifier($class, $data);
2642 854
        $idHash = implode(' ', $id);
2643
2644 854
        if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2645 325
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
2646 325
            $oid = spl_object_hash($entity);
2647
2648
            if (
2649 325
                isset($hints[Query::HINT_REFRESH])
2650 325
                && isset($hints[Query::HINT_REFRESH_ENTITY])
2651 325
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
2652 325
                && $unmanagedProxy instanceof Proxy
2653 325
                && $this->isIdentifierEquals($unmanagedProxy, $entity)
2654
            ) {
2655
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
2656
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
2657
                // refreshed object may be anything
2658
2659 2
                foreach ($class->identifier as $fieldName) {
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2660 2
                    $class->reflFields[$fieldName]->setValue($unmanagedProxy, null);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2661
                }
2662
2663 2
                return $unmanagedProxy;
2664
            }
2665
2666 323
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2667 23
                $entity->__setInitialized(true);
2668
2669 23
                if ($entity instanceof NotifyPropertyChanged) {
2670 23
                    $entity->addPropertyChangedListener($this);
2671
                }
2672
            } else {
2673 302
                if ( ! isset($hints[Query::HINT_REFRESH])
2674 302
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2675 231
                    return $entity;
2676
                }
2677
            }
2678
2679
            // inject ObjectManager upon refresh.
2680 115
            if ($entity instanceof ObjectManagerAware) {
2681 3
                $entity->injectObjectManager($this->em, $class);
2682
            }
2683
2684 115
            $this->originalEntityData[$oid] = $data;
2685
        } else {
2686 707
            $entity = $this->newInstance($class);
2687 707
            $oid    = spl_object_hash($entity);
2688
2689 707
            $this->entityIdentifiers[$oid]  = $id;
2690 707
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2691 707
            $this->originalEntityData[$oid] = $data;
2692
2693 707
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
2694
2695 707
            if ($entity instanceof NotifyPropertyChanged) {
2696 2
                $entity->addPropertyChangedListener($this);
2697
            }
2698
        }
2699
2700 745
        foreach ($data as $field => $value) {
2701 745
            if (isset($class->fieldMappings[$field])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2702 745
                $class->reflFields[$field]->setValue($entity, $value);
2703
            }
2704
        }
2705
2706
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2707 745
        unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2708
2709 745
        if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) {
2710
            unset($this->eagerLoadingEntities[$class->rootEntityName]);
2711
        }
2712
2713
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2714 745
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2715 34
            return $entity;
2716
        }
2717
2718 711
        foreach ($class->associationMappings as $field => $assoc) {
2719
            // Check if the association is not among the fetch-joined associations already.
2720 610
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2721 260
                continue;
2722
            }
2723
2724 586
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2725
2726
            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...
2727 586
                case ($assoc['type'] & ClassMetadata::TO_ONE):
2728 506
                    if ( ! $assoc['isOwningSide']) {
2729
2730
                        // use the given entity association
2731 67
                        if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2732
2733 3
                            $this->originalEntityData[$oid][$field] = $data[$field];
2734
2735 3
                            $class->reflFields[$field]->setValue($entity, $data[$field]);
2736 3
                            $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
2737
2738 3
                            continue 2;
2739
                        }
2740
2741
                        // Inverse side of x-to-one can never be lazy
2742 64
                        $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
2743
2744 64
                        continue 2;
2745
                    }
2746
2747
                    // use the entity association
2748 506
                    if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2749 38
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2750 38
                        $this->originalEntityData[$oid][$field] = $data[$field];
2751
2752 38
                        continue;
2753
                    }
2754
2755 499
                    $associatedId = [];
2756
2757
                    // TODO: Is this even computed right in all cases of composite keys?
2758 499
                    foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
2759 499
                        $joinColumnValue = $data[$srcColumn] ?? null;
2760
2761 499
                        if ($joinColumnValue !== null) {
2762 300
                            if ($targetClass->containsForeignIdentifier) {
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2763 12
                                $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
2764
                            } else {
2765 300
                                $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2766
                            }
2767 293
                        } elseif ($targetClass->containsForeignIdentifier
2768 293
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2769
                        ) {
2770
                            // the missing key is part of target's entity primary key
2771 7
                            $associatedId = [];
2772 499
                            break;
2773
                        }
2774
                    }
2775
2776 499
                    if ( ! $associatedId) {
2777
                        // Foreign key is NULL
2778 293
                        $class->reflFields[$field]->setValue($entity, null);
2779 293
                        $this->originalEntityData[$oid][$field] = null;
2780
2781 293
                        continue;
2782
                    }
2783
2784 300
                    if ( ! isset($hints['fetchMode'][$class->name][$field])) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2785 297
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2786
                    }
2787
2788
                    // Foreign key is set
2789
                    // Check identity map first
2790
                    // FIXME: Can break easily with composite keys if join column values are in
2791
                    //        wrong order. The correct order is the one in ClassMetadata#identifier.
2792 300
                    $relatedIdHash = implode(' ', $associatedId);
2793
2794
                    switch (true) {
2795 300
                        case (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2796 174
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2797
2798
                            // If this is an uninitialized proxy, we are deferring eager loads,
2799
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2800
                            // then we can append this entity for eager loading!
2801 174
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2802 174
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2803 174
                                !$targetClass->isIdentifierComposite &&
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2804 174
                                $newValue instanceof Proxy &&
2805 174
                                $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...
2806
2807
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2808
                            }
2809
2810 174
                            break;
2811
2812 204
                        case ($targetClass->subClasses):
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2813
                            // If it might be a subtype, it can not be lazy. There isn't even
2814
                            // a way to solve this with deferred eager loading, which means putting
2815
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2816 32
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2817 32
                            break;
2818
2819
                        default:
2820
                            switch (true) {
2821
                                // We are negating the condition here. Other cases will assume it is valid!
2822 174
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2823 167
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2824 167
                                    break;
2825
2826
                                // Deferred eager load only works for single identifier classes
2827 7
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
2828
                                    // TODO: Is there a faster approach?
2829 7
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2830
2831 7
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2832 7
                                    break;
2833
2834
                                default:
2835
                                    // TODO: This is very imperformant, ignore it?
2836
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2837
                                    break;
2838
                            }
2839
2840
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2841 174
                            $newValueOid = spl_object_hash($newValue);
2842 174
                            $this->entityIdentifiers[$newValueOid] = $associatedId;
2843 174
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2844
2845
                            if (
2846 174
                                $newValue instanceof NotifyPropertyChanged &&
2847 174
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
2848
                            ) {
2849
                                $newValue->addPropertyChangedListener($this);
2850
                            }
2851 174
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2852
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2853 174
                            break;
2854
                    }
2855
2856 300
                    $this->originalEntityData[$oid][$field] = $newValue;
2857 300
                    $class->reflFields[$field]->setValue($entity, $newValue);
2858
2859 300
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2860 59
                        $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2861 59
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2862
                    }
2863
2864 300
                    break;
2865
2866
                default:
2867
                    // Ignore if its a cached collection
2868 497
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2869
                        break;
2870
                    }
2871
2872
                    // use the given collection
2873 497
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2874
2875 3
                        $data[$field]->setOwner($entity, $assoc);
2876
2877 3
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2878 3
                        $this->originalEntityData[$oid][$field] = $data[$field];
2879
2880 3
                        break;
2881
                    }
2882
2883
                    // Inject collection
2884 497
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2885 497
                    $pColl->setOwner($entity, $assoc);
2886 497
                    $pColl->setInitialized(false);
2887
2888 497
                    $reflField = $class->reflFields[$field];
2889 497
                    $reflField->setValue($entity, $pColl);
2890
2891 497
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2892 4
                        $this->loadCollection($pColl);
2893 4
                        $pColl->takeSnapshot();
2894
                    }
2895
2896 497
                    $this->originalEntityData[$oid][$field] = $pColl;
2897 586
                    break;
2898
            }
2899
        }
2900
2901
        // defer invoking of postLoad event to hydration complete step
2902 711
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2903
2904 711
        return $entity;
2905
    }
2906
2907
    /**
2908
     * @return void
2909
     */
2910 922
    public function triggerEagerLoads()
2911
    {
2912 922
        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...
2913 922
            return;
2914
        }
2915
2916
        // avoid infinite recursion
2917 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2918 7
        $this->eagerLoadingEntities = [];
2919
2920 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2921 7
            if ( ! $ids) {
2922
                continue;
2923
            }
2924
2925 7
            $class = $this->em->getClassMetadata($entityName);
2926
2927 7
            $this->getEntityPersister($entityName)->loadAll(
2928 7
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2929
            );
2930
        }
2931 7
    }
2932
2933
    /**
2934
     * Initializes (loads) an uninitialized persistent collection of an entity.
2935
     *
2936
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2937
     *
2938
     * @return void
2939
     *
2940
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2941
     */
2942 148
    public function loadCollection(PersistentCollection $collection)
2943
    {
2944 148
        $assoc     = $collection->getMapping();
2945 148
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2946
2947 148
        switch ($assoc['type']) {
2948 148
            case ClassMetadata::ONE_TO_MANY:
2949 78
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2950 78
                break;
2951
2952 84
            case ClassMetadata::MANY_TO_MANY:
2953 84
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2954 84
                break;
2955
        }
2956
2957 148
        $collection->setInitialized(true);
2958 148
    }
2959
2960
    /**
2961
     * Gets the identity map of the UnitOfWork.
2962
     *
2963
     * @return array
2964
     */
2965 2
    public function getIdentityMap()
2966
    {
2967 2
        return $this->identityMap;
2968
    }
2969
2970
    /**
2971
     * Gets the original data of an entity. The original data is the data that was
2972
     * present at the time the entity was reconstituted from the database.
2973
     *
2974
     * @param object $entity
2975
     *
2976
     * @return array
2977
     */
2978 125
    public function getOriginalEntityData($entity)
2979
    {
2980 125
        $oid = spl_object_hash($entity);
2981
2982 125
        return isset($this->originalEntityData[$oid])
2983 121
            ? $this->originalEntityData[$oid]
2984 125
            : [];
2985
    }
2986
2987
    /**
2988
     * @ignore
2989
     *
2990
     * @param object $entity
2991
     * @param array  $data
2992
     *
2993
     * @return void
2994
     */
2995
    public function setOriginalEntityData($entity, array $data)
2996
    {
2997
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2998
    }
2999
3000
    /**
3001
     * INTERNAL:
3002
     * Sets a property value of the original data array of an entity.
3003
     *
3004
     * @ignore
3005
     *
3006
     * @param string $oid
3007
     * @param string $property
3008
     * @param mixed  $value
3009
     *
3010
     * @return void
3011
     */
3012 314
    public function setOriginalEntityProperty($oid, $property, $value)
3013
    {
3014 314
        $this->originalEntityData[$oid][$property] = $value;
3015 314
    }
3016
3017
    /**
3018
     * Gets the identifier of an entity.
3019
     * The returned value is always an array of identifier values. If the entity
3020
     * has a composite identifier then the identifier values are in the same
3021
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
3022
     *
3023
     * @param object $entity
3024
     *
3025
     * @return array The identifier values.
3026
     */
3027 874
    public function getEntityIdentifier($entity)
3028
    {
3029 874
        return $this->entityIdentifiers[spl_object_hash($entity)];
3030
    }
3031
3032
    /**
3033
     * Processes an entity instance to extract their identifier values.
3034
     *
3035
     * @param object $entity The entity instance.
3036
     *
3037
     * @return mixed A scalar value.
3038
     *
3039
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
3040
     */
3041 133
    public function getSingleIdentifierValue($entity)
3042
    {
3043 133
        $class = $this->em->getClassMetadata(get_class($entity));
3044
3045 133
        if ($class->isIdentifierComposite) {
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3046
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
3047
        }
3048
3049 133
        $values = $this->isInIdentityMap($entity)
3050 120
            ? $this->getEntityIdentifier($entity)
3051 133
            : $class->getIdentifierValues($entity);
3052
3053 133
        return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null;
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3054
    }
3055
3056
    /**
3057
     * Tries to find an entity with the given identifier in the identity map of
3058
     * this UnitOfWork.
3059
     *
3060
     * @param mixed  $id            The entity identifier to look for.
3061
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
3062
     *
3063
     * @return object|bool Returns the entity with the specified identifier if it exists in
3064
     *                     this UnitOfWork, FALSE otherwise.
3065
     */
3066 556
    public function tryGetById($id, $rootClassName)
3067
    {
3068 556
        $idHash = implode(' ', (array) $id);
3069
3070 556
        return isset($this->identityMap[$rootClassName][$idHash])
3071 89
            ? $this->identityMap[$rootClassName][$idHash]
3072 556
            : false;
3073
    }
3074
3075
    /**
3076
     * Schedules an entity for dirty-checking at commit-time.
3077
     *
3078
     * @param object $entity The entity to schedule for dirty-checking.
3079
     *
3080
     * @return void
3081
     *
3082
     * @todo Rename: scheduleForSynchronization
3083
     */
3084 6
    public function scheduleForDirtyCheck($entity)
3085
    {
3086 6
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3087
3088 6
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
3089 6
    }
3090
3091
    /**
3092
     * Checks whether the UnitOfWork has any pending insertions.
3093
     *
3094
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
3095
     */
3096
    public function hasPendingInsertions()
3097
    {
3098
        return ! empty($this->entityInsertions);
3099
    }
3100
3101
    /**
3102
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
3103
     * number of entities in the identity map.
3104
     *
3105
     * @return integer
3106
     */
3107 1
    public function size()
3108
    {
3109 1
        $countArray = array_map('count', $this->identityMap);
3110
3111 1
        return array_sum($countArray);
3112
    }
3113
3114
    /**
3115
     * Gets the EntityPersister for an Entity.
3116
     *
3117
     * @param string $entityName The name of the Entity.
3118
     *
3119
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
3120
     */
3121 1137
    public function getEntityPersister($entityName)
3122
    {
3123 1137
        if (isset($this->persisters[$entityName])) {
3124 895
            return $this->persisters[$entityName];
3125
        }
3126
3127 1137
        $class = $this->em->getClassMetadata($entityName);
3128
3129
        switch (true) {
3130 1137
            case ($class->isInheritanceTypeNone()):
3131 1088
                $persister = new BasicEntityPersister($this->em, $class);
3132 1088
                break;
3133
3134 394
            case ($class->isInheritanceTypeSingleTable()):
3135 226
                $persister = new SingleTablePersister($this->em, $class);
3136 226
                break;
3137
3138 361
            case ($class->isInheritanceTypeJoined()):
3139 361
                $persister = new JoinedSubclassPersister($this->em, $class);
3140 361
                break;
3141
3142
            default:
3143
                throw new \RuntimeException('No persister found for entity.');
3144
        }
3145
3146 1137
        if ($this->hasCache && $class->cache !== null) {
0 ignored issues
show
Bug introduced by
Accessing cache on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3147 126
            $persister = $this->em->getConfiguration()
3148 126
                ->getSecondLevelCacheConfiguration()
3149 126
                ->getCacheFactory()
3150 126
                ->buildCachedEntityPersister($this->em, $persister, $class);
3151
        }
3152
3153 1137
        $this->persisters[$entityName] = $persister;
3154
3155 1137
        return $this->persisters[$entityName];
3156
    }
3157
3158
    /**
3159
     * Gets a collection persister for a collection-valued association.
3160
     *
3161
     * @param array $association
3162
     *
3163
     * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister
3164
     */
3165 583
    public function getCollectionPersister(array $association)
3166
    {
3167 583
        $role = isset($association['cache'])
3168 78
            ? $association['sourceEntity'] . '::' . $association['fieldName']
3169 583
            : $association['type'];
3170
3171 583
        if (isset($this->collectionPersisters[$role])) {
3172 457
            return $this->collectionPersisters[$role];
3173
        }
3174
3175 583
        $persister = ClassMetadata::ONE_TO_MANY === $association['type']
3176 412
            ? new OneToManyPersister($this->em)
3177 583
            : new ManyToManyPersister($this->em);
3178
3179 583
        if ($this->hasCache && isset($association['cache'])) {
3180 77
            $persister = $this->em->getConfiguration()
3181 77
                ->getSecondLevelCacheConfiguration()
3182 77
                ->getCacheFactory()
3183 77
                ->buildCachedCollectionPersister($this->em, $persister, $association);
3184
        }
3185
3186 583
        $this->collectionPersisters[$role] = $persister;
3187
3188 583
        return $this->collectionPersisters[$role];
3189
    }
3190
3191
    /**
3192
     * INTERNAL:
3193
     * Registers an entity as managed.
3194
     *
3195
     * @param object $entity The entity.
3196
     * @param array  $id     The identifier values.
3197
     * @param array  $data   The original entity data.
3198
     *
3199
     * @return void
3200
     */
3201 210
    public function registerManaged($entity, array $id, array $data)
3202
    {
3203 210
        $oid = spl_object_hash($entity);
3204
3205 210
        $this->entityIdentifiers[$oid]  = $id;
3206 210
        $this->entityStates[$oid]       = self::STATE_MANAGED;
3207 210
        $this->originalEntityData[$oid] = $data;
3208
3209 210
        $this->addToIdentityMap($entity);
3210
3211 204
        if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) {
3212 2
            $entity->addPropertyChangedListener($this);
3213
        }
3214 204
    }
3215
3216
    /**
3217
     * INTERNAL:
3218
     * Clears the property changeset of the entity with the given OID.
3219
     *
3220
     * @param string $oid The entity's OID.
3221
     *
3222
     * @return void
3223
     */
3224 16
    public function clearEntityChangeSet($oid)
3225
    {
3226 16
        unset($this->entityChangeSets[$oid]);
3227 16
    }
3228
3229
    /* PropertyChangedListener implementation */
3230
3231
    /**
3232
     * Notifies this UnitOfWork of a property change in an entity.
3233
     *
3234
     * @param object $entity       The entity that owns the property.
3235
     * @param string $propertyName The name of the property that changed.
3236
     * @param mixed  $oldValue     The old value of the property.
3237
     * @param mixed  $newValue     The new value of the property.
3238
     *
3239
     * @return void
3240
     */
3241 4
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
3242
    {
3243 4
        $oid   = spl_object_hash($entity);
3244 4
        $class = $this->em->getClassMetadata(get_class($entity));
3245
3246 4
        $isAssocField = isset($class->associationMappings[$propertyName]);
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3247
3248 4
        if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
0 ignored issues
show
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3249 1
            return; // ignore non-persistent fields
3250
        }
3251
3252
        // Update changeset and mark entity for synchronization
3253 4
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
3254
3255 4
        if ( ! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3256 4
            $this->scheduleForDirtyCheck($entity);
3257
        }
3258 4
    }
3259
3260
    /**
3261
     * Gets the currently scheduled entity insertions in this UnitOfWork.
3262
     *
3263
     * @return array
3264
     */
3265 2
    public function getScheduledEntityInsertions()
3266
    {
3267 2
        return $this->entityInsertions;
3268
    }
3269
3270
    /**
3271
     * Gets the currently scheduled entity updates in this UnitOfWork.
3272
     *
3273
     * @return array
3274
     */
3275 3
    public function getScheduledEntityUpdates()
3276
    {
3277 3
        return $this->entityUpdates;
3278
    }
3279
3280
    /**
3281
     * Gets the currently scheduled entity deletions in this UnitOfWork.
3282
     *
3283
     * @return array
3284
     */
3285 1
    public function getScheduledEntityDeletions()
3286
    {
3287 1
        return $this->entityDeletions;
3288
    }
3289
3290
    /**
3291
     * Gets the currently scheduled complete collection deletions
3292
     *
3293
     * @return array
3294
     */
3295 1
    public function getScheduledCollectionDeletions()
3296
    {
3297 1
        return $this->collectionDeletions;
3298
    }
3299
3300
    /**
3301
     * Gets the currently scheduled collection inserts, updates and deletes.
3302
     *
3303
     * @return array
3304
     */
3305
    public function getScheduledCollectionUpdates()
3306
    {
3307
        return $this->collectionUpdates;
3308
    }
3309
3310
    /**
3311
     * Helper method to initialize a lazy loading proxy or persistent collection.
3312
     *
3313
     * @param object $obj
3314
     *
3315
     * @return void
3316
     */
3317 2
    public function initializeObject($obj)
3318
    {
3319 2
        if ($obj instanceof Proxy) {
3320 1
            $obj->__load();
3321
3322 1
            return;
3323
        }
3324
3325 1
        if ($obj instanceof PersistentCollection) {
3326 1
            $obj->initialize();
3327
        }
3328 1
    }
3329
3330
    /**
3331
     * Helper method to show an object as string.
3332
     *
3333
     * @param object $obj
3334
     *
3335
     * @return string
3336
     */
3337 1
    private static function objToStr($obj)
3338
    {
3339 1
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj).'@'.spl_object_hash($obj);
3340
    }
3341
3342
    /**
3343
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3344
     *
3345
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3346
     * on this object that might be necessary to perform a correct update.
3347
     *
3348
     * @param object $object
3349
     *
3350
     * @return void
3351
     *
3352
     * @throws ORMInvalidArgumentException
3353
     */
3354 6
    public function markReadOnly($object)
3355
    {
3356 6
        if ( ! is_object($object) || ! $this->isInIdentityMap($object)) {
3357 1
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3358
        }
3359
3360 5
        $this->readOnlyObjects[spl_object_hash($object)] = true;
3361 5
    }
3362
3363
    /**
3364
     * Is this entity read only?
3365
     *
3366
     * @param object $object
3367
     *
3368
     * @return bool
3369
     *
3370
     * @throws ORMInvalidArgumentException
3371
     */
3372 3
    public function isReadOnly($object)
3373
    {
3374 3
        if ( ! is_object($object)) {
3375
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3376
        }
3377
3378 3
        return isset($this->readOnlyObjects[spl_object_hash($object)]);
3379
    }
3380
3381
    /**
3382
     * Perform whatever processing is encapsulated here after completion of the transaction.
3383
     */
3384
    private function afterTransactionComplete()
3385
    {
3386 1073
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3387 95
            $persister->afterTransactionComplete();
3388 1073
        });
3389 1073
    }
3390
3391
    /**
3392
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
3393
     */
3394
    private function afterTransactionRolledBack()
3395
    {
3396 11
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3397 3
            $persister->afterTransactionRolledBack();
3398 11
        });
3399 11
    }
3400
3401
    /**
3402
     * Performs an action after the transaction.
3403
     *
3404
     * @param callable $callback
3405
     */
3406 1078
    private function performCallbackOnCachedPersister(callable $callback)
3407
    {
3408 1078
        if ( ! $this->hasCache) {
3409 983
            return;
3410
        }
3411
3412 95
        foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) {
3413 95
            if ($persister instanceof CachedPersister) {
3414 95
                $callback($persister);
3415
            }
3416
        }
3417 95
    }
3418
3419 1082
    private function dispatchOnFlushEvent()
3420
    {
3421 1082
        if ($this->evm->hasListeners(Events::onFlush)) {
3422 4
            $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3423
        }
3424 1082
    }
3425
3426 1077
    private function dispatchPostFlushEvent()
3427
    {
3428 1077
        if ($this->evm->hasListeners(Events::postFlush)) {
3429 5
            $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3430
        }
3431 1076
    }
3432
3433
    /**
3434
     * Verifies if two given entities actually are the same based on identifier comparison
3435
     *
3436
     * @param object $entity1
3437
     * @param object $entity2
3438
     *
3439
     * @return bool
3440
     */
3441 14
    private function isIdentifierEquals($entity1, $entity2)
3442
    {
3443 14
        if ($entity1 === $entity2) {
3444
            return true;
3445
        }
3446
3447 14
        $class = $this->em->getClassMetadata(get_class($entity1));
3448
3449 14
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
3450 11
            return false;
3451
        }
3452
3453 3
        $oid1 = spl_object_hash($entity1);
3454 3
        $oid2 = spl_object_hash($entity2);
3455
3456 3
        $id1 = isset($this->entityIdentifiers[$oid1])
3457 3
            ? $this->entityIdentifiers[$oid1]
3458 3
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
3459 3
        $id2 = isset($this->entityIdentifiers[$oid2])
3460 3
            ? $this->entityIdentifiers[$oid2]
3461 3
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
3462
3463 3
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
3464
    }
3465
3466
    /**
3467
     * @throws ORMInvalidArgumentException
3468
     */
3469 1080
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
3470
    {
3471 1080
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
3472
3473 1080
        $this->nonCascadedNewDetectedEntities = [];
3474
3475 1080
        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...
3476 5
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
3477 5
                \array_values($entitiesNeedingCascadePersist)
3478
            );
3479
        }
3480 1078
    }
3481
3482
    /**
3483
     * @param object $entity
3484
     * @param object $managedCopy
3485
     *
3486
     * @throws ORMException
3487
     * @throws OptimisticLockException
3488
     * @throws TransactionRequiredException
3489
     */
3490 40
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
3491
    {
3492 40
        if (! $this->isLoaded($entity)) {
3493 7
            return;
3494
        }
3495
3496 33
        if (! $this->isLoaded($managedCopy)) {
3497 4
            $managedCopy->__load();
3498
        }
3499
3500 33
        $class = $this->em->getClassMetadata(get_class($entity));
3501
3502 33
        foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
3503 33
            $name = $prop->name;
3504
3505 33
            $prop->setAccessible(true);
3506
3507 33
            if ( ! isset($class->associationMappings[$name])) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3508 33
                if ( ! $class->isIdentifier($name)) {
3509 33
                    $prop->setValue($managedCopy, $prop->getValue($entity));
3510
                }
3511
            } else {
3512 29
                $assoc2 = $class->associationMappings[$name];
3513
3514 29
                if ($assoc2['type'] & ClassMetadata::TO_ONE) {
3515 25
                    $other = $prop->getValue($entity);
3516 25
                    if ($other === null) {
3517 12
                        $prop->setValue($managedCopy, null);
3518
                    } else {
3519 16
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
3520
                            // do not merge fields marked lazy that have not been fetched.
3521 4
                            continue;
3522
                        }
3523
3524 12
                        if ( ! $assoc2['isCascadeMerge']) {
3525 6
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
3526 3
                                $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
3527 3
                                $relatedId   = $targetClass->getIdentifierValues($other);
3528
3529 3
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3530 2
                                    $other = $this->em->find($targetClass->name, $relatedId);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
3531
                                } else {
3532 1
                                    $other = $this->em->getProxyFactory()->getProxy(
3533 1
                                        $assoc2['targetEntity'],
3534 1
                                        $relatedId
3535
                                    );
3536 1
                                    $this->registerManaged($other, $relatedId, []);
3537
                                }
3538
                            }
3539
3540 21
                            $prop->setValue($managedCopy, $other);
3541
                        }
3542
                    }
3543
                } else {
3544 17
                    $mergeCol = $prop->getValue($entity);
3545
3546 17
                    if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
3547
                        // do not merge fields marked lazy that have not been fetched.
3548
                        // keep the lazy persistent collection of the managed copy.
3549 5
                        continue;
3550
                    }
3551
3552 14
                    $managedCol = $prop->getValue($managedCopy);
3553
3554 14
                    if ( ! $managedCol) {
3555 4
                        $managedCol = new PersistentCollection(
3556 4
                            $this->em,
3557 4
                            $this->em->getClassMetadata($assoc2['targetEntity']),
3558 4
                            new ArrayCollection
3559
                        );
3560 4
                        $managedCol->setOwner($managedCopy, $assoc2);
3561 4
                        $prop->setValue($managedCopy, $managedCol);
3562
                    }
3563
3564 14
                    if ($assoc2['isCascadeMerge']) {
3565 9
                        $managedCol->initialize();
3566
3567
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
3568 9
                        if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
3569 1
                            $managedCol->unwrap()->clear();
3570 1
                            $managedCol->setDirty(true);
3571
3572 1
                            if ($assoc2['isOwningSide']
3573 1
                                && $assoc2['type'] == ClassMetadata::MANY_TO_MANY
3574 1
                                && $class->isChangeTrackingNotify()
3575
                            ) {
3576
                                $this->scheduleForDirtyCheck($managedCopy);
3577
                            }
3578
                        }
3579
                    }
3580
                }
3581
            }
3582
3583 33
            if ($class->isChangeTrackingNotify()) {
3584
                // Just treat all properties as changed, there is no other choice.
3585 33
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
3586
            }
3587
        }
3588 33
    }
3589
3590
    /**
3591
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
3592
     * Unit of work able to fire deferred events, related to loading events here.
3593
     *
3594
     * @internal should be called internally from object hydrators
3595
     */
3596 937
    public function hydrationComplete()
3597
    {
3598 937
        $this->hydrationCompleteHandler->hydrationComplete();
3599 937
    }
3600
3601
    /**
3602
     * @param string $entityName
3603
     */
3604 4
    private function clearIdentityMapForEntityName($entityName)
3605
    {
3606 4
        if (! isset($this->identityMap[$entityName])) {
3607
            return;
3608
        }
3609
3610 4
        $visited = [];
3611
3612 4
        foreach ($this->identityMap[$entityName] as $entity) {
3613 4
            $this->doDetach($entity, $visited, false);
3614
        }
3615 4
    }
3616
3617
    /**
3618
     * @param string $entityName
3619
     */
3620 4
    private function clearEntityInsertionsForEntityName($entityName)
3621
    {
3622 4
        foreach ($this->entityInsertions as $hash => $entity) {
3623
            // note: performance optimization - `instanceof` is much faster than a function call
3624 1
            if ($entity instanceof $entityName && get_class($entity) === $entityName) {
3625 1
                unset($this->entityInsertions[$hash]);
3626
            }
3627
        }
3628 4
    }
3629
3630
    /**
3631
     * @param ClassMetadata $class
3632
     * @param mixed         $identifierValue
3633
     *
3634
     * @return mixed the identifier after type conversion
3635
     *
3636
     * @throws \Doctrine\ORM\Mapping\MappingException if the entity has more than a single identifier
3637
     */
3638 976
    private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue)
3639
    {
3640 976
        return $this->em->getConnection()->convertToPHPValue(
3641 976
            $identifierValue,
3642 976
            $class->getTypeOfField($class->getSingleIdentifierFieldName())
3643
        );
3644
    }
3645
}
3646