Passed
Pull Request — master (#6869)
by Michael
09:57
created

UnitOfWork::performCallbackOnCachedPersister()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 4
nop 1
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 2453
    public function __construct(EntityManagerInterface $em)
303
    {
304 2453
        $this->em                         = $em;
305 2453
        $this->evm                        = $em->getEventManager();
306 2453
        $this->listenersInvoker           = new ListenersInvoker($em);
307 2453
        $this->hasCache                   = $em->getConfiguration()->isSecondLevelCacheEnabled();
308 2453
        $this->identifierFlattener        = new IdentifierFlattener($this, $em->getMetadataFactory());
309 2453
        $this->hydrationCompleteHandler   = new HydrationCompleteHandler($this->listenersInvoker, $em);
310 2453
        $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
311 2453
    }
312
313
    /**
314
     * Commits the UnitOfWork, executing all operations that have been postponed
315
     * up to this point. The state of all managed entities will be synchronized with
316
     * the database.
317
     *
318
     * The operations are executed in the following order:
319
     *
320
     * 1) All entity insertions
321
     * 2) All entity updates
322
     * 3) All collection deletions
323
     * 4) All collection updates
324
     * 5) All entity deletions
325
     *
326
     * @param null|object|array $entity
327
     *
328
     * @return void
329
     *
330
     * @throws \Exception
331
     */
332 1070
    public function commit($entity = null)
333
    {
334
        // Raise preFlush
335 1070
        if ($this->evm->hasListeners(Events::preFlush)) {
336 2
            $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
337
        }
338
339
        // Compute changes done since last commit.
340 1070
        if (null === $entity) {
341 1063
            $this->computeChangeSets();
342 13
        } elseif (is_object($entity)) {
343 11
            $this->computeSingleEntityChangeSet($entity);
344 2
        } elseif (is_array($entity)) {
345 2
            foreach ($entity as $object) {
346 2
                $this->computeSingleEntityChangeSet($object);
347
            }
348
        }
349
350 1067
        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 171
                $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 134
                $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 41
                $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 37
                $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 1067
                $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 25
            $this->dispatchOnFlushEvent();
357 25
            $this->dispatchPostFlushEvent();
358
359 25
            return; // Nothing to do.
360
        }
361
362 1063
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
363
364 1061
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
365 16
            foreach ($this->orphanRemovals as $orphan) {
366 16
                $this->remove($orphan);
367
            }
368
        }
369
370 1061
        $this->dispatchOnFlushEvent();
371
372
        // Now we need a commit order to maintain referential integrity
373 1061
        $commitOrder = $this->getCommitOrder();
374
375 1061
        $conn = $this->em->getConnection();
376 1061
        $conn->beginTransaction();
377
378
        try {
379
            // Collection deletions (deletions of complete collections)
380 1061
            foreach ($this->collectionDeletions as $collectionToDelete) {
381 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
382
            }
383
384 1061
            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 1057
                foreach ($commitOrder as $class) {
386 1057
                    $this->executeInserts($class);
387
                }
388
            }
389
390 1060
            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 119
                foreach ($commitOrder as $class) {
392 119
                    $this->executeUpdates($class);
393
                }
394
            }
395
396
            // Extra updates that were requested by persisters.
397 1056
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
398 44
                $this->executeExtraUpdates();
399
            }
400
401
            // Collection updates (deleteRows, updateRows, insertRows)
402 1056
            foreach ($this->collectionUpdates as $collectionToUpdate) {
403 540
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
404
            }
405
406
            // Entity deletions come last and need to be in reverse commit order
407 1056
            if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
408 64
                for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
409 64
                    $this->executeDeletions($commitOrder[$i]);
410
                }
411
            }
412
413 1056
            $conn->commit();
414 11
        } catch (Throwable $e) {
415 11
            $this->em->close();
416 11
            $conn->rollBack();
417
418 11
            $this->afterTransactionRolledBack();
419
420 11
            throw $e;
421
        }
422
423 1056
        $this->afterTransactionComplete();
424
425
        // Take new snapshots from visited collections
426 1056
        foreach ($this->visitedCollections as $coll) {
427 539
            $coll->takeSnapshot();
428
        }
429
430 1056
        $this->dispatchPostFlushEvent();
431
432 1055
        $this->postCommitCleanup($entity);
433 1055
    }
434
435
    /**
436
     * @param null|object|object[] $entity
437
     */
438 1055
    private function postCommitCleanup($entity) : void
439
    {
440 1055
        $this->entityInsertions =
441 1055
        $this->entityUpdates =
442 1055
        $this->entityDeletions =
443 1055
        $this->extraUpdates =
444 1055
        $this->collectionUpdates =
445 1055
        $this->nonCascadedNewDetectedEntities =
446 1055
        $this->collectionDeletions =
447 1055
        $this->visitedCollections =
448 1055
        $this->orphanRemovals = [];
449
450 1055
        if (null === $entity) {
451 1049
            $this->entityChangeSets = $this->scheduledForSynchronization = [];
452
453 1049
            return;
454
        }
455
456 11
        $entities = \is_object($entity)
457 9
            ? [$entity]
458 11
            : $entity;
459
460 11
        foreach ($entities as $object) {
461 11
            $oid = \spl_object_hash($object);
462
463 11
            $this->clearEntityChangeSet($oid);
464
465 11
            unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
466
        }
467 11
    }
468
469
    /**
470
     * Computes the changesets of all entities scheduled for insertion.
471
     *
472
     * @return void
473
     */
474 1069
    private function computeScheduleInsertsChangeSets()
475
    {
476 1069
        foreach ($this->entityInsertions as $entity) {
477 1061
            $class = $this->em->getClassMetadata(get_class($entity));
478
479 1061
            $this->computeChangeSet($class, $entity);
480
        }
481 1067
    }
482
483
    /**
484
     * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
485
     *
486
     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
487
     * 2. Read Only entities are skipped.
488
     * 3. Proxies are skipped.
489
     * 4. Only if entity is properly managed.
490
     *
491
     * @param object $entity
492
     *
493
     * @return void
494
     *
495
     * @throws \InvalidArgumentException
496
     */
497 13
    private function computeSingleEntityChangeSet($entity)
498
    {
499 13
        $state = $this->getEntityState($entity);
500
501 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
502 1
            throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity));
503
        }
504
505 12
        $class = $this->em->getClassMetadata(get_class($entity));
506
507 12
        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 12
            $this->persist($entity);
509
        }
510
511
        // Compute changes for INSERTed entities first. This must always happen even in this case.
512 12
        $this->computeScheduleInsertsChangeSets();
513
514 12
        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 12
        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 1
            return;
521
        }
522
523
        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
524 11
        $oid = spl_object_hash($entity);
525
526 11 View Code Duplication
        if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
527 6
            $this->computeChangeSet($class, $entity);
528
        }
529 11
    }
530
531
    /**
532
     * Executes any extra updates that have been scheduled.
533
     */
534 44
    private function executeExtraUpdates()
535
    {
536 44
        foreach ($this->extraUpdates as $oid => $update) {
537 44
            list ($entity, $changeset) = $update;
538
539 44
            $this->entityChangeSets[$oid] = $changeset;
540 44
            $this->getEntityPersister(get_class($entity))->update($entity);
541
        }
542
543 44
        $this->extraUpdates = [];
544 44
    }
545
546
    /**
547
     * Gets the changeset for an entity.
548
     *
549
     * @param object $entity
550
     *
551
     * @return array
552
     */
553 1056
    public function & getEntityChangeSet($entity)
554
    {
555 1056
        $oid  = spl_object_hash($entity);
556 1056
        $data = [];
557
558 1056
        if (!isset($this->entityChangeSets[$oid])) {
559 3
            return $data;
560
        }
561
562 1056
        return $this->entityChangeSets[$oid];
563
    }
564
565
    /**
566
     * Computes the changes that happened to a single entity.
567
     *
568
     * Modifies/populates the following properties:
569
     *
570
     * {@link _originalEntityData}
571
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
572
     * then it was not fetched from the database and therefore we have no original
573
     * entity data yet. All of the current entity data is stored as the original entity data.
574
     *
575
     * {@link _entityChangeSets}
576
     * The changes detected on all properties of the entity are stored there.
577
     * A change is a tuple array where the first entry is the old value and the second
578
     * entry is the new value of the property. Changesets are used by persisters
579
     * to INSERT/UPDATE the persistent entity state.
580
     *
581
     * {@link _entityUpdates}
582
     * If the entity is already fully MANAGED (has been fetched from the database before)
583
     * and any changes to its properties are detected, then a reference to the entity is stored
584
     * there to mark it for an update.
585
     *
586
     * {@link _collectionDeletions}
587
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
588
     * then this collection is marked for deletion.
589
     *
590
     * @ignore
591
     *
592
     * @internal Don't call from the outside.
593
     *
594
     * @param ClassMetadata $class  The class descriptor of the entity.
595
     * @param object        $entity The entity for which to compute the changes.
596
     *
597
     * @return void
598
     */
599 1071
    public function computeChangeSet(ClassMetadata $class, $entity)
600
    {
601 1071
        $oid = spl_object_hash($entity);
602
603 1071
        if (isset($this->readOnlyObjects[$oid])) {
604 2
            return;
605
        }
606
607 1071
        if ( ! $class->isInheritanceTypeNone()) {
608 332
            $class = $this->em->getClassMetadata(get_class($entity));
609
        }
610
611 1071
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
612
613 1071 View Code Duplication
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
614 138
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
615
        }
616
617 1071
        $actualData = [];
618
619 1071
        foreach ($class->reflFields as $name => $refProp) {
620 1071
            $value = $refProp->getValue($entity);
621
622 1071
            if ($class->isCollectionValuedAssociation($name) && $value !== null) {
623 808
                if ($value instanceof PersistentCollection) {
624 202
                    if ($value->getOwner() === $entity) {
625 202
                        continue;
626
                    }
627
628 5
                    $value = new ArrayCollection($value->getValues());
629
                }
630
631
                // If $value is not a Collection then use an ArrayCollection.
632 803
                if ( ! $value instanceof Collection) {
633 242
                    $value = new ArrayCollection($value);
634
                }
635
636 803
                $assoc = $class->associationMappings[$name];
637
638
                // Inject PersistentCollection
639 803
                $value = new PersistentCollection(
640 803
                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
641
                );
642 803
                $value->setOwner($entity, $assoc);
643 803
                $value->setDirty( ! $value->isEmpty());
644
645 803
                $class->reflFields[$name]->setValue($entity, $value);
646
647 803
                $actualData[$name] = $value;
648
649 803
                continue;
650
            }
651
652 1071
            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 1071
                $actualData[$name] = $value;
654
            }
655
        }
656
657 1071
        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 1067
            $this->originalEntityData[$oid] = $actualData;
661 1067
            $changeSet = [];
662
663 1067
            foreach ($actualData as $propName => $actualValue) {
664 1045 View Code Duplication
                if ( ! isset($class->associationMappings[$propName])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
665 991
                    $changeSet[$propName] = [null, $actualValue];
666
667 991
                    continue;
668
                }
669
670 934
                $assoc = $class->associationMappings[$propName];
671
672 934
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
673 934
                    $changeSet[$propName] = [null, $actualValue];
674
                }
675
            }
676
677 1067
            $this->entityChangeSets[$oid] = $changeSet;
678
        } else {
679
            // Entity is "fully" MANAGED: it was already fully persisted before
680
            // and we have a copy of the original data
681 270
            $originalData           = $this->originalEntityData[$oid];
682 270
            $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 270
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
684
                ? $this->entityChangeSets[$oid]
685 270
                : [];
686
687 270
            foreach ($actualData as $propName => $actualValue) {
688
                // skip field, its a partially omitted one!
689 255
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
690 8
                    continue;
691
                }
692
693 255
                $orgValue = $originalData[$propName];
694
695
                // skip if value haven't changed
696 255
                if ($orgValue === $actualValue) {
697 239
                    continue;
698
                }
699
700
                // if regular field
701 115 View Code Duplication
                if ( ! isset($class->associationMappings[$propName])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
702 60
                    if ($isChangeTrackingNotify) {
703
                        continue;
704
                    }
705
706 60
                    $changeSet[$propName] = [$orgValue, $actualValue];
707
708 60
                    continue;
709
                }
710
711 59
                $assoc = $class->associationMappings[$propName];
712
713
                // Persistent collection was exchanged with the "originally"
714
                // created one. This can only mean it was cloned and replaced
715
                // on another entity.
716 59
                if ($actualValue instanceof PersistentCollection) {
717 8
                    $owner = $actualValue->getOwner();
718 8
                    if ($owner === null) { // cloned
719
                        $actualValue->setOwner($entity, $assoc);
720 8
                    } else if ($owner !== $entity) { // no clone, we have to fix
721
                        if (!$actualValue->isInitialized()) {
722
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
723
                        }
724
                        $newValue = clone $actualValue;
725
                        $newValue->setOwner($entity, $assoc);
726
                        $class->reflFields[$propName]->setValue($entity, $newValue);
727
                    }
728
                }
729
730 59
                if ($orgValue instanceof PersistentCollection) {
731
                    // A PersistentCollection was de-referenced, so delete it.
732 8
                    $coid = spl_object_hash($orgValue);
733
734 8
                    if (isset($this->collectionDeletions[$coid])) {
735
                        continue;
736
                    }
737
738 8
                    $this->collectionDeletions[$coid] = $orgValue;
739 8
                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
740
741 8
                    continue;
742
                }
743
744 51
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
745 50
                    if ($assoc['isOwningSide']) {
746 22
                        $changeSet[$propName] = [$orgValue, $actualValue];
747
                    }
748
749 50
                    if ($orgValue !== null && $assoc['orphanRemoval']) {
750 51
                        $this->scheduleOrphanRemoval($orgValue);
751
                    }
752
                }
753
            }
754
755 270
            if ($changeSet) {
756 88
                $this->entityChangeSets[$oid]   = $changeSet;
757 88
                $this->originalEntityData[$oid] = $actualData;
758 88
                $this->entityUpdates[$oid]      = $entity;
759
            }
760
        }
761
762
        // Look for changes in associations of the entity
763 1071
        foreach ($class->associationMappings as $field => $assoc) {
764 934
            if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
765 658
                continue;
766
            }
767
768 905
            $this->computeAssociationChanges($assoc, $val);
769
770 897
            if ( ! isset($this->entityChangeSets[$oid]) &&
771 897
                $assoc['isOwningSide'] &&
772 897
                $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
773 897
                $val instanceof PersistentCollection &&
774 897
                $val->isDirty()) {
775
776 35
                $this->entityChangeSets[$oid]   = [];
777 35
                $this->originalEntityData[$oid] = $actualData;
778 897
                $this->entityUpdates[$oid]      = $entity;
779
            }
780
        }
781 1063
    }
782
783
    /**
784
     * Computes all the changes that have been done to entities and collections
785
     * since the last commit and stores these changes in the _entityChangeSet map
786
     * temporarily for access by the persisters, until the UoW commit is finished.
787
     *
788
     * @return void
789
     */
790 1063
    public function computeChangeSets()
791
    {
792
        // Compute changes for INSERTed entities first. This must always happen.
793 1063
        $this->computeScheduleInsertsChangeSets();
794
795
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
796 1061
        foreach ($this->identityMap as $className => $entities) {
797 466
            $class = $this->em->getClassMetadata($className);
798
799
            // Skip class if instances are read-only
800 466
            if ($class->isReadOnly) {
0 ignored issues
show
Bug introduced by
Accessing isReadOnly on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
801 1
                continue;
802
            }
803
804
            // If change tracking is explicit or happens through notification, then only compute
805
            // changes on entities of that type that are explicitly marked for synchronization.
806
            switch (true) {
807 465
                case ($class->isChangeTrackingDeferredImplicit()):
808 463
                    $entitiesToProcess = $entities;
809 463
                    break;
810
811 3
                case (isset($this->scheduledForSynchronization[$className])):
812 3
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
813 3
                    break;
814
815
                default:
816 1
                    $entitiesToProcess = [];
817
818
            }
819
820 465
            foreach ($entitiesToProcess as $entity) {
821
                // Ignore uninitialized proxy objects
822 445
                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
823 37
                    continue;
824
                }
825
826
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
827 443
                $oid = spl_object_hash($entity);
828
829 443 View Code Duplication
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
830 465
                    $this->computeChangeSet($class, $entity);
831
                }
832
            }
833
        }
834 1061
    }
835
836
    /**
837
     * Computes the changes of an association.
838
     *
839
     * @param array $assoc The association mapping.
840
     * @param mixed $value The value of the association.
841
     *
842
     * @throws ORMInvalidArgumentException
843
     * @throws ORMException
844
     *
845
     * @return void
846
     */
847 905
    private function computeAssociationChanges($assoc, $value)
848
    {
849 905
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ORM\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
850 29
            return;
851
        }
852
853 904
        if ($value instanceof PersistentCollection && $value->isDirty()) {
854 544
            $coid = spl_object_hash($value);
855
856 544
            $this->collectionUpdates[$coid] = $value;
857 544
            $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 904
        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap();
864 904
        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
865
866 904
        foreach ($unwrappedValue as $key => $entry) {
867 747
            if (! ($entry instanceof $targetClass->name)) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
868 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
869
            }
870
871 739
            $state = $this->getEntityState($entry, self::STATE_NEW);
872
873 739
            if ( ! ($entry instanceof $assoc['targetEntity'])) {
874
                throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
875
            }
876
877
            switch ($state) {
878 739
                case self::STATE_NEW:
879 42
                    if ( ! $assoc['isCascadePersist']) {
880
                        /*
881
                         * For now just record the details, because this may
882
                         * not be an issue if we later discover another pathway
883
                         * through the object-graph where cascade-persistence
884
                         * is enabled for this object.
885
                         */
886 6
                        $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry];
887
888 6
                        break;
889
                    }
890
891 37
                    $this->persistNew($targetClass, $entry);
892 37
                    $this->computeChangeSet($targetClass, $entry);
893
894 37
                    break;
895
896 731
                case self::STATE_REMOVED:
897
                    // Consume the $value as array (it's either an array or an ArrayAccess)
898
                    // and remove the element from Collection.
899 4
                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
900 3
                        unset($value[$key]);
901
                    }
902 4
                    break;
903
904 731
                case self::STATE_DETACHED:
905
                    // Can actually not happen right now as we assume STATE_NEW,
906
                    // so the exception will be raised from the DBAL layer (constraint violation).
907
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
908
                    break;
909
910 739
                default:
911
                    // MANAGED associated entities are already taken into account
912
                    // during changeset calculation anyway, since they are in the identity map.
913
            }
914
        }
915 896
    }
916
917
    /**
918
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
919
     * @param object                              $entity
920
     *
921
     * @return void
922
     */
923 1093
    private function persistNew($class, $entity)
924
    {
925 1093
        $oid    = spl_object_hash($entity);
926 1093
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
927
928 1093 View Code Duplication
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
929 141
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
930
        }
931
932 1093
        $idGen = $class->idGenerator;
933
934 1093
        if ( ! $idGen->isPostInsertGenerator()) {
935 288
            $idValue = $idGen->generate($this->em, $entity);
936
937 288
            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
938 2
                $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
939
940 2
                $class->setIdentifierValues($entity, $idValue);
941
            }
942
943 288
            $this->entityIdentifiers[$oid] = $idValue;
944
        }
945
946 1093
        $this->entityStates[$oid] = self::STATE_MANAGED;
947
948 1093
        $this->scheduleForInsert($entity);
949 1093
    }
950
951
    /**
952
     * INTERNAL:
953
     * Computes the changeset of an individual entity, independently of the
954
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
955
     *
956
     * The passed entity must be a managed entity. If the entity already has a change set
957
     * because this method is invoked during a commit cycle then the change sets are added.
958
     * whereby changes detected in this method prevail.
959
     *
960
     * @ignore
961
     *
962
     * @param ClassMetadata $class  The class descriptor of the entity.
963
     * @param object        $entity The entity for which to (re)calculate the change set.
964
     *
965
     * @return void
966
     *
967
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
968
     */
969 16
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
970
    {
971 16
        $oid = spl_object_hash($entity);
972
973 16
        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
974
            throw ORMInvalidArgumentException::entityNotManaged($entity);
975
        }
976
977
        // skip if change tracking is "NOTIFY"
978 16
        if ($class->isChangeTrackingNotify()) {
979
            return;
980
        }
981
982 16
        if ( ! $class->isInheritanceTypeNone()) {
983 3
            $class = $this->em->getClassMetadata(get_class($entity));
984
        }
985
986 16
        $actualData = [];
987
988 16
        foreach ($class->reflFields as $name => $refProp) {
989 16
            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
990 16
                && ($name !== $class->versionField)
991 16
                && ! $class->isCollectionValuedAssociation($name)) {
992 16
                $actualData[$name] = $refProp->getValue($entity);
993
            }
994
        }
995
996 16
        if ( ! isset($this->originalEntityData[$oid])) {
997
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
998
        }
999
1000 16
        $originalData = $this->originalEntityData[$oid];
1001 16
        $changeSet = [];
1002
1003 16
        foreach ($actualData as $propName => $actualValue) {
1004 16
            $orgValue = $originalData[$propName] ?? null;
1005
1006 16
            if ($orgValue !== $actualValue) {
1007 16
                $changeSet[$propName] = [$orgValue, $actualValue];
1008
            }
1009
        }
1010
1011 16
        if ($changeSet) {
1012 7
            if (isset($this->entityChangeSets[$oid])) {
1013 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
1014 1
            } else if ( ! isset($this->entityInsertions[$oid])) {
1015 1
                $this->entityChangeSets[$oid] = $changeSet;
1016 1
                $this->entityUpdates[$oid]    = $entity;
1017
            }
1018 7
            $this->originalEntityData[$oid] = $actualData;
1019
        }
1020 16
    }
1021
1022
    /**
1023
     * Executes all entity insertions for entities of the specified type.
1024
     *
1025
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1026
     *
1027
     * @return void
1028
     */
1029 1057
    private function executeInserts($class)
1030
    {
1031 1057
        $entities   = [];
1032 1057
        $className  = $class->name;
1033 1057
        $persister  = $this->getEntityPersister($className);
1034 1057
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
1035
1036 1057
        foreach ($this->entityInsertions as $oid => $entity) {
1037
1038 1057
            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...
1039 898
                continue;
1040
            }
1041
1042 1057
            $persister->addInsert($entity);
1043
1044 1057
            unset($this->entityInsertions[$oid]);
1045
1046 1057
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1047 1057
                $entities[] = $entity;
1048
            }
1049
        }
1050
1051 1057
        $postInsertIds = $persister->executeInserts();
1052
1053 1057
        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...
1054
            // Persister returned post-insert IDs
1055 957
            foreach ($postInsertIds as $postInsertId) {
1056 957
                $idField = $class->getSingleIdentifierFieldName();
1057 957
                $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']);
1058
1059 957
                $entity  = $postInsertId['entity'];
1060 957
                $oid     = spl_object_hash($entity);
1061
1062 957
                $class->reflFields[$idField]->setValue($entity, $idValue);
1063
1064 957
                $this->entityIdentifiers[$oid] = [$idField => $idValue];
1065 957
                $this->entityStates[$oid] = self::STATE_MANAGED;
1066 957
                $this->originalEntityData[$oid][$idField] = $idValue;
1067
1068 957
                $this->addToIdentityMap($entity);
1069
            }
1070
        }
1071
1072 1057
        foreach ($entities as $entity) {
1073 136
            $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1074
        }
1075 1057
    }
1076
1077
    /**
1078
     * Executes all entity updates for entities of the specified type.
1079
     *
1080
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1081
     *
1082
     * @return void
1083
     */
1084 119
    private function executeUpdates($class)
1085
    {
1086 119
        $className          = $class->name;
1087 119
        $persister          = $this->getEntityPersister($className);
1088 119
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1089 119
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1090
1091 119
        foreach ($this->entityUpdates as $oid => $entity) {
1092 119
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1093 77
                continue;
1094
            }
1095
1096 119
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
1097 13
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1098
1099 13
                $this->recomputeSingleEntityChangeSet($class, $entity);
1100
            }
1101
1102 119
            if ( ! empty($this->entityChangeSets[$oid])) {
1103 85
                $persister->update($entity);
1104
            }
1105
1106 115
            unset($this->entityUpdates[$oid]);
1107
1108 115 View Code Duplication
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1109 115
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1110
            }
1111
        }
1112 115
    }
1113
1114
    /**
1115
     * Executes all entity deletions for entities of the specified type.
1116
     *
1117
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1118
     *
1119
     * @return void
1120
     */
1121 64
    private function executeDeletions($class)
1122
    {
1123 64
        $className  = $class->name;
1124 64
        $persister  = $this->getEntityPersister($className);
1125 64
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1126
1127 64
        foreach ($this->entityDeletions as $oid => $entity) {
1128 64
            if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1129 25
                continue;
1130
            }
1131
1132 64
            $persister->delete($entity);
1133
1134
            unset(
1135 64
                $this->entityDeletions[$oid],
1136 64
                $this->entityIdentifiers[$oid],
1137 64
                $this->originalEntityData[$oid],
1138 64
                $this->entityStates[$oid]
1139
            );
1140
1141
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1142
            // is obtained by a new entity because the old one went out of scope.
1143
            //$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...
1144 64
            if ( ! $class->isIdentifierNatural()) {
1145 53
                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1146
            }
1147
1148 64 View Code Duplication
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1149 64
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1150
            }
1151
        }
1152 63
    }
1153
1154
    /**
1155
     * Gets the commit order.
1156
     *
1157
     * @param array|null $entityChangeSet
1158
     *
1159
     * @return array
1160
     */
1161 1061
    private function getCommitOrder(array $entityChangeSet = null)
1162
    {
1163 1061
        if ($entityChangeSet === null) {
1164 1061
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1165
        }
1166
1167 1061
        $calc = $this->getCommitOrderCalculator();
1168
1169
        // See if there are any new classes in the changeset, that are not in the
1170
        // commit order graph yet (don't have a node).
1171
        // We have to inspect changeSet to be able to correctly build dependencies.
1172
        // It is not possible to use IdentityMap here because post inserted ids
1173
        // are not yet available.
1174 1061
        $newNodes = [];
1175
1176 1061
        foreach ($entityChangeSet as $entity) {
1177 1061
            $class = $this->em->getClassMetadata(get_class($entity));
1178
1179 1061
            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...
1180 653
                continue;
1181
            }
1182
1183 1061
            $calc->addNode($class->name, $class);
1184
1185 1061
            $newNodes[] = $class;
1186
        }
1187
1188
        // Calculate dependencies for new nodes
1189 1061
        while ($class = array_pop($newNodes)) {
1190 1061
            foreach ($class->associationMappings as $assoc) {
1191 923
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1192 878
                    continue;
1193
                }
1194
1195 874
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1196
1197 874
                if ( ! $calc->hasNode($targetClass->name)) {
1198 674
                    $calc->addNode($targetClass->name, $targetClass);
1199
1200 674
                    $newNodes[] = $targetClass;
1201
                }
1202
1203 874
                $joinColumns = reset($assoc['joinColumns']);
1204
1205 874
                $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable']));
1206
1207
                // If the target class has mapped subclasses, these share the same dependency.
1208 874
                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...
1209 867
                    continue;
1210
                }
1211
1212 235
                foreach ($targetClass->subClasses as $subClassName) {
1213 235
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1214
1215 235
                    if ( ! $calc->hasNode($subClassName)) {
1216 205
                        $calc->addNode($targetSubClass->name, $targetSubClass);
1217
1218 205
                        $newNodes[] = $targetSubClass;
1219
                    }
1220
1221 235
                    $calc->addDependency($targetSubClass->name, $class->name, 1);
1222
                }
1223
            }
1224
        }
1225
1226 1061
        return $calc->sort();
1227
    }
1228
1229
    /**
1230
     * Schedules an entity for insertion into the database.
1231
     * If the entity already has an identifier, it will be added to the identity map.
1232
     *
1233
     * @param object $entity The entity to schedule for insertion.
1234
     *
1235
     * @return void
1236
     *
1237
     * @throws ORMInvalidArgumentException
1238
     * @throws \InvalidArgumentException
1239
     */
1240 1094
    public function scheduleForInsert($entity)
1241
    {
1242 1094
        $oid = spl_object_hash($entity);
1243
1244 1094
        if (isset($this->entityUpdates[$oid])) {
1245
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1246
        }
1247
1248 1094
        if (isset($this->entityDeletions[$oid])) {
1249 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1250
        }
1251 1094
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1252 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1253
        }
1254
1255 1094
        if (isset($this->entityInsertions[$oid])) {
1256 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1257
        }
1258
1259 1094
        $this->entityInsertions[$oid] = $entity;
1260
1261 1094
        if (isset($this->entityIdentifiers[$oid])) {
1262 288
            $this->addToIdentityMap($entity);
1263
        }
1264
1265 1094
        if ($entity instanceof NotifyPropertyChanged) {
1266 7
            $entity->addPropertyChangedListener($this);
1267
        }
1268 1094
    }
1269
1270
    /**
1271
     * Checks whether an entity is scheduled for insertion.
1272
     *
1273
     * @param object $entity
1274
     *
1275
     * @return boolean
1276
     */
1277 648
    public function isScheduledForInsert($entity)
1278
    {
1279 648
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1280
    }
1281
1282
    /**
1283
     * Schedules an entity for being updated.
1284
     *
1285
     * @param object $entity The entity to schedule for being updated.
1286
     *
1287
     * @return void
1288
     *
1289
     * @throws ORMInvalidArgumentException
1290
     */
1291 1
    public function scheduleForUpdate($entity)
1292
    {
1293 1
        $oid = spl_object_hash($entity);
1294
1295 1
        if ( ! isset($this->entityIdentifiers[$oid])) {
1296
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1297
        }
1298
1299 1
        if (isset($this->entityDeletions[$oid])) {
1300
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1301
        }
1302
1303 1
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1304 1
            $this->entityUpdates[$oid] = $entity;
1305
        }
1306 1
    }
1307
1308
    /**
1309
     * INTERNAL:
1310
     * Schedules an extra update that will be executed immediately after the
1311
     * regular entity updates within the currently running commit cycle.
1312
     *
1313
     * Extra updates for entities are stored as (entity, changeset) tuples.
1314
     *
1315
     * @ignore
1316
     *
1317
     * @param object $entity    The entity for which to schedule an extra update.
1318
     * @param array  $changeset The changeset of the entity (what to update).
1319
     *
1320
     * @return void
1321
     */
1322 44
    public function scheduleExtraUpdate($entity, array $changeset)
1323
    {
1324 44
        $oid         = spl_object_hash($entity);
1325 44
        $extraUpdate = [$entity, $changeset];
1326
1327 44
        if (isset($this->extraUpdates[$oid])) {
1328 1
            list(, $changeset2) = $this->extraUpdates[$oid];
1329
1330 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1331
        }
1332
1333 44
        $this->extraUpdates[$oid] = $extraUpdate;
1334 44
    }
1335
1336
    /**
1337
     * Checks whether an entity is registered as dirty in the unit of work.
1338
     * Note: Is not very useful currently as dirty entities are only registered
1339
     * at commit time.
1340
     *
1341
     * @param object $entity
1342
     *
1343
     * @return boolean
1344
     */
1345
    public function isScheduledForUpdate($entity)
1346
    {
1347
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1348
    }
1349
1350
    /**
1351
     * Checks whether an entity is registered to be checked in the unit of work.
1352
     *
1353
     * @param object $entity
1354
     *
1355
     * @return boolean
1356
     */
1357 1
    public function isScheduledForDirtyCheck($entity)
1358
    {
1359 1
        $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...
1360
1361 1
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1362
    }
1363
1364
    /**
1365
     * INTERNAL:
1366
     * Schedules an entity for deletion.
1367
     *
1368
     * @param object $entity
1369
     *
1370
     * @return void
1371
     */
1372 67
    public function scheduleForDelete($entity)
1373
    {
1374 67
        $oid = spl_object_hash($entity);
1375
1376 67
        if (isset($this->entityInsertions[$oid])) {
1377 1
            if ($this->isInIdentityMap($entity)) {
1378
                $this->removeFromIdentityMap($entity);
1379
            }
1380
1381 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1382
1383 1
            return; // entity has not been persisted yet, so nothing more to do.
1384
        }
1385
1386 67
        if ( ! $this->isInIdentityMap($entity)) {
1387 1
            return;
1388
        }
1389
1390 66
        $this->removeFromIdentityMap($entity);
1391
1392 66
        unset($this->entityUpdates[$oid]);
1393
1394 66
        if ( ! isset($this->entityDeletions[$oid])) {
1395 66
            $this->entityDeletions[$oid] = $entity;
1396 66
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1397
        }
1398 66
    }
1399
1400
    /**
1401
     * Checks whether an entity is registered as removed/deleted with the unit
1402
     * of work.
1403
     *
1404
     * @param object $entity
1405
     *
1406
     * @return boolean
1407
     */
1408 17
    public function isScheduledForDelete($entity)
1409
    {
1410 17
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1411
    }
1412
1413
    /**
1414
     * Checks whether an entity is scheduled for insertion, update or deletion.
1415
     *
1416
     * @param object $entity
1417
     *
1418
     * @return boolean
1419
     */
1420
    public function isEntityScheduled($entity)
1421
    {
1422
        $oid = spl_object_hash($entity);
1423
1424
        return isset($this->entityInsertions[$oid])
1425
            || isset($this->entityUpdates[$oid])
1426
            || isset($this->entityDeletions[$oid]);
1427
    }
1428
1429
    /**
1430
     * INTERNAL:
1431
     * Registers an entity in the identity map.
1432
     * Note that entities in a hierarchy are registered with the class name of
1433
     * the root entity.
1434
     *
1435
     * @ignore
1436
     *
1437
     * @param object $entity The entity to register.
1438
     *
1439
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1440
     *                 the entity in question is already managed.
1441
     *
1442
     * @throws ORMInvalidArgumentException
1443
     */
1444 1158
    public function addToIdentityMap($entity)
1445
    {
1446 1158
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1447 1158
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1448
1449 1158
        if (empty($identifier) || in_array(null, $identifier, true)) {
1450 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1451
        }
1452
1453 1152
        $idHash    = implode(' ', $identifier);
1454 1152
        $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...
1455
1456 1152
        if (isset($this->identityMap[$className][$idHash])) {
1457 86
            return false;
1458
        }
1459
1460 1152
        $this->identityMap[$className][$idHash] = $entity;
1461
1462 1152
        return true;
1463
    }
1464
1465
    /**
1466
     * Gets the state of an entity with regard to the current unit of work.
1467
     *
1468
     * @param object   $entity
1469
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1470
     *                         This parameter can be set to improve performance of entity state detection
1471
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1472
     *                         is either known or does not matter for the caller of the method.
1473
     *
1474
     * @return int The entity state.
1475
     */
1476 1108
    public function getEntityState($entity, $assume = null)
1477
    {
1478 1108
        $oid = spl_object_hash($entity);
1479
1480 1108
        if (isset($this->entityStates[$oid])) {
1481 808
            return $this->entityStates[$oid];
1482
        }
1483
1484 1102
        if ($assume !== null) {
1485 1098
            return $assume;
1486
        }
1487
1488
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1489
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1490
        // the UoW does not hold references to such objects and the object hash can be reused.
1491
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1492 13
        $class = $this->em->getClassMetadata(get_class($entity));
1493 13
        $id    = $class->getIdentifierValues($entity);
1494
1495 13
        if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1496 5
            return self::STATE_NEW;
1497
        }
1498
1499 10
        if ($class->containsForeignIdentifier) {
1500 1
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1501
        }
1502
1503
        switch (true) {
1504 10
            case ($class->isIdentifierNatural()):
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...
1505
                // Check for a version field, if available, to avoid a db lookup.
1506 5
                if ($class->isVersioned) {
1507 1
                    return ($class->getFieldValue($entity, $class->versionField))
1508
                        ? self::STATE_DETACHED
1509 1
                        : self::STATE_NEW;
1510
                }
1511
1512
                // Last try before db lookup: check the identity map.
1513 4
                if ($this->tryGetById($id, $class->rootEntityName)) {
1514 1
                    return self::STATE_DETACHED;
1515
                }
1516
1517
                // db lookup
1518 4
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1519
                    return self::STATE_DETACHED;
1520
                }
1521
1522 4
                return self::STATE_NEW;
1523
1524 5
            case ( ! $class->idGenerator->isPostInsertGenerator()):
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...
1525
                // if we have a pre insert generator we can't be sure that having an id
1526
                // really means that the entity exists. We have to verify this through
1527
                // the last resort: a db lookup
1528
1529
                // Last try before db lookup: check the identity map.
1530
                if ($this->tryGetById($id, $class->rootEntityName)) {
1531
                    return self::STATE_DETACHED;
1532
                }
1533
1534
                // db lookup
1535
                if ($this->getEntityPersister($class->name)->exists($entity)) {
1536
                    return self::STATE_DETACHED;
1537
                }
1538
1539
                return self::STATE_NEW;
1540
1541
            default:
1542 5
                return self::STATE_DETACHED;
1543
        }
1544
    }
1545
1546
    /**
1547
     * INTERNAL:
1548
     * Removes an entity from the identity map. This effectively detaches the
1549
     * entity from the persistence management of Doctrine.
1550
     *
1551
     * @ignore
1552
     *
1553
     * @param object $entity
1554
     *
1555
     * @return boolean
1556
     *
1557
     * @throws ORMInvalidArgumentException
1558
     */
1559 80
    public function removeFromIdentityMap($entity)
1560
    {
1561 80
        $oid           = spl_object_hash($entity);
1562 80
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1563 80
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1564
1565 80
        if ($idHash === '') {
1566
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1567
        }
1568
1569 80
        $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...
1570
1571 80
        if (isset($this->identityMap[$className][$idHash])) {
1572 80
            unset($this->identityMap[$className][$idHash]);
1573 80
            unset($this->readOnlyObjects[$oid]);
1574
1575
            //$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...
1576
1577 80
            return true;
1578
        }
1579
1580
        return false;
1581
    }
1582
1583
    /**
1584
     * INTERNAL:
1585
     * Gets an entity in the identity map by its identifier hash.
1586
     *
1587
     * @ignore
1588
     *
1589
     * @param string $idHash
1590
     * @param string $rootClassName
1591
     *
1592
     * @return object
1593
     */
1594 6
    public function getByIdHash($idHash, $rootClassName)
1595
    {
1596 6
        return $this->identityMap[$rootClassName][$idHash];
1597
    }
1598
1599
    /**
1600
     * INTERNAL:
1601
     * Tries to get an entity by its identifier hash. If no entity is found for
1602
     * the given hash, FALSE is returned.
1603
     *
1604
     * @ignore
1605
     *
1606
     * @param mixed  $idHash        (must be possible to cast it to string)
1607
     * @param string $rootClassName
1608
     *
1609
     * @return object|bool The found entity or FALSE.
1610
     */
1611 35 View Code Duplication
    public function tryGetByIdHash($idHash, $rootClassName)
1612
    {
1613 35
        $stringIdHash = (string) $idHash;
1614
1615 35
        return isset($this->identityMap[$rootClassName][$stringIdHash])
1616 35
            ? $this->identityMap[$rootClassName][$stringIdHash]
1617 35
            : false;
1618
    }
1619
1620
    /**
1621
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1622
     *
1623
     * @param object $entity
1624
     *
1625
     * @return boolean
1626
     */
1627 219
    public function isInIdentityMap($entity)
1628
    {
1629 219
        $oid = spl_object_hash($entity);
1630
1631 219
        if (empty($this->entityIdentifiers[$oid])) {
1632 33
            return false;
1633
        }
1634
1635 203
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1636 203
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1637
1638 203
        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...
1639
    }
1640
1641
    /**
1642
     * INTERNAL:
1643
     * Checks whether an identifier hash exists in the identity map.
1644
     *
1645
     * @ignore
1646
     *
1647
     * @param string $idHash
1648
     * @param string $rootClassName
1649
     *
1650
     * @return boolean
1651
     */
1652
    public function containsIdHash($idHash, $rootClassName)
1653
    {
1654
        return isset($this->identityMap[$rootClassName][$idHash]);
1655
    }
1656
1657
    /**
1658
     * Persists an entity as part of the current unit of work.
1659
     *
1660
     * @param object $entity The entity to persist.
1661
     *
1662
     * @return void
1663
     */
1664 1089
    public function persist($entity)
1665
    {
1666 1089
        $visited = [];
1667
1668 1089
        $this->doPersist($entity, $visited);
1669 1082
    }
1670
1671
    /**
1672
     * Persists an entity as part of the current unit of work.
1673
     *
1674
     * This method is internally called during persist() cascades as it tracks
1675
     * the already visited entities to prevent infinite recursions.
1676
     *
1677
     * @param object $entity  The entity to persist.
1678
     * @param array  $visited The already visited entities.
1679
     *
1680
     * @return void
1681
     *
1682
     * @throws ORMInvalidArgumentException
1683
     * @throws UnexpectedValueException
1684
     */
1685 1089
    private function doPersist($entity, array &$visited)
1686
    {
1687 1089
        $oid = spl_object_hash($entity);
1688
1689 1089
        if (isset($visited[$oid])) {
1690 110
            return; // Prevent infinite recursion
1691
        }
1692
1693 1089
        $visited[$oid] = $entity; // Mark visited
1694
1695 1089
        $class = $this->em->getClassMetadata(get_class($entity));
1696
1697
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1698
        // If we would detect DETACHED here we would throw an exception anyway with the same
1699
        // consequences (not recoverable/programming error), so just assuming NEW here
1700
        // lets us avoid some database lookups for entities with natural identifiers.
1701 1089
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1702
1703
        switch ($entityState) {
1704 1089
            case self::STATE_MANAGED:
1705
                // Nothing to do, except if policy is "deferred explicit"
1706 235
                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

1706
                if ($class->/** @scrutinizer ignore-call */ isChangeTrackingDeferredExplicit()) {
Loading history...
1707 2
                    $this->scheduleForDirtyCheck($entity);
1708
                }
1709 235
                break;
1710
1711 1089
            case self::STATE_NEW:
1712 1088
                $this->persistNew($class, $entity);
1713 1088
                break;
1714
1715 1
            case self::STATE_REMOVED:
1716
                // Entity becomes managed again
1717 1
                unset($this->entityDeletions[$oid]);
1718 1
                $this->addToIdentityMap($entity);
1719
1720 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1721 1
                break;
1722
1723
            case self::STATE_DETACHED:
1724
                // Can actually not happen right now since we assume STATE_NEW.
1725
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1726
1727
            default:
1728
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1729
        }
1730
1731 1089
        $this->cascadePersist($entity, $visited);
1732 1082
    }
1733
1734
    /**
1735
     * Deletes an entity as part of the current unit of work.
1736
     *
1737
     * @param object $entity The entity to remove.
1738
     *
1739
     * @return void
1740
     */
1741 66
    public function remove($entity)
1742
    {
1743 66
        $visited = [];
1744
1745 66
        $this->doRemove($entity, $visited);
1746 66
    }
1747
1748
    /**
1749
     * Deletes an entity as part of the current unit of work.
1750
     *
1751
     * This method is internally called during delete() cascades as it tracks
1752
     * the already visited entities to prevent infinite recursions.
1753
     *
1754
     * @param object $entity  The entity to delete.
1755
     * @param array  $visited The map of the already visited entities.
1756
     *
1757
     * @return void
1758
     *
1759
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1760
     * @throws UnexpectedValueException
1761
     */
1762 66
    private function doRemove($entity, array &$visited)
1763
    {
1764 66
        $oid = spl_object_hash($entity);
1765
1766 66
        if (isset($visited[$oid])) {
1767 1
            return; // Prevent infinite recursion
1768
        }
1769
1770 66
        $visited[$oid] = $entity; // mark visited
1771
1772
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1773
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1774 66
        $this->cascadeRemove($entity, $visited);
1775
1776 66
        $class       = $this->em->getClassMetadata(get_class($entity));
1777 66
        $entityState = $this->getEntityState($entity);
1778
1779
        switch ($entityState) {
1780 66
            case self::STATE_NEW:
1781 66
            case self::STATE_REMOVED:
1782
                // nothing to do
1783 2
                break;
1784
1785 66
            case self::STATE_MANAGED:
1786 66
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1787
1788 66 View Code Duplication
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1789 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1790
                }
1791
1792 66
                $this->scheduleForDelete($entity);
1793 66
                break;
1794
1795
            case self::STATE_DETACHED:
1796
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1797
            default:
1798
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1799
        }
1800
1801 66
    }
1802
1803
    /**
1804
     * Merges the state of the given detached entity into this UnitOfWork.
1805
     *
1806
     * @param object $entity
1807
     *
1808
     * @return object The managed copy of the entity.
1809
     *
1810
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1811
     *         attribute and the version check against the managed copy fails.
1812
     *
1813
     * @deprecated
1814
     */
1815 44
    public function merge($entity)
1816
    {
1817 44
        $visited = [];
1818
1819 44
        return $this->doMerge($entity, $visited);
1820
    }
1821
1822
    /**
1823
     * Executes a merge operation on an entity.
1824
     *
1825
     * @param object      $entity
1826
     * @param array       $visited
1827
     * @param object|null $prevManagedCopy
1828
     * @param array|null  $assoc
1829
     *
1830
     * @return object The managed copy of the entity.
1831
     *
1832
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1833
     *         attribute and the version check against the managed copy fails.
1834
     * @throws ORMInvalidArgumentException If the entity instance is NEW.
1835
     * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided
1836
     */
1837 44
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = [])
1838
    {
1839 44
        $oid = spl_object_hash($entity);
1840
1841 44
        if (isset($visited[$oid])) {
1842 4
            $managedCopy = $visited[$oid];
1843
1844 4
            if ($prevManagedCopy !== null) {
1845 4
                $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1846
            }
1847
1848 4
            return $managedCopy;
1849
        }
1850
1851 44
        $class = $this->em->getClassMetadata(get_class($entity));
1852
1853
        // First we assume DETACHED, although it can still be NEW but we can avoid
1854
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1855
        // we need to fetch it from the db anyway in order to merge.
1856
        // MANAGED entities are ignored by the merge operation.
1857 44
        $managedCopy = $entity;
1858
1859 44
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1860
            // Try to look the entity up in the identity map.
1861 42
            $id = $class->getIdentifierValues($entity);
1862
1863
            // If there is no ID, it is actually NEW.
1864 42
            if ( ! $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1865 6
                $managedCopy = $this->newInstance($class);
1866
1867 6
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1868 6
                $this->persistNew($class, $managedCopy);
1869
            } else {
1870 37
                $flatId = ($class->containsForeignIdentifier)
1871 3
                    ? $this->identifierFlattener->flattenIdentifier($class, $id)
1872 37
                    : $id;
1873
1874 37
                $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
1875
1876 37
                if ($managedCopy) {
1877
                    // We have the entity in-memory already, just make sure its not removed.
1878 15
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
1879 15
                        throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge");
1880
                    }
1881
                } else {
1882
                    // We need to fetch the managed copy in order to merge.
1883 25
                    $managedCopy = $this->em->find($class->name, $flatId);
1884
                }
1885
1886 37
                if ($managedCopy === null) {
1887
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1888
                    // since the managed entity was not found.
1889 3
                    if ( ! $class->isIdentifierNatural()) {
1890 1
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1891 1
                            $class->getName(),
1892 1
                            $this->identifierFlattener->flattenIdentifier($class, $id)
1893
                        );
1894
                    }
1895
1896 2
                    $managedCopy = $this->newInstance($class);
1897 2
                    $class->setIdentifierValues($managedCopy, $id);
1898
1899 2
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1900 2
                    $this->persistNew($class, $managedCopy);
1901
                } else {
1902 34
                    $this->ensureVersionMatch($class, $entity, $managedCopy);
1903 33
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1904
                }
1905
            }
1906
1907 40
            $visited[$oid] = $managedCopy; // mark visited
1908
1909 40
            if ($class->isChangeTrackingDeferredExplicit()) {
1910
                $this->scheduleForDirtyCheck($entity);
1911
            }
1912
        }
1913
1914 42
        if ($prevManagedCopy !== null) {
1915 6
            $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1916
        }
1917
1918
        // Mark the managed copy visited as well
1919 42
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1920
1921 42
        $this->cascadeMerge($entity, $managedCopy, $visited);
1922
1923 42
        return $managedCopy;
1924
    }
1925
1926
    /**
1927
     * @param ClassMetadata $class
1928
     * @param object        $entity
1929
     * @param object        $managedCopy
1930
     *
1931
     * @return void
1932
     *
1933
     * @throws OptimisticLockException
1934
     */
1935 34
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
1936
    {
1937 34
        if (! ($class->isVersioned && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
1938 31
            return;
1939
        }
1940
1941 4
        $reflField          = $class->reflFields[$class->versionField];
1942 4
        $managedCopyVersion = $reflField->getValue($managedCopy);
1943 4
        $entityVersion      = $reflField->getValue($entity);
1944
1945
        // Throw exception if versions don't match.
1946 4
        if ($managedCopyVersion == $entityVersion) {
1947 3
            return;
1948
        }
1949
1950 1
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
1951
    }
1952
1953
    /**
1954
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
1955
     *
1956
     * @param object $entity
1957
     *
1958
     * @return bool
1959
     */
1960 41
    private function isLoaded($entity)
1961
    {
1962 41
        return !($entity instanceof Proxy) || $entity->__isInitialized();
1963
    }
1964
1965
    /**
1966
     * Sets/adds associated managed copies into the previous entity's association field
1967
     *
1968
     * @param object $entity
1969
     * @param array  $association
1970
     * @param object $previousManagedCopy
1971
     * @param object $managedCopy
1972
     *
1973
     * @return void
1974
     */
1975 6
    private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy)
1976
    {
1977 6
        $assocField = $association['fieldName'];
1978 6
        $prevClass  = $this->em->getClassMetadata(get_class($previousManagedCopy));
1979
1980 6
        if ($association['type'] & ClassMetadata::TO_ONE) {
1981 6
            $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1982
1983 6
            return;
1984
        }
1985
1986 1
        $value   = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
1987 1
        $value[] = $managedCopy;
1988
1989 1 View Code Duplication
        if ($association['type'] == ClassMetadata::ONE_TO_MANY) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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

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

2677
                                $associatedId[$targetClass->/** @scrutinizer ignore-call */ getFieldForColumn($targetColumn)] = $joinColumnValue;
Loading history...
2678
                            } else {
2679 298
                                $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...
2680
                            }
2681 294
                        } elseif ($targetClass->containsForeignIdentifier
2682 294
                            && in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
2683
                        ) {
2684
                            // the missing key is part of target's entity primary key
2685 7
                            $associatedId = [];
2686 499
                            break;
2687
                        }
2688
                    }
2689
2690 499
                    if ( ! $associatedId) {
2691
                        // Foreign key is NULL
2692 294
                        $class->reflFields[$field]->setValue($entity, null);
2693 294
                        $this->originalEntityData[$oid][$field] = null;
2694
2695 294
                        continue;
2696
                    }
2697
2698 298
                    if ( ! isset($hints['fetchMode'][$class->name][$field])) {
2699 295
                        $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2700
                    }
2701
2702
                    // Foreign key is set
2703
                    // Check identity map first
2704
                    // FIXME: Can break easily with composite keys if join column values are in
2705
                    //        wrong order. The correct order is the one in ClassMetadata#identifier.
2706 298
                    $relatedIdHash = implode(' ', $associatedId);
2707
2708
                    switch (true) {
2709 298
                        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...
2710 171
                            $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2711
2712
                            // If this is an uninitialized proxy, we are deferring eager loads,
2713
                            // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2714
                            // then we can append this entity for eager loading!
2715 171
                            if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2716 171
                                isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2717 171
                                !$targetClass->isIdentifierComposite &&
2718 171
                                $newValue instanceof Proxy &&
2719 171
                                $newValue->__isInitialized__ === false) {
2720
2721
                                $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2722
                            }
2723
2724 171
                            break;
2725
2726 203
                        case ($targetClass->subClasses):
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...
2727
                            // If it might be a subtype, it can not be lazy. There isn't even
2728
                            // a way to solve this with deferred eager loading, which means putting
2729
                            // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2730 32
                            $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2731 32
                            break;
2732
2733
                        default:
2734
                            switch (true) {
2735
                                // We are negating the condition here. Other cases will assume it is valid!
2736 173
                                case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2737 166
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2738 166
                                    break;
2739
2740
                                // Deferred eager load only works for single identifier classes
2741 7
                                case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite):
2742
                                    // TODO: Is there a faster approach?
2743 7
                                    $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2744
2745 7
                                    $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2746 7
                                    break;
2747
2748
                                default:
2749
                                    // TODO: This is very imperformant, ignore it?
2750
                                    $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2751
                                    break;
2752
                            }
2753
2754
                            // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2755 173
                            $newValueOid = spl_object_hash($newValue);
2756 173
                            $this->entityIdentifiers[$newValueOid] = $associatedId;
2757 173
                            $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2758
2759
                            if (
2760 173
                                $newValue instanceof NotifyPropertyChanged &&
2761 173
                                ( ! $newValue instanceof Proxy || $newValue->__isInitialized())
2762
                            ) {
2763
                                $newValue->addPropertyChangedListener($this);
2764
                            }
2765 173
                            $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2766
                            // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2767 173
                            break;
2768
                    }
2769
2770 298
                    $this->originalEntityData[$oid][$field] = $newValue;
2771 298
                    $class->reflFields[$field]->setValue($entity, $newValue);
2772
2773 298
                    if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2774 58
                        $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
2775 58
                        $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2776
                    }
2777
2778 298
                    break;
2779
2780
                default:
2781
                    // Ignore if its a cached collection
2782 497
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
0 ignored issues
show
Bug introduced by
The method getFieldValue() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getFieldNames()? ( Ignorable by Annotation )

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

2782
                    if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->/** @scrutinizer ignore-call */ getFieldValue($entity, $field) instanceof PersistentCollection) {

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...
2783
                        break;
2784
                    }
2785
2786
                    // use the given collection
2787 497
                    if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
2788
2789 3
                        $data[$field]->setOwner($entity, $assoc);
2790
2791 3
                        $class->reflFields[$field]->setValue($entity, $data[$field]);
2792 3
                        $this->originalEntityData[$oid][$field] = $data[$field];
2793
2794 3
                        break;
2795
                    }
2796
2797
                    // Inject collection
2798 497
                    $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2799 497
                    $pColl->setOwner($entity, $assoc);
2800 497
                    $pColl->setInitialized(false);
2801
2802 497
                    $reflField = $class->reflFields[$field];
2803 497
                    $reflField->setValue($entity, $pColl);
2804
2805 497
                    if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2806 4
                        $this->loadCollection($pColl);
2807 4
                        $pColl->takeSnapshot();
2808
                    }
2809
2810 497
                    $this->originalEntityData[$oid][$field] = $pColl;
2811 586
                    break;
2812
            }
2813
        }
2814
2815
        // defer invoking of postLoad event to hydration complete step
2816 706
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2817
2818 706
        return $entity;
2819
    }
2820
2821
    /**
2822
     * @return void
2823
     */
2824 916
    public function triggerEagerLoads()
2825
    {
2826 916
        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...
2827 916
            return;
2828
        }
2829
2830
        // avoid infinite recursion
2831 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2832 7
        $this->eagerLoadingEntities = [];
2833
2834 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2835 7
            if ( ! $ids) {
2836
                continue;
2837
            }
2838
2839 7
            $class = $this->em->getClassMetadata($entityName);
2840
2841 7
            $this->getEntityPersister($entityName)->loadAll(
2842 7
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2843
            );
2844
        }
2845 7
    }
2846
2847
    /**
2848
     * Initializes (loads) an uninitialized persistent collection of an entity.
2849
     *
2850
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2851
     *
2852
     * @return void
2853
     *
2854
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2855
     */
2856 147
    public function loadCollection(PersistentCollection $collection)
2857
    {
2858 147
        $assoc     = $collection->getMapping();
2859 147
        $persister = $this->getEntityPersister($assoc['targetEntity']);
2860
2861 147
        switch ($assoc['type']) {
2862 147
            case ClassMetadata::ONE_TO_MANY:
2863 77
                $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2864 77
                break;
2865
2866 84
            case ClassMetadata::MANY_TO_MANY:
2867 84
                $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2868 84
                break;
2869
        }
2870
2871 147
        $collection->setInitialized(true);
2872 147
    }
2873
2874
    /**
2875
     * Gets the identity map of the UnitOfWork.
2876
     *
2877
     * @return array
2878
     */
2879 2
    public function getIdentityMap()
2880
    {
2881 2
        return $this->identityMap;
2882
    }
2883
2884
    /**
2885
     * Gets the original data of an entity. The original data is the data that was
2886
     * present at the time the entity was reconstituted from the database.
2887
     *
2888
     * @param object $entity
2889
     *
2890
     * @return array
2891
     */
2892 121
    public function getOriginalEntityData($entity)
2893
    {
2894 121
        $oid = spl_object_hash($entity);
2895
2896 121
        return isset($this->originalEntityData[$oid])
2897 117
            ? $this->originalEntityData[$oid]
2898 121
            : [];
2899
    }
2900
2901
    /**
2902
     * @ignore
2903
     *
2904
     * @param object $entity
2905
     * @param array  $data
2906
     *
2907
     * @return void
2908
     */
2909
    public function setOriginalEntityData($entity, array $data)
2910
    {
2911
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2912
    }
2913
2914
    /**
2915
     * INTERNAL:
2916
     * Sets a property value of the original data array of an entity.
2917
     *
2918
     * @ignore
2919
     *
2920
     * @param string $oid
2921
     * @param string $property
2922
     * @param mixed  $value
2923
     *
2924
     * @return void
2925
     */
2926 313
    public function setOriginalEntityProperty($oid, $property, $value)
2927
    {
2928 313
        $this->originalEntityData[$oid][$property] = $value;
2929 313
    }
2930
2931
    /**
2932
     * Gets the identifier of an entity.
2933
     * The returned value is always an array of identifier values. If the entity
2934
     * has a composite identifier then the identifier values are in the same
2935
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2936
     *
2937
     * @param object $entity
2938
     *
2939
     * @return array The identifier values.
2940
     */
2941 868
    public function getEntityIdentifier($entity)
2942
    {
2943 868
        return $this->entityIdentifiers[spl_object_hash($entity)];
2944
    }
2945
2946
    /**
2947
     * Processes an entity instance to extract their identifier values.
2948
     *
2949
     * @param object $entity The entity instance.
2950
     *
2951
     * @return mixed A scalar value.
2952
     *
2953
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
2954
     */
2955 128
    public function getSingleIdentifierValue($entity)
2956
    {
2957 128
        $class = $this->em->getClassMetadata(get_class($entity));
2958
2959 128
        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...
2960
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2961
        }
2962
2963 128
        $values = $this->isInIdentityMap($entity)
2964 115
            ? $this->getEntityIdentifier($entity)
2965 128
            : $class->getIdentifierValues($entity);
2966
2967 128
        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...
2968
    }
2969
2970
    /**
2971
     * Tries to find an entity with the given identifier in the identity map of
2972
     * this UnitOfWork.
2973
     *
2974
     * @param mixed  $id            The entity identifier to look for.
2975
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
2976
     *
2977
     * @return object|bool Returns the entity with the specified identifier if it exists in
2978
     *                     this UnitOfWork, FALSE otherwise.
2979
     */
2980 553 View Code Duplication
    public function tryGetById($id, $rootClassName)
2981
    {
2982 553
        $idHash = implode(' ', (array) $id);
2983
2984 553
        return isset($this->identityMap[$rootClassName][$idHash])
2985 86
            ? $this->identityMap[$rootClassName][$idHash]
2986 553
            : false;
2987
    }
2988
2989
    /**
2990
     * Schedules an entity for dirty-checking at commit-time.
2991
     *
2992
     * @param object $entity The entity to schedule for dirty-checking.
2993
     *
2994
     * @return void
2995
     *
2996
     * @todo Rename: scheduleForSynchronization
2997
     */
2998 5
    public function scheduleForDirtyCheck($entity)
2999
    {
3000 5
        $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...
3001
3002 5
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
3003 5
    }
3004
3005
    /**
3006
     * Checks whether the UnitOfWork has any pending insertions.
3007
     *
3008
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
3009
     */
3010
    public function hasPendingInsertions()
3011
    {
3012
        return ! empty($this->entityInsertions);
3013
    }
3014
3015
    /**
3016
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
3017
     * number of entities in the identity map.
3018
     *
3019
     * @return integer
3020
     */
3021 1
    public function size()
3022
    {
3023 1
        $countArray = array_map('count', $this->identityMap);
3024
3025 1
        return array_sum($countArray);
3026
    }
3027
3028
    /**
3029
     * Gets the EntityPersister for an Entity.
3030
     *
3031
     * @param string $entityName The name of the Entity.
3032
     *
3033
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
3034
     */
3035 1126
    public function getEntityPersister($entityName)
3036
    {
3037 1126
        if (isset($this->persisters[$entityName])) {
3038 887
            return $this->persisters[$entityName];
3039
        }
3040
3041 1126
        $class = $this->em->getClassMetadata($entityName);
3042
3043
        switch (true) {
3044 1126
            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

3044
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeNone()):
Loading history...
3045 1080
                $persister = new BasicEntityPersister($this->em, $class);
3046 1080
                break;
3047
3048 388
            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

3048
            case ($class->/** @scrutinizer ignore-call */ isInheritanceTypeSingleTable()):
Loading history...
3049 225
                $persister = new SingleTablePersister($this->em, $class);
3050 225
                break;
3051
3052 356
            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

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