Failed Conditions
Pull Request — 2.6 (#7180)
by Ben
11:16
created

UnitOfWork::isIdentifierEquals()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.0106

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 10
nop 2
dl 0
loc 23
ccs 14
cts 15
cp 0.9333
crap 6.0106
rs 8.5906
c 0
b 0
f 0
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 2465
    public function __construct(EntityManagerInterface $em)
303
    {
304 2465
        $this->em                         = $em;
305 2465
        $this->evm                        = $em->getEventManager();
306 2465
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2465
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2465
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2465
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2465
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2465
    }
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 1083
    public function commit($entity = null)
333
    {
334
        // Raise preFlush
335 1083
        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 1083
        if (null === $entity) {
341 1073
            $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 1080
        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 175
                $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 138
                $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 1080
                $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 1076
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
363
364 1074
        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 1074
        $this->dispatchOnFlushEvent();
371
372
        // Now we need a commit order to maintain referential integrity
373 1074
        $commitOrder = $this->getCommitOrder();
374
375 1074
        $conn = $this->em->getConnection();
376 1074
        $conn->beginTransaction();
377
378
        try {
379
            // Collection deletions (deletions of complete collections)
380 1074
            foreach ($this->collectionDeletions as $collectionToDelete) {
381 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
382
            }
383
384 1074
            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 1070
                foreach ($commitOrder as $class) {
386 1070
                    $this->executeInserts($class);
387
                }
388
            }
389
390 1073
            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 123
                foreach ($commitOrder as $class) {
392 123
                    $this->executeUpdates($class);
393
                }
394
            }
395
396
            // Extra updates that were requested by persisters.
397 1069
            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 45
                $this->executeExtraUpdates();
399
            }
400
401
            // Collection updates (deleteRows, updateRows, insertRows)
402 1069
            foreach ($this->collectionUpdates as $collectionToUpdate) {
403 543
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
404
            }
405
406
            // Entity deletions come last and need to be in reverse commit order
407 1069
            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 1069
            $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 1069
        $this->afterTransactionComplete();
424
425
        // Take new snapshots from visited collections
426 1069
        foreach ($this->visitedCollections as $coll) {
427 542
            $coll->takeSnapshot();
428
        }
429
430 1069
        $this->dispatchPostFlushEvent();
431
432 1068
        $this->postCommitCleanup($entity);
433 1068
    }
434
435
    /**
436
     * @param null|object|object[] $entity
437
     */
438 1068
    private function postCommitCleanup($entity) : void
439
    {
440 1068
        $this->entityInsertions =
441 1068
        $this->entityUpdates =
442 1068
        $this->entityDeletions =
443 1068
        $this->extraUpdates =
444 1068
        $this->collectionUpdates =
445 1068
        $this->nonCascadedNewDetectedEntities =
446 1068
        $this->collectionDeletions =
447 1068
        $this->visitedCollections =
448 1068
        $this->orphanRemovals = [];
449
450 1068
        if (null === $entity) {
451 1058
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
452
453 1058
            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 1082
    private function computeScheduleInsertsChangeSets()
475
    {
476 1082
        foreach ($this->entityInsertions as $entity) {
477 1074
            $class = $this->em->getClassMetadata(get_class($entity));
478
479 1074
            $this->computeChangeSet($class, $entity);
480
        }
481 1080
    }
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()) {
0 ignored issues
show
Bug introduced by
The method isChangeTrackingDeferredImplicit() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

507
        if ($state === self::STATE_MANAGED && $class->/** @scrutinizer ignore-call */ isChangeTrackingDeferredImplicit()) {
Loading history...
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 45
    private function executeExtraUpdates()
535
    {
536 45
        foreach ($this->extraUpdates as $oid => $update) {
537 45
            list ($entity, $changeset) = $update;
538
539 45
            $this->entityChangeSets[$oid] = $changeset;
540 45
            $this->getEntityPersister(get_class($entity))->update($entity);
541
        }
542
543 45
        $this->extraUpdates = [];
544 45
    }
545
546
    /**
547
     * Gets the changeset for an entity.
548
     *
549
     * @param object $entity
550
     *
551
     * @return array
552
     */
553 1069
    public function & getEntityChangeSet($entity)
554
    {
555 1069
        $oid  = spl_object_hash($entity);
556 1069
        $data = [];
557
558 1069
        if (!isset($this->entityChangeSets[$oid])) {
559 4
            return $data;
560
        }
561
562 1069
        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 1084
    public function computeChangeSet(ClassMetadata $class, $entity)
600
    {
601 1084
        $oid = spl_object_hash($entity);
602
603 1084
        if (isset($this->readOnlyObjects[$oid])) {
604 2
            return;
605
        }
606
607 1084
        if ( ! $class->isInheritanceTypeNone()) {
608 337
            $class = $this->em->getClassMetadata(get_class($entity));
609
        }
610
611 1084
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
612
613 1084
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
614 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
615
        }
616
617 1084
        $actualData = [];
618
619 1084
        foreach ($class->reflFields as $name => $refProp) {
620 1084
            $value = $refProp->getValue($entity);
621
622 1084
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
623 815
                if ($value instanceof PersistentCollection) {
624 205
                    if ($value->getOwner() === $entity) {
625 205
                        continue;
626
                    }
627
628 5
                    $value = new ArrayCollection($value->getValues());
629
                }
630
631
                // If $value is not a Collection then use an ArrayCollection.
632 810
                if ( ! $value instanceof Collection) {
633 243
                    $value = new ArrayCollection($value);
634
                }
635
636 810
                $assoc = $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...
637
638
                // Inject PersistentCollection
639 810
                $value = new PersistentCollection(
640 810
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
641
                );
642 810
                $value->setOwner($entity, $assoc);
643 810
                $value->setDirty( ! $value->isEmpty());
644
645 810
                $class->reflFields[$name]->setValue($entity, $value);
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...
646
647 810
                $actualData[$name] = $value;
648
649 810
                continue;
650
            }
651
652 1084
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
0 ignored issues
show
Bug introduced by
The method isIdGeneratorIdentity() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

652
            if (( ! $class->isIdentifier($name) || ! $class->/** @scrutinizer ignore-call */ isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
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...
653 1084
                $actualData[$name] = $value;
654
            }
655
        }
656
657 1084
        if ( ! isset($this->originalEntityData[$oid])) {
658
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
659
            // These result in an INSERT.
660 1080
            $this->originalEntityData[$oid] = $actualData;
661 1080
            $changeSet = [];
662
663 1080
            foreach ($actualData as $propName => $actualValue) {
664 1058
                if ( ! isset($class->associationMappings[$propName])) {
665 1001
                    $changeSet[$propName] = [null, $actualValue];
666
667 1001
                    continue;
668
                }
669
670 944
                $assoc = $class->associationMappings[$propName];
671
672 944
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
673 944
                    $changeSet[$propName] = [null, $actualValue];
674
                }
675
            }
676
677 1080
            $this->entityChangeSets[$oid] = $changeSet;
678
        } else {
679
            // Entity is "fully" MANAGED: it was already fully persisted before
680
            // and we have a copy of the original data
681 275
            $originalData           = $this->originalEntityData[$oid];
682 275
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
0 ignored issues
show
Bug introduced by
The method isChangeTrackingNotify() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

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

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

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

Loading history...
1556 5
            return self::STATE_NEW;
1557
        }
1558
1559 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...
1560 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1561
        }
1562
1563
        switch (true) {
1564 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

1564
            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...
1565
                // Check for a version field, if available, to avoid a db lookup.
1566 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...
1567 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

1567
                    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...
1568
                        ? self::STATE_DETACHED
1569 1
                        : self::STATE_NEW;
1570
                }
1571
1572
                // Last try before db lookup: check the identity map.
1573 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...
1574 1
                    return self::STATE_DETACHED;
1575
                }
1576
1577
                // db lookup
1578 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...
1579
                    return self::STATE_DETACHED;
1580
                }
1581
1582 4
                return self::STATE_NEW;
1583
1584 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...
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...
1585
                // if we have a pre insert generator we can't be sure that having an id
1586
                // really means that the entity exists. We have to verify this through
1587
                // the last resort: a db lookup
1588
1589
                // Last try before db lookup: check the identity map.
1590
                if ($this->tryGetById($id, $class->rootEntityName)) {
1591
                    return self::STATE_DETACHED;
1592
                }
1593
1594
                // db lookup
1595
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1596
                    return self::STATE_DETACHED;
1597
                }
1598
1599
                return self::STATE_NEW;
1600
1601
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

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

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

1766
                if ($class->/** @scrutinizer ignore-call */ isChangeTrackingDeferredExplicit()) {
Loading history...
1767 2
                    $this->scheduleForDirtyCheck($entity);
1768
                }
1769 239
                break;
1770
1771 1099
            case self::STATE_NEW:
1772 1098
                $this->persistNew($class, $entity);
1773 1098
                break;
1774
1775 1
            case self::STATE_REMOVED:
1776
                // Entity becomes managed again
1777 1
                unset($this->entityDeletions[$oid]);
1778 1
                $this->addToIdentityMap($entity);
1779
1780 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1781 1
                break;
1782
1783
            case self::STATE_DETACHED:
1784
                // Can actually not happen right now since we assume STATE_NEW.
1785
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1786
1787
            default:
1788
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1789
        }
1790
1791 1099
        $this->cascadePersist($entity, $visited);
1792 1092
    }
1793
1794
    /**
1795
     * Deletes an entity as part of the current unit of work.
1796
     *
1797
     * @param object $entity The entity to remove.
1798
     *
1799
     * @return void
1800
     */
1801 66
    public function remove($entity)
1802
    {
1803 66
        $visited = [];
1804
1805 66
        $this->doRemove($entity, $visited);
1806 66
    }
1807
1808
    /**
1809
     * Deletes an entity as part of the current unit of work.
1810
     *
1811
     * This method is internally called during delete() cascades as it tracks
1812
     * the already visited entities to prevent infinite recursions.
1813
     *
1814
     * @param object $entity  The entity to delete.
1815
     * @param array  $visited The map of the already visited entities.
1816
     *
1817
     * @return void
1818
     *
1819
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1820
     * @throws UnexpectedValueException
1821
     */
1822 66
    private function doRemove($entity, array &$visited)
1823
    {
1824 66
        $oid = spl_object_hash($entity);
1825
1826 66
        if (isset($visited[$oid])) {
1827 1
            return; // Prevent infinite recursion
1828
        }
1829
1830 66
        $visited[$oid] = $entity; // mark visited
1831
1832
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1833
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1834 66
        $this->cascadeRemove($entity, $visited);
1835
1836 66
        $class       = $this->em->getClassMetadata(get_class($entity));
1837 66
        $entityState = $this->getEntityState($entity);
1838
1839
        switch ($entityState) {
1840 66
            case self::STATE_NEW:
1841 66
            case self::STATE_REMOVED:
1842
                // nothing to do
1843 2
                break;
1844
1845 66
            case self::STATE_MANAGED:
1846 66
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1847
1848 66
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1849 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1850
                }
1851
1852 66
                $this->scheduleForDelete($entity);
1853 66
                break;
1854
1855
            case self::STATE_DETACHED:
1856
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1857
            default:
1858
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1859
        }
1860
1861 66
    }
1862
1863
    /**
1864
     * Merges the state of the given detached entity into this UnitOfWork.
1865
     *
1866
     * @param object $entity
1867
     *
1868
     * @return object The managed copy of the entity.
1869
     *
1870
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1871
     *         attribute and the version check against the managed copy fails.
1872
     *
1873
     * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
1874
     */
1875 43
    public function merge($entity)
1876
    {
1877 43
        $visited = [];
1878
1879 43
        return $this->doMerge($entity, $visited);
1880
    }
1881
1882
    /**
1883
     * Executes a merge operation on an entity.
1884
     *
1885
     * @param object      $entity
1886
     * @param array       $visited
1887
     * @param object|null $prevManagedCopy
1888
     * @param array|null  $assoc
1889
     *
1890
     * @return object The managed copy of the entity.
1891
     *
1892
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1893
     *         attribute and the version check against the managed copy fails.
1894
     * @throws ORMInvalidArgumentException If the entity instance is NEW.
1895
     * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided
1896
     */
1897 43
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
1898
    {
1899 43
        $oid = spl_object_hash($entity);
1900
1901 43
        if (isset($visited[$oid])) {
1902 4
            $managedCopy = $visited[$oid];
1903
1904 4
            if ($prevManagedCopy !== null) {
1905 4
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1906
            }
1907
1908 4
            return $managedCopy;
1909
        }
1910
1911 43
        $class = $this->em->getClassMetadata(get_class($entity));
1912
1913
        // First we assume DETACHED, although it can still be NEW but we can avoid
1914
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1915
        // we need to fetch it from the db anyway in order to merge.
1916
        // MANAGED entities are ignored by the merge operation.
1917 43
        $managedCopy = $entity;
1918
1919 43
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1920
            // Try to look the entity up in the identity map.
1921 42
            $id = $class->getIdentifierValues($entity);
1922
1923
            // If there is no ID, it is actually NEW.
1924 42
            if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1925 6
                $managedCopy = $this->newInstance($class);
1926
1927 6
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1928 6
                $this->persistNew($class, $managedCopy);
1929
            } else {
1930 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...
1931 3
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1932 37
                    : $id;
1933
1934 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...
1935
1936 37
                if ($managedCopy) {
1937
                    // We have the entity in-memory already, just make sure its not removed.
1938 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

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

1939
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, "merge");
Loading history...
1940
                    }
1941
                } else {
1942
                    // We need to fetch the managed copy in order to merge.
1943 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...
1944
                }
1945
1946 37
                if ($managedCopy === null) {
1947
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1948
                    // since the managed entity was not found.
1949 3
                    if ( ! $class->isIdentifierNatural()) {
1950 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1951 1
                            $class->getName(),
1952 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1953
                        );
1954
                    }
1955
1956 2
                    $managedCopy = $this->newInstance($class);
1957 2
                    $class->setIdentifierValues($managedCopy, $id);
0 ignored issues
show
Bug introduced by
The method setIdentifierValues() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

1957
                    $class->/** @scrutinizer ignore-call */ 
1958
                            setIdentifierValues($managedCopy, $id);
Loading history...
1958
1959 2
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1960 2
                    $this->persistNew($class, $managedCopy);
1961
                } else {
1962 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

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

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

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

2735
                                $associatedId[$targetClass->/** @scrutinizer ignore-call */ getFieldForColumn($targetColumn)] = $joinColumnValue;
Loading history...
2736
                            } else {
2737 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...
2738
                            }
2739 294
                        } elseif ($targetClass->containsForeignIdentifier
2740 294
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2741
                        ) {
2742
                            // the missing key is part of target's entity primary key
2743 7
                            $associatedId = [];
2744 500
                            break;
2745
                        }
2746
                    }
2747
2748 500
                    if ( ! $associatedId) {
2749
                        // Foreign key is NULL
2750 294
                        $class->reflFields[$field]->setValue($entity, null);
2751 294
                        $this->originalEntityData[$oid][$field] = null;
2752
2753 294
                        continue;
2754
                    }
2755
2756 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...
2757 297
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2758
                    }
2759
2760
                    // Foreign key is set
2761
                    // Check identity map first
2762
                    // FIXME: Can break easily with composite keys if join column values are in
2763
                    //        wrong order. The correct order is the one in ClassMetadata#identifier.
2764 300
                    $relatedIdHash = implode(' ', $associatedId);
2765
2766
                    switch (true) {
2767 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...
2768 174
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2769
2770
                            // If this is an uninitialized proxy, we are deferring eager loads,
2771
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2772
                            // then we can append this entity for eager loading!
2773 174
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2774 174
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2775 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...
2776 174
                                $newValue instanceof Proxy &&
2777 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...
2778
2779
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2780
                            }
2781
2782 174
                            break;
2783
2784 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...
2785
                            // If it might be a subtype, it can not be lazy. There isn't even
2786
                            // a way to solve this with deferred eager loading, which means putting
2787
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2788 32
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2789 32
                            break;
2790
2791
                        default:
2792
                            switch (true) {
2793
                                // We are negating the condition here. Other cases will assume it is valid!
2794 174
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2795 167
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2796 167
                                    break;
2797
2798
                                // Deferred eager load only works for single identifier classes
2799 7
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
2800
                                    // TODO: Is there a faster approach?
2801 7
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2802
2803 7
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2804 7
                                    break;
2805
2806
                                default:
2807
                                    // TODO: This is very imperformant, ignore it?
2808
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2809
                                    break;
2810
                            }
2811
2812
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2813 174
                            $newValueOid = spl_object_hash($newValue);
2814 174
                            $this->entityIdentifiers[$newValueOid] = $associatedId;
2815 174
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2816
2817
                            if (
2818 174
                                $newValue instanceof NotifyPropertyChanged &&
2819 174
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
2820
                            ) {
2821
                                $newValue->addPropertyChangedListener($this);
2822
                            }
2823 174
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2824
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2825 174
                            break;
2826
                    }
2827
2828 300
                    $this->originalEntityData[$oid][$field] = $newValue;
2829 300
                    $class->reflFields[$field]->setValue($entity, $newValue);
2830
2831 300
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2832 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...
2833 59
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2834
                    }
2835
2836 300
                    break;
2837
2838
                default:
2839
                    // Ignore if its a cached collection
2840 498
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2841
                        break;
2842
                    }
2843
2844
                    // use the given collection
2845 498
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2846
2847 3
                        $data[$field]->setOwner($entity, $assoc);
2848
2849 3
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2850 3
                        $this->originalEntityData[$oid][$field] = $data[$field];
2851
2852 3
                        break;
2853
                    }
2854
2855
                    // Inject collection
2856 498
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2857 498
                    $pColl->setOwner($entity, $assoc);
2858 498
                    $pColl->setInitialized(false);
2859
2860 498
                    $reflField = $class->reflFields[$field];
2861 498
                    $reflField->setValue($entity, $pColl);
2862
2863 498
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2864 4
                        $this->loadCollection($pColl);
2865 4
                        $pColl->takeSnapshot();
2866
                    }
2867
2868 498
                    $this->originalEntityData[$oid][$field] = $pColl;
2869 587
                    break;
2870
            }
2871
        }
2872
2873
        // defer invoking of postLoad event to hydration complete step
2874 711
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2875
2876 711
        return $entity;
2877
    }
2878
2879
    /**
2880
     * @return void
2881
     */
2882 920
    public function triggerEagerLoads()
2883
    {
2884 920
        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...
2885 920
            return;
2886
        }
2887
2888
        // avoid infinite recursion
2889 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2890 7
        $this->eagerLoadingEntities = [];
2891
2892 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2893 7
            if ( ! $ids) {
2894
                continue;
2895
            }
2896
2897 7
            $class = $this->em->getClassMetadata($entityName);
2898
2899 7
            $this->getEntityPersister($entityName)->loadAll(
2900 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...
2901
            );
2902
        }
2903 7
    }
2904
2905
    /**
2906
     * Initializes (loads) an uninitialized persistent collection of an entity.
2907
     *
2908
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2909
     *
2910
     * @return void
2911
     *
2912
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2913
     */
2914 148
    public function loadCollection(PersistentCollection $collection)
2915
    {
2916 148
        $assoc     = $collection->getMapping();
2917 148
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2918
2919 148
        switch ($assoc['type']) {
2920 148
            case ClassMetadata::ONE_TO_MANY:
2921 78
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2922 78
                break;
2923
2924 84
            case ClassMetadata::MANY_TO_MANY:
2925 84
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2926 84
                break;
2927
        }
2928
2929 148
        $collection->setInitialized(true);
2930 148
    }
2931
2932
    /**
2933
     * Gets the identity map of the UnitOfWork.
2934
     *
2935
     * @return array
2936
     */
2937 2
    public function getIdentityMap()
2938
    {
2939 2
        return $this->identityMap;
2940
    }
2941
2942
    /**
2943
     * Gets the original data of an entity. The original data is the data that was
2944
     * present at the time the entity was reconstituted from the database.
2945
     *
2946
     * @param object $entity
2947
     *
2948
     * @return array
2949
     */
2950 122
    public function getOriginalEntityData($entity)
2951
    {
2952 122
        $oid = spl_object_hash($entity);
2953
2954 122
        return isset($this->originalEntityData[$oid])
2955 118
            ? $this->originalEntityData[$oid]
2956 122
            : [];
2957
    }
2958
2959
    /**
2960
     * @ignore
2961
     *
2962
     * @param object $entity
2963
     * @param array  $data
2964
     *
2965
     * @return void
2966
     */
2967
    public function setOriginalEntityData($entity, array $data)
2968
    {
2969
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2970
    }
2971
2972
    /**
2973
     * INTERNAL:
2974
     * Sets a property value of the original data array of an entity.
2975
     *
2976
     * @ignore
2977
     *
2978
     * @param string $oid
2979
     * @param string $property
2980
     * @param mixed  $value
2981
     *
2982
     * @return void
2983
     */
2984 314
    public function setOriginalEntityProperty($oid, $property, $value)
2985
    {
2986 314
        $this->originalEntityData[$oid][$property] = $value;
2987 314
    }
2988
2989
    /**
2990
     * Gets the identifier of an entity.
2991
     * The returned value is always an array of identifier values. If the entity
2992
     * has a composite identifier then the identifier values are in the same
2993
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2994
     *
2995
     * @param object $entity
2996
     *
2997
     * @return array The identifier values.
2998
     */
2999 876
    public function getEntityIdentifier($entity)
3000
    {
3001 876
        return $this->entityIdentifiers[spl_object_hash($entity)];
3002
    }
3003
3004
    /**
3005
     * Processes an entity instance to extract their identifier values.
3006
     *
3007
     * @param object $entity The entity instance.
3008
     *
3009
     * @return mixed A scalar value.
3010
     *
3011
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
3012
     */
3013 133
    public function getSingleIdentifierValue($entity)
3014
    {
3015 133
        $class = $this->em->getClassMetadata(get_class($entity));
3016
3017 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...
3018
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
3019
        }
3020
3021 133
        $values = $this->isInIdentityMap($entity)
3022 120
            ? $this->getEntityIdentifier($entity)
3023 133
            : $class->getIdentifierValues($entity);
3024
3025 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...
3026
    }
3027
3028
    /**
3029
     * Tries to find an entity with the given identifier in the identity map of
3030
     * this UnitOfWork.
3031
     *
3032
     * @param mixed  $id            The entity identifier to look for.
3033
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
3034
     *
3035
     * @return object|bool Returns the entity with the specified identifier if it exists in
3036
     *                     this UnitOfWork, FALSE otherwise.
3037
     */
3038 559
    public function tryGetById($id, $rootClassName)
3039
    {
3040 559
        $idHash = implode(' ', (array) $id);
3041
3042 559
        return isset($this->identityMap[$rootClassName][$idHash])
3043 89
            ? $this->identityMap[$rootClassName][$idHash]
3044 559
            : false;
3045
    }
3046
3047
    /**
3048
     * Schedules an entity for dirty-checking at commit-time.
3049
     *
3050
     * @param object $entity The entity to schedule for dirty-checking.
3051
     *
3052
     * @return void
3053
     *
3054
     * @todo Rename: scheduleForSynchronization
3055
     */
3056 6
    public function scheduleForDirtyCheck($entity)
3057
    {
3058 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...
3059
3060 6
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
3061 6
    }
3062
3063
    /**
3064
     * Checks whether the UnitOfWork has any pending insertions.
3065
     *
3066
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
3067
     */
3068
    public function hasPendingInsertions()
3069
    {
3070
        return ! empty($this->entityInsertions);
3071
    }
3072
3073
    /**
3074
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
3075
     * number of entities in the identity map.
3076
     *
3077
     * @return integer
3078
     */
3079 1
    public function size()
3080
    {
3081 1
        $countArray = array_map('count', $this->identityMap);
3082
3083 1
        return array_sum($countArray);
3084
    }
3085
3086
    /**
3087
     * Gets the EntityPersister for an Entity.
3088
     *
3089
     * @param string $entityName The name of the Entity.
3090
     *
3091
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
3092
     */
3093 1138
    public function getEntityPersister($entityName)
3094
    {
3095 1138
        if (isset($this->persisters[$entityName])) {
3096 894
            return $this->persisters[$entityName];
3097
        }
3098
3099 1138
        $class = $this->em->getClassMetadata($entityName);
3100
3101
        switch (true) {
3102 1138
            case ($class->isInheritanceTypeNone()):
0 ignored issues
show
Bug introduced by
The method isInheritanceTypeNone() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

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

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

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

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

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