Failed Conditions
Pull Request — master (#7146)
by
unknown
64:29 queued 01:48
created

UnitOfWork::hasMissingIdsWhichAreForeignKeys()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 4
nc 3
nop 2
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 4
rs 9.2
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
    public function __construct(EntityManagerInterface $em)
303 2247
    {
304
        $this->em                         = $em;
305 2247
        $this->evm                        = $em->getEventManager();
306 2247
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2247
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2247
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2247
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2247
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2247
    }
312 2247
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 1012
     *
330
     * @throws \Exception
331
     */
332 1012
    public function commit($entity = null)
333 2
    {
334
        // Raise preFlush
335
        if ($this->evm->hasListeners(Events::preFlush)) {
336 1012
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
337
        }
338 1010
339 157
        // Compute changes done since last commit.
340 122
        if (null === $entity) {
341 36
            $this->computeChangeSets();
342 33
        } elseif (is_object($entity)) {
343 1010
            $this->computeSingleEntityChangeSet($entity);
344 21
        } elseif (is_array($entity)) {
0 ignored issues
show
introduced by
The condition is_array($entity) is always true.
Loading history...
345 21
            foreach ($entity as $object) {
346
                $this->computeSingleEntityChangeSet($object);
347 21
            }
348
        }
349
350 1005
        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
                $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 1003
                $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 15
                $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 15
                $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
                $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
            $this->dispatchOnFlushEvent();
357
            $this->dispatchPostFlushEvent();
358 1003
359
            return; // Nothing to do.
360
        }
361 1003
362
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
363 1003
364 1003
        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
            foreach ($this->orphanRemovals as $orphan) {
366
                $this->remove($orphan);
367
            }
368 1003
        }
369 19
370
        $this->dispatchOnFlushEvent();
371
372 1003
        // Now we need a commit order to maintain referential integrity
373 999
        $commitOrder = $this->getCommitOrder();
374 999
375
        $conn = $this->em->getConnection();
376
        $conn->beginTransaction();
377
378 1002
        try {
379 111
            // Collection deletions (deletions of complete collections)
380 111
            foreach ($this->collectionDeletions as $collectionToDelete) {
381
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
382
            }
383
384
            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 998
                foreach ($commitOrder as $class) {
386 29
                    $this->executeInserts($class);
387
                }
388
            }
389
390 998
            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 526
                foreach ($commitOrder as $class) {
392
                    $this->executeUpdates($class);
393
                }
394
            }
395 998
396 60
            // Extra updates that were requested by persisters.
397 60
            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 33
                $this->executeExtraUpdates();
399
            }
400
401 60
            // Collection updates (deleteRows, updateRows, insertRows)
402
            foreach ($this->collectionUpdates as $collectionToUpdate) {
403
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
404
            }
405 998
406 10
            // Entity deletions come last and need to be in reverse commit order
407 10
            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 10
                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
                    $this->executeDeletions($commitOrder[$i]);
410 10
                }
411
            }
412 10
413
            $conn->commit();
414
        } catch (Throwable $e) {
415 998
            $this->em->close();
416
            $conn->rollBack();
417
418 998
            $this->afterTransactionRolledBack();
419 525
420
            throw $e;
421
        }
422 998
423
        $this->afterTransactionComplete();
424
425 997
        // Take new snapshots from visited collections
426 997
        foreach ($this->visitedCollections as $coll) {
427 997
            $coll->takeSnapshot();
428 997
        }
429 997
430 997
        $this->dispatchPostFlushEvent();
431 997
432 997
        $this->postCommitCleanup($entity);
433 997
    }
434 997
435 997
    /**
436
     * @param null|object|object[] $entity
437
     */
438
    private function postCommitCleanup($entity) : void
439
    {
440 1012
        $this->entityInsertions =
441
        $this->entityUpdates =
442 1012
        $this->entityDeletions =
443 1003
        $this->extraUpdates =
444
        $this->collectionUpdates =
445 1003
        $this->nonCascadedNewDetectedEntities =
446
        $this->collectionDeletions =
447 1010
        $this->visitedCollections =
448
        $this->orphanRemovals = [];
449
450
        if (null === $entity) {
451
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
452 29
453
            return;
454 29
        }
455 29
456
        $entities = \is_object($entity)
457 29
            ? [$entity]
458
            : $entity;
459
460
        foreach ($entities as $object) {
461
            $oid = \spl_object_hash($object);
462 29
463
            $this->clearEntityChangeSet($oid);
464
465 29
            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 29
        }
467
    }
468
469
    /**
470
     * Computes the changesets of all entities scheduled for insertion.
471
     *
472
     * @return void
473
     */
474
    private function computeScheduleInsertsChangeSets()
475 998
    {
476
        foreach ($this->entityInsertions as $entity) {
477 998
            $class = $this->em->getClassMetadata(get_class($entity));
478 998
479
            $this->computeChangeSet($class, $entity);
480 998
        }
481 2
    }
482
483
    /**
484 998
     * 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
    private function computeSingleEntityChangeSet($entity)
498
    {
499
        $state = $this->getEntityState($entity);
500
501
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
502
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
503
        }
504
505
        $class = $this->em->getClassMetadata(get_class($entity));
506
507
        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
            $this->persist($entity);
509
        }
510
511
        // Compute changes for INSERTed entities first. This must always happen even in this case.
512
        $this->computeScheduleInsertsChangeSets();
513
514
        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
        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 1013
            return;
521
        }
522 1013
523
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
524 1013
        $oid = spl_object_hash($entity);
525 2
526
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
527
            $this->computeChangeSet($class, $entity);
528 1013
        }
529 333
    }
530
531
    /**
532 1013
     * Executes any extra updates that have been scheduled.
533
     */
534 1013
    private function executeExtraUpdates()
535 130
    {
536
        foreach ($this->extraUpdates as $oid => $update) {
537
            list ($entity, $changeset) = $update;
538 1013
539
            $this->entityChangeSets[$oid] = $changeset;
540 1013
            $this->getEntityPersister(get_class($entity))->update($entity);
541 1013
        }
542
543 1013
        $this->extraUpdates = [];
544 773
    }
545 186
546
    /**
547
     * Gets the changeset for an entity.
548 770
     *
549
     * @param object $entity
550 770
     *
551
     * @return array
552 770
     */
553
    public function & getEntityChangeSet($entity)
554 770
    {
555
        $oid  = spl_object_hash($entity);
556
        $data = [];
557 1013
558 1013
        if (!isset($this->entityChangeSets[$oid])) {
559 1013
            return $data;
560 1013
        }
561 1013
562 1013
        return $this->entityChangeSets[$oid];
563
    }
564
565
    /**
566 1013
     * Computes the changes that happened to a single entity.
567
     *
568
     * Modifies/populates the following properties:
569 1009
     *
570 1009
     * {@link _originalEntityData}
571
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
572 1009
     * then it was not fetched from the database and therefore we have no original
573 989
     * entity data yet. All of the current entity data is stored as the original entity data.
574
     *
575 989
     * {@link _entityChangeSets}
576 989
     * The changes detected on all properties of the entity are stored there.
577 989
     * 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 1009
     * {@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 254
     *
586 254
     * {@link _collectionDeletions}
587 254
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
588
     * then this collection is marked for deletion.
589 254
     *
590
     * @ignore
591 254
     *
592
     * @internal Don't call from the outside.
593 243
     *
594 40
     * @param ClassMetadata $class  The class descriptor of the entity.
595
     * @param object        $entity The entity for which to compute the changes.
596
     *
597 243
     * @return void
598
     */
599
    public function computeChangeSet(ClassMetadata $class, $entity)
600 243
    {
601 226
        $oid = spl_object_hash($entity);
602
603
        if (isset($this->readOnlyObjects[$oid])) {
604 109
            return;
605
        }
606
607
        if ( ! $class->isInheritanceTypeNone()) {
608
            $class = $this->em->getClassMetadata(get_class($entity));
609 109
        }
610 8
611
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
612 8
613
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
614 8
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
615
        }
616
617
        $actualData = [];
618
619
        foreach ($class->reflFields as $name => $refProp) {
620
            $value = $refProp->getValue($entity);
621
622
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
623
                if ($value instanceof PersistentCollection) {
624
                    if ($value->getOwner() === $entity) {
625
                        continue;
626
                    }
627
628 109
                    $value = new ArrayCollection($value->getValues());
629 58
                }
630
631
                // If $value is not a Collection then use an ArrayCollection.
632
                if ( ! $value instanceof Collection) {
633
                    $value = new ArrayCollection($value);
634
                }
635 58
636 58
                $assoc = $class->associationMappings[$name];
637
638 57
                // Inject PersistentCollection
639 46
                $value = new PersistentCollection(
640 20
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
641
                );
642
                $value->setOwner($entity, $assoc);
643 46
                $value->setDirty( ! $value->isEmpty());
644 4
645
                $class->reflFields[$name]->setValue($entity, $value);
646
647 46
                $actualData[$name] = $value;
648
649 12
                continue;
650
            }
651 9
652
            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 8
                $actualData[$name] = $value;
654 8
            }
655
        }
656 8
657
        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 9
            $this->originalEntityData[$oid] = $actualData;
661
            $changeSet = [];
662 109
663
            foreach ($actualData as $propName => $actualValue) {
664
                if ( ! isset($class->associationMappings[$propName])) {
665
                    $changeSet[$propName] = [null, $actualValue];
666
667 254
                    continue;
668 84
                }
669 84
670 84
                $assoc = $class->associationMappings[$propName];
671
672
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
673
                    $changeSet[$propName] = [null, $actualValue];
674
                }
675 1013
            }
676 1013
677 1013
            $this->entityChangeSets[$oid] = $changeSet;
678
        } else {
679
            // Entity is "fully" MANAGED: it was already fully persisted before
680 884
            // and we have a copy of the original data
681
            $originalData           = $this->originalEntityData[$oid];
682 884
            $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 623
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
684
                ? $this->entityChangeSets[$oid]
685
                : [];
686 860
687
            foreach ($actualData as $propName => $actualValue) {
688 852
                // skip field, its a partially omitted one!
689 852
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
690 852
                    continue;
691 852
                }
692 852
693 31
                $orgValue = $originalData[$propName];
694 31
695 852
                // skip if value haven't changed
696
                if ($orgValue === $actualValue) {
697
                    continue;
698 1005
                }
699
700
                // if regular field
701
                if ( ! isset($class->associationMappings[$propName])) {
702
                    if ($isChangeTrackingNotify) {
703
                        continue;
704
                    }
705 1012
706
                    $changeSet[$propName] = [$orgValue, $actualValue];
707
708 1012
                    continue;
709
                }
710
711 1010
                $assoc = $class->associationMappings[$propName];
712 443
713
                // Persistent collection was exchanged with the "originally"
714
                // created one. This can only mean it was cloned and replaced
715 443
                // on another entity.
716 1
                if ($actualValue instanceof PersistentCollection) {
717
                    $owner = $actualValue->getOwner();
718
                    if ($owner === null) { // cloned
719
                        $actualValue->setOwner($entity, $assoc);
720
                    } else if ($owner !== $entity) { // no clone, we have to fix
721
                        if (!$actualValue->isInitialized()) {
722 442
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
723 440
                        }
724 440
                        $newValue = clone $actualValue;
725
                        $newValue->setOwner($entity, $assoc);
726 3
                        $class->reflFields[$propName]->setValue($entity, $newValue);
727 3
                    }
728 3
                }
729
730
                if ($orgValue instanceof PersistentCollection) {
731 1
                    // A PersistentCollection was de-referenced, so delete it.
732
                    $coid = spl_object_hash($orgValue);
733
734 442
                    if (isset($this->collectionDeletions[$coid])) {
735
                        continue;
736 422
                    }
737 38
738
                    $this->collectionDeletions[$coid] = $orgValue;
739
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
740
741 420
                    continue;
742
                }
743 420
744 442
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
745
                    if ($assoc['isOwningSide']) {
746
                        $changeSet[$propName] = [$orgValue, $actualValue];
747
                    }
748 1010
749
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
750
                        $this->scheduleOrphanRemoval($orgValue);
751
                    }
752
                }
753
            }
754
755
            if ($changeSet) {
756
                $this->entityChangeSets[$oid]   = $changeSet;
757
                $this->originalEntityData[$oid] = $actualData;
758
                $this->entityUpdates[$oid]      = $entity;
759 860
            }
760
        }
761 860
762 31
        // Look for changes in associations of the entity
763
        foreach ($class->associationMappings as $field => $assoc) {
764
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
765 859
                continue;
766 529
            }
767
768 529
            $this->computeAssociationChanges($assoc, $val);
769 529
770
            if ( ! isset($this->entityChangeSets[$oid]) &&
771
                $assoc['isOwningSide'] &&
772
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
773
                $val instanceof PersistentCollection &&
774
                $val->isDirty()) {
775 859
776 859
                $this->entityChangeSets[$oid]   = [];
777 859
                $this->originalEntityData[$oid] = $actualData;
778
                $this->entityUpdates[$oid]      = $entity;
779 859
            }
780 716
        }
781 8
    }
782
783
    /**
784 708
     * 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 708
     * temporarily for access by the persisters, until the UoW commit is finished.
787
     *
788
     * @return void
789
     */
790
    public function computeChangeSets()
791
    {
792
        // Compute changes for INSERTed entities first. This must always happen.
793
        $this->computeScheduleInsertsChangeSets();
794
795
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
796 708
        foreach ($this->identityMap as $className => $entities) {
797 41
            $class = $this->em->getClassMetadata($className);
798 5
799
            // Skip class if instances are read-only
800 5
            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
                continue;
802
            }
803 37
804 37
            // 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 37
            switch (true) {
807
                case ($class->isChangeTrackingDeferredImplicit()):
808 701
                    $entitiesToProcess = $entities;
809
                    break;
810
811 4
                case (isset($this->scheduledForSynchronization[$className])):
812 3
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
813
                    break;
814 4
815
                default:
816 701
                    $entitiesToProcess = [];
817
818
            }
819
820
            foreach ($entitiesToProcess as $entity) {
821
                // Ignore uninitialized proxy objects
822 708
                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
                    continue;
824
                }
825
826
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
827 851
                $oid = spl_object_hash($entity);
828
829
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
830
                    $this->computeChangeSet($class, $entity);
831
                }
832
            }
833 1024
        }
834
    }
835 1024
836 1024
    /**
837
     * Computes the changes of an association.
838 1024
     *
839 132
     * @param array $assoc The association mapping.
840
     * @param mixed $value The value of the association.
841
     *
842 1024
     * @throws ORMInvalidArgumentException
843 1024
     * @throws ORMException
844 1024
     *
845
     * @return void
846 1024
     */
847 221
    private function computeAssociationChanges($assoc, $value)
848 221
    {
849
        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
            return;
851 1024
        }
852
853 1024
        if ($value instanceof PersistentCollection && $value->isDirty()) {
854 1024
            $coid = spl_object_hash($value);
855
856
            $this->collectionUpdates[$coid] = $value;
857
            $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
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
864
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
865
866
        foreach ($unwrappedValue as $key => $entry) {
867
            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
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
869
            }
870
871
            $state = $this->getEntityState($entry, self::STATE_NEW);
872
873 15
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
874
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
875 15
            }
876
877 15
            switch ($state) {
878
                case self::STATE_NEW:
879
                    if ( ! $assoc['isCascadePersist']) {
880
                        /*
881
                         * For now just record the details, because this may
882 15
                         * 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 15
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
887 3
888
                        break;
889
                    }
890 15
891
                    $this->persistNew($targetClass, $entry);
892 15
                    $this->computeChangeSet($targetClass, $entry);
893
894 15
                    break;
895
896
                case self::STATE_REMOVED:
897
                    // Consume the $value as array (it's either an array or an ArrayAccess)
898 15
                    // and remove the element from Collection.
899 15
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
900 15
                        unset($value[$key]);
901 15
                    }
902 15
                    break;
903
904
                case self::STATE_DETACHED:
905 15
                    // 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 11
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
908 9
                    break;
909 15
910
                default:
911
                    // MANAGED associated entities are already taken into account
912
                    // during changeset calculation anyway, since they are in the identity map.
913 15
            }
914
        }
915
    }
916
917 15
    /**
918 15
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
919
     * @param object                              $entity
920 15
     *
921 15
     * @return void
922
     */
923 15
    private function persistNew($class, $entity)
924 15
    {
925
        $oid    = spl_object_hash($entity);
926
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
927
928 15
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
929 7
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
930 6
        }
931 1
932 1
        $idGen = $class->idGenerator;
933 1
934
        if ( ! $idGen->isPostInsertGenerator()) {
935 7
            $idValue = $idGen->generate($this->em, $entity);
936
937 15
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
938
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
939
940
                $class->setIdentifierValues($entity, $idValue);
941
            }
942 999
943
            // Some identifiers may be foreign keys to new entities.
944 999
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
945 999
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
946 999
                $this->entityIdentifiers[$oid] = $idValue;
947 999
            }
948
        }
949 999
950 999
        $this->entityStates[$oid] = self::STATE_MANAGED;
951 852
952
        $this->scheduleForInsert($entity);
953
    }
954 999
955
    /**
956 998
     * @param mixed[] $idValue
957
     */
958 935
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
959 935
    {
960
        foreach ($idValue as $idField => $idFieldValue) {
961 935
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
962 935
                return true;
963 935
            }
964
        }
965 935
966
        return false;
967
    }
968 998
969
    /**
970 998
     * INTERNAL:
971 128
     * Computes the changeset of an individual entity, independently of the
972
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
973 998
     *
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 999
     * 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 111
     * @return void
984
     *
985 111
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
986 111
     */
987 111
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
988 111
    {
989
        $oid = spl_object_hash($entity);
990 111
991 111
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
992 71
            throw ORMInvalidArgumentException::entityNotManaged($entity);
993
        }
994
995 111
        // skip if change tracking is "NOTIFY"
996 12
        if ($class->isChangeTrackingNotify()) {
997
            return;
998 12
        }
999
1000
        if ( ! $class->isInheritanceTypeNone()) {
1001 111
            $class = $this->em->getClassMetadata(get_class($entity));
1002
        }
1003
1004
        $actualData = [];
1005 81
1006
        foreach ($class->reflFields as $name => $refProp) {
1007
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
1008 107
                && ($name !== $class->versionField)
1009
                && ! $class->isCollectionValuedAssociation($name)) {
1010 107
                $actualData[$name] = $refProp->getValue($entity);
1011 107
            }
1012
        }
1013
1014 107
        if ( ! isset($this->originalEntityData[$oid])) {
1015
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
1016
        }
1017
1018
        $originalData = $this->originalEntityData[$oid];
1019
        $changeSet = [];
1020
1021 60
        foreach ($actualData as $propName => $actualValue) {
1022
            $orgValue = $originalData[$propName] ?? null;
1023 60
1024 60
            if ($orgValue !== $actualValue) {
1025 60
                $changeSet[$propName] = [$orgValue, $actualValue];
1026
            }
1027 60
        }
1028 60
1029 24
        if ($changeSet) {
1030
            if (isset($this->entityChangeSets[$oid])) {
1031
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1032 60
            } else if ( ! isset($this->entityInsertions[$oid])) {
1033
                $this->entityChangeSets[$oid] = $changeSet;
1034
                $this->entityUpdates[$oid]    = $entity;
1035 60
            }
1036 60
            $this->originalEntityData[$oid] = $actualData;
1037 60
        }
1038 60
    }
1039
1040
    /**
1041
     * Executes all entity insertions for entities of the specified type.
1042
     *
1043
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1044 60
     *
1045 57
     * @return void
1046
     */
1047 57
    private function executeInserts($class)
1048 50
    {
1049
        $entities   = [];
1050
        $className  = $class->name;
1051
        $persister  = $this->getEntityPersister($className);
1052 60
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1053 9
1054
        $insertionsForClass = [];
1055 60
1056
        foreach ($this->entityInsertions as $oid => $entity) {
1057
1058 59
            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
                continue;
1060
            }
1061
1062
            $insertionsForClass[$oid] = $entity;
1063
1064
            $persister->addInsert($entity);
1065 1003
1066
            unset($this->entityInsertions[$oid]);
1067 1003
1068
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1069
                $entities[] = $entity;
1070
            }
1071
        }
1072
1073
        $postInsertIds = $persister->executeInserts();
1074 1003
1075
        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 1003
            // Persister returned post-insert IDs
1077 1003
            foreach ($postInsertIds as $postInsertId) {
1078
                $idField = $class->getSingleIdentifierFieldName();
1079 1003
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1080 633
1081
                $entity  = $postInsertId['entity'];
1082
                $oid     = spl_object_hash($entity);
1083 1003
1084
                $class->reflFields[$idField]->setValue($entity, $idValue);
1085 1003
1086
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1087
                $this->entityStates[$oid] = self::STATE_MANAGED;
1088
                $this->originalEntityData[$oid][$idField] = $idValue;
1089 1003
1090 1003
                $this->addToIdentityMap($entity);
1091 1003
            }
1092 1003
        } else {
1093
            foreach ($insertionsForClass as $oid => $entity) {
1094
                if (! isset($this->entityIdentifiers[$oid])) {
1095 832
                    //entity was not added to identity map because some identifiers are foreign keys to new entities.
1096
                    //add it now
1097 832
                    $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
1098 635
                }
1099
            }
1100 635
        }
1101
1102
        foreach ($entities as $entity) {
1103 832
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1104 832
        }
1105 832
    }
1106 832
1107 832
    /**
1108
     * @param object $entity
1109
     */
1110 832
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
1111
    {
1112
        $identifier = [];
1113 832
1114 827
        foreach ($class->getIdentifierFieldNames() as $idField) {
1115
            $value = $class->getFieldValue($entity, $idField);
1116
1117 225
            if (isset($class->associationMappings[$idField])) {
1118 225
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1119
                $value = $this->getSingleIdentifierValue($value);
1120 225
            }
1121 199
1122
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1123 199
        }
1124
1125
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1126 225
        $this->entityIdentifiers[$oid] = $identifier;
1127
1128
        $this->addToIdentityMap($entity);
1129
    }
1130
1131 1003
    /**
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
    private function executeUpdates($class)
1139
    {
1140
        $className          = $class->name;
1141
        $persister          = $this->getEntityPersister($className);
1142
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1143 1025
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1144
1145 1025
        foreach ($this->entityUpdates as $oid => $entity) {
1146
            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 1025
                continue;
1148
            }
1149
1150
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1151 1025
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1152 1
1153
                $this->recomputeSingleEntityChangeSet($class, $entity);
1154 1025
            }
1155 1
1156
            if ( ! empty($this->entityChangeSets[$oid])) {
1157
                $persister->update($entity);
1158 1025
            }
1159 1
1160
            unset($this->entityUpdates[$oid]);
1161
1162 1025
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1163
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1164 1025
            }
1165 221
        }
1166
    }
1167
1168 1025
    /**
1169 5
     * Executes all entity deletions for entities of the specified type.
1170
     *
1171 1025
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1172
     *
1173
     * @return void
1174
     */
1175
    private function executeDeletions($class)
1176
    {
1177
        $className  = $class->name;
1178
        $persister  = $this->getEntityPersister($className);
1179
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1180 619
1181
        foreach ($this->entityDeletions as $oid => $entity) {
1182 619
            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
                continue;
1184
            }
1185
1186
            $persister->delete($entity);
1187
1188
            unset(
1189
                $this->entityDeletions[$oid],
1190
                $this->entityIdentifiers[$oid],
1191
                $this->originalEntityData[$oid],
1192 1
                $this->entityStates[$oid]
1193
            );
1194 1
1195
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1196 1
            // 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
            if ( ! $class->isIdentifierNatural()) {
1199
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1200 1
            }
1201
1202
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1203
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1204 1
            }
1205 1
        }
1206
    }
1207 1
1208
    /**
1209
     * Gets the commit order.
1210
     *
1211
     * @param array|null $entityChangeSet
1212
     *
1213
     * @return array
1214
     */
1215
    private function getCommitOrder(array $entityChangeSet = null)
1216
    {
1217
        if ($entityChangeSet === null) {
1218
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1219
        }
1220
1221
        $calc = $this->getCommitOrderCalculator();
1222 29
1223
        // See if there are any new classes in the changeset, that are not in the
1224 29
        // commit order graph yet (don't have a node).
1225 29
        // 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 29
        // are not yet available.
1228 1
        $newNodes = [];
1229
1230 1
        foreach ($entityChangeSet as $entity) {
1231
            $class = $this->em->getClassMetadata(get_class($entity));
1232
1233 29
            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 29
                continue;
1235
            }
1236
1237
            $calc->addNode($class->name, $class);
1238
1239
            $newNodes[] = $class;
1240
        }
1241
1242
        // Calculate dependencies for new nodes
1243
        while ($class = array_pop($newNodes)) {
1244
            foreach ($class->associationMappings as $assoc) {
1245
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1246
                    continue;
1247
                }
1248
1249
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1250
1251
                if ( ! $calc->hasNode($targetClass->name)) {
1252
                    $calc->addNode($targetClass->name, $targetClass);
1253 1
1254
                    $newNodes[] = $targetClass;
1255 1
                }
1256
1257 1
                $joinColumns = reset($assoc['joinColumns']);
1258
1259
                $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
                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
                    continue;
1264
                }
1265
1266 63
                foreach ($targetClass->subClasses as $subClassName) {
1267
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1268 63
1269
                    if ( ! $calc->hasNode($subClassName)) {
1270 63
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1271 1
1272
                        $newNodes[] = $targetSubClass;
1273
                    }
1274
1275 1
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1276
                }
1277 1
            }
1278
        }
1279
1280 63
        return $calc->sort();
1281 1
    }
1282
1283
    /**
1284 62
     * 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 62
     *
1287
     * @param object $entity The entity to schedule for insertion.
1288 62
     *
1289 62
     * @return void
1290 62
     *
1291
     * @throws ORMInvalidArgumentException
1292 62
     * @throws \InvalidArgumentException
1293
     */
1294
    public function scheduleForInsert($entity)
1295
    {
1296
        $oid = spl_object_hash($entity);
1297
1298
        if (isset($this->entityUpdates[$oid])) {
1299
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1300
        }
1301
1302 13
        if (isset($this->entityDeletions[$oid])) {
1303
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1304 13
        }
1305
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1306
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1307
        }
1308
1309
        if (isset($this->entityInsertions[$oid])) {
1310
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1311
        }
1312
1313
        $this->entityInsertions[$oid] = $entity;
1314
1315
        if (isset($this->entityIdentifiers[$oid])) {
1316
            $this->addToIdentityMap($entity);
1317
        }
1318
1319
        if ($entity instanceof NotifyPropertyChanged) {
1320
            $entity->addPropertyChangedListener($this);
1321
        }
1322
    }
1323
1324
    /**
1325
     * Checks whether an entity is scheduled for insertion.
1326
     *
1327
     * @param object $entity
1328
     *
1329
     * @return boolean
1330
     */
1331
    public function isScheduledForInsert($entity)
1332
    {
1333
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1334
    }
1335
1336
    /**
1337
     * Schedules an entity for being updated.
1338 1093
     *
1339
     * @param object $entity The entity to schedule for being updated.
1340 1093
     *
1341 1093
     * @return void
1342
     *
1343 1093
     * @throws ORMInvalidArgumentException
1344 6
     */
1345
    public function scheduleForUpdate($entity)
1346
    {
1347 1087
        $oid = spl_object_hash($entity);
1348 1087
1349
        if ( ! isset($this->entityIdentifiers[$oid])) {
1350 1087
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1351 31
        }
1352
1353
        if (isset($this->entityDeletions[$oid])) {
1354 1087
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1355
        }
1356 1087
1357
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1358
            $this->entityUpdates[$oid] = $entity;
1359
        }
1360
    }
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 1033
     *
1371
     * @param object $entity    The entity for which to schedule an extra update.
1372 1033
     * @param array  $changeset The changeset of the entity (what to update).
1373
     *
1374 1033
     * @return void
1375 748
     */
1376
    public function scheduleExtraUpdate($entity, array $changeset)
1377
    {
1378 1028
        $oid         = spl_object_hash($entity);
1379 1025
        $extraUpdate = [$entity, $changeset];
1380
1381
        if (isset($this->extraUpdates[$oid])) {
1382
            list(, $changeset2) = $this->extraUpdates[$oid];
1383
1384
            $extraUpdate = [$entity, $changeset + $changeset2];
1385
        }
1386 8
1387 8
        $this->extraUpdates[$oid] = $extraUpdate;
1388 8
    }
1389
1390 8
    /**
1391 3
     * 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 6
     *
1395
     * @param object $entity
1396 6
     *
1397 5
     * @return boolean
1398 6
     */
1399
    public function isScheduledForUpdate($entity)
1400
    {
1401 5
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1402 1
    }
1403
1404 1
    /**
1405
     * Checks whether an entity is registered to be checked in the unit of work.
1406
     *
1407
     * @param object $entity
1408 4
     *
1409 1
     * @return boolean
1410
     */
1411
    public function isScheduledForDirtyCheck($entity)
1412
    {
1413 4
        $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
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1416
    }
1417 4
1418
    /**
1419
     * INTERNAL:
1420 1
     * Schedules an entity for deletion.
1421 1
     *
1422 1
     * @param object $entity
1423
     *
1424
     * @return void
1425
     */
1426
    public function scheduleForDelete($entity)
1427
    {
1428
        $oid = spl_object_hash($entity);
1429
1430
        if (isset($this->entityInsertions[$oid])) {
1431
            if ($this->isInIdentityMap($entity)) {
1432
                $this->removeFromIdentityMap($entity);
1433
            }
1434
1435
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1436
1437
            return; // entity has not been persisted yet, so nothing more to do.
1438
        }
1439
1440 1
        if ( ! $this->isInIdentityMap($entity)) {
1441
            return;
1442
        }
1443
1444
        $this->removeFromIdentityMap($entity);
1445
1446
        unset($this->entityUpdates[$oid]);
1447
1448
        if ( ! isset($this->entityDeletions[$oid])) {
1449
            $this->entityDeletions[$oid] = $entity;
1450
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1451
        }
1452
    }
1453
1454
    /**
1455
     * Checks whether an entity is registered as removed/deleted with the unit
1456 62
     * of work.
1457
     *
1458 62
     * @param object $entity
1459 62
     *
1460 62
     * @return boolean
1461
     */
1462 62
    public function isScheduledForDelete($entity)
1463
    {
1464
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1465
    }
1466 62
1467
    /**
1468 62
     * Checks whether an entity is scheduled for insertion, update or deletion.
1469 62
     *
1470
     * @param object $entity
1471
     *
1472
     * @return boolean
1473 62
     */
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 6
     *
1491
     * @param object $entity The entity to register.
1492 6
     *
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
    public function addToIdentityMap($entity)
1499
    {
1500
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1501
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1502
1503
        if (empty($identifier) || in_array(null, $identifier, true)) {
1504
            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
        $idHash    = implode(' ', $identifier);
1508
        $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
        if (isset($this->identityMap[$className][$idHash])) {
1511
            return false;
1512
        }
1513
1514
        $this->identityMap[$className][$idHash] = $entity;
1515
1516
        return true;
1517
    }
1518
1519
    /**
1520
     * Gets the state of an entity with regard to the current unit of work.
1521 147
     *
1522
     * @param object   $entity
1523 147
     * @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 147
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1526 23
     *                         is either known or does not matter for the caller of the method.
1527
     *
1528
     * @return int The entity state.
1529 133
     */
1530 133
    public function getEntityState($entity, $assume = null)
1531
    {
1532 133
        $oid = spl_object_hash($entity);
1533
1534
        if (isset($this->entityStates[$oid])) {
1535
            return $this->entityStates[$oid];
1536
        }
1537
1538
        if ($assume !== null) {
1539
            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
        $class = $this->em->getClassMetadata(get_class($entity));
1547
        $id    = $class->getIdentifierValues($entity);
1548
1549
        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
            return self::STATE_NEW;
1551
        }
1552
1553
        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
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1555
        }
1556 1025
1557
        switch (true) {
1558 1025
            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 1025
                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 1018
                    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
                        : self::STATE_NEW;
1564
                }
1565
1566
                // Last try before db lookup: check the identity map.
1567
                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
                    return self::STATE_DETACHED;
1569
                }
1570
1571
                // db lookup
1572
                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 1025
1576
                return self::STATE_NEW;
1577 1025
1578
            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 1025
                // if we have a pre insert generator we can't be sure that having an id
1580 109
                // really means that the entity exists. We have to verify this through
1581
                // the last resort: a db lookup
1582
1583 1025
                // Last try before db lookup: check the identity map.
1584
                if ($this->tryGetById($id, $class->rootEntityName)) {
1585 1025
                    return self::STATE_DETACHED;
1586
                }
1587
1588
                // db lookup
1589
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1590
                    return self::STATE_DETACHED;
1591 1025
                }
1592
1593
                return self::STATE_NEW;
1594 1025
1595
            default:
1596 219
                return self::STATE_DETACHED;
1597 2
        }
1598
    }
1599 219
1600
    /**
1601 1025
     * INTERNAL:
1602 1024
     * Removes an entity from the identity map. This effectively detaches the
1603 1024
     * entity from the persistence management of Doctrine.
1604
     *
1605 1
     * @ignore
1606
     *
1607 1
     * @param object $entity
1608 1
     *
1609
     * @return boolean
1610 1
     *
1611 1
     * @throws ORMInvalidArgumentException
1612
     */
1613
    public function removeFromIdentityMap($entity)
1614
    {
1615
        $oid           = spl_object_hash($entity);
1616
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1617
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1618
1619
        if ($idHash === '') {
1620
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1621
        }
1622
1623 1025
        $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 1018
1625
        if (isset($this->identityMap[$className][$idHash])) {
1626
            unset($this->identityMap[$className][$idHash]);
1627
            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 62
            return true;
1632
        }
1633 62
1634
        return false;
1635 62
    }
1636 62
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
    public function getByIdHash($idHash, $rootClassName)
1649
    {
1650 62
        return $this->identityMap[$rootClassName][$idHash];
1651
    }
1652 62
1653
    /**
1654 62
     * INTERNAL:
1655 1
     * Tries to get an entity by its identifier hash. If no entity is found for
1656
     * the given hash, FALSE is returned.
1657
     *
1658 62
     * @ignore
1659
     *
1660
     * @param mixed  $idHash        (must be possible to cast it to string)
1661
     * @param string $rootClassName
1662 62
     *
1663
     * @return object|bool The found entity or FALSE.
1664 62
     */
1665 62
    public function tryGetByIdHash($idHash, $rootClassName)
1666
    {
1667
        $stringIdHash = (string) $idHash;
1668 62
1669 62
        return isset($this->identityMap[$rootClassName][$stringIdHash])
1670
            ? $this->identityMap[$rootClassName][$stringIdHash]
1671 2
            : false;
1672
    }
1673 62
1674 62
    /**
1675
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1676 62
     *
1677 8
     * @param object $entity
1678
     *
1679
     * @return boolean
1680 62
     */
1681 62
    public function isInIdentityMap($entity)
1682
    {
1683
        $oid = spl_object_hash($entity);
1684
1685
        if (empty($this->entityIdentifiers[$oid])) {
1686
            return false;
1687
        }
1688
1689
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1690 62
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1691
1692
        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 15
     *
1701
     * @param string $idHash
1702 15
     * @param string $rootClassName
1703
     *
1704 15
     * @return boolean
1705 15
     */
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 15
     *
1716
     * @return void
1717 15
     */
1718
    public function persist($entity)
1719 15
    {
1720
        $visited = [];
1721
1722
        $this->doPersist($entity, $visited);
1723 15
    }
1724
1725 15
    /**
1726
     * Persists an entity as part of the current unit of work.
1727 15
     *
1728
     * This method is internally called during persist() cascades as it tracks
1729
     * the already visited entities to prevent infinite recursions.
1730
     *
1731 15
     * @param object $entity  The entity to persist.
1732 15
     * @param array  $visited The already visited entities.
1733 15
     *
1734
     * @return void
1735
     *
1736 15
     * @throws ORMInvalidArgumentException
1737 15
     * @throws UnexpectedValueException
1738
     */
1739
    private function doPersist($entity, array &$visited)
1740
    {
1741
        $oid = spl_object_hash($entity);
1742
1743
        if (isset($visited[$oid])) {
1744
            return; // Prevent infinite recursion
1745 15
        }
1746
1747 15
        $visited[$oid] = $entity; // Mark visited
1748
1749 15
        $class = $this->em->getClassMetadata(get_class($entity));
1750 15
1751 15
        // 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 4
        // lets us avoid some database lookups for entities with natural identifiers.
1755
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1756
1757 4
        switch ($entityState) {
1758
            case self::STATE_MANAGED:
1759 4
                // Nothing to do, except if policy is "deferred explicit"
1760
                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
                    $this->scheduleForDirtyCheck($entity);
1762
                }
1763
                break;
1764 4
1765
            case self::STATE_NEW:
1766
                $this->persistNew($class, $entity);
1767 4
                break;
1768
1769
            case self::STATE_REMOVED:
1770
                // Entity becomes managed again
1771
                unset($this->entityDeletions[$oid]);
1772
                $this->addToIdentityMap($entity);
1773 4
1774
                $this->entityStates[$oid] = self::STATE_MANAGED;
1775
                break;
1776
1777 15
            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
        $this->cascadePersist($entity, $visited);
1786
    }
1787 1025
1788
    /**
1789 1025
     * Deletes an entity as part of the current unit of work.
1790
     *
1791 1025
     * @param object $entity The entity to remove.
1792
     *
1793 1
     * @return void
1794
     */
1795
    public function remove($entity)
1796 1025
    {
1797 1025
        $visited = [];
1798 1025
1799
        $this->doRemove($entity, $visited);
1800
    }
1801
1802 647
    /**
1803 647
     * 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 647
     * the already visited entities to prevent infinite recursions.
1807
     *
1808 13
     * @param object $entity  The entity to delete.
1809
     * @param array  $visited The map of the already visited entities.
1810
     *
1811 647
     * @return void
1812 584
     *
1813 542
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1814 3
     * @throws UnexpectedValueException
1815 3
     */
1816 3
    private function doRemove($entity, array &$visited)
1817 3
    {
1818
        $oid = spl_object_hash($entity);
1819
1820
        if (isset($visited[$oid])) {
1821 539
            return; // Prevent infinite recursion
1822 283
        }
1823
1824
        $visited[$oid] = $entity; // mark visited
1825 539
1826
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1827 573
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1828 241
        $this->cascadeRemove($entity, $visited);
1829 4
1830 4
        $class       = $this->em->getClassMetadata(get_class($entity));
1831 4
        $entityState = $this->getEntityState($entity);
1832 4
1833
        switch ($entityState) {
1834
            case self::STATE_NEW:
1835
            case self::STATE_REMOVED:
1836 237
                // nothing to do
1837 237
                break;
1838
1839 641
            case self::STATE_MANAGED:
1840
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1841
1842
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1843 1018
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1844
                }
1845
1846
                $this->scheduleForDelete($entity);
1847
                break;
1848
1849
            case self::STATE_DETACHED:
1850
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1851 62
            default:
1852
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1853 62
        }
1854 62
1855
    }
1856 62
1857 62
    /**
1858 62
     * Merges the state of the given detached entity into this UnitOfWork.
1859
     *
1860
     * @param object $entity
1861 25
     *
1862 6
     * @return object The managed copy of the entity.
1863
     *
1864
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1865 25
     *         attribute and the version check against the managed copy fails.
1866
     *
1867
     * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
1868 25
     */
1869 18
    public function merge($entity)
1870
    {
1871 20
        $visited = [];
1872 10
1873
        return $this->doMerge($entity, $visited);
1874 20
    }
1875
1876 18
    /**
1877 7
     * Executes a merge operation on an entity.
1878 7
     *
1879
     * @param object      $entity
1880 25
     * @param array       $visited
1881
     * @param object|null $prevManagedCopy
1882
     * @param array|null  $assoc
1883
     *
1884
     * @return object The managed copy of the entity.
1885 62
     *
1886 16
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1887
     *         attribute and the version check against the managed copy fails.
1888 62
     * @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
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
1892
    {
1893
        $oid = spl_object_hash($entity);
1894
1895
        if (isset($visited[$oid])) {
1896
            $managedCopy = $visited[$oid];
1897
1898
            if ($prevManagedCopy !== null) {
1899
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1900
            }
1901
1902 10
            return $managedCopy;
1903
        }
1904 10
1905 1
        $class = $this->em->getClassMetadata(get_class($entity));
1906
1907
        // First we assume DETACHED, although it can still be NEW but we can avoid
1908 9
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1909 1
        // we need to fetch it from the db anyway in order to merge.
1910
        // MANAGED entities are ignored by the merge operation.
1911
        $managedCopy = $entity;
1912 8
1913
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1914
            // Try to look the entity up in the identity map.
1915 8
            $id = $class->getIdentifierValues($entity);
1916 6
1917 2
            // If there is no ID, it is actually NEW.
1918
            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
                $managedCopy = $this->newInstance($class);
1920 4
1921
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1922
                $this->persistNew($class, $managedCopy);
1923
            } else {
1924 4
                $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 1
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1926
                    : $id;
1927
1928 4
                $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 4
                if ($managedCopy) {
1931 2
                    // We have the entity in-memory already, just make sure its not removed.
1932
                    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
                        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 2
                    }
1935
                } else {
1936 2
                    // We need to fetch the managed copy in order to merge.
1937 2
                    $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 1
                }
1939 2
1940 2
                if ($managedCopy === null) {
1941
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1942
                    // since the managed entity was not found.
1943
                    if ( ! $class->isIdentifierNatural()) {
1944
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1945
                            $class->getName(),
1946
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1947
                        );
1948
                    }
1949
1950
                    $managedCopy = $this->newInstance($class);
1951
                    $class->setIdentifierValues($managedCopy, $id);
0 ignored issues
show
Bug introduced by
The method setIdentifierValues() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

1951
                    $class->/** @scrutinizer ignore-call */ 
1952
                            setIdentifierValues($managedCopy, $id);
Loading history...
1952
1953
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1954 2
                    $this->persistNew($class, $managedCopy);
1955
                } else {
1956
                    $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
                    $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 1199
            }
1960
1961 1199
            $visited[$oid] = $managedCopy; // mark visited
1962 1199
1963 1199
            if ($class->isChangeTrackingDeferredExplicit()) {
1964 1199
                $this->scheduleForDirtyCheck($entity);
1965 1199
            }
1966 1199
        }
1967 1199
1968 1199
        if ($prevManagedCopy !== null) {
1969 1199
            $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 1199
        }
1971 1199
1972 1199
        // Mark the managed copy visited as well
1973 1199
        $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 1199
1975 1199
        $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 1199
1977 1199
        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 1199
    }
1979 1199
1980 1199
    /**
1981
     * @param ClassMetadata $class
1982
     * @param object        $entity
1983
     * @param object        $managedCopy
1984
     *
1985
     * @return void
1986
     *
1987
     * @throws OptimisticLockException
1988
     */
1989
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
1990
    {
1991
        if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
1992 16
            return;
1993
        }
1994 16
1995 16
        $reflField          = $class->reflFields[$class->versionField];
1996
        $managedCopyVersion = $reflField->getValue($managedCopy);
1997
        $entityVersion      = $reflField->getValue($entity);
1998
1999
        // Throw exception if versions don't match.
2000
        if ($managedCopyVersion == $entityVersion) {
2001
            return;
2002
        }
2003
2004
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
2005 111
    }
2006
2007 111
    /**
2008 111
     * 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 22
    private function isLoaded($entity)
2015
    {
2016 22
        return !($entity instanceof Proxy) || $entity->__isInitialized();
2017
    }
2018
2019
    /**
2020 22
     * Sets/adds associated managed copies into the previous entity's association field
2021
     *
2022 22
     * @param object $entity
2023 22
     * @param array  $association
2024
     * @param object $previousManagedCopy
2025
     * @param object $managedCopy
2026
     *
2027
     * @return void
2028 8
     */
2029
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
2030 8
    {
2031
        $assocField = $association['fieldName'];
2032
        $prevClass  = $this->em->getClassMetadata(get_class($previousManagedCopy));
2033
2034
        if ($association['type'] & ClassMetadata::TO_ONE) {
2035
            $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
            return;
2038
        }
2039
2040
        $value   = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
2041
        $value[] = $managedCopy;
2042 662
2043
        if ($association['type'] == ClassMetadata::ONE_TO_MANY) {
2044 662
            $class = $this->em->getClassMetadata(get_class($entity));
2045
2046 662
            $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
2047 5
        }
2048
    }
2049
2050 662
    /**
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
    public function detach($entity)
2059
    {
2060
        $visited = [];
2061
2062
        $this->doDetach($entity, $visited);
2063
    }
2064
2065
    /**
2066
     * Executes a detach operation on the given entity.
2067
     *
2068
     * @param object  $entity
2069 799
     * @param array   $visited
2070
     * @param boolean $noCascade if true, don't cascade detach operation.
2071 799
     *
2072 799
     * @return void
2073 799
     */
2074
    private function doDetach($entity, array &$visited, $noCascade = false)
2075 799
    {
2076 305
        $oid = spl_object_hash($entity);
2077 305
2078
        if (isset($visited[$oid])) {
2079 305
            return; // Prevent infinite recursion
2080 65
        }
2081 65
2082 65
        $visited[$oid] = $entity; // mark visited
2083 65
2084
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
2085
            case self::STATE_MANAGED:
2086
                if ($this->isInIdentityMap($entity)) {
2087 5
                    $this->removeFromIdentityMap($entity);
2088
                }
2089
2090
                unset(
2091 305
                    $this->entityInsertions[$oid],
2092 21
                    $this->entityUpdates[$oid],
2093
                    $this->entityDeletions[$oid],
2094 21
                    $this->entityIdentifiers[$oid],
2095 21
                    $this->entityStates[$oid],
2096
                    $this->originalEntityData[$oid]
2097
                );
2098 291
                break;
2099 291
            case self::STATE_NEW:
2100 229
            case self::STATE_DETACHED:
2101
                return;
2102
        }
2103
2104
        if ( ! $noCascade) {
2105 103
            $this->cascadeDetach($entity, $visited);
2106 3
        }
2107
    }
2108
2109 103
    /**
2110
     * Refreshes the state of the given entity from the database, overwriting
2111 659
     * any local, unpersisted changes.
2112 659
     *
2113
     * @param object $entity The entity to refresh.
2114 659
     *
2115 659
     * @return void
2116 659
     *
2117
     * @throws InvalidArgumentException If the entity is not MANAGED.
2118 659
     */
2119
    public function refresh($entity)
2120
    {
2121 692
        $visited = [];
2122 3
2123
        $this->doRefresh($entity, $visited);
2124
    }
2125 692
2126 692
    /**
2127
     * Executes a refresh operation on an entity.
2128 692
     *
2129 692
     * @param object $entity  The entity to refresh.
2130
     * @param array  $visited The already visited entities during cascades.
2131
     *
2132
     * @return void
2133
     *
2134 692
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
2135
     */
2136 692
    private function doRefresh($entity, array &$visited)
2137
    {
2138
        $oid = spl_object_hash($entity);
2139
2140
        if (isset($visited[$oid])) {
2141 692
            return; // Prevent infinite recursion
2142 34
        }
2143
2144
        $visited[$oid] = $entity; // mark visited
2145 658
2146 658
        $class = $this->em->getClassMetadata(get_class($entity));
2147 658
2148
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
2149
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2150
        }
2151 565
2152 243
        $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
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2154
            $entity
2155 542
        );
2156 542
2157
        $this->cascadeRefresh($entity, $visited);
2158 542
    }
2159
2160 464
    /**
2161 464
     * Cascades a refresh operation to associated entities.
2162
     *
2163
     * @param object $entity
2164
     * @param array  $visited
2165 464
     *
2166
     * @return void
2167
     */
2168 464
    private function cascadeRefresh($entity, array &$visited)
2169
    {
2170
        $class = $this->em->getClassMetadata(get_class($entity));
2171
2172
        $associationMappings = array_filter(
2173
            $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
        foreach ($associationMappings as $assoc) {
2178
            $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 464
2180
            switch (true) {
2181 464
                case ($relatedEntities instanceof PersistentCollection):
2182
                    // Unwrap so that foreach() does not initialize
2183 464
                    $relatedEntities = $relatedEntities->unwrap();
2184
                    // break; is commented intentionally!
2185 464
2186 4
                case ($relatedEntities instanceof Collection):
2187 4
                case (is_array($relatedEntities)):
2188
                    foreach ($relatedEntities as $relatedEntity) {
2189
                        $this->doRefresh($relatedEntity, $visited);
2190 464
                    }
2191
                    break;
2192 464
2193
                case ($relatedEntities !== null):
2194
                    $this->doRefresh($relatedEntities, $visited);
2195 469
                    break;
2196
2197 67
                default:
2198 67
                    // Do nothing
2199 3
            }
2200
        }
2201 3
    }
2202 3
2203
    /**
2204 3
     * Cascades a detach operation to associated entities.
2205
     *
2206 3
     * @param object $entity
2207
     * @param array  $visited
2208
     *
2209
     * @return void
2210 64
     */
2211
    private function cascadeDetach($entity, array &$visited)
2212 64
    {
2213
        $class = $this->em->getClassMetadata(get_class($entity));
2214 64
2215
        $associationMappings = array_filter(
2216
            $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 469
        );
2219 38
2220
        foreach ($associationMappings as $assoc) {
2221 38
            $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 38
            switch (true) {
2224
                case ($relatedEntities instanceof PersistentCollection):
2225
                    // Unwrap so that foreach() does not initialize
2226 462
                    $relatedEntities = $relatedEntities->unwrap();
2227
                    // break; is commented intentionally!
2228
2229 462
                case ($relatedEntities instanceof Collection):
2230
                case (is_array($relatedEntities)):
2231 462
                    foreach ($relatedEntities as $relatedEntity) {
2232 462
                        $this->doDetach($relatedEntity, $visited);
2233 462
                    }
2234
                    break;
2235 462
2236
                case ($relatedEntities !== null):
2237 266
                    $this->doDetach($relatedEntities, $visited);
2238
                    break;
2239 266
2240
                default:
2241
                    // Do nothing
2242 284
            }
2243
        }
2244
    }
2245 462
2246
    /**
2247 266
     * Cascades a merge operation to associated entities.
2248 266
     *
2249
     * @param object $entity
2250 266
     * @param object $managedCopy
2251
     * @param array  $visited
2252
     *
2253
     * @return void
2254 284
     */
2255 281
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2256
    {
2257
        $class = $this->em->getClassMetadata(get_class($entity));
2258
2259
        $associationMappings = array_filter(
2260
            $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 284
        );
2263
2264
        foreach ($associationMappings as $assoc) {
2265 284
            $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 168
2267
            if ($relatedEntities instanceof Collection) {
2268
                if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2269
                    continue;
2270
                }
2271 168
2272 168
                if ($relatedEntities instanceof PersistentCollection) {
2273 168
                    // Unwrap so that foreach() does not initialize
2274 168
                    $relatedEntities = $relatedEntities->unwrap();
2275 168
                }
2276
2277
                foreach ($relatedEntities as $relatedEntity) {
2278
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2279
                }
2280 168
            } else if ($relatedEntities !== null) {
2281
                $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2282 190
            }
2283
        }
2284
    }
2285
2286 29
    /**
2287 29
     * Cascades the save operation to associated entities.
2288 29
     *
2289
     * @param object $entity
2290
     * @param array  $visited
2291
     *
2292 163
     * @return void
2293
     */
2294 163
    private function cascadePersist($entity, array &$visited)
2295 163
    {
2296 163
        $class = $this->em->getClassMetadata(get_class($entity));
2297 163
2298
        $associationMappings = array_filter(
2299
            $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 163
2303 156
        foreach ($associationMappings as $assoc) {
2304 156
            $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 7
                case ($relatedEntities instanceof PersistentCollection):
2308
                    // Unwrap so that foreach() does not initialize
2309 7
                    $relatedEntities = $relatedEntities->unwrap();
2310
                    // break; is commented intentionally!
2311 7
2312 7
                case ($relatedEntities instanceof Collection):
2313
                case (is_array($relatedEntities)):
2314
                    if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
2315
                        throw ORMInvalidArgumentException::invalidAssociation(
2316
                            $this->em->getClassMetadata($assoc['targetEntity']),
2317
                            $assoc,
2318
                            $relatedEntities
2319
                        );
2320
                    }
2321
2322
                    foreach ($relatedEntities as $relatedEntity) {
2323 163
                        $this->doPersist($relatedEntity, $visited);
2324
                    }
2325 163
2326
                    break;
2327
2328 284
                case ($relatedEntities !== null):
2329 284
                    if (! $relatedEntities instanceof $assoc['targetEntity']) {
2330
                        throw ORMInvalidArgumentException::invalidAssociation(
2331 284
                            $this->em->getClassMetadata($assoc['targetEntity']),
2332 284
                            $assoc,
2333
                            $relatedEntities
2334
                        );
2335
                    }
2336 56
2337 284
                    $this->doPersist($relatedEntities, $visited);
2338
                    break;
2339
2340 19
                default:
2341
                    // Do nothing
2342 284
            }
2343
        }
2344
    }
2345
2346
    /**
2347 658
     * Cascades the delete operation to associated entities.
2348
     *
2349 658
     * @param object $entity
2350
     * @param array  $visited
2351
     *
2352 855
     * @return void
2353
     */
2354 855
    private function cascadeRemove($entity, array &$visited)
2355 855
    {
2356
        $class = $this->em->getClassMetadata(get_class($entity));
2357
2358
        $associationMappings = array_filter(
2359 7
            $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 7
            function ($assoc) { return $assoc['isCascadeRemove']; }
2361
        );
2362 7
2363 7
        $entitiesToCascade = [];
2364
2365
        foreach ($associationMappings as $assoc) {
2366
            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 7
                $entity->__load();
2368
            }
2369 7
2370 7
            $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 7
                case ($relatedEntities instanceof Collection):
2374
                case (is_array($relatedEntities)):
2375
                    // If its a PersistentCollection initialization is intended! No unwrap!
2376
                    foreach ($relatedEntities as $relatedEntity) {
2377
                        $entitiesToCascade[] = $relatedEntity;
2378
                    }
2379
                    break;
2380
2381
                case ($relatedEntities !== null):
2382 139
                    $entitiesToCascade[] = $relatedEntities;
2383
                    break;
2384 139
2385 139
                default:
2386
                    // Do nothing
2387 139
            }
2388 74
        }
2389
2390 75
        foreach ($entitiesToCascade as $relatedEntity) {
2391
            $this->doRemove($relatedEntity, $visited);
2392
        }
2393 139
    }
2394 139
2395
    /**
2396
     * Acquire a lock on the given entity.
2397
     *
2398
     * @param object $entity
2399
     * @param int    $lockMode
2400
     * @param int    $lockVersion
2401 1
     *
2402
     * @return void
2403 1
     *
2404
     * @throws ORMInvalidArgumentException
2405
     * @throws TransactionRequiredException
2406
     * @throws OptimisticLockException
2407
     */
2408
    public function lock($entity, $lockMode, $lockVersion = null)
2409
    {
2410
        if ($entity === null) {
2411
            throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock().");
2412
        }
2413
2414 121
        if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2415
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2416 121
        }
2417
2418 121
        $class = $this->em->getClassMetadata(get_class($entity));
2419
2420
        switch (true) {
2421
            case LockMode::OPTIMISTIC === $lockMode:
2422
                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
                    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
                if ($lockVersion === null) {
2427
                    return;
2428
                }
2429
2430
                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
                    $entity->__load();
2432
                }
2433
2434
                $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
                if ($entityVersion != $lockVersion) {
2437
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2438
                }
2439
2440
                break;
2441
2442 301
            case LockMode::NONE === $lockMode:
2443
            case LockMode::PESSIMISTIC_READ === $lockMode:
2444 301
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2445 301
                if (!$this->em->getConnection()->isTransactionActive()) {
2446
                    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 568
            default:
2458
                // Do nothing
2459 568
        }
2460
    }
2461
2462
    /**
2463
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
2464
     *
2465
     * @return \Doctrine\ORM\Internal\CommitOrderCalculator
2466
     */
2467
    public function getCommitOrderCalculator()
2468
    {
2469
        return new Internal\CommitOrderCalculator();
2470
    }
2471 70
2472
    /**
2473 70
     * Clears the UnitOfWork.
2474 70
     *
2475
     * @param string|null $entityName if given, only entities of this type will get detached.
2476 70
     *
2477
     * @return void
2478
     *
2479
     * @throws ORMInvalidArgumentException if an invalid entity name is given
2480 70
     */
2481 58
    public function clear($entityName = null)
2482 70
    {
2483
        if ($entityName === null) {
2484 70
            $this->identityMap =
2485
            $this->entityIdentifiers =
2486
            $this->originalEntityData =
2487
            $this->entityChangeSets =
2488
            $this->entityStates =
2489
            $this->scheduledForSynchronization =
2490
            $this->entityInsertions =
2491
            $this->entityUpdates =
2492
            $this->entityDeletions =
2493
            $this->nonCascadedNewDetectedEntities =
2494
            $this->collectionDeletions =
2495
            $this->collectionUpdates =
2496
            $this->extraUpdates =
2497 538
            $this->readOnlyObjects =
2498
            $this->visitedCollections =
2499 538
            $this->orphanRemovals = [];
2500
        } else {
2501 538
            $this->clearIdentityMapForEntityName($entityName);
2502
            $this->clearEntityInsertionsForEntityName($entityName);
2503
        }
2504
2505
        if ($this->evm->hasListeners(Events::onClear)) {
2506
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
2507
        }
2508
    }
2509 5
2510
    /**
2511 5
     * INTERNAL:
2512
     * Schedules an orphaned entity for removal. The remove() operation will be
2513 5
     * invoked on that entity at the beginning of the next commit of this
2514 5
     * UnitOfWork.
2515
     *
2516
     * @ignore
2517
     *
2518
     * @param object $entity
2519
     *
2520
     * @return void
2521
     */
2522
    public function scheduleOrphanRemoval($entity)
2523
    {
2524
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2525
    }
2526
2527
    /**
2528
     * INTERNAL:
2529
     * Cancels a previously scheduled orphan removal.
2530
     *
2531
     * @ignore
2532 1
     *
2533
     * @param object $entity
2534 1
     *
2535
     * @return void
2536
     */
2537
    public function cancelOrphanRemoval($entity)
2538
    {
2539
        unset($this->orphanRemovals[spl_object_hash($entity)]);
2540
    }
2541
2542
    /**
2543
     * INTERNAL:
2544 1082
     * Schedules a complete collection for removal when this UnitOfWork commits.
2545
     *
2546 1082
     * @param PersistentCollection $coll
2547 1025
     *
2548
     * @return void
2549
     */
2550 1082
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2551
    {
2552
        $coid = spl_object_hash($coll);
2553 1082
2554 1039
        // TODO: if $coll is already scheduled for recreation ... what to do?
2555 1039
        // Just remove $coll from the scheduled recreations?
2556
        unset($this->collectionUpdates[$coid]);
2557 385
2558 223
        $this->collectionDeletions[$coid] = $coll;
2559 223
    }
2560
2561 355
    /**
2562 355
     * @param PersistentCollection $coll
2563 355
     *
2564
     * @return bool
2565
     */
2566
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2567
    {
2568
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2569 1082
    }
2570 131
2571 131
    /**
2572 131
     * @param ClassMetadata $class
2573 131
     *
2574
     * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
2575
     */
2576 1082
    private function newInstance($class)
2577
    {
2578 1082
        $entity = $class->newInstance();
2579
2580
        if ($entity instanceof \Doctrine\Common\Persistence\ObjectManagerAware) {
2581
            $entity->injectObjectManager($this->em, $class);
2582
        }
2583
2584
        return $entity;
2585
    }
2586 565
2587
    /**
2588 565
     * INTERNAL:
2589 78
     * Creates an entity. Used for reconstitution of persistent entities.
2590 565
     *
2591
     * Internal note: Highly performance-sensitive method.
2592 565
     *
2593 431
     * @ignore
2594
     *
2595
     * @param string $className The name of the entity class.
2596 565
     * @param array  $data      The data for the entity.
2597 402
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the entity.
2598 565
     *
2599
     * @return object The managed entity instance.
2600 565
     *
2601 77
     * @todo Rename: getOrCreateEntity
2602 77
     */
2603 77
    public function createEntity($className, array $data, &$hints = [])
2604 77
    {
2605
        $class = $this->em->getClassMetadata($className);
2606
2607 565
        $id = $this->identifierFlattener->flattenIdentifier($class, $data);
2608
        $idHash = implode(' ', $id);
2609 565
2610
        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
            $entity = $this->identityMap[$class->rootEntityName][$idHash];
2612
            $oid = spl_object_hash($entity);
2613
2614
            if (
2615
                isset($hints[Query::HINT_REFRESH])
2616
                && isset($hints[Query::HINT_REFRESH_ENTITY])
2617
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
2618
                && $unmanagedProxy instanceof Proxy
2619
                && $this->isIdentifierEquals($unmanagedProxy, $entity)
2620 291
            ) {
2621
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
2622 291
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
2623 291
                // refreshed object may be anything
2624
2625 291
                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 291
                    $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 291
                }
2628
2629 291
                return $unmanagedProxy;
2630
            }
2631 285
2632 1
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2633
                $entity->__setInitialized(true);
2634 285
2635
                if ($entity instanceof NotifyPropertyChanged) {
2636
                    $entity->addPropertyChangedListener($this);
2637
                }
2638
            } else {
2639
                if ( ! isset($hints[Query::HINT_REFRESH])
2640
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2641
                    return $entity;
2642
                }
2643
            }
2644
2645
            // inject ObjectManager upon refresh.
2646
            if ($entity instanceof ObjectManagerAware) {
2647
                $entity->injectObjectManager($this->em, $class);
2648
            }
2649
2650
            $this->originalEntityData[$oid] = $data;
2651
        } else {
2652
            $entity = $this->newInstance($class);
2653
            $oid    = spl_object_hash($entity);
2654
2655
            $this->entityIdentifiers[$oid]  = $id;
2656
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2657 3
            $this->originalEntityData[$oid] = $data;
2658
2659 3
            $this->identityMap[$class->rootEntityName][$idHash] = $entity;
2660
2661 3
            if ($entity instanceof NotifyPropertyChanged) {
2662
                $entity->addPropertyChangedListener($this);
2663
            }
2664
        }
2665 3
2666
        foreach ($data as $field => $value) {
2667
            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 3
                $class->reflFields[$field]->setValue($entity, $value);
2669
            }
2670 3
        }
2671 3
2672
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2673 3
        unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2674
2675
        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 2
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2681
            return $entity;
2682 2
        }
2683
2684
        foreach ($class->associationMappings as $field => $assoc) {
2685
            // Check if the association is not among the fetch-joined associations already.
2686
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2687
                continue;
2688
            }
2689
2690 3
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2691
2692 3
            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
                case ($assoc['type'] & ClassMetadata::TO_ONE):
2694
                    if ( ! $assoc['isOwningSide']) {
2695
2696
                        // use the given entity association
2697
                        if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2698
2699
                            $this->originalEntityData[$oid][$field] = $data[$field];
2700 1
2701
                            $class->reflFields[$field]->setValue($entity, $data[$field]);
2702 1
                            $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
2703
2704
                            continue 2;
2705
                        }
2706
2707
                        // Inverse side of x-to-one can never be lazy
2708
                        $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
2709
2710 1
                        continue 2;
2711
                    }
2712 1
2713
                    // use the entity association
2714
                    if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2715
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2716
                        $this->originalEntityData[$oid][$field] = $data[$field];
2717
2718
                        continue;
2719
                    }
2720
2721
                    $associatedId = [];
2722
2723
                    // TODO: Is this even computed right in all cases of composite keys?
2724
                    foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
2725
                        $joinColumnValue = $data[$srcColumn] ?? null;
2726
2727
                        if ($joinColumnValue !== null) {
2728
                            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
                                $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 2
                            } else {
2731
                                $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 2
                            }
2733 1
                        } elseif ($targetClass->containsForeignIdentifier
2734
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2735 1
                        ) {
2736
                            // the missing key is part of target's entity primary key
2737
                            $associatedId = [];
2738 1
                            break;
2739 1
                        }
2740
                    }
2741 1
2742
                    if ( ! $associatedId) {
2743
                        // Foreign key is NULL
2744
                        $class->reflFields[$field]->setValue($entity, null);
2745
                        $this->originalEntityData[$oid][$field] = null;
2746
2747
                        continue;
2748
                    }
2749
2750
                    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
                        $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
                    $relatedIdHash = implode(' ', $associatedId);
2759
2760
                    switch (true) {
2761
                        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
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2763
2764
                            // If this is an uninitialized proxy, we are deferring eager loads,
2765 6
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2766
                            // then we can append this entity for eager loading!
2767 6
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2768 1
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2769
                                !$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
                                $newValue instanceof Proxy &&
2771 5
                                $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 5
2773
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2774
                            }
2775
2776
                            break;
2777
2778
                        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
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2783 3
                            break;
2784
2785 3
                        default:
2786
                            switch (true) {
2787
                                // We are negating the condition here. Other cases will assume it is valid!
2788
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2789 3
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2790
                                    break;
2791
2792
                                // Deferred eager load only works for single identifier classes
2793
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
2794
                                    // TODO: Is there a faster approach?
2795
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2796
2797 998
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2798 95
                                    break;
2799 998
2800 998
                                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 10
                            $newValueOid = spl_object_hash($newValue);
2808
                            $this->entityIdentifiers[$newValueOid] = $associatedId;
2809 10
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2810 10
2811
                            if (
2812
                                $newValue instanceof NotifyPropertyChanged &&
2813
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
2814
                            ) {
2815 1003
                                $newValue->addPropertyChangedListener($this);
2816
                            }
2817 1003
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2818 908
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2819
                            break;
2820
                    }
2821 95
2822 95
                    $this->originalEntityData[$oid][$field] = $newValue;
2823 95
                    $class->reflFields[$field]->setValue($entity, $newValue);
2824
2825
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2826 95
                        $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
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2828 1008
                    }
2829
2830 1008
                    break;
2831 4
2832
                default:
2833 1008
                    // Ignore if its a cached collection
2834
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
2835 1003
                        break;
2836
                    }
2837 1003
2838 5
                    // use the given collection
2839
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2840 1002
2841
                        $data[$field]->setOwner($entity, $assoc);
2842
2843
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2844
                        $this->originalEntityData[$oid][$field] = $data[$field];
2845
2846
                        break;
2847
                    }
2848
2849
                    // Inject collection
2850 17
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2851
                    $pColl->setOwner($entity, $assoc);
2852 17
                    $pColl->setInitialized(false);
2853
2854
                    $reflField = $class->reflFields[$field];
2855
                    $reflField->setValue($entity, $pColl);
2856 17
2857 17
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2858
                        $this->loadCollection($pColl);
2859 17
                        $pColl->takeSnapshot();
2860 11
                    }
2861
2862
                    $this->originalEntityData[$oid][$field] = $pColl;
2863 6
                    break;
2864
            }
2865 6
        }
2866 6
2867
        // defer invoking of postLoad event to hydration complete step
2868 6
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2869 6
2870 6
        return $entity;
2871 6
    }
2872
2873 6
    /**
2874
     * @return void
2875
     */
2876
    public function triggerEagerLoads()
2877
    {
2878
        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 1005
            return;
2880
        }
2881 1005
2882
        // avoid infinite recursion
2883 1005
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2884
        $this->eagerLoadingEntities = [];
2885 1005
2886 4
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2887 4
            if ( ! $ids) {
2888
                continue;
2889
            }
2890 1003
2891
            $class = $this->em->getClassMetadata($entityName);
2892
2893
            $this->getEntityPersister($entityName)->loadAll(
2894
                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
    }
2898 871
2899
    /**
2900 871
     * Initializes (loads) an uninitialized persistent collection of an entity.
2901 871
     *
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
    public function loadCollection(PersistentCollection $collection)
2909
    {
2910
        $assoc     = $collection->getMapping();
2911
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2912
2913
        switch ($assoc['type']) {
2914
            case ClassMetadata::ONE_TO_MANY:
2915
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2916
                break;
2917
2918
            case ClassMetadata::MANY_TO_MANY:
2919
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2920
                break;
2921
        }
2922
2923
        $collection->setInitialized(true);
2924
    }
2925
2926
    /**
2927
     * Gets the identity map of the UnitOfWork.
2928
     *
2929
     * @return array
2930
     */
2931
    public function getIdentityMap()
2932
    {
2933
        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
    public function getOriginalEntityData($entity)
2945
    {
2946
        $oid = spl_object_hash($entity);
2947
2948
        return isset($this->originalEntityData[$oid])
2949
            ? $this->originalEntityData[$oid]
2950
            : [];
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
    public function setOriginalEntityProperty($oid, $property, $value)
2979
    {
2980
        $this->originalEntityData[$oid][$property] = $value;
2981
    }
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
    public function getEntityIdentifier($entity)
2994
    {
2995
        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
    public function getSingleIdentifierValue($entity)
3008
    {
3009
        $class = $this->em->getClassMetadata(get_class($entity));
3010
3011
        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
        $values = $this->isInIdentityMap($entity)
3016
            ? $this->getEntityIdentifier($entity)
3017
            : $class->getIdentifierValues($entity);
3018
3019
        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
    public function tryGetById($id, $rootClassName)
3033
    {
3034
        $idHash = implode(' ', (array) $id);
3035
3036
        return isset($this->identityMap[$rootClassName][$idHash])
3037
            ? $this->identityMap[$rootClassName][$idHash]
3038
            : 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
    public function scheduleForDirtyCheck($entity)
3051
    {
3052
        $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
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
3055
    }
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
    public function size()
3074
    {
3075
        $countArray = array_map('count', $this->identityMap);
3076
3077
        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
    public function getEntityPersister($entityName)
3088
    {
3089
        if (isset($this->persisters[$entityName])) {
3090
            return $this->persisters[$entityName];
3091
        }
3092
3093
        $class = $this->em->getClassMetadata($entityName);
3094
3095
        switch (true) {
3096
            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
                $persister = new BasicEntityPersister($this->em, $class);
3098
                break;
3099
3100
            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
                $persister = new SingleTablePersister($this->em, $class);
3102
                break;
3103
3104
            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
                $persister = new JoinedSubclassPersister($this->em, $class);
3106
                break;
3107
3108
            default:
3109
                throw new \RuntimeException('No persister found for entity.');
3110
        }
3111
3112
        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
            $persister = $this->em->getConfiguration()
3114
                ->getSecondLevelCacheConfiguration()
3115
                ->getCacheFactory()
3116
                ->buildCachedEntityPersister($this->em, $persister, $class);
3117
        }
3118
3119
        $this->persisters[$entityName] = $persister;
3120
3121
        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
    public function getCollectionPersister(array $association)
3132
    {
3133
        $role = isset($association['cache'])
3134
            ? $association['sourceEntity'] . '::' . $association['fieldName']
3135
            : $association['type'];
3136
3137
        if (isset($this->collectionPersisters[$role])) {
3138
            return $this->collectionPersisters[$role];
3139
        }
3140
3141
        $persister = ClassMetadata::ONE_TO_MANY === $association['type']
3142
            ? new OneToManyPersister($this->em)
3143
            : new ManyToManyPersister($this->em);
3144
3145
        if ($this->hasCache && isset($association['cache'])) {
3146
            $persister = $this->em->getConfiguration()
3147
                ->getSecondLevelCacheConfiguration()
3148
                ->getCacheFactory()
3149
                ->buildCachedCollectionPersister($this->em, $persister, $association);
3150
        }
3151
3152
        $this->collectionPersisters[$role] = $persister;
3153
3154
        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
    public function registerManaged($entity, array $id, array $data)
3168
    {
3169
        $oid = spl_object_hash($entity);
3170
3171
        $this->entityIdentifiers[$oid]  = $id;
3172
        $this->entityStates[$oid]       = self::STATE_MANAGED;
3173
        $this->originalEntityData[$oid] = $data;
3174
3175
        $this->addToIdentityMap($entity);
3176
3177
        if ($entity instanceof NotifyPropertyChanged && ( ! $entity instanceof Proxy || $entity->__isInitialized())) {
3178
            $entity->addPropertyChangedListener($this);
3179
        }
3180
    }
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
    public function clearEntityChangeSet($oid)
3191
    {
3192
        unset($this->entityChangeSets[$oid]);
3193
    }
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
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
3208
    {
3209
        $oid   = spl_object_hash($entity);
3210
        $class = $this->em->getClassMetadata(get_class($entity));
3211
3212
        $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
        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
            return; // ignore non-persistent fields
3216
        }
3217
3218
        // Update changeset and mark entity for synchronization
3219
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
3220
3221
        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
            $this->scheduleForDirtyCheck($entity);
3223
        }
3224
    }
3225
3226
    /**
3227
     * Gets the currently scheduled entity insertions in this UnitOfWork.
3228
     *
3229
     * @return array
3230
     */
3231
    public function getScheduledEntityInsertions()
3232
    {
3233
        return $this->entityInsertions;
3234
    }
3235
3236
    /**
3237
     * Gets the currently scheduled entity updates in this UnitOfWork.
3238
     *
3239
     * @return array
3240
     */
3241
    public function getScheduledEntityUpdates()
3242
    {
3243
        return $this->entityUpdates;
3244
    }
3245
3246
    /**
3247
     * Gets the currently scheduled entity deletions in this UnitOfWork.
3248
     *
3249
     * @return array
3250
     */
3251
    public function getScheduledEntityDeletions()
3252
    {
3253
        return $this->entityDeletions;
3254
    }
3255
3256
    /**
3257
     * Gets the currently scheduled complete collection deletions
3258
     *
3259
     * @return array
3260
     */
3261
    public function getScheduledCollectionDeletions()
3262
    {
3263
        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
    public function initializeObject($obj)
3284
    {
3285
        if ($obj instanceof Proxy) {
3286
            $obj->__load();
3287
3288
            return;
3289
        }
3290
3291
        if ($obj instanceof PersistentCollection) {
3292
            $obj->initialize();
3293
        }
3294
    }
3295
3296
    /**
3297
     * Helper method to show an object as string.
3298
     *
3299
     * @param object $obj
3300
     *
3301
     * @return string
3302
     */
3303
    private static function objToStr($obj)
3304
    {
3305
        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
    public function markReadOnly($object)
3321
    {
3322
        if ( ! is_object($object) || ! $this->isInIdentityMap($object)) {
3323
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3324
        }
3325
3326
        $this->readOnlyObjects[spl_object_hash($object)] = true;
3327
    }
3328
3329
    /**
3330
     * Is this entity read only?
3331
     *
3332
     * @param object $object
3333
     *
3334
     * @return bool
3335
     *
3336
     * @throws ORMInvalidArgumentException
3337
     */
3338
    public function isReadOnly($object)
3339
    {
3340
        if ( ! is_object($object)) {
3341
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3342
        }
3343
3344
        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
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3353
            $persister->afterTransactionComplete();
3354
        });
3355
    }
3356
3357
    /**
3358
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
3359
     */
3360
    private function afterTransactionRolledBack()
3361
    {
3362
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3363
            $persister->afterTransactionRolledBack();
3364
        });
3365
    }
3366
3367
    /**
3368
     * Performs an action after the transaction.
3369
     *
3370
     * @param callable $callback
3371
     */
3372
    private function performCallbackOnCachedPersister(callable $callback)
3373
    {
3374
        if ( ! $this->hasCache) {
3375
            return;
3376
        }
3377
3378
        foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) {
3379
            if ($persister instanceof CachedPersister) {
3380
                $callback($persister);
3381
            }
3382
        }
3383
    }
3384
3385
    private function dispatchOnFlushEvent()
3386
    {
3387
        if ($this->evm->hasListeners(Events::onFlush)) {
3388
            $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3389
        }
3390
    }
3391
3392
    private function dispatchPostFlushEvent()
3393
    {
3394
        if ($this->evm->hasListeners(Events::postFlush)) {
3395
            $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3396
        }
3397
    }
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
    private function isIdentifierEquals($entity1, $entity2)
3408
    {
3409
        if ($entity1 === $entity2) {
3410
            return true;
3411
        }
3412
3413
        $class = $this->em->getClassMetadata(get_class($entity1));
3414
3415
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
3416
            return false;
3417
        }
3418
3419
        $oid1 = spl_object_hash($entity1);
3420
        $oid2 = spl_object_hash($entity2);
3421
3422
        $id1 = isset($this->entityIdentifiers[$oid1])
3423
            ? $this->entityIdentifiers[$oid1]
3424
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
3425
        $id2 = isset($this->entityIdentifiers[$oid2])
3426
            ? $this->entityIdentifiers[$oid2]
3427
            : $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
3428
3429
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
3430
    }
3431
3432
    /**
3433
     * @throws ORMInvalidArgumentException
3434
     */
3435
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
3436
    {
3437
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
3438
3439
        $this->nonCascadedNewDetectedEntities = [];
3440
3441
        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
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
3443
                \array_values($entitiesNeedingCascadePersist)
3444
            );
3445
        }
3446
    }
3447
3448
    /**
3449
     * @param object $entity
3450
     * @param object $managedCopy
3451
     *
3452
     * @throws ORMException
3453
     * @throws OptimisticLockException
3454
     * @throws TransactionRequiredException
3455
     */
3456
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
3457
    {
3458
        if (! $this->isLoaded($entity)) {
3459
            return;
3460
        }
3461
3462
        if (! $this->isLoaded($managedCopy)) {
3463
            $managedCopy->__load();
3464
        }
3465
3466
        $class = $this->em->getClassMetadata(get_class($entity));
3467
3468
        foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
3469
            $name = $prop->name;
3470
3471
            $prop->setAccessible(true);
3472
3473
            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
                if ( ! $class->isIdentifier($name)) {
3475
                    $prop->setValue($managedCopy, $prop->getValue($entity));
3476
                }
3477
            } else {
3478
                $assoc2 = $class->associationMappings[$name];
3479
3480
                if ($assoc2['type'] & ClassMetadata::TO_ONE) {
3481
                    $other = $prop->getValue($entity);
3482
                    if ($other === null) {
3483
                        $prop->setValue($managedCopy, null);
3484
                    } else {
3485
                        if ($other instanceof Proxy && !$other->__isInitialized()) {
3486
                            // do not merge fields marked lazy that have not been fetched.
3487
                            continue;
3488
                        }
3489
3490
                        if ( ! $assoc2['isCascadeMerge']) {
3491
                            if ($this->getEntityState($other) === self::STATE_DETACHED) {
3492
                                $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
3493
                                $relatedId   = $targetClass->getIdentifierValues($other);
3494
3495
                                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
                                    $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
                                    $other = $this->em->getProxyFactory()->getProxy(
3499
                                        $assoc2['targetEntity'],
3500
                                        $relatedId
3501
                                    );
3502
                                    $this->registerManaged($other, $relatedId, []);
3503
                                }
3504
                            }
3505
3506
                            $prop->setValue($managedCopy, $other);
3507
                        }
3508
                    }
3509
                } else {
3510
                    $mergeCol = $prop->getValue($entity);
3511
3512
                    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
                        continue;
3516
                    }
3517
3518
                    $managedCol = $prop->getValue($managedCopy);
3519
3520
                    if ( ! $managedCol) {
3521
                        $managedCol = new PersistentCollection(
3522
                            $this->em,
3523
                            $this->em->getClassMetadata($assoc2['targetEntity']),
3524
                            new ArrayCollection
3525
                        );
3526
                        $managedCol->setOwner($managedCopy, $assoc2);
3527
                        $prop->setValue($managedCopy, $managedCol);
3528
                    }
3529
3530
                    if ($assoc2['isCascadeMerge']) {
3531
                        $managedCol->initialize();
3532
3533
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
3534
                        if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
3535
                            $managedCol->unwrap()->clear();
3536
                            $managedCol->setDirty(true);
3537
3538
                            if ($assoc2['isOwningSide']
3539
                                && $assoc2['type'] == ClassMetadata::MANY_TO_MANY
3540
                                && $class->isChangeTrackingNotify()
3541
                            ) {
3542
                                $this->scheduleForDirtyCheck($managedCopy);
3543
                            }
3544
                        }
3545
                    }
3546
                }
3547
            }
3548
3549
            if ($class->isChangeTrackingNotify()) {
3550
                // Just treat all properties as changed, there is no other choice.
3551
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
3552
            }
3553
        }
3554
    }
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
    public function hydrationComplete()
3563
    {
3564
        $this->hydrationCompleteHandler->hydrationComplete();
3565
    }
3566
3567
    /**
3568
     * @param string $entityName
3569
     */
3570
    private function clearIdentityMapForEntityName($entityName)
3571
    {
3572
        if (! isset($this->identityMap[$entityName])) {
3573
            return;
3574
        }
3575
3576
        $visited = [];
3577
3578
        foreach ($this->identityMap[$entityName] as $entity) {
3579
            $this->doDetach($entity, $visited, false);
3580
        }
3581
    }
3582
3583
    /**
3584
     * @param string $entityName
3585
     */
3586
    private function clearEntityInsertionsForEntityName($entityName)
3587
    {
3588
        foreach ($this->entityInsertions as $hash => $entity) {
3589
            // note: performance optimization - `instanceof` is much faster than a function call
3590
            if ($entity instanceof $entityName && get_class($entity) === $entityName) {
3591
                unset($this->entityInsertions[$hash]);
3592
            }
3593
        }
3594
    }
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
    private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue)
3605
    {
3606
        return $this->em->getConnection()->convertToPHPValue(
3607
            $identifierValue,
3608
            $class->getTypeOfField($class->getSingleIdentifierFieldName())
3609
        );
3610
    }
3611
}
3612