Completed
Pull Request — 2.6 (#7180)
by Ben
13:18
created

UnitOfWork::newInstance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 9.6666
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 2466
    public function __construct(EntityManagerInterface $em)
303
    {
304 2466
        $this->em                         = $em;
305 2466
        $this->evm                        = $em->getEventManager();
306 2466
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2466
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2466
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2466
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2466
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2466
    }
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 1084
    public function commit($entity = null)
333
    {
334
        // Raise preFlush
335 1084
        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 1084
        if (null === $entity) {
341 1074
            $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 1081
        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 1081
                $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 1077
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
363
364 1075
        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 1075
        $this->dispatchOnFlushEvent();
371
372
        // Now we need a commit order to maintain referential integrity
373 1075
        $commitOrder = $this->getCommitOrder();
374
375 1075
        $conn = $this->em->getConnection();
376 1075
        $conn->beginTransaction();
377
378
        try {
379
            // Collection deletions (deletions of complete collections)
380 1075
            foreach ($this->collectionDeletions as $collectionToDelete) {
381 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
382
            }
383
384 1075
            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 1071
                foreach ($commitOrder as $class) {
386 1071
                    $this->executeInserts($class);
387
                }
388
            }
389
390 1074
            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 1070
            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 46
                $this->executeExtraUpdates();
399
            }
400
401
            // Collection updates (deleteRows, updateRows, insertRows)
402 1070
            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 1070
            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 1070
            $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 1070
        $this->afterTransactionComplete();
424
425
        // Take new snapshots from visited collections
426 1070
        foreach ($this->visitedCollections as $coll) {
427 542
            $coll->takeSnapshot();
428
        }
429
430 1070
        $this->dispatchPostFlushEvent();
431
432 1069
        $this->postCommitCleanup($entity);
433 1069
    }
434
435
    /**
436
     * @param null|object|object[] $entity
437
     */
438 1069
    private function postCommitCleanup($entity) : void
439
    {
440 1069
        $this->entityInsertions =
441 1069
        $this->entityUpdates =
442 1069
        $this->entityDeletions =
443 1069
        $this->extraUpdates =
444 1069
        $this->collectionUpdates =
445 1069
        $this->nonCascadedNewDetectedEntities =
446 1069
        $this->collectionDeletions =
447 1069
        $this->visitedCollections =
448 1069
        $this->orphanRemovals = [];
449
450 1069
        if (null === $entity) {
451 1059
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
452
453 1059
            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 1083
    private function computeScheduleInsertsChangeSets()
475
    {
476 1083
        foreach ($this->entityInsertions as $entity) {
477 1075
            $class = $this->em->getClassMetadata(get_class($entity));
478
479 1075
            $this->computeChangeSet($class, $entity);
480
        }
481 1081
    }
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 46
    private function executeExtraUpdates()
535
    {
536 46
        foreach ($this->extraUpdates as $oid => $update) {
537 46
            list ($entity, $changeset) = $update;
538
539 46
            $this->entityChangeSets[$oid] = $changeset;
540 46
            $this->getEntityPersister(get_class($entity))->update($entity);
541
        }
542
543 46
        $this->extraUpdates = [];
544 46
    }
545
546
    /**
547
     * Gets the changeset for an entity.
548
     *
549
     * @param object $entity
550
     *
551
     * @return array
552
     */
553 1070
    public function & getEntityChangeSet($entity)
554
    {
555 1070
        $oid  = spl_object_hash($entity);
556 1070
        $data = [];
557
558 1070
        if (!isset($this->entityChangeSets[$oid])) {
559 4
            return $data;
560
        }
561
562 1070
        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 1085
    public function computeChangeSet(ClassMetadata $class, $entity)
600
    {
601 1085
        $oid = spl_object_hash($entity);
602
603 1085
        if (isset($this->readOnlyObjects[$oid])) {
604 2
            return;
605
        }
606
607 1085
        if ( ! $class->isInheritanceTypeNone()) {
608 337
            $class = $this->em->getClassMetadata(get_class($entity));
609
        }
610
611 1085
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
612
613 1085
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
614 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
615
        }
616
617 1085
        $actualData = [];
618
619 1085
        foreach ($class->reflFields as $name => $refProp) {
620 1085
            $value = $refProp->getValue($entity);
621
622 1085
            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 1085
            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 1085
                $actualData[$name] = $value;
654
            }
655
        }
656
657 1085
        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 1081
            $this->originalEntityData[$oid] = $actualData;
661 1081
            $changeSet = [];
662
663 1081
            foreach ($actualData as $propName => $actualValue) {
664 1059
                if ( ! isset($class->associationMappings[$propName])) {
665 1001
                    $changeSet[$propName] = [null, $actualValue];
666
667 1001
                    continue;
668
                }
669
670 945
                $assoc = $class->associationMappings[$propName];
671
672 945
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
673 945
                    $changeSet[$propName] = [null, $actualValue];
674
                }
675
            }
676
677 1081
            $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 1085
        foreach ($class->associationMappings as $field => $assoc) {
764 945
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
765 663
                continue;
766
            }
767
768 916
            $this->computeAssociationChanges($assoc, $val);
769
770 908
            if ( ! isset($this->entityChangeSets[$oid]) &&
771 908
                $assoc['isOwningSide'] &&
772 908
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
773 908
                $val instanceof PersistentCollection &&
774 908
                $val->isDirty()) {
775
776 35
                $this->entityChangeSets[$oid]   = [];
777 35
                $this->originalEntityData[$oid] = $actualData;
778 908
                $this->entityUpdates[$oid]      = $entity;
779
            }
780
        }
781 1077
    }
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 1074
    public function computeChangeSets()
791
    {
792
        // Compute changes for INSERTed entities first. This must always happen.
793 1074
        $this->computeScheduleInsertsChangeSets();
794
795
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
796 1072
        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 1072
    }
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 916
    private function computeAssociationChanges($assoc, $value)
848
    {
849 916
        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 915
        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 915
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
864 915
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
865
866 915
        foreach ($unwrappedValue as $key => $entry) {
867 755
            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 747
            $state = $this->getEntityState($entry, self::STATE_NEW);
872
873 747
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
874
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
875
            }
876
877
            switch ($state) {
878 747
                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 739
                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 739
                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 747
                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 907
    }
916
917
    /**
918
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
919
     * @param object                              $entity
920
     *
921
     * @return void
922
     */
923 1104
    private function persistNew($class, $entity)
924
    {
925 1104
        $oid    = spl_object_hash($entity);
926 1104
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
927
928 1104
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
929 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
930
        }
931
932 1104
        $idGen = $class->idGenerator;
933
934 1104
        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 1104
        $this->entityStates[$oid] = self::STATE_MANAGED;
951
952 1104
        $this->scheduleForInsert($entity);
953 1104
    }
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 1071
    private function executeInserts($class)
1048
    {
1049 1071
        $entities   = [];
1050 1071
        $className  = $class->name;
1051 1071
        $persister  = $this->getEntityPersister($className);
1052 1071
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1053
1054 1071
        $insertionsForClass = [];
1055
1056 1071
        foreach ($this->entityInsertions as $oid => $entity) {
1057
1058 1071
            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 909
                continue;
1060
            }
1061
1062 1071
            $insertionsForClass[$oid] = $entity;
1063
1064 1071
            $persister->addInsert($entity);
1065
1066 1071
            unset($this->entityInsertions[$oid]);
1067
1068 1071
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1069 1071
                $entities[] = $entity;
1070
            }
1071
        }
1072
1073 1071
        $postInsertIds = $persister->executeInserts();
1074
1075 1071
        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 971
            foreach ($postInsertIds as $postInsertId) {
1078 971
                $idField = $class->getSingleIdentifierFieldName();
1079 971
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1080
1081 971
                $entity  = $postInsertId['entity'];
1082 971
                $oid     = spl_object_hash($entity);
1083
1084 971
                $class->reflFields[$idField]->setValue($entity, $idValue);
1085
1086 971
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1087 971
                $this->entityStates[$oid] = self::STATE_MANAGED;
1088 971
                $this->originalEntityData[$oid][$idField] = $idValue;
1089
1090 971
                $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 1071
        foreach ($entities as $entity) {
1103 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1104
        }
1105 1071
    }
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 26
                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 1075
    private function getCommitOrder(array $entityChangeSet = null)
1216
    {
1217 1075
        if ($entityChangeSet === null) {
1218 1075
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1219
        }
1220
1221 1075
        $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 1075
        $newNodes = [];
1229
1230 1075
        foreach ($entityChangeSet as $entity) {
1231 1075
            $class = $this->em->getClassMetadata(get_class($entity));
1232
1233 1075
            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 1075
            $calc->addNode($class->name, $class);
1238
1239 1075
            $newNodes[] = $class;
1240
        }
1241
1242
        // Calculate dependencies for new nodes
1243 1075
        while ($class = array_pop($newNodes)) {
1244 1075
            foreach ($class->associationMappings as $assoc) {
1245 934
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1246 885
                    continue;
1247
                }
1248
1249 884
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1250
1251 884
                if ( ! $calc->hasNode($targetClass->name)) {
1252 679
                    $calc->addNode($targetClass->name, $targetClass);
1253
1254 679
                    $newNodes[] = $targetClass;
1255
                }
1256
1257 884
                $joinColumns = reset($assoc['joinColumns']);
1258 884
                $joinColumnsNullable = $joinColumns['nullable'] ?? true;
1259
1260 884
                $calc->addDependency(
1261 884
                    $targetClass->name,
1262 884
                    $class->name,
1263 884
                    $joinColumnsNullable === false ? 1 : 0
1264
                );
1265
1266
                // If the target class has mapped subclasses, these share the same dependency.
1267 884
                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...
1268 877
                    continue;
1269
                }
1270
1271 238
                foreach ($targetClass->subClasses as $subClassName) {
1272 238
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1273
1274 238
                    if ( ! $calc->hasNode($subClassName)) {
1275 208
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1276
1277 208
                        $newNodes[] = $targetSubClass;
1278
                    }
1279
1280 238
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1281
                }
1282
            }
1283
        }
1284
1285 1075
        return $calc->sort();
1286
    }
1287
1288
    /**
1289
     * Schedules an entity for insertion into the database.
1290
     * If the entity already has an identifier, it will be added to the identity map.
1291
     *
1292
     * @param object $entity The entity to schedule for insertion.
1293
     *
1294
     * @return void
1295
     *
1296
     * @throws ORMInvalidArgumentException
1297
     * @throws \InvalidArgumentException
1298
     */
1299 1105
    public function scheduleForInsert($entity)
1300
    {
1301 1105
        $oid = spl_object_hash($entity);
1302
1303 1105
        if (isset($this->entityUpdates[$oid])) {
1304
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1305
        }
1306
1307 1105
        if (isset($this->entityDeletions[$oid])) {
1308 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1309
        }
1310 1105
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1311 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1312
        }
1313
1314 1105
        if (isset($this->entityInsertions[$oid])) {
1315 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1316
        }
1317
1318 1105
        $this->entityInsertions[$oid] = $entity;
1319
1320 1105
        if (isset($this->entityIdentifiers[$oid])) {
1321 286
            $this->addToIdentityMap($entity);
1322
        }
1323
1324 1105
        if ($entity instanceof NotifyPropertyChanged) {
1325 8
            $entity->addPropertyChangedListener($this);
1326
        }
1327 1105
    }
1328
1329
    /**
1330
     * Checks whether an entity is scheduled for insertion.
1331
     *
1332
     * @param object $entity
1333
     *
1334
     * @return boolean
1335
     */
1336 656
    public function isScheduledForInsert($entity)
1337
    {
1338 656
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1339
    }
1340
1341
    /**
1342
     * Schedules an entity for being updated.
1343
     *
1344
     * @param object $entity The entity to schedule for being updated.
1345
     *
1346
     * @return void
1347
     *
1348
     * @throws ORMInvalidArgumentException
1349
     */
1350 1
    public function scheduleForUpdate($entity)
1351
    {
1352 1
        $oid = spl_object_hash($entity);
1353
1354 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1355
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1356
        }
1357
1358 1
        if (isset($this->entityDeletions[$oid])) {
1359
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1360
        }
1361
1362 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1363 1
            $this->entityUpdates[$oid] = $entity;
1364
        }
1365 1
    }
1366
1367
    /**
1368
     * INTERNAL:
1369
     * Schedules an extra update that will be executed immediately after the
1370
     * regular entity updates within the currently running commit cycle.
1371
     *
1372
     * Extra updates for entities are stored as (entity, changeset) tuples.
1373
     *
1374
     * @ignore
1375
     *
1376
     * @param object $entity    The entity for which to schedule an extra update.
1377
     * @param array  $changeset The changeset of the entity (what to update).
1378
     *
1379
     * @return void
1380
     */
1381 46
    public function scheduleExtraUpdate($entity, array $changeset)
1382
    {
1383 46
        $oid         = spl_object_hash($entity);
1384 46
        $extraUpdate = [$entity, $changeset];
1385
1386 46
        if (isset($this->extraUpdates[$oid])) {
1387 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1388
1389 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1390
        }
1391
1392 46
        $this->extraUpdates[$oid] = $extraUpdate;
1393 46
    }
1394
1395
    /**
1396
     * Checks whether an entity is registered as dirty in the unit of work.
1397
     * Note: Is not very useful currently as dirty entities are only registered
1398
     * at commit time.
1399
     *
1400
     * @param object $entity
1401
     *
1402
     * @return boolean
1403
     */
1404
    public function isScheduledForUpdate($entity)
1405
    {
1406
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1407
    }
1408
1409
    /**
1410
     * Checks whether an entity is registered to be checked in the unit of work.
1411
     *
1412
     * @param object $entity
1413
     *
1414
     * @return boolean
1415
     */
1416 2
    public function isScheduledForDirtyCheck($entity)
1417
    {
1418 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...
1419
1420 2
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1421
    }
1422
1423
    /**
1424
     * INTERNAL:
1425
     * Schedules an entity for deletion.
1426
     *
1427
     * @param object $entity
1428
     *
1429
     * @return void
1430
     */
1431 67
    public function scheduleForDelete($entity)
1432
    {
1433 67
        $oid = spl_object_hash($entity);
1434
1435 67
        if (isset($this->entityInsertions[$oid])) {
1436 1
            if ($this->isInIdentityMap($entity)) {
1437
                $this->removeFromIdentityMap($entity);
1438
            }
1439
1440 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1441
1442 1
            return; // entity has not been persisted yet, so nothing more to do.
1443
        }
1444
1445 67
        if ( ! $this->isInIdentityMap($entity)) {
1446 1
            return;
1447
        }
1448
1449 66
        $this->removeFromIdentityMap($entity);
1450
1451 66
        unset($this->entityUpdates[$oid]);
1452
1453 66
        if ( ! isset($this->entityDeletions[$oid])) {
1454 66
            $this->entityDeletions[$oid] = $entity;
1455 66
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1456
        }
1457 66
    }
1458
1459
    /**
1460
     * Checks whether an entity is registered as removed/deleted with the unit
1461
     * of work.
1462
     *
1463
     * @param object $entity
1464
     *
1465
     * @return boolean
1466
     */
1467 17
    public function isScheduledForDelete($entity)
1468
    {
1469 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1470
    }
1471
1472
    /**
1473
     * Checks whether an entity is scheduled for insertion, update or deletion.
1474
     *
1475
     * @param object $entity
1476
     *
1477
     * @return boolean
1478
     */
1479
    public function isEntityScheduled($entity)
1480
    {
1481
        $oid = spl_object_hash($entity);
1482
1483
        return isset($this->entityInsertions[$oid])
1484
            || isset($this->entityUpdates[$oid])
1485
            || isset($this->entityDeletions[$oid]);
1486
    }
1487
1488
    /**
1489
     * INTERNAL:
1490
     * Registers an entity in the identity map.
1491
     * Note that entities in a hierarchy are registered with the class name of
1492
     * the root entity.
1493
     *
1494
     * @ignore
1495
     *
1496
     * @param object $entity The entity to register.
1497
     *
1498
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1499
     *                 the entity in question is already managed.
1500
     *
1501
     * @throws ORMInvalidArgumentException
1502
     */
1503 1169
    public function addToIdentityMap($entity)
1504
    {
1505 1169
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1506 1169
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1507
1508 1169
        if (empty($identifier) || in_array(null, $identifier, true)) {
1509 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...
1510
        }
1511
1512 1163
        $idHash    = implode(' ', $identifier);
1513 1163
        $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...
1514
1515 1163
        if (isset($this->identityMap[$className][$idHash])) {
1516 86
            return false;
1517
        }
1518
1519 1163
        $this->identityMap[$className][$idHash] = $entity;
1520
1521 1163
        return true;
1522
    }
1523
1524
    /**
1525
     * Gets the state of an entity with regard to the current unit of work.
1526
     *
1527
     * @param object   $entity
1528
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1529
     *                         This parameter can be set to improve performance of entity state detection
1530
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1531
     *                         is either known or does not matter for the caller of the method.
1532
     *
1533
     * @return int The entity state.
1534
     */
1535 1119
    public function getEntityState($entity, $assume = null)
1536
    {
1537 1119
        $oid = spl_object_hash($entity);
1538
1539 1119
        if (isset($this->entityStates[$oid])) {
1540 818
            return $this->entityStates[$oid];
1541
        }
1542
1543 1113
        if ($assume !== null) {
1544 1109
            return $assume;
1545
        }
1546
1547
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1548
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1549
        // the UoW does not hold references to such objects and the object hash can be reused.
1550
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1551 13
        $class = $this->em->getClassMetadata(get_class($entity));
1552 13
        $id    = $class->getIdentifierValues($entity);
1553
1554 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...
1555 5
            return self::STATE_NEW;
1556
        }
1557
1558 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...
1559 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1560
        }
1561
1562
        switch (true) {
1563 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

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

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

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

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

1938
                        throw ORMInvalidArgumentException::entityIsRemoved(/** @scrutinizer ignore-type */ $managedCopy, "merge");
Loading history...
1939
                    }
1940
                } else {
1941
                    // We need to fetch the managed copy in order to merge.
1942 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...
1943
                }
1944
1945 37
                if ($managedCopy === null) {
1946
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1947
                    // since the managed entity was not found.
1948 3
                    if ( ! $class->isIdentifierNatural()) {
1949 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1950 1
                            $class->getName(),
1951 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1952
                        );
1953
                    }
1954
1955 2
                    $managedCopy = $this->newInstance($class);
1956 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

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

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

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

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

3101
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeNone()):
Loading history...
3102 1090
                $persister = new BasicEntityPersister($this->em, $class);
3103 1090
                break;
3104
3105 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

3105
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeSingleTable()):
Loading history...
3106 226
                $persister = new SingleTablePersister($this->em, $class);
3107 226
                break;
3108
3109 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

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