Completed
Push — 2.6 ( a912fc...30a063 )
by Luís
13s
created

UnitOfWork::addToEntityIdentifiersAndEntityMap()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 3
dl 0
loc 19
ccs 10
cts 10
cp 1
crap 3
rs 9.4285
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 2461
    public function __construct(EntityManagerInterface $em)
303
    {
304 2461
        $this->em                         = $em;
305 2461
        $this->evm                        = $em->getEventManager();
306 2461
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2461
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2461
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2461
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2461
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2461
    }
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 1079
    public function commit($entity = null)
333
    {
334
        // Raise preFlush
335 1079
        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 1079
        if (null === $entity) {
341 1069
            $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) can never be false.
Loading history...
345 2
            foreach ($entity as $object) {
346 2
                $this->computeSingleEntityChangeSet($object);
347
            }
348
        }
349
350 1076
        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 173
                $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 136
                $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 1076
                $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 1072
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
363
364 1070
        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 1070
        $this->dispatchOnFlushEvent();
371
372
        // Now we need a commit order to maintain referential integrity
373 1070
        $commitOrder = $this->getCommitOrder();
374
375 1070
        $conn = $this->em->getConnection();
376 1070
        $conn->beginTransaction();
377
378
        try {
379
            // Collection deletions (deletions of complete collections)
380 1070
            foreach ($this->collectionDeletions as $collectionToDelete) {
381 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
382
            }
383
384 1070
            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 1066
                foreach ($commitOrder as $class) {
386 1066
                    $this->executeInserts($class);
387
                }
388
            }
389
390 1069
            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 121
                foreach ($commitOrder as $class) {
392 121
                    $this->executeUpdates($class);
393
                }
394
            }
395
396
            // Extra updates that were requested by persisters.
397 1065
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
398 44
                $this->executeExtraUpdates();
399
            }
400
401
            // Collection updates (deleteRows, updateRows, insertRows)
402 1065
            foreach ($this->collectionUpdates as $collectionToUpdate) {
403 542
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
404
            }
405
406
            // Entity deletions come last and need to be in reverse commit order
407 1065
            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 1065
            $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 1065
        $this->afterTransactionComplete();
424
425
        // Take new snapshots from visited collections
426 1065
        foreach ($this->visitedCollections as $coll) {
427 541
            $coll->takeSnapshot();
428
        }
429
430 1065
        $this->dispatchPostFlushEvent();
431
432 1064
        $this->postCommitCleanup($entity);
433 1064
    }
434
435
    /**
436
     * @param null|object|object[] $entity
437
     */
438 1064
    private function postCommitCleanup($entity) : void
439
    {
440 1064
        $this->entityInsertions =
441 1064
        $this->entityUpdates =
442 1064
        $this->entityDeletions =
443 1064
        $this->extraUpdates =
444 1064
        $this->collectionUpdates =
445 1064
        $this->nonCascadedNewDetectedEntities =
446 1064
        $this->collectionDeletions =
447 1064
        $this->visitedCollections =
448 1064
        $this->orphanRemovals = [];
449
450 1064
        if (null === $entity) {
451 1054
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
452
453 1054
            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 1078
    private function computeScheduleInsertsChangeSets()
475
    {
476 1078
        foreach ($this->entityInsertions as $entity) {
477 1070
            $class = $this->em->getClassMetadata(get_class($entity));
478
479 1070
            $this->computeChangeSet($class, $entity);
480
        }
481 1076
    }
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 44
    private function executeExtraUpdates()
535
    {
536 44
        foreach ($this->extraUpdates as $oid => $update) {
537 44
            list ($entity, $changeset) = $update;
538
539 44
            $this->entityChangeSets[$oid] = $changeset;
540 44
            $this->getEntityPersister(get_class($entity))->update($entity);
541
        }
542
543 44
        $this->extraUpdates = [];
544 44
    }
545
546
    /**
547
     * Gets the changeset for an entity.
548
     *
549
     * @param object $entity
550
     *
551
     * @return array
552
     */
553 1065
    public function & getEntityChangeSet($entity)
554
    {
555 1065
        $oid  = spl_object_hash($entity);
556 1065
        $data = [];
557
558 1065
        if (!isset($this->entityChangeSets[$oid])) {
559 4
            return $data;
560
        }
561
562 1065
        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 1080
    public function computeChangeSet(ClassMetadata $class, $entity)
600
    {
601 1080
        $oid = spl_object_hash($entity);
602
603 1080
        if (isset($this->readOnlyObjects[$oid])) {
604 2
            return;
605
        }
606
607 1080
        if ( ! $class->isInheritanceTypeNone()) {
608 337
            $class = $this->em->getClassMetadata(get_class($entity));
609
        }
610
611 1080
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
612
613 1080
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
614 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
615
        }
616
617 1080
        $actualData = [];
618
619 1080
        foreach ($class->reflFields as $name => $refProp) {
620 1080
            $value = $refProp->getValue($entity);
621
622 1080
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
623 813
                if ($value instanceof PersistentCollection) {
624 203
                    if ($value->getOwner() === $entity) {
625 203
                        continue;
626
                    }
627
628 5
                    $value = new ArrayCollection($value->getValues());
629
                }
630
631
                // If $value is not a Collection then use an ArrayCollection.
632 808
                if ( ! $value instanceof Collection) {
633 243
                    $value = new ArrayCollection($value);
634
                }
635
636 808
                $assoc = $class->associationMappings[$name];
637
638
                // Inject PersistentCollection
639 808
                $value = new PersistentCollection(
640 808
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
641
                );
642 808
                $value->setOwner($entity, $assoc);
643 808
                $value->setDirty( ! $value->isEmpty());
644
645 808
                $class->reflFields[$name]->setValue($entity, $value);
646
647 808
                $actualData[$name] = $value;
648
649 808
                continue;
650
            }
651
652 1080
            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...
653 1080
                $actualData[$name] = $value;
654
            }
655
        }
656
657 1080
        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 1076
            $this->originalEntityData[$oid] = $actualData;
661 1076
            $changeSet = [];
662
663 1076
            foreach ($actualData as $propName => $actualValue) {
664 1054
                if ( ! isset($class->associationMappings[$propName])) {
665 999
                    $changeSet[$propName] = [null, $actualValue];
666
667 999
                    continue;
668
                }
669
670 940
                $assoc = $class->associationMappings[$propName];
671
672 940
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
673 940
                    $changeSet[$propName] = [null, $actualValue];
674
                }
675
            }
676
677 1076
            $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 273
            $originalData           = $this->originalEntityData[$oid];
682 273
            $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 273
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
684
                ? $this->entityChangeSets[$oid]
685 273
                : [];
686
687 273
            foreach ($actualData as $propName => $actualValue) {
688
                // skip field, its a partially omitted one!
689 257
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
690 8
                    continue;
691
                }
692
693 257
                $orgValue = $originalData[$propName];
694
695
                // skip if value haven't changed
696 257
                if ($orgValue === $actualValue) {
697 240
                    continue;
698
                }
699
700
                // if regular field
701 117
                if ( ! isset($class->associationMappings[$propName])) {
702 62
                    if ($isChangeTrackingNotify) {
703
                        continue;
704
                    }
705
706 62
                    $changeSet[$propName] = [$orgValue, $actualValue];
707
708 62
                    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 273
            if ($changeSet) {
756 90
                $this->entityChangeSets[$oid]   = $changeSet;
757 90
                $this->originalEntityData[$oid] = $actualData;
758 90
                $this->entityUpdates[$oid]      = $entity;
759
            }
760
        }
761
762
        // Look for changes in associations of the entity
763 1080
        foreach ($class->associationMappings as $field => $assoc) {
764 940
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
765 660
                continue;
766
            }
767
768 911
            $this->computeAssociationChanges($assoc, $val);
769
770 903
            if ( ! isset($this->entityChangeSets[$oid]) &&
771 903
                $assoc['isOwningSide'] &&
772 903
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
773 903
                $val instanceof PersistentCollection &&
774 903
                $val->isDirty()) {
775
776 35
                $this->entityChangeSets[$oid]   = [];
777 35
                $this->originalEntityData[$oid] = $actualData;
778 903
                $this->entityUpdates[$oid]      = $entity;
779
            }
780
        }
781 1072
    }
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 1069
    public function computeChangeSets()
791
    {
792
        // Compute changes for INSERTed entities first. This must always happen.
793 1069
        $this->computeScheduleInsertsChangeSets();
794
795
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
796 1067
        foreach ($this->identityMap as $className => $entities) {
797 467
            $class = $this->em->getClassMetadata($className);
798
799
            // Skip class if instances are read-only
800 467
            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 466
                case ($class->isChangeTrackingDeferredImplicit()):
808 464
                    $entitiesToProcess = $entities;
809 464
                    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 466
            foreach ($entitiesToProcess as $entity) {
821
                // Ignore uninitialized proxy objects
822 446
                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 36
                    continue;
824
                }
825
826
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
827 445
                $oid = spl_object_hash($entity);
828
829 445
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
830 466
                    $this->computeChangeSet($class, $entity);
831
                }
832
            }
833
        }
834 1067
    }
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 911
    private function computeAssociationChanges($assoc, $value)
848
    {
849 911
        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 29
            return;
851
        }
852
853 910
        if ($value instanceof PersistentCollection && $value->isDirty()) {
854 546
            $coid = spl_object_hash($value);
855
856 546
            $this->collectionUpdates[$coid] = $value;
857 546
            $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 910
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
864 910
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
865
866 910
        foreach ($unwrappedValue as $key => $entry) {
867 750
            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 742
            $state = $this->getEntityState($entry, self::STATE_NEW);
872
873 742
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
874
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
875
            }
876
877
            switch ($state) {
878 742
                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 734
                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 734
                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 742
                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 902
    }
916
917
    /**
918
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
919
     * @param object                              $entity
920
     *
921
     * @return void
922
     */
923 1099
    private function persistNew($class, $entity)
924
    {
925 1099
        $oid    = spl_object_hash($entity);
926 1099
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
927
928 1099
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
929 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
930
        }
931
932 1099
        $idGen = $class->idGenerator;
933
934 1099
        if ( ! $idGen->isPostInsertGenerator()) {
935 287
            $idValue = $idGen->generate($this->em, $entity);
936
937 287
            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 287
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
946 284
                $this->entityIdentifiers[$oid] = $idValue;
947
            }
948
        }
949
950 1099
        $this->entityStates[$oid] = self::STATE_MANAGED;
951
952 1099
        $this->scheduleForInsert($entity);
953 1099
    }
954
955
    /**
956
     * @param mixed[] $idValue
957
     */
958 287
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
959
    {
960 287
        foreach ($idValue as $idField => $idFieldValue) {
961 287
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
962 287
                return true;
963
            }
964
        }
965
966 284
        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) {
1007 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1008 16
                && ($name !== $class->versionField)
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 1066
    private function executeInserts($class)
1048
    {
1049 1066
        $entities   = [];
1050 1066
        $className  = $class->name;
1051 1066
        $persister  = $this->getEntityPersister($className);
1052 1066
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1053
1054 1066
        $insertionsForClass = [];
1055
1056 1066
        foreach ($this->entityInsertions as $oid => $entity) {
1057
1058 1066
            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 903
                continue;
1060
            }
1061
1062 1066
            $insertionsForClass[$oid] = $entity;
1063
1064 1066
            $persister->addInsert($entity);
1065
1066 1066
            unset($this->entityInsertions[$oid]);
1067
1068 1066
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1069 1066
                $entities[] = $entity;
1070
            }
1071
        }
1072
1073 1066
        $postInsertIds = $persister->executeInserts();
1074
1075 1066
        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 967
            foreach ($postInsertIds as $postInsertId) {
1078 967
                $idField = $class->getSingleIdentifierFieldName();
1079 967
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1080
1081 967
                $entity  = $postInsertId['entity'];
1082 967
                $oid     = spl_object_hash($entity);
1083
1084 967
                $class->reflFields[$idField]->setValue($entity, $idValue);
1085
1086 967
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1087 967
                $this->entityStates[$oid] = self::STATE_MANAGED;
1088 967
                $this->originalEntityData[$oid][$idField] = $idValue;
1089
1090 967
                $this->addToIdentityMap($entity);
1091
            }
1092
        } else {
1093 810
            foreach ($insertionsForClass as $oid => $entity) {
1094 274
                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 274
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1098
                }
1099
            }
1100
        }
1101
1102 1066
        foreach ($entities as $entity) {
1103 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1104
        }
1105 1066
    }
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 121
    private function executeUpdates($class)
1139
    {
1140 121
        $className          = $class->name;
1141 121
        $persister          = $this->getEntityPersister($className);
1142 121
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1143 121
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1144
1145 121
        foreach ($this->entityUpdates as $oid => $entity) {
1146 121
            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 77
                continue;
1148
            }
1149
1150 121
            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 121
            if ( ! empty($this->entityChangeSets[$oid])) {
1157 87
                $persister->update($entity);
1158
            }
1159
1160 117
            unset($this->entityUpdates[$oid]);
1161
1162 117
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1163 117
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1164
            }
1165
        }
1166 117
    }
1167
1168
    /**
1169
     * Executes all entity deletions for entities of the specified type.
1170
     *
1171
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1172
     *
1173
     * @return void
1174
     */
1175 64
    private function executeDeletions($class)
1176
    {
1177 64
        $className  = $class->name;
1178 64
        $persister  = $this->getEntityPersister($className);
1179 64
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1180
1181 64
        foreach ($this->entityDeletions as $oid => $entity) {
1182 64
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1183 25
                continue;
1184
            }
1185
1186 64
            $persister->delete($entity);
1187
1188
            unset(
1189 64
                $this->entityDeletions[$oid],
1190 64
                $this->entityIdentifiers[$oid],
1191 64
                $this->originalEntityData[$oid],
1192 64
                $this->entityStates[$oid]
1193
            );
1194
1195
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1196
            // is obtained by a new entity because the old one went out of scope.
1197
            //$this->entityStates[$oid] = self::STATE_NEW;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

1558
            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...
1559
                // Check for a version field, if available, to avoid a db lookup.
1560 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...
1561 1
                    return ($class->getFieldValue($entity, $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...
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

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

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

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

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

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

1957
                    $this->mergeEntityStateIntoManagedCopy($entity, /** @scrutinizer ignore-type */ $managedCopy);
Loading history...
1958
                }
1959
            }
1960
1961 40
            $visited[$oid] = $managedCopy; // mark visited
1962
1963 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1964
                $this->scheduleForDirtyCheck($entity);
1965
            }
1966
        }
1967
1968 41
        if ($prevManagedCopy !== null) {
1969 6
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy can also be of type true; however, parameter $managedCopy of Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

3096
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeNone()):
Loading history...
3097 1085
                $persister = new BasicEntityPersister($this->em, $class);
3098 1085
                break;
3099
3100 393
            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

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

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