Completed
Pull Request — master (#7162)
by
unknown
11:54
created

UnitOfWork::getSingleIdentifierValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 14
ccs 8
cts 9
cp 0.8889
crap 3.0123
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Collections\Collection;
8
use Doctrine\Common\EventManager;
9
use Doctrine\Common\NotifyPropertyChanged;
10
use Doctrine\Common\PropertyChangedListener;
11
use Doctrine\DBAL\LockMode;
12
use Doctrine\Instantiator\Instantiator;
13
use Doctrine\ORM\Cache\Persister\CachedPersister;
14
use Doctrine\ORM\Event\LifecycleEventArgs;
15
use Doctrine\ORM\Event\ListenersInvoker;
16
use Doctrine\ORM\Event\OnFlushEventArgs;
17
use Doctrine\ORM\Event\PostFlushEventArgs;
18
use Doctrine\ORM\Event\PreFlushEventArgs;
19
use Doctrine\ORM\Event\PreUpdateEventArgs;
20
use Doctrine\ORM\Internal\HydrationCompleteHandler;
21
use Doctrine\ORM\Mapping\AssociationMetadata;
22
use Doctrine\ORM\Mapping\ChangeTrackingPolicy;
23
use Doctrine\ORM\Mapping\ClassMetadata;
24
use Doctrine\ORM\Mapping\FetchMode;
25
use Doctrine\ORM\Mapping\FieldMetadata;
26
use Doctrine\ORM\Mapping\GeneratorType;
27
use Doctrine\ORM\Mapping\InheritanceType;
28
use Doctrine\ORM\Mapping\JoinColumnMetadata;
29
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
30
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
31
use Doctrine\ORM\Mapping\OneToOneAssociationMetadata;
32
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
33
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
34
use Doctrine\ORM\Mapping\VersionFieldMetadata;
35
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
36
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
37
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
38
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
39
use Doctrine\ORM\Persisters\Entity\EntityPersister;
40
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
41
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
42
use Doctrine\ORM\Utility\NormalizeIdentifier;
43
use InvalidArgumentException;
44
use ProxyManager\Proxy\GhostObjectInterface;
45
use UnexpectedValueException;
46
use function array_combine;
47
use function array_diff_key;
48
use function array_filter;
49
use function array_key_exists;
50
use function array_map;
51
use function array_merge;
52
use function array_pop;
53
use function array_reverse;
54
use function array_sum;
55
use function array_values;
56
use function current;
57
use function get_class;
58
use function implode;
59
use function in_array;
60
use function is_array;
61
use function is_object;
62
use function method_exists;
63
use function spl_object_id;
64
use function sprintf;
65
66
/**
67
 * The UnitOfWork is responsible for tracking changes to objects during an
68
 * "object-level" transaction and for writing out changes to the database
69
 * in the correct order.
70
 *
71
 * {@internal This class contains highly performance-sensitive code. }}
72
 */
73
class UnitOfWork implements PropertyChangedListener
74
{
75
    /**
76
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
77
     */
78
    public const STATE_MANAGED = 1;
79
80
    /**
81
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
82
     * and is not (yet) managed by an EntityManager.
83
     */
84
    public const STATE_NEW = 2;
85
86
    /**
87
     * A detached entity is an instance with persistent state and identity that is not
88
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
89
     */
90
    public const STATE_DETACHED = 3;
91
92
    /**
93
     * A removed entity instance is an instance with a persistent identity,
94
     * associated with an EntityManager, whose persistent state will be deleted
95
     * on commit.
96
     */
97
    public const STATE_REMOVED = 4;
98
99
    /**
100
     * Hint used to collect all primary keys of associated entities during hydration
101
     * and execute it in a dedicated query afterwards
102
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
103
     */
104
    public const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
105
106
    /**
107
     * The identity map that holds references to all managed entities that have
108
     * an identity. The entities are grouped by their class name.
109
     * Since all classes in a hierarchy must share the same identifier set,
110
     * we always take the root class name of the hierarchy.
111
     *
112
     * @var object[]
113
     */
114
    private $identityMap = [];
115
116
    /**
117
     * Map of all identifiers of managed entities.
118
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
119
     * Values are maps of entity identifiers, where its key is the column name and the value is the raw value.
120
     *
121
     * @var mixed[][]
122
     */
123
    private $entityIdentifiers = [];
124
125
    /**
126
     * Map of the original entity data of managed entities.
127
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
128
     * Values are maps of entity data, where its key is the field name and the value is the converted
129
     * (convertToPHPValue) value.
130
     * This structure is used for calculating changesets at commit time.
131
     *
132
     * Internal: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
133
     *           A value will only really be copied if the value in the entity is modified by the user.
134
     *
135
     * @var mixed[][]
136
     */
137
    private $originalEntityData = [];
138
139
    /**
140
     * Map of entity changes. Keys are object ids (spl_object_id).
141
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
142
     *
143
     * @var mixed[][]
144
     */
145
    private $entityChangeSets = [];
146
147
    /**
148
     * The (cached) states of any known entities.
149
     * Keys are object ids (spl_object_id).
150
     *
151
     * @var int[]
152
     */
153
    private $entityStates = [];
154
155
    /**
156
     * Map of entities that are scheduled for dirty checking at commit time.
157
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
158
     * Keys are object ids (spl_object_id).
159
     *
160
     * @var object[][]
161
     */
162
    private $scheduledForSynchronization = [];
163
164
    /**
165
     * A list of all pending entity insertions.
166
     *
167
     * @var object[]
168
     */
169
    private $entityInsertions = [];
170
171
    /**
172
     * A list of all pending entity updates.
173
     *
174
     * @var object[]
175
     */
176
    private $entityUpdates = [];
177
178
    /**
179
     * Any pending extra updates that have been scheduled by persisters.
180
     *
181
     * @var object[]
182
     */
183
    private $extraUpdates = [];
184
185
    /**
186
     * A list of all pending entity deletions.
187
     *
188
     * @var object[]
189
     */
190
    private $entityDeletions = [];
191
192
    /**
193
     * New entities that were discovered through relationships that were not
194
     * marked as cascade-persist. During flush, this array is populated and
195
     * then pruned of any entities that were discovered through a valid
196
     * cascade-persist path. (Leftovers cause an error.)
197
     *
198
     * Keys are OIDs, payload is a two-item array describing the association
199
     * and the entity.
200
     *
201
     * @var object[][]|array[][] indexed by respective object spl_object_id()
202
     */
203
    private $nonCascadedNewDetectedEntities = [];
204
205
    /**
206
     * All pending collection deletions.
207
     *
208
     * @var Collection[]|object[][]
209
     */
210
    private $collectionDeletions = [];
211
212
    /**
213
     * All pending collection updates.
214
     *
215
     * @var Collection[]|object[][]
216
     */
217
    private $collectionUpdates = [];
218
219
    /**
220
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
221
     * At the end of the UnitOfWork all these collections will make new snapshots
222
     * of their data.
223
     *
224
     * @var Collection[]|object[][]
225
     */
226
    private $visitedCollections = [];
227
228
    /**
229
     * The EntityManager that "owns" this UnitOfWork instance.
230
     *
231
     * @var EntityManagerInterface
232
     */
233
    private $em;
234
235
    /**
236
     * The entity persister instances used to persist entity instances.
237
     *
238
     * @var EntityPersister[]
239
     */
240
    private $entityPersisters = [];
241
242
    /**
243
     * The collection persister instances used to persist collections.
244
     *
245
     * @var CollectionPersister[]
246
     */
247
    private $collectionPersisters = [];
248
249
    /**
250
     * The EventManager used for dispatching events.
251
     *
252
     * @var EventManager
253
     */
254
    private $eventManager;
255
256
    /**
257
     * The ListenersInvoker used for dispatching events.
258
     *
259
     * @var ListenersInvoker
260
     */
261
    private $listenersInvoker;
262
263
    /** @var Instantiator */
264
    private $instantiator;
265
266
    /**
267
     * Orphaned entities that are scheduled for removal.
268
     *
269
     * @var object[]
270
     */
271
    private $orphanRemovals = [];
272
273
    /**
274
     * Read-Only objects are never evaluated
275
     *
276
     * @var object[]
277
     */
278
    private $readOnlyObjects = [];
279
280
    /**
281
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
282
     *
283
     * @var mixed[][][]
284
     */
285
    private $eagerLoadingEntities = [];
286
287
    /** @var bool */
288
    protected $hasCache = false;
289
290
    /**
291
     * Helper for handling completion of hydration
292
     *
293
     * @var HydrationCompleteHandler
294
     */
295
    private $hydrationCompleteHandler;
296
297
    /** @var NormalizeIdentifier */
298
    private $normalizeIdentifier;
299
300
    /**
301
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
302
     */
303 2248
    public function __construct(EntityManagerInterface $em)
304
    {
305 2248
        $this->em                       = $em;
306 2248
        $this->eventManager             = $em->getEventManager();
307 2248
        $this->listenersInvoker         = new ListenersInvoker($em);
308 2248
        $this->hasCache                 = $em->getConfiguration()->isSecondLevelCacheEnabled();
309 2248
        $this->instantiator             = new Instantiator();
310 2248
        $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
311 2248
        $this->normalizeIdentifier      = new NormalizeIdentifier();
312 2248
    }
313
314
    /**
315
     * Commits the UnitOfWork, executing all operations that have been postponed
316
     * up to this point. The state of all managed entities will be synchronized with
317
     * the database.
318
     *
319
     * The operations are executed in the following order:
320
     *
321
     * 1) All entity insertions
322
     * 2) All entity updates
323
     * 3) All collection deletions
324
     * 4) All collection updates
325
     * 5) All entity deletions
326
     *
327
     * @throws \Exception
328
     */
329 1013
    public function commit()
330
    {
331
        // Raise preFlush
332 1013
        if ($this->eventManager->hasListeners(Events::preFlush)) {
333 2
            $this->eventManager->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
334
        }
335
336 1013
        $this->computeChangeSets();
337
338 1011
        if (! ($this->entityInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array<mixed,object> 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...
339 158
                $this->entityDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array<mixed,object> 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...
340 123
                $this->entityUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array<mixed,object> 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...
341 37
                $this->collectionUpdates ||
342 33
                $this->collectionDeletions ||
343 1011
                $this->orphanRemovals)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array<mixed,object> 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...
344 21
            $this->dispatchOnFlushEvent();
345 21
            $this->dispatchPostFlushEvent();
346
347 21
            return; // Nothing to do.
348
        }
349
350 1006
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
351
352 1004
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array<mixed,object> 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 16
            foreach ($this->orphanRemovals as $orphan) {
354 16
                $this->remove($orphan);
355
            }
356
        }
357
358 1004
        $this->dispatchOnFlushEvent();
359
360
        // Now we need a commit order to maintain referential integrity
361 1004
        $commitOrder = $this->getCommitOrder();
362
363 1004
        $conn = $this->em->getConnection();
364 1004
        $conn->beginTransaction();
365
366
        try {
367
            // Collection deletions (deletions of complete collections)
368 1004
            foreach ($this->collectionDeletions as $collectionToDelete) {
369 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
370
            }
371
372 1004
            if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array<mixed,object> 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...
373 1000
                foreach ($commitOrder as $class) {
374 1000
                    $this->executeInserts($class);
375
                }
376
            }
377
378 1003
            if ($this->entityUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array<mixed,object> 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...
379 111
                foreach ($commitOrder as $class) {
380 111
                    $this->executeUpdates($class);
381
                }
382
            }
383
384
            // Extra updates that were requested by persisters.
385 999
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates of type array<mixed,object> 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...
386 29
                $this->executeExtraUpdates();
387
            }
388
389
            // Collection updates (deleteRows, updateRows, insertRows)
390 999
            foreach ($this->collectionUpdates as $collectionToUpdate) {
391 527
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
392
            }
393
394
            // Entity deletions come last and need to be in reverse commit order
395 999
            if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array<mixed,object> 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...
396 61
                foreach (array_reverse($commitOrder) as $committedEntityName) {
397 61
                    if (! $this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array<mixed,object> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
398 33
                        break; // just a performance optimisation
399
                    }
400
401 61
                    $this->executeDeletions($committedEntityName);
402
                }
403
            }
404
405 999
            $conn->commit();
406 10
        } catch (\Throwable $e) {
407 10
            $this->em->close();
408 10
            $conn->rollBack();
409
410 10
            $this->afterTransactionRolledBack();
411
412 10
            throw $e;
413
        }
414
415 999
        $this->afterTransactionComplete();
416
417
        // Take new snapshots from visited collections
418 999
        foreach ($this->visitedCollections as $coll) {
419 526
            $coll->takeSnapshot();
420
        }
421
422 999
        $this->dispatchPostFlushEvent();
423
424
        // Clean up
425 998
        $this->entityInsertions            =
426 998
        $this->entityUpdates               =
427 998
        $this->entityDeletions             =
428 998
        $this->extraUpdates                =
429 998
        $this->entityChangeSets            =
430 998
        $this->collectionUpdates           =
431 998
        $this->collectionDeletions         =
432 998
        $this->visitedCollections          =
433 998
        $this->scheduledForSynchronization =
434 998
        $this->orphanRemovals              = [];
435 998
    }
436
437
    /**
438
     * Computes the changesets of all entities scheduled for insertion.
439
     */
440 1013
    private function computeScheduleInsertsChangeSets()
441
    {
442 1013
        foreach ($this->entityInsertions as $entity) {
443 1004
            $class = $this->em->getClassMetadata(get_class($entity));
444
445 1004
            $this->computeChangeSet($class, $entity);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\UnitOfWork::computeChangeSet(). ( Ignorable by Annotation )

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

445
            $this->computeChangeSet(/** @scrutinizer ignore-type */ $class, $entity);
Loading history...
446
        }
447 1011
    }
448
449
    /**
450
     * Executes any extra updates that have been scheduled.
451
     */
452 29
    private function executeExtraUpdates()
453
    {
454 29
        foreach ($this->extraUpdates as $oid => $update) {
455 29
            list ($entity, $changeset) = $update;
456
457 29
            $this->entityChangeSets[$oid] = $changeset;
458
459
//            echo 'Extra update: ';
460
//            \Doctrine\Common\Util\Debug::dump($changeset, 3);
461
462 29
            $this->getEntityPersister(get_class($entity))->update($entity);
463
        }
464
465 29
        $this->extraUpdates = [];
466 29
    }
467
468
    /**
469
     * Gets the changeset for an entity.
470
     *
471
     * @param object $entity
472
     *
473
     * @return mixed[]
474
     */
475 999
    public function & getEntityChangeSet($entity)
476
    {
477 999
        $oid  = spl_object_id($entity);
478 999
        $data = [];
479
480 999
        if (! isset($this->entityChangeSets[$oid])) {
481 2
            return $data;
482
        }
483
484 999
        return $this->entityChangeSets[$oid];
485
    }
486
487
    /**
488
     * Computes the changes that happened to a single entity.
489
     *
490
     * Modifies/populates the following properties:
491
     *
492
     * {@link originalEntityData}
493
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
494
     * then it was not fetched from the database and therefore we have no original
495
     * entity data yet. All of the current entity data is stored as the original entity data.
496
     *
497
     * {@link entityChangeSets}
498
     * The changes detected on all properties of the entity are stored there.
499
     * A change is a tuple array where the first entry is the old value and the second
500
     * entry is the new value of the property. Changesets are used by persisters
501
     * to INSERT/UPDATE the persistent entity state.
502
     *
503
     * {@link entityUpdates}
504
     * If the entity is already fully MANAGED (has been fetched from the database before)
505
     * and any changes to its properties are detected, then a reference to the entity is stored
506
     * there to mark it for an update.
507
     *
508
     * {@link collectionDeletions}
509
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
510
     * then this collection is marked for deletion.
511
     *
512
     * @ignore
513
     *
514
     * @internal Don't call from the outside.
515
     *
516
     * @param ClassMetadata $class  The class descriptor of the entity.
517
     * @param object        $entity The entity for which to compute the changes.
518
     *
519
     */
520 1014
    public function computeChangeSet(ClassMetadata $class, $entity)
521
    {
522 1014
        $oid = spl_object_id($entity);
523
524 1014
        if (isset($this->readOnlyObjects[$oid])) {
525 2
            return;
526
        }
527
528 1014
        if ($class->inheritanceType !== InheritanceType::NONE) {
529 333
            $class = $this->em->getClassMetadata(get_class($entity));
530
        }
531
532 1014
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type Doctrine\Common\Persistence\Mapping\ClassMetadata; however, parameter $metadata of Doctrine\ORM\Event\Liste...:getSubscribedSystems() does only seem to accept Doctrine\ORM\Mapping\ClassMetadata, maybe add an additional type check? ( Ignorable by Annotation )

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

532
        $invoke = $this->listenersInvoker->getSubscribedSystems(/** @scrutinizer ignore-type */ $class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
Loading history...
533
534 1014
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
535 130
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type Doctrine\Common\Persistence\Mapping\ClassMetadata; however, parameter $metadata of Doctrine\ORM\Event\ListenersInvoker::invoke() does only seem to accept Doctrine\ORM\Mapping\ClassMetadata, maybe add an additional type check? ( Ignorable by Annotation )

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

535
            $this->listenersInvoker->invoke(/** @scrutinizer ignore-type */ $class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
Loading history...
536
        }
537
538 1014
        $actualData = [];
539
540 1014
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

540
        foreach ($class->/** @scrutinizer ignore-call */ getDeclaredPropertiesIterator() as $name => $property) {

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...
541 1014
            $value = $property->getValue($entity);
542
543 1014
            if ($property instanceof ToManyAssociationMetadata && $value !== null) {
544 774
                if ($value instanceof PersistentCollection && $value->getOwner() === $entity) {
545 187
                    continue;
546
                }
547
548 771
                $value = $property->wrap($entity, $value, $this->em);
549
550 771
                $property->setValue($entity, $value);
551
552 771
                $actualData[$name] = $value;
553
554 771
                continue;
555
            }
556
557 1014
            if (( ! $class->isIdentifier($name)
558 1014
                    || ! $class->getProperty($name) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

558
                    || ! $class->/** @scrutinizer ignore-call */ getProperty($name) instanceof FieldMetadata

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...
559 1014
                    || ! $class->getProperty($name)->hasValueGenerator()
0 ignored issues
show
Bug introduced by
The method hasValueGenerator() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

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

559
                    || ! $class->getProperty($name)->/** @scrutinizer ignore-call */ hasValueGenerator()
Loading history...
560 1014
                    || $class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
0 ignored issues
show
Bug introduced by
The method getValueGenerator() does not exist on Doctrine\ORM\Mapping\Property. Did you maybe mean getValue()? ( Ignorable by Annotation )

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

560
                    || $class->getProperty($name)->/** @scrutinizer ignore-call */ getValueGenerator()->getType() !== GeneratorType::IDENTITY

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...
561 1014
                ) && (! $class->isVersioned() || $name !== $class->versionProperty->getName())) {
0 ignored issues
show
Bug introduced by
The method isVersioned() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

561
                ) && (! $class->/** @scrutinizer ignore-call */ isVersioned() || $name !== $class->versionProperty->getName())) {

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...
562 1014
                $actualData[$name] = $value;
563
            }
564
        }
565
566 1014
        if (! isset($this->originalEntityData[$oid])) {
567
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
568
            // These result in an INSERT.
569 1010
            $this->originalEntityData[$oid] = $actualData;
570 1010
            $changeSet                      = [];
571
572 1010
            foreach ($actualData as $propName => $actualValue) {
573 990
                $property = $class->getProperty($propName);
574
575 990
                if (($property instanceof FieldMetadata) ||
576 990
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
577 990
                    $changeSet[$propName] = [null, $actualValue];
578
                }
579
            }
580
581 1010
            $this->entityChangeSets[$oid] = $changeSet;
582
        } else {
583
            // Entity is "fully" MANAGED: it was already fully persisted before
584
            // and we have a copy of the original data
585 255
            $originalData           = $this->originalEntityData[$oid];
586 255
            $isChangeTrackingNotify = $class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY;
587 255
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
588
                ? $this->entityChangeSets[$oid]
589 255
                : [];
590
591 255
            foreach ($actualData as $propName => $actualValue) {
592
                // skip field, its a partially omitted one!
593 243
                if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
594 40
                    continue;
595
                }
596
597 243
                $orgValue = $originalData[$propName];
598
599
                // skip if value haven't changed
600 243
                if ($orgValue === $actualValue) {
601 226
                    continue;
602
                }
603
604 109
                $property = $class->getProperty($propName);
605
606
                // Persistent collection was exchanged with the "originally"
607
                // created one. This can only mean it was cloned and replaced
608
                // on another entity.
609 109
                if ($actualValue instanceof PersistentCollection) {
610 8
                    $owner = $actualValue->getOwner();
611
612 8
                    if ($owner === null) { // cloned
613
                        $actualValue->setOwner($entity, $property);
614 8
                    } elseif ($owner !== $entity) { // no clone, we have to fix
615
                        if (! $actualValue->isInitialized()) {
616
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
617
                        }
618
619
                        $newValue = clone $actualValue;
620
621
                        $newValue->setOwner($entity, $property);
622
623
                        $property->setValue($entity, $newValue);
624
                    }
625
                }
626
627
                switch (true) {
628 109
                    case ($property instanceof FieldMetadata):
629 58
                        if ($isChangeTrackingNotify) {
630
                            // Continue inside switch behaves as break.
631
                            // We are required to use continue 2, since we need to continue to next $actualData item
632
                            continue 2;
633
                        }
634
635 58
                        $changeSet[$propName] = [$orgValue, $actualValue];
636 58
                        break;
637
638 57
                    case ($property instanceof ToOneAssociationMetadata):
639 46
                        if ($property->isOwningSide()) {
640 20
                            $changeSet[$propName] = [$orgValue, $actualValue];
641
                        }
642
643 46
                        if ($orgValue !== null && $property->isOrphanRemoval()) {
644 4
                            $this->scheduleOrphanRemoval($orgValue);
645
                        }
646
647 46
                        break;
648
649 12
                    case ($property instanceof ToManyAssociationMetadata):
650
                        // Check if original value exists
651 9
                        if ($orgValue instanceof PersistentCollection) {
652
                            // A PersistentCollection was de-referenced, so delete it.
653 8
                            if (! $this->isCollectionScheduledForDeletion($orgValue)) {
654 8
                                $this->scheduleCollectionDeletion($orgValue);
655
656 8
                                $changeSet[$propName] = $orgValue; // Signal changeset, to-many associations will be ignored
657
                            }
658
                        }
659
660 9
                        break;
661
662 109
                    default:
663
                        // Do nothing
664
                }
665
            }
666
667 255
            if ($changeSet) {
668 84
                $this->entityChangeSets[$oid]   = $changeSet;
669 84
                $this->originalEntityData[$oid] = $actualData;
670 84
                $this->entityUpdates[$oid]      = $entity;
671
            }
672
        }
673
674
        // Look for changes in associations of the entity
675 1014
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
676 1014
            if (! $property instanceof AssociationMetadata) {
677 1014
                continue;
678
            }
679
680 885
            $value = $property->getValue($entity);
681
682 885
            if ($value === null) {
683 623
                continue;
684
            }
685
686 861
            $this->computeAssociationChanges($property, $value);
687
688 853
            if ($property instanceof ManyToManyAssociationMetadata &&
689 853
                $value instanceof PersistentCollection &&
690 853
                ! isset($this->entityChangeSets[$oid]) &&
691 853
                $property->isOwningSide() &&
692 853
                $value->isDirty()) {
693 31
                $this->entityChangeSets[$oid]   = [];
694 31
                $this->originalEntityData[$oid] = $actualData;
695 853
                $this->entityUpdates[$oid]      = $entity;
696
            }
697
        }
698 1006
    }
699
700
    /**
701
     * Computes all the changes that have been done to entities and collections
702
     * since the last commit and stores these changes in the _entityChangeSet map
703
     * temporarily for access by the persisters, until the UoW commit is finished.
704
     */
705 1013
    public function computeChangeSets()
706
    {
707
        // Compute changes for INSERTed entities first. This must always happen.
708 1013
        $this->computeScheduleInsertsChangeSets();
709
710
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
711 1011
        foreach ($this->identityMap as $className => $entities) {
712 444
            $class = $this->em->getClassMetadata($className);
713
714
            // Skip class if instances are read-only
715 444
            if ($class->isReadOnly()) {
0 ignored issues
show
Bug introduced by
The method isReadOnly() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

715
            if ($class->/** @scrutinizer ignore-call */ isReadOnly()) {

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...
716 1
                continue;
717
            }
718
719
            // If change tracking is explicit or happens through notification, then only compute
720
            // changes on entities of that type that are explicitly marked for synchronization.
721
            switch (true) {
722 443
                case ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_IMPLICIT):
0 ignored issues
show
Bug introduced by
Accessing changeTrackingPolicy on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
723 441
                    $entitiesToProcess = $entities;
724 441
                    break;
725
726 3
                case (isset($this->scheduledForSynchronization[$className])):
727 3
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
728 3
                    break;
729
730
                default:
731 1
                    $entitiesToProcess = [];
732
            }
733
734 443
            foreach ($entitiesToProcess as $entity) {
735
                // Ignore uninitialized proxy objects
736 423
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
737 38
                    continue;
738
                }
739
740
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
741 421
                $oid = spl_object_id($entity);
742
743 421
                if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
744 443
                    $this->computeChangeSet($class, $entity);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\UnitOfWork::computeChangeSet(). ( Ignorable by Annotation )

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

744
                    $this->computeChangeSet(/** @scrutinizer ignore-type */ $class, $entity);
Loading history...
745
                }
746
            }
747
        }
748 1011
    }
749
750
    /**
751
     * Computes the changes of an association.
752
     *
753
     * @param AssociationMetadata $association The association mapping.
754
     * @param mixed               $value       The value of the association.
755
     *
756
     * @throws ORMInvalidArgumentException
757
     * @throws ORMException
758
     */
759 861
    private function computeAssociationChanges(AssociationMetadata $association, $value)
760
    {
761 861
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
762 31
            return;
763
        }
764
765 860
        if ($value instanceof PersistentCollection && $value->isDirty()) {
766 530
            $coid = spl_object_id($value);
767
768 530
            $this->collectionUpdates[$coid]  = $value;
769 530
            $this->visitedCollections[$coid] = $value;
770
        }
771
772
        // Look through the entities, and in any of their associations,
773
        // for transient (new) entities, recursively. ("Persistence by reachability")
774
        // Unwrap. Uninitialized collections will simply be empty.
775 860
        $unwrappedValue = ($association instanceof ToOneAssociationMetadata) ? [$value] : $value->unwrap();
776 860
        $targetEntity   = $association->getTargetEntity();
777 860
        $targetClass    = $this->em->getClassMetadata($targetEntity);
778
779 860
        foreach ($unwrappedValue as $key => $entry) {
780 716
            if (! ($entry instanceof $targetEntity)) {
781 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $association, $entry);
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetClass of Doctrine\ORM\ORMInvalidA...n::invalidAssociation(). ( Ignorable by Annotation )

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

781
                throw ORMInvalidArgumentException::invalidAssociation(/** @scrutinizer ignore-type */ $targetClass, $association, $entry);
Loading history...
782
            }
783
784 708
            $state = $this->getEntityState($entry, self::STATE_NEW);
785
786 708
            if (! ($entry instanceof $targetEntity)) {
787
                throw ORMException::unexpectedAssociationValue(
788
                    $association->getSourceEntity(),
789
                    $association->getName(),
790
                    get_class($entry),
791
                    $targetEntity
792
                );
793
            }
794
795
            switch ($state) {
796 708
                case self::STATE_NEW:
797 41
                    if (! in_array('persist', $association->getCascade(), true)) {
798 5
                        $this->nonCascadedNewDetectedEntities[spl_object_id($entry)] = [$association, $entry];
799
800 5
                        break;
801
                    }
802
803 37
                    $this->persistNew($targetClass, $entry);
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\UnitOfWork::persistNew(). ( Ignorable by Annotation )

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

803
                    $this->persistNew(/** @scrutinizer ignore-type */ $targetClass, $entry);
Loading history...
804 37
                    $this->computeChangeSet($targetClass, $entry);
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\UnitOfWork::computeChangeSet(). ( Ignorable by Annotation )

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

804
                    $this->computeChangeSet(/** @scrutinizer ignore-type */ $targetClass, $entry);
Loading history...
805
806 37
                    break;
807
808 701
                case self::STATE_REMOVED:
809
                    // Consume the $value as array (it's either an array or an ArrayAccess)
810
                    // and remove the element from Collection.
811 4
                    if ($association instanceof ToManyAssociationMetadata) {
812 3
                        unset($value[$key]);
813
                    }
814 4
                    break;
815
816 701
                case self::STATE_DETACHED:
817
                    // Can actually not happen right now as we assume STATE_NEW,
818
                    // so the exception will be raised from the DBAL layer (constraint violation).
819
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($association, $entry);
820
                    break;
821
822 708
                default:
823
                    // MANAGED associated entities are already taken into account
824
                    // during changeset calculation anyway, since they are in the identity map.
825
            }
826
        }
827 852
    }
828
829
    /**
830
     * @param ClassMetadata $class
831
     * @param object        $entity
832
     */
833 1025
    private function persistNew($class, $entity)
834
    {
835 1025
        $oid    = spl_object_id($entity);
836 1025
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
837
838 1025
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
839 132
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
840
        }
841
842 1025
        $generationPlan = $class->getValueGenerationPlan();
843 1025
        $persister      = $this->getEntityPersister($class->getClassName());
844 1025
        $generationPlan->executeImmediate($this->em, $entity);
845
846 1025
        if (! $generationPlan->containsDeferred()) {
847 221
            $id                            = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
848 221
            $this->entityIdentifiers[$oid] = $id;
849
        }
850
851 1025
        $this->entityStates[$oid] = self::STATE_MANAGED;
852
853 1025
        $this->scheduleForInsert($entity);
854 1025
    }
855
856
    /**
857
     * INTERNAL:
858
     * Computes the changeset of an individual entity, independently of the
859
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
860
     *
861
     * The passed entity must be a managed entity. If the entity already has a change set
862
     * because this method is invoked during a commit cycle then the change sets are added.
863
     * whereby changes detected in this method prevail.
864
     *
865
     * @ignore
866
     *
867
     * @param ClassMetadata $class  The class descriptor of the entity.
868
     * @param object        $entity The entity for which to (re)calculate the change set.
869
     *
870
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
871
     * @throws \RuntimeException
872
     */
873 15
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) : void
874
    {
875 15
        $oid = spl_object_id($entity);
876
877 15
        if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) {
878
            throw ORMInvalidArgumentException::entityNotManaged($entity);
879
        }
880
881
        // skip if change tracking is "NOTIFY"
882 15
        if ($class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) {
883
            return;
884
        }
885
886 15
        if ($class->inheritanceType !== InheritanceType::NONE) {
887 3
            $class = $this->em->getClassMetadata(get_class($entity));
888
        }
889
890 15
        $actualData = [];
891
892 15
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
893
            switch (true) {
894 15
                case ($property instanceof VersionFieldMetadata):
895
                    // Ignore version field
896
                    break;
897
898 15
                case ($property instanceof FieldMetadata):
899 15
                    if (! $property->isPrimaryKey()
900 15
                        || ! $property->getValueGenerator()
901 15
                        || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY) {
902 15
                        $actualData[$name] = $property->getValue($entity);
903
                    }
904
905 15
                    break;
906
907 11
                case ($property instanceof ToOneAssociationMetadata):
908 9
                    $actualData[$name] = $property->getValue($entity);
909 15
                    break;
910
            }
911
        }
912
913 15
        if (! isset($this->originalEntityData[$oid])) {
914
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
915
        }
916
917 15
        $originalData = $this->originalEntityData[$oid];
918 15
        $changeSet    = [];
919
920 15
        foreach ($actualData as $propName => $actualValue) {
921 15
            $orgValue = $originalData[$propName] ?? null;
922
923 15
            if ($orgValue !== $actualValue) {
924 15
                $changeSet[$propName] = [$orgValue, $actualValue];
925
            }
926
        }
927
928 15
        if ($changeSet) {
929 7
            if (isset($this->entityChangeSets[$oid])) {
930 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
931 1
            } elseif (! isset($this->entityInsertions[$oid])) {
932 1
                $this->entityChangeSets[$oid] = $changeSet;
933 1
                $this->entityUpdates[$oid]    = $entity;
934
            }
935 7
            $this->originalEntityData[$oid] = $actualData;
936
        }
937 15
    }
938
939
    /**
940
     * Executes all entity insertions for entities of the specified type.
941
     */
942 1000
    private function executeInserts(ClassMetadata $class) : void
943
    {
944 1000
        $className      = $class->getClassName();
945 1000
        $persister      = $this->getEntityPersister($className);
946 1000
        $invoke         = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
947 1000
        $generationPlan = $class->getValueGenerationPlan();
948
949 1000
        foreach ($this->entityInsertions as $oid => $entity) {
950 1000
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

950
            if ($this->em->getClassMetadata(get_class($entity))->/** @scrutinizer ignore-call */ getClassName() !== $className) {

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...
951 852
                continue;
952
            }
953
954 1000
            $persister->insert($entity);
955
956 999
            if ($generationPlan->containsDeferred()) {
957
                // Entity has post-insert IDs
958 936
                $oid = spl_object_id($entity);
959 936
                $id  = $persister->getIdentifier($entity);
960
961 936
                $this->entityIdentifiers[$oid]  = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
962 936
                $this->entityStates[$oid]       = self::STATE_MANAGED;
963 936
                $this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid];
964
965 936
                $this->addToIdentityMap($entity);
966
            }
967
968 999
            unset($this->entityInsertions[$oid]);
969
970 999
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
971 128
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
972
973 999
                $this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke);
974
            }
975
        }
976 1000
    }
977
978
    /**
979
     * Executes all entity updates for entities of the specified type.
980
     *
981
     * @param ClassMetadata $class
982
     */
983 111
    private function executeUpdates($class)
984
    {
985 111
        $className        = $class->getClassName();
986 111
        $persister        = $this->getEntityPersister($className);
987 111
        $preUpdateInvoke  = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
988 111
        $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
989
990 111
        foreach ($this->entityUpdates as $oid => $entity) {
991 111
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
992 72
                continue;
993
            }
994
995 111
            if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
996 12
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
997
998 12
                $this->recomputeSingleEntityChangeSet($class, $entity);
999
            }
1000
1001 111
            if (! empty($this->entityChangeSets[$oid])) {
1002
//                echo 'Update: ';
1003
//                \Doctrine\Common\Util\Debug::dump($this->entityChangeSets[$oid], 3);
1004
1005 81
                $persister->update($entity);
1006
            }
1007
1008 107
            unset($this->entityUpdates[$oid]);
1009
1010 107
            if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
1011 107
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1012
            }
1013
        }
1014 107
    }
1015
1016
    /**
1017
     * Executes all entity deletions for entities of the specified type.
1018
     *
1019
     * @param ClassMetadata $class
1020
     */
1021 61
    private function executeDeletions($class)
1022
    {
1023 61
        $className = $class->getClassName();
1024 61
        $persister = $this->getEntityPersister($className);
1025 61
        $invoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1026
1027 61
        foreach ($this->entityDeletions as $oid => $entity) {
1028 61
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
1029 25
                continue;
1030
            }
1031
1032 61
            $persister->delete($entity);
1033
1034
            unset(
1035 61
                $this->entityDeletions[$oid],
1036 61
                $this->entityIdentifiers[$oid],
1037 61
                $this->originalEntityData[$oid],
1038 61
                $this->entityStates[$oid]
1039
            );
1040
1041
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1042
            // is obtained by a new entity because the old one went out of scope.
1043
            //$this->entityStates[$oid] = self::STATE_NEW;
1044 61
            if (! $class->isIdentifierComposite()) {
1045 58
                $property = $class->getProperty($class->getSingleIdentifierFieldName());
1046
1047 58
                if ($property instanceof FieldMetadata && $property->hasValueGenerator()) {
1048 51
                    $property->setValue($entity, null);
1049
                }
1050
            }
1051
1052 61
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1053 9
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
1054
1055 61
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, $eventArgs, $invoke);
1056
            }
1057
        }
1058 60
    }
1059
1060
    /**
1061
     * Gets the commit order.
1062
     *
1063
     * @return ClassMetadata[]
1064
     */
1065 1004
    private function getCommitOrder()
1066
    {
1067 1004
        $calc = new Internal\CommitOrderCalculator();
1068
1069
        // See if there are any new classes in the changeset, that are not in the
1070
        // commit order graph yet (don't have a node).
1071
        // We have to inspect changeSet to be able to correctly build dependencies.
1072
        // It is not possible to use IdentityMap here because post inserted ids
1073
        // are not yet available.
1074 1004
        $newNodes = [];
1075
1076 1004
        foreach (array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) {
1077 1004
            $class = $this->em->getClassMetadata(get_class($entity));
1078
1079 1004
            if ($calc->hasNode($class->getClassName())) {
1080 633
                continue;
1081
            }
1082
1083 1004
            $calc->addNode($class->getClassName(), $class);
1084
1085 1004
            $newNodes[] = $class;
1086
        }
1087
1088
        // Calculate dependencies for new nodes
1089 1004
        while ($class = array_pop($newNodes)) {
1090 1004
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
1091 1004
                if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
1092 1004
                    continue;
1093
                }
1094
1095 832
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1096
1097 832
                if (! $calc->hasNode($targetClass->getClassName())) {
1098 635
                    $calc->addNode($targetClass->getClassName(), $targetClass);
1099
1100 635
                    $newNodes[] = $targetClass;
1101
                }
1102
1103 832
                $weight = ! array_filter(
1104 832
                    $property->getJoinColumns(),
1105 832
                    function (JoinColumnMetadata $joinColumn) {
1106 832
                        return $joinColumn->isNullable();
1107 832
                    }
1108
                );
1109
1110 832
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), $weight);
0 ignored issues
show
Bug introduced by
$weight of type boolean is incompatible with the type integer expected by parameter $weight of Doctrine\ORM\Internal\Co...ulator::addDependency(). ( Ignorable by Annotation )

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

1110
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), /** @scrutinizer ignore-type */ $weight);
Loading history...
1111
1112
                // If the target class has mapped subclasses, these share the same dependency.
1113 832
                if (! $targetClass->getSubClasses()) {
0 ignored issues
show
Bug introduced by
The method getSubClasses() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

1113
                if (! $targetClass->/** @scrutinizer ignore-call */ getSubClasses()) {

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...
1114 827
                    continue;
1115
                }
1116
1117 225
                foreach ($targetClass->getSubClasses() as $subClassName) {
1118 225
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1119
1120 225
                    if (! $calc->hasNode($subClassName)) {
1121 199
                        $calc->addNode($targetSubClass->getClassName(), $targetSubClass);
1122
1123 199
                        $newNodes[] = $targetSubClass;
1124
                    }
1125
1126 225
                    $calc->addDependency($targetSubClass->getClassName(), $class->getClassName(), 1);
1127
                }
1128
            }
1129
        }
1130
1131 1004
        return $calc->sort();
1132
    }
1133
1134
    /**
1135
     * Schedules an entity for insertion into the database.
1136
     * If the entity already has an identifier, it will be added to the identity map.
1137
     *
1138
     * @param object $entity The entity to schedule for insertion.
1139
     *
1140
     * @throws ORMInvalidArgumentException
1141
     * @throws \InvalidArgumentException
1142
     */
1143 1026
    public function scheduleForInsert($entity)
1144
    {
1145 1026
        $oid = spl_object_id($entity);
1146
1147 1026
        if (isset($this->entityUpdates[$oid])) {
1148
            throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
1149
        }
1150
1151 1026
        if (isset($this->entityDeletions[$oid])) {
1152 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1153
        }
1154 1026
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1155 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1156
        }
1157
1158 1026
        if (isset($this->entityInsertions[$oid])) {
1159 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1160
        }
1161
1162 1026
        $this->entityInsertions[$oid] = $entity;
1163
1164 1026
        if (isset($this->entityIdentifiers[$oid])) {
1165 221
            $this->addToIdentityMap($entity);
1166
        }
1167
1168 1026
        if ($entity instanceof NotifyPropertyChanged) {
1169 5
            $entity->addPropertyChangedListener($this);
1170
        }
1171 1026
    }
1172
1173
    /**
1174
     * Checks whether an entity is scheduled for insertion.
1175
     *
1176
     * @param object $entity
1177
     *
1178
     * @return bool
1179
     */
1180 620
    public function isScheduledForInsert($entity)
1181
    {
1182 620
        return isset($this->entityInsertions[spl_object_id($entity)]);
1183
    }
1184
1185
    /**
1186
     * Schedules an entity for being updated.
1187
     *
1188
     * @param object $entity The entity to schedule for being updated.
1189
     *
1190
     * @throws ORMInvalidArgumentException
1191
     */
1192 1
    public function scheduleForUpdate($entity) : void
1193
    {
1194 1
        $oid = spl_object_id($entity);
1195
1196 1
        if (! isset($this->entityIdentifiers[$oid])) {
1197
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update');
1198
        }
1199
1200 1
        if (isset($this->entityDeletions[$oid])) {
1201
            throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update');
1202
        }
1203
1204 1
        if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1205 1
            $this->entityUpdates[$oid] = $entity;
1206
        }
1207 1
    }
1208
1209
    /**
1210
     * INTERNAL:
1211
     * Schedules an extra update that will be executed immediately after the
1212
     * regular entity updates within the currently running commit cycle.
1213
     *
1214
     * Extra updates for entities are stored as (entity, changeset) tuples.
1215
     *
1216
     * @ignore
1217
     *
1218
     * @param object  $entity    The entity for which to schedule an extra update.
1219
     * @param mixed[] $changeset The changeset of the entity (what to update).
1220
     *
1221
     */
1222 29
    public function scheduleExtraUpdate($entity, array $changeset) : void
1223
    {
1224 29
        $oid         = spl_object_id($entity);
1225 29
        $extraUpdate = [$entity, $changeset];
1226
1227 29
        if (isset($this->extraUpdates[$oid])) {
1228 1
            [$unused, $changeset2] = $this->extraUpdates[$oid];
1229
1230 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1231
        }
1232
1233 29
        $this->extraUpdates[$oid] = $extraUpdate;
1234 29
    }
1235
1236
    /**
1237
     * Checks whether an entity is registered as dirty in the unit of work.
1238
     * Note: Is not very useful currently as dirty entities are only registered
1239
     * at commit time.
1240
     *
1241
     * @param object $entity
1242
     */
1243
    public function isScheduledForUpdate($entity) : bool
1244
    {
1245
        return isset($this->entityUpdates[spl_object_id($entity)]);
1246
    }
1247
1248
    /**
1249
     * Checks whether an entity is registered to be checked in the unit of work.
1250
     *
1251
     * @param object $entity
1252
     */
1253 1
    public function isScheduledForDirtyCheck($entity) : bool
1254
    {
1255 1
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->getRootClassName();
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

1255
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->/** @scrutinizer ignore-call */ getRootClassName();

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...
1256
1257 1
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]);
1258
    }
1259
1260
    /**
1261
     * INTERNAL:
1262
     * Schedules an entity for deletion.
1263
     *
1264
     * @param object $entity
1265
     */
1266 64
    public function scheduleForDelete($entity)
1267
    {
1268 64
        $oid = spl_object_id($entity);
1269
1270 64
        if (isset($this->entityInsertions[$oid])) {
1271 1
            if ($this->isInIdentityMap($entity)) {
1272
                $this->removeFromIdentityMap($entity);
1273
            }
1274
1275 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1276
1277 1
            return; // entity has not been persisted yet, so nothing more to do.
1278
        }
1279
1280 64
        if (! $this->isInIdentityMap($entity)) {
1281 1
            return;
1282
        }
1283
1284 63
        $this->removeFromIdentityMap($entity);
1285
1286 63
        unset($this->entityUpdates[$oid]);
1287
1288 63
        if (! isset($this->entityDeletions[$oid])) {
1289 63
            $this->entityDeletions[$oid] = $entity;
1290 63
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1291
        }
1292 63
    }
1293
1294
    /**
1295
     * Checks whether an entity is registered as removed/deleted with the unit
1296
     * of work.
1297
     *
1298
     * @param object $entity
1299
     *
1300
     * @return bool
1301
     */
1302 13
    public function isScheduledForDelete($entity)
1303
    {
1304 13
        return isset($this->entityDeletions[spl_object_id($entity)]);
1305
    }
1306
1307
    /**
1308
     * Checks whether an entity is scheduled for insertion, update or deletion.
1309
     *
1310
     * @param object $entity
1311
     *
1312
     * @return bool
1313
     */
1314
    public function isEntityScheduled($entity)
1315
    {
1316
        $oid = spl_object_id($entity);
1317
1318
        return isset($this->entityInsertions[$oid])
1319
            || isset($this->entityUpdates[$oid])
1320
            || isset($this->entityDeletions[$oid]);
1321
    }
1322
1323
    /**
1324
     * INTERNAL:
1325
     * Registers an entity in the identity map.
1326
     * Note that entities in a hierarchy are registered with the class name of
1327
     * the root entity.
1328
     *
1329
     * @ignore
1330
     *
1331
     * @param object $entity The entity to register.
1332
     *
1333
     * @return bool  TRUE if the registration was successful, FALSE if the identity of
1334
     *               the entity in question is already managed.
1335
     *
1336
     * @throws ORMInvalidArgumentException
1337
     */
1338 1094
    public function addToIdentityMap($entity)
1339
    {
1340 1094
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1341 1094
        $identifier    = $this->entityIdentifiers[spl_object_id($entity)];
1342
1343 1094
        if (empty($identifier) || in_array(null, $identifier, true)) {
1344 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->getClassName(), $entity);
1345
        }
1346
1347 1088
        $idHash    = implode(' ', $identifier);
1348 1088
        $className = $classMetadata->getRootClassName();
1349
1350 1088
        if (isset($this->identityMap[$className][$idHash])) {
1351 31
            return false;
1352
        }
1353
1354 1088
        $this->identityMap[$className][$idHash] = $entity;
1355
1356 1088
        return true;
1357
    }
1358
1359
    /**
1360
     * Gets the state of an entity with regard to the current unit of work.
1361
     *
1362
     * @param object   $entity
1363
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1364
     *                         This parameter can be set to improve performance of entity state detection
1365
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1366
     *                         is either known or does not matter for the caller of the method.
1367
     *
1368
     * @return int The entity state.
1369
     */
1370 1034
    public function getEntityState($entity, $assume = null)
1371
    {
1372 1034
        $oid = spl_object_id($entity);
1373
1374 1034
        if (isset($this->entityStates[$oid])) {
1375 748
            return $this->entityStates[$oid];
1376
        }
1377
1378 1029
        if ($assume !== null) {
1379 1026
            return $assume;
1380
        }
1381
1382
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1383
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1384
        // the UoW does not hold references to such objects and the object hash can be reused.
1385
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1386 7
        $class     = $this->em->getClassMetadata(get_class($entity));
1387 7
        $persister = $this->getEntityPersister($class->getClassName());
1388 7
        $id        = $persister->getIdentifier($entity);
1389
1390 7
        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...
1391 2
            return self::STATE_NEW;
1392
        }
1393
1394 6
        $flatId = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Ide...er::flattenIdentifier(). ( Ignorable by Annotation )

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

1394
        $flatId = $this->em->getIdentifierFlattener()->flattenIdentifier(/** @scrutinizer ignore-type */ $class, $id);
Loading history...
1395
1396 6
        if ($class->isIdentifierComposite()
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()? ( Ignorable by Annotation )

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

1396
        if ($class->/** @scrutinizer ignore-call */ isIdentifierComposite()

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...
1397 5
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

1397
            || ! $class->getProperty($class->/** @scrutinizer ignore-call */ getSingleIdentifierFieldName()) instanceof FieldMetadata

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...
1398 6
            || ! $class->getProperty($class->getSingleIdentifierFieldName())->hasValueGenerator()
1399
        ) {
1400
            // Check for a version field, if available, to avoid a db lookup.
1401 5
            if ($class->isVersioned()) {
1402 1
                return $class->versionProperty->getValue($entity)
0 ignored issues
show
Bug introduced by
Accessing versionProperty on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1403
                    ? self::STATE_DETACHED
1404 1
                    : self::STATE_NEW;
1405
            }
1406
1407
            // Last try before db lookup: check the identity map.
1408 4
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
1409 1
                return self::STATE_DETACHED;
1410
            }
1411
1412
            // db lookup
1413 4
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
1414
                return self::STATE_DETACHED;
1415
            }
1416
1417 4
            return self::STATE_NEW;
1418
        }
1419
1420 1
        if ($class->isIdentifierComposite()
1421 1
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
1422 1
            || ! $class->getValueGenerationPlan()->containsDeferred()) {
0 ignored issues
show
Bug introduced by
The method getValueGenerationPlan() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

1422
            || ! $class->/** @scrutinizer ignore-call */ getValueGenerationPlan()->containsDeferred()) {

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...
1423
            // if we have a pre insert generator we can't be sure that having an id
1424
            // really means that the entity exists. We have to verify this through
1425
            // the last resort: a db lookup
1426
1427
            // Last try before db lookup: check the identity map.
1428
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
1429
                return self::STATE_DETACHED;
1430
            }
1431
1432
            // db lookup
1433
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
1434
                return self::STATE_DETACHED;
1435
            }
1436
1437
            return self::STATE_NEW;
1438
        }
1439
1440 1
        return self::STATE_DETACHED;
1441
    }
1442
1443
    /**
1444
     * INTERNAL:
1445
     * Removes an entity from the identity map. This effectively detaches the
1446
     * entity from the persistence management of Doctrine.
1447
     *
1448
     * @ignore
1449
     *
1450
     * @param object $entity
1451
     *
1452
     * @return bool
1453
     *
1454
     * @throws ORMInvalidArgumentException
1455
     */
1456 63
    public function removeFromIdentityMap($entity)
1457
    {
1458 63
        $oid           = spl_object_id($entity);
1459 63
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1460 63
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1461
1462 63
        if ($idHash === '') {
1463
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
1464
        }
1465
1466 63
        $className = $classMetadata->getRootClassName();
1467
1468 63
        if (isset($this->identityMap[$className][$idHash])) {
1469 63
            unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]);
1470
1471
            //$this->entityStates[$oid] = self::STATE_DETACHED;
1472
1473 63
            return true;
1474
        }
1475
1476
        return false;
1477
    }
1478
1479
    /**
1480
     * INTERNAL:
1481
     * Gets an entity in the identity map by its identifier hash.
1482
     *
1483
     * @ignore
1484
     *
1485
     * @param string $idHash
1486
     * @param string $rootClassName
1487
     *
1488
     * @return object
1489
     */
1490 6
    public function getByIdHash($idHash, $rootClassName)
1491
    {
1492 6
        return $this->identityMap[$rootClassName][$idHash];
1493
    }
1494
1495
    /**
1496
     * INTERNAL:
1497
     * Tries to get an entity by its identifier hash. If no entity is found for
1498
     * the given hash, FALSE is returned.
1499
     *
1500
     * @ignore
1501
     *
1502
     * @param mixed  $idHash        (must be possible to cast it to string)
1503
     * @param string $rootClassName
1504
     *
1505
     * @return object|bool The found entity or FALSE.
1506
     */
1507
    public function tryGetByIdHash($idHash, $rootClassName)
1508
    {
1509
        $stringIdHash = (string) $idHash;
1510
1511
        return $this->identityMap[$rootClassName][$stringIdHash] ?? false;
1512
    }
1513
1514
    /**
1515
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1516
     *
1517
     * @param object $entity
1518
     *
1519
     * @return bool
1520
     */
1521 149
    public function isInIdentityMap($entity)
1522
    {
1523 149
        $oid = spl_object_id($entity);
1524
1525 149
        if (empty($this->entityIdentifiers[$oid])) {
1526 25
            return false;
1527
        }
1528
1529 134
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1530 134
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1531
1532 134
        return isset($this->identityMap[$classMetadata->getRootClassName()][$idHash]);
1533
    }
1534
1535
    /**
1536
     * INTERNAL:
1537
     * Checks whether an identifier hash exists in the identity map.
1538
     *
1539
     * @ignore
1540
     *
1541
     * @param string $idHash
1542
     * @param string $rootClassName
1543
     *
1544
     * @return bool
1545
     */
1546
    public function containsIdHash($idHash, $rootClassName)
1547
    {
1548
        return isset($this->identityMap[$rootClassName][$idHash]);
1549
    }
1550
1551
    /**
1552
     * Persists an entity as part of the current unit of work.
1553
     *
1554
     * @param object $entity The entity to persist.
1555
     */
1556 1026
    public function persist($entity)
1557
    {
1558 1026
        $visited = [];
1559
1560 1026
        $this->doPersist($entity, $visited);
1561 1019
    }
1562
1563
    /**
1564
     * Persists an entity as part of the current unit of work.
1565
     *
1566
     * This method is internally called during persist() cascades as it tracks
1567
     * the already visited entities to prevent infinite recursions.
1568
     *
1569
     * @param object   $entity  The entity to persist.
1570
     * @param object[] $visited The already visited entities.
1571
     *
1572
     * @throws ORMInvalidArgumentException
1573
     * @throws UnexpectedValueException
1574
     */
1575 1026
    private function doPersist($entity, array &$visited)
1576
    {
1577 1026
        $oid = spl_object_id($entity);
1578
1579 1026
        if (isset($visited[$oid])) {
1580 109
            return; // Prevent infinite recursion
1581
        }
1582
1583 1026
        $visited[$oid] = $entity; // Mark visited
1584
1585 1026
        $class = $this->em->getClassMetadata(get_class($entity));
1586
1587
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1588
        // If we would detect DETACHED here we would throw an exception anyway with the same
1589
        // consequences (not recoverable/programming error), so just assuming NEW here
1590
        // lets us avoid some database lookups for entities with natural identifiers.
1591 1026
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1592
1593
        switch ($entityState) {
1594 1026
            case self::STATE_MANAGED:
1595
                // Nothing to do, except if policy is "deferred explicit"
1596 219
                if ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_EXPLICIT) {
0 ignored issues
show
Bug introduced by
Accessing changeTrackingPolicy on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1597 2
                    $this->scheduleForSynchronization($entity);
1598
                }
1599 219
                break;
1600
1601 1026
            case self::STATE_NEW:
1602 1025
                $this->persistNew($class, $entity);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\UnitOfWork::persistNew(). ( Ignorable by Annotation )

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

1602
                $this->persistNew(/** @scrutinizer ignore-type */ $class, $entity);
Loading history...
1603 1025
                break;
1604
1605 1
            case self::STATE_REMOVED:
1606
                // Entity becomes managed again
1607 1
                unset($this->entityDeletions[$oid]);
1608 1
                $this->addToIdentityMap($entity);
1609
1610 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1611 1
                break;
1612
1613
            case self::STATE_DETACHED:
1614
                // Can actually not happen right now since we assume STATE_NEW.
1615
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted');
1616
1617
            default:
1618
                throw new UnexpectedValueException(
1619
                    sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity))
1620
                );
1621
        }
1622
1623 1026
        $this->cascadePersist($entity, $visited);
1624 1019
    }
1625
1626
    /**
1627
     * Deletes an entity as part of the current unit of work.
1628
     *
1629
     * @param object $entity The entity to remove.
1630
     */
1631 63
    public function remove($entity)
1632
    {
1633 63
        $visited = [];
1634
1635 63
        $this->doRemove($entity, $visited);
1636 63
    }
1637
1638
    /**
1639
     * Deletes an entity as part of the current unit of work.
1640
     *
1641
     * This method is internally called during delete() cascades as it tracks
1642
     * the already visited entities to prevent infinite recursions.
1643
     *
1644
     * @param object   $entity  The entity to delete.
1645
     * @param object[] $visited The map of the already visited entities.
1646
     *
1647
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1648
     * @throws UnexpectedValueException
1649
     */
1650 63
    private function doRemove($entity, array &$visited)
1651
    {
1652 63
        $oid = spl_object_id($entity);
1653
1654 63
        if (isset($visited[$oid])) {
1655 1
            return; // Prevent infinite recursion
1656
        }
1657
1658 63
        $visited[$oid] = $entity; // mark visited
1659
1660
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1661
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1662 63
        $this->cascadeRemove($entity, $visited);
1663
1664 63
        $class       = $this->em->getClassMetadata(get_class($entity));
1665 63
        $entityState = $this->getEntityState($entity);
1666
1667
        switch ($entityState) {
1668 63
            case self::STATE_NEW:
1669 63
            case self::STATE_REMOVED:
1670
                // nothing to do
1671 1
                break;
1672
1673 63
            case self::STATE_MANAGED:
1674 63
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Event\Liste...:getSubscribedSystems(). ( Ignorable by Annotation )

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

1674
                $invoke = $this->listenersInvoker->getSubscribedSystems(/** @scrutinizer ignore-type */ $class, Events::preRemove);
Loading history...
1675
1676 63
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1677 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Event\ListenersInvoker::invoke(). ( Ignorable by Annotation )

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

1677
                    $this->listenersInvoker->invoke(/** @scrutinizer ignore-type */ $class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
Loading history...
1678
                }
1679
1680 63
                $this->scheduleForDelete($entity);
1681 63
                break;
1682
1683
            case self::STATE_DETACHED:
1684
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed');
1685
            default:
1686
                throw new UnexpectedValueException(
1687
                    sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity))
1688
                );
1689
        }
1690 63
    }
1691
1692
    /**
1693
     * Refreshes the state of the given entity from the database, overwriting
1694
     * any local, unpersisted changes.
1695
     *
1696
     * @param object $entity The entity to refresh.
1697
     *
1698
     * @throws InvalidArgumentException If the entity is not MANAGED.
1699
     */
1700 15
    public function refresh($entity)
1701
    {
1702 15
        $visited = [];
1703
1704 15
        $this->doRefresh($entity, $visited);
1705 15
    }
1706
1707
    /**
1708
     * Executes a refresh operation on an entity.
1709
     *
1710
     * @param object   $entity  The entity to refresh.
1711
     * @param object[] $visited The already visited entities during cascades.
1712
     *
1713
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
1714
     */
1715 15
    private function doRefresh($entity, array &$visited)
1716
    {
1717 15
        $oid = spl_object_id($entity);
1718
1719 15
        if (isset($visited[$oid])) {
1720
            return; // Prevent infinite recursion
1721
        }
1722
1723 15
        $visited[$oid] = $entity; // mark visited
1724
1725 15
        $class = $this->em->getClassMetadata(get_class($entity));
1726
1727 15
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1728
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1729
        }
1730
1731 15
        $this->getEntityPersister($class->getClassName())->refresh(
1732 15
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1733 15
            $entity
1734
        );
1735
1736 15
        $this->cascadeRefresh($entity, $visited);
1737 15
    }
1738
1739
    /**
1740
     * Cascades a refresh operation to associated entities.
1741
     *
1742
     * @param object   $entity
1743
     * @param object[] $visited
1744
     */
1745 15
    private function cascadeRefresh($entity, array &$visited)
1746
    {
1747 15
        $class = $this->em->getClassMetadata(get_class($entity));
1748
1749 15
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1750 15
            if (! ($association instanceof AssociationMetadata && in_array('refresh', $association->getCascade(), true))) {
1751 15
                continue;
1752
            }
1753
1754 4
            $relatedEntities = $association->getValue($entity);
1755
1756
            switch (true) {
1757 4
                case ($relatedEntities instanceof PersistentCollection):
1758
                    // Unwrap so that foreach() does not initialize
1759 4
                    $relatedEntities = $relatedEntities->unwrap();
1760
                    // break; is commented intentionally!
1761
1762
                case ($relatedEntities instanceof Collection):
1763
                case (is_array($relatedEntities)):
1764 4
                    foreach ($relatedEntities as $relatedEntity) {
1765
                        $this->doRefresh($relatedEntity, $visited);
1766
                    }
1767 4
                    break;
1768
1769
                case ($relatedEntities !== null):
1770
                    $this->doRefresh($relatedEntities, $visited);
1771
                    break;
1772
1773 4
                default:
1774
                    // Do nothing
1775
            }
1776
        }
1777 15
    }
1778
1779
    /**
1780
     * Cascades the save operation to associated entities.
1781
     *
1782
     * @param object   $entity
1783
     * @param object[] $visited
1784
     *
1785
     * @throws ORMInvalidArgumentException
1786
     */
1787 1026
    private function cascadePersist($entity, array &$visited)
1788
    {
1789 1026
        $class = $this->em->getClassMetadata(get_class($entity));
1790
1791 1026
        if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1792
            // nothing to do - proxy is not initialized, therefore we don't do anything with it
1793 1
            return;
1794
        }
1795
1796 1026
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1797 1026
            if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade(), true))) {
1798 1026
                continue;
1799
            }
1800
1801
            /** @var AssociationMetadata $association */
1802 648
            $relatedEntities = $association->getValue($entity);
1803 648
            $targetEntity    = $association->getTargetEntity();
1804
1805
            switch (true) {
1806 648
                case ($relatedEntities instanceof PersistentCollection):
1807
                    // Unwrap so that foreach() does not initialize
1808 13
                    $relatedEntities = $relatedEntities->unwrap();
1809
                    // break; is commented intentionally!
1810
1811 648
                case ($relatedEntities instanceof Collection):
1812 584
                case (is_array($relatedEntities)):
1813 543
                    if (! ($association instanceof ToManyAssociationMetadata)) {
1814 3
                        throw ORMInvalidArgumentException::invalidAssociation(
1815 3
                            $this->em->getClassMetadata($targetEntity),
0 ignored issues
show
Bug introduced by
$this->em->getClassMetadata($targetEntity) of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetClass of Doctrine\ORM\ORMInvalidA...n::invalidAssociation(). ( Ignorable by Annotation )

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

1815
                            /** @scrutinizer ignore-type */ $this->em->getClassMetadata($targetEntity),
Loading history...
1816 3
                            $association,
1817 3
                            $relatedEntities
1818
                        );
1819
                    }
1820
1821 540
                    foreach ($relatedEntities as $relatedEntity) {
1822 283
                        $this->doPersist($relatedEntity, $visited);
1823
                    }
1824
1825 540
                    break;
1826
1827 573
                case ($relatedEntities !== null):
1828 241
                    if (! $relatedEntities instanceof $targetEntity) {
1829 4
                        throw ORMInvalidArgumentException::invalidAssociation(
1830 4
                            $this->em->getClassMetadata($targetEntity),
1831 4
                            $association,
1832 4
                            $relatedEntities
1833
                        );
1834
                    }
1835
1836 237
                    $this->doPersist($relatedEntities, $visited);
1837 237
                    break;
1838
1839 642
                default:
1840
                    // Do nothing
1841
            }
1842
        }
1843 1019
    }
1844
1845
    /**
1846
     * Cascades the delete operation to associated entities.
1847
     *
1848
     * @param object   $entity
1849
     * @param object[] $visited
1850
     */
1851 63
    private function cascadeRemove($entity, array &$visited)
1852
    {
1853 63
        $entitiesToCascade = [];
1854 63
        $class             = $this->em->getClassMetadata(get_class($entity));
1855
1856 63
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1857 63
            if (! ($association instanceof AssociationMetadata && in_array('remove', $association->getCascade(), true))) {
1858 63
                continue;
1859
            }
1860
1861 25
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1862 6
                $entity->initializeProxy();
1863
            }
1864
1865 25
            $relatedEntities = $association->getValue($entity);
1866
1867
            switch (true) {
1868 25
                case ($relatedEntities instanceof Collection):
1869 18
                case (is_array($relatedEntities)):
1870
                    // If its a PersistentCollection initialization is intended! No unwrap!
1871 20
                    foreach ($relatedEntities as $relatedEntity) {
1872 10
                        $entitiesToCascade[] = $relatedEntity;
1873
                    }
1874 20
                    break;
1875
1876 18
                case ($relatedEntities !== null):
1877 7
                    $entitiesToCascade[] = $relatedEntities;
1878 7
                    break;
1879
1880 25
                default:
1881
                    // Do nothing
1882
            }
1883
        }
1884
1885 63
        foreach ($entitiesToCascade as $relatedEntity) {
1886 16
            $this->doRemove($relatedEntity, $visited);
1887
        }
1888 63
    }
1889
1890
    /**
1891
     * Acquire a lock on the given entity.
1892
     *
1893
     * @param object $entity
1894
     * @param int    $lockMode
1895
     * @param int    $lockVersion
1896
     *
1897
     * @throws ORMInvalidArgumentException
1898
     * @throws TransactionRequiredException
1899
     * @throws OptimisticLockException
1900
     * @throws \InvalidArgumentException
1901
     */
1902 10
    public function lock($entity, $lockMode, $lockVersion = null)
1903
    {
1904 10
        if ($entity === null) {
1905 1
            throw new \InvalidArgumentException('No entity passed to UnitOfWork#lock().');
1906
        }
1907
1908 9
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1909 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1910
        }
1911
1912 8
        $class = $this->em->getClassMetadata(get_class($entity));
1913
1914
        switch (true) {
1915 8
            case $lockMode === LockMode::OPTIMISTIC:
1916 6
                if (! $class->isVersioned()) {
1917 2
                    throw OptimisticLockException::notVersioned($class->getClassName());
1918
                }
1919
1920 4
                if ($lockVersion === null) {
1921
                    return;
1922
                }
1923
1924 4
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1925 1
                    $entity->initializeProxy();
1926
                }
1927
1928 4
                $entityVersion = $class->versionProperty->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing versionProperty on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1929
1930 4
                if ($entityVersion !== $lockVersion) {
1931 2
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
1932
                }
1933
1934 2
                break;
1935
1936 2
            case $lockMode === LockMode::NONE:
1937 2
            case $lockMode === LockMode::PESSIMISTIC_READ:
1938 1
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1939 2
                if (! $this->em->getConnection()->isTransactionActive()) {
1940 2
                    throw TransactionRequiredException::transactionRequired();
1941
                }
1942
1943
                $oid = spl_object_id($entity);
1944
1945
                $this->getEntityPersister($class->getClassName())->lock(
1946
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1947
                    $lockMode
1948
                );
1949
                break;
1950
1951
            default:
1952
                // Do nothing
1953
        }
1954 2
    }
1955
1956
    /**
1957
     * Clears the UnitOfWork.
1958
     */
1959 1200
    public function clear()
1960
    {
1961 1200
        $this->entityPersisters               =
1962 1200
        $this->collectionPersisters           =
1963 1200
        $this->eagerLoadingEntities           =
1964 1200
        $this->identityMap                    =
1965 1200
        $this->entityIdentifiers              =
1966 1200
        $this->originalEntityData             =
1967 1200
        $this->entityChangeSets               =
1968 1200
        $this->entityStates                   =
1969 1200
        $this->scheduledForSynchronization    =
1970 1200
        $this->entityInsertions               =
1971 1200
        $this->entityUpdates                  =
1972 1200
        $this->entityDeletions                =
1973 1200
        $this->collectionDeletions            =
1974 1200
        $this->collectionUpdates              =
1975 1200
        $this->extraUpdates                   =
1976 1200
        $this->readOnlyObjects                =
1977 1200
        $this->visitedCollections             =
1978 1200
        $this->nonCascadedNewDetectedEntities =
1979 1200
        $this->orphanRemovals                 = [];
1980 1200
    }
1981
1982
    /**
1983
     * INTERNAL:
1984
     * Schedules an orphaned entity for removal. The remove() operation will be
1985
     * invoked on that entity at the beginning of the next commit of this
1986
     * UnitOfWork.
1987
     *
1988
     * @ignore
1989
     *
1990
     * @param object $entity
1991
     */
1992 17
    public function scheduleOrphanRemoval($entity)
1993
    {
1994
        // Entities should not be scheduled for orphan removal,
1995
        // if they do not exist in the identity map or if they are not about to be inserted.
1996 17
        if (!$this->isInIdentityMap($entity) && !$this->isScheduledForInsert($entity)) {
0 ignored issues
show
Coding Style introduced by
There must be a single space after a NOT operator; 0 found
Loading history...
1997 2
            return;
1998
        }
1999
2000 16
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
0 ignored issues
show
introduced by
Function spl_object_hash() should not be referenced via a fallback global name, but via a use statement.
Loading history...
2001 16
    }
2002
2003
    /**
2004
     * INTERNAL:
2005
     * Cancels a previously scheduled orphan removal.
2006
     *
2007
     * @ignore
2008
     *
2009
     * @param object $entity
2010
     */
2011 112
    public function cancelOrphanRemoval($entity)
2012
    {
2013 112
        unset($this->orphanRemovals[spl_object_id($entity)]);
2014 112
    }
2015
2016
    /**
2017
     * INTERNAL:
2018
     * Schedules a complete collection for removal when this UnitOfWork commits.
2019
     */
2020 22
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2021
    {
2022 22
        $coid = spl_object_id($coll);
2023
2024
        // TODO: if $coll is already scheduled for recreation ... what to do?
2025
        // Just remove $coll from the scheduled recreations?
2026 22
        unset($this->collectionUpdates[$coid]);
2027
2028 22
        $this->collectionDeletions[$coid] = $coll;
2029 22
    }
2030
2031
    /**
2032
     * @return bool
2033
     */
2034 8
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2035
    {
2036 8
        return isset($this->collectionDeletions[spl_object_id($coll)]);
2037
    }
2038
2039
    /**
2040
     * INTERNAL:
2041
     * Creates a new instance of the mapped class, without invoking the constructor.
2042
     * This is only meant to be used internally, and should not be consumed by end users.
2043
     *
2044
     * @ignore
2045
     *
2046
     * @return EntityManagerAware|object
2047
     */
2048 662
    public function newInstance(ClassMetadata $class)
2049
    {
2050 662
        $entity = $this->instantiator->instantiate($class->getClassName());
2051
2052 662
        if ($entity instanceof EntityManagerAware) {
2053 5
            $entity->injectEntityManager($this->em, $class);
2054
        }
2055
2056 662
        return $entity;
2057
    }
2058
2059
    /**
2060
     * INTERNAL:
2061
     * Creates an entity. Used for reconstitution of persistent entities.
2062
     *
2063
     * {@internal Highly performance-sensitive method. }}
2064
     *
2065
     * @ignore
2066
     *
2067
     * @param string  $className The name of the entity class.
2068
     * @param mixed[] $data      The data for the entity.
2069
     * @param mixed[] $hints     Any hints to account for during reconstitution/lookup of the entity.
2070
     *
2071
     * @return object The managed entity instance.
2072
     *
2073
     * @todo Rename: getOrCreateEntity
2074
     */
2075 799
    public function createEntity($className, array $data, &$hints = [])
2076
    {
2077 799
        $class  = $this->em->getClassMetadata($className);
2078 799
        $id     = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Ide...er::flattenIdentifier(). ( Ignorable by Annotation )

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

2078
        $id     = $this->em->getIdentifierFlattener()->flattenIdentifier(/** @scrutinizer ignore-type */ $class, $data);
Loading history...
2079 799
        $idHash = implode(' ', $id);
2080
2081 799
        if (isset($this->identityMap[$class->getRootClassName()][$idHash])) {
2082 304
            $entity = $this->identityMap[$class->getRootClassName()][$idHash];
2083 304
            $oid    = spl_object_id($entity);
2084
2085 304
            if (isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])) {
2086 65
                $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
2087 65
                if ($unmanagedProxy !== $entity
2088 65
                    && $unmanagedProxy instanceof GhostObjectInterface
2089 65
                    && $this->isIdentifierEquals($unmanagedProxy, $entity)
2090
                ) {
2091
                    // We will hydrate the given un-managed proxy anyway:
2092
                    // continue work, but consider it the entity from now on
2093 5
                    $entity = $unmanagedProxy;
2094
                }
2095
            }
2096
2097 304
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
2098 21
                $entity->setProxyInitializer(null);
2099
2100 21
                if ($entity instanceof NotifyPropertyChanged) {
2101 21
                    $entity->addPropertyChangedListener($this);
2102
                }
2103
            } else {
2104 290
                if (! isset($hints[Query::HINT_REFRESH])
2105 290
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2106 228
                    return $entity;
2107
                }
2108
            }
2109
2110
            // inject EntityManager upon refresh.
2111 103
            if ($entity instanceof EntityManagerAware) {
2112 3
                $entity->injectEntityManager($this->em, $class);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $classMetadata of Doctrine\ORM\EntityManag...::injectEntityManager(). ( Ignorable by Annotation )

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

2112
                $entity->injectEntityManager($this->em, /** @scrutinizer ignore-type */ $class);
Loading history...
2113
            }
2114
2115 103
            $this->originalEntityData[$oid] = $data;
2116
        } else {
2117 659
            $entity = $this->newInstance($class);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\UnitOfWork::newInstance(). ( Ignorable by Annotation )

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

2117
            $entity = $this->newInstance(/** @scrutinizer ignore-type */ $class);
Loading history...
2118 659
            $oid    = spl_object_id($entity);
2119
2120 659
            $this->entityIdentifiers[$oid]  = $id;
2121 659
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2122 659
            $this->originalEntityData[$oid] = $data;
2123
2124 659
            $this->identityMap[$class->getRootClassName()][$idHash] = $entity;
2125
        }
2126
2127 692
        if ($entity instanceof NotifyPropertyChanged) {
2128 3
            $entity->addPropertyChangedListener($this);
2129
        }
2130
2131 692
        foreach ($data as $field => $value) {
2132 692
            $property = $class->getProperty($field);
2133
2134 692
            if ($property instanceof FieldMetadata) {
2135 692
                $property->setValue($entity, $value);
2136
            }
2137
        }
2138
2139
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2140 692
        unset($this->eagerLoadingEntities[$class->getRootClassName()][$idHash]);
2141
2142 692
        if (isset($this->eagerLoadingEntities[$class->getRootClassName()]) && ! $this->eagerLoadingEntities[$class->getRootClassName()]) {
2143
            unset($this->eagerLoadingEntities[$class->getRootClassName()]);
2144
        }
2145
2146
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2147 692
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2148 34
            return $entity;
2149
        }
2150
2151 658
        foreach ($class->getDeclaredPropertiesIterator() as $field => $association) {
2152 658
            if (! ($association instanceof AssociationMetadata)) {
2153 658
                continue;
2154
            }
2155
2156
            // Check if the association is not among the fetch-joined associations already.
2157 566
            if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
2158 243
                continue;
2159
            }
2160
2161 543
            $targetEntity = $association->getTargetEntity();
2162 543
            $targetClass  = $this->em->getClassMetadata($targetEntity);
2163
2164 543
            if ($association instanceof ToManyAssociationMetadata) {
2165
                // Ignore if its a cached collection
2166 465
                if (isset($hints[Query::HINT_CACHE_ENABLED]) &&
2167 465
                    $association->getValue($entity) instanceof PersistentCollection) {
2168
                    continue;
2169
                }
2170
2171 465
                $hasDataField = isset($data[$field]);
2172
2173
                // use the given collection
2174 465
                if ($hasDataField && $data[$field] instanceof PersistentCollection) {
2175
                    $data[$field]->setOwner($entity, $association);
2176
2177
                    $association->setValue($entity, $data[$field]);
2178
2179
                    $this->originalEntityData[$oid][$field] = $data[$field];
2180
2181
                    continue;
2182
                }
2183
2184
                // Inject collection
2185 465
                $pColl = $association->wrap($entity, $hasDataField ? $data[$field] : [], $this->em);
2186
2187 465
                $pColl->setInitialized($hasDataField);
2188
2189 465
                $association->setValue($entity, $pColl);
2190
2191 465
                if ($association->getFetchMode() === FetchMode::EAGER) {
2192 4
                    $this->loadCollection($pColl);
2193 4
                    $pColl->takeSnapshot();
2194
                }
2195
2196 465
                $this->originalEntityData[$oid][$field] = $pColl;
2197
2198 465
                continue;
2199
            }
2200
2201 469
            if (! $association->isOwningSide()) {
2202
                // use the given entity association
2203 67
                if (isset($data[$field]) && is_object($data[$field]) &&
2204 67
                    isset($this->entityStates[spl_object_id($data[$field])])) {
2205 3
                    $inverseAssociation = $targetClass->getProperty($association->getMappedBy());
2206
2207 3
                    $association->setValue($entity, $data[$field]);
2208 3
                    $inverseAssociation->setValue($data[$field], $entity);
2209
2210 3
                    $this->originalEntityData[$oid][$field] = $data[$field];
2211
2212 3
                    continue;
2213
                }
2214
2215
                // Inverse side of x-to-one can never be lazy
2216 64
                $persister = $this->getEntityPersister($targetEntity);
2217
2218 64
                $association->setValue($entity, $persister->loadToOneEntity($association, $entity));
2219
2220 64
                continue;
2221
            }
2222
2223
            // use the entity association
2224 469
            if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
2225 38
                $association->setValue($entity, $data[$field]);
2226
2227 38
                $this->originalEntityData[$oid][$field] = $data[$field];
2228
2229 38
                continue;
2230
            }
2231
2232 462
            $associatedId = [];
2233
2234
            // TODO: Is this even computed right in all cases of composite keys?
2235 462
            foreach ($association->getJoinColumns() as $joinColumn) {
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

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

2235
            foreach ($association->/** @scrutinizer ignore-call */ getJoinColumns() as $joinColumn) {
Loading history...
2236
                /** @var JoinColumnMetadata $joinColumn */
2237 462
                $joinColumnName  = $joinColumn->getColumnName();
2238 462
                $joinColumnValue = $data[$joinColumnName] ?? null;
2239 462
                $targetField     = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
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...
2240
2241 462
                if ($joinColumnValue === null && in_array($targetField, $targetClass->identifier, true)) {
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...
2242
                    // the missing key is part of target's entity primary key
2243 266
                    $associatedId = [];
2244
2245 266
                    continue;
2246
                }
2247
2248 284
                $associatedId[$targetField] = $joinColumnValue;
2249
            }
2250
2251 462
            if (! $associatedId) {
2252
                // Foreign key is NULL
2253 266
                $association->setValue($entity, null);
2254 266
                $this->originalEntityData[$oid][$field] = null;
2255
2256 266
                continue;
2257
            }
2258
2259
            // @todo guilhermeblanco Can we remove the need of this somehow?
2260 284
            if (! isset($hints['fetchMode'][$class->getClassName()][$field])) {
2261 281
                $hints['fetchMode'][$class->getClassName()][$field] = $association->getFetchMode();
2262
            }
2263
2264
            // Foreign key is set
2265
            // Check identity map first
2266
            // FIXME: Can break easily with composite keys if join column values are in
2267
            //        wrong order. The correct order is the one in ClassMetadata#identifier.
2268 284
            $relatedIdHash = implode(' ', $associatedId);
2269
2270
            switch (true) {
2271 284
                case (isset($this->identityMap[$targetClass->getRootClassName()][$relatedIdHash])):
2272 168
                    $newValue = $this->identityMap[$targetClass->getRootClassName()][$relatedIdHash];
2273
2274
                    // If this is an uninitialized proxy, we are deferring eager loads,
2275
                    // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2276
                    // then we can append this entity for eager loading!
2277 168
                    if (! $targetClass->isIdentifierComposite() &&
2278 168
                        $newValue instanceof GhostObjectInterface &&
2279 168
                        isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2280 168
                        $hints['fetchMode'][$class->getClassName()][$field] === FetchMode::EAGER &&
2281 168
                        ! $newValue->isProxyInitialized()
2282
                    ) {
2283
                        $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($associatedId);
2284
                    }
2285
2286 168
                    break;
2287
2288 190
                case ($targetClass->getSubClasses()):
2289
                    // If it might be a subtype, it can not be lazy. There isn't even
2290
                    // a way to solve this with deferred eager loading, which means putting
2291
                    // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2292 29
                    $persister = $this->getEntityPersister($targetEntity);
2293 29
                    $newValue  = $persister->loadToOneEntity($association, $entity, $associatedId);
2294 29
                    break;
2295
2296
                default:
2297
                    // Proxies do not carry any kind of original entity data until they're fully loaded/initialized
2298 163
                    $managedData = [];
2299
2300 163
                    $normalizedAssociatedId = $this->normalizeIdentifier->__invoke(
2301 163
                        $this->em,
2302 163
                        $targetClass,
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetClass of Doctrine\ORM\Utility\Nor...eIdentifier::__invoke(). ( Ignorable by Annotation )

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

2302
                        /** @scrutinizer ignore-type */ $targetClass,
Loading history...
2303 163
                        $associatedId
2304
                    );
2305
2306
                    switch (true) {
2307
                        // We are negating the condition here. Other cases will assume it is valid!
2308 163
                        case ($hints['fetchMode'][$class->getClassName()][$field] !== FetchMode::EAGER):
2309 156
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Proxy\Facto...roxyFactory::getProxy(). ( Ignorable by Annotation )

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

2309
                            $newValue = $this->em->getProxyFactory()->getProxy(/** @scrutinizer ignore-type */ $targetClass, $normalizedAssociatedId);
Loading history...
2310 156
                            break;
2311
2312
                        // Deferred eager load only works for single identifier classes
2313 7
                        case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite()):
2314
                            // TODO: Is there a faster approach?
2315 7
                            $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($normalizedAssociatedId);
2316
2317 7
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
2318 7
                            break;
2319
2320
                        default:
2321
                            // TODO: This is very imperformant, ignore it?
2322
                            $newValue = $this->em->find($targetEntity, $normalizedAssociatedId);
2323
                            // Needed to re-assign original entity data for freshly loaded entity
2324
                            $managedData = $this->originalEntityData[spl_object_id($newValue)];
2325
                            break;
2326
                    }
2327
2328
                    // @TODO using `$associatedId` here seems to be risky.
2329 163
                    $this->registerManaged($newValue, $associatedId, $managedData);
2330
2331 163
                    break;
2332
            }
2333
2334 284
            $this->originalEntityData[$oid][$field] = $newValue;
2335 284
            $association->setValue($entity, $newValue);
2336
2337 284
            if ($association->getInversedBy()
2338 284
                && $association instanceof OneToOneAssociationMetadata
2339
                // @TODO refactor this
2340
                // we don't want to set any values in un-initialized proxies
2341
                && ! (
2342 56
                    $newValue instanceof GhostObjectInterface
2343 284
                    && ! $newValue->isProxyInitialized()
2344
                )
2345
            ) {
2346 19
                $inverseAssociation = $targetClass->getProperty($association->getInversedBy());
2347
2348 284
                $inverseAssociation->setValue($newValue, $entity);
2349
            }
2350
        }
2351
2352
        // defer invoking of postLoad event to hydration complete step
2353 658
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Internal\Hy...deferPostLoadInvoking(). ( Ignorable by Annotation )

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

2353
        $this->hydrationCompleteHandler->deferPostLoadInvoking(/** @scrutinizer ignore-type */ $class, $entity);
Loading history...
2354
2355 658
        return $entity;
2356
    }
2357
2358 856
    public function triggerEagerLoads()
2359
    {
2360 856
        if (! $this->eagerLoadingEntities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->eagerLoadingEntities of type array<mixed,array<mixed,array<mixed,mixed>>> 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...
2361 856
            return;
2362
        }
2363
2364
        // avoid infinite recursion
2365 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2366 7
        $this->eagerLoadingEntities = [];
2367
2368 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2369 7
            if (! $ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array<mixed,array<mixed,mixed>> 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...
2370
                continue;
2371
            }
2372
2373 7
            $class = $this->em->getClassMetadata($entityName);
2374
2375 7
            $this->getEntityPersister($entityName)->loadAll(
2376 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...
2377
            );
2378
        }
2379 7
    }
2380
2381
    /**
2382
     * Initializes (loads) an uninitialized persistent collection of an entity.
2383
     *
2384
     * @param PersistentCollection $collection The collection to initialize.
2385
     *
2386
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2387
     */
2388 140
    public function loadCollection(PersistentCollection $collection)
2389
    {
2390 140
        $association = $collection->getMapping();
2391 140
        $persister   = $this->getEntityPersister($association->getTargetEntity());
2392
2393 140
        if ($association instanceof OneToManyAssociationMetadata) {
2394 75
            $persister->loadOneToManyCollection($association, $collection->getOwner(), $collection);
2395
        } else {
2396 75
            $persister->loadManyToManyCollection($association, $collection->getOwner(), $collection);
2397
        }
2398
2399 140
        $collection->setInitialized(true);
2400 140
    }
2401
2402
    /**
2403
     * Gets the identity map of the UnitOfWork.
2404
     *
2405
     * @return object[]
2406
     */
2407 1
    public function getIdentityMap()
2408
    {
2409 1
        return $this->identityMap;
2410
    }
2411
2412
    /**
2413
     * Gets the original data of an entity. The original data is the data that was
2414
     * present at the time the entity was reconstituted from the database.
2415
     *
2416
     * @param object $entity
2417
     *
2418
     * @return mixed[]
2419
     */
2420 121
    public function getOriginalEntityData($entity)
2421
    {
2422 121
        $oid = spl_object_id($entity);
2423
2424 121
        return $this->originalEntityData[$oid] ?? [];
2425
    }
2426
2427
    /**
2428
     * @ignore
2429
     *
2430
     * @param object  $entity
2431
     * @param mixed[] $data
2432
     */
2433
    public function setOriginalEntityData($entity, array $data)
2434
    {
2435
        $this->originalEntityData[spl_object_id($entity)] = $data;
2436
    }
2437
2438
    /**
2439
     * INTERNAL:
2440
     * Sets a property value of the original data array of an entity.
2441
     *
2442
     * @ignore
2443
     *
2444
     * @param string $oid
2445
     * @param string $property
2446
     * @param mixed  $value
2447
     */
2448 301
    public function setOriginalEntityProperty($oid, $property, $value)
2449
    {
2450 301
        $this->originalEntityData[$oid][$property] = $value;
2451 301
    }
2452
2453
    /**
2454
     * Gets the identifier of an entity.
2455
     * The returned value is always an array of identifier values. If the entity
2456
     * has a composite identifier then the identifier values are in the same
2457
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2458
     *
2459
     * @param object $entity
2460
     *
2461
     * @return mixed[] The identifier values.
2462
     */
2463 568
    public function getEntityIdentifier($entity)
2464
    {
2465 568
        return $this->entityIdentifiers[spl_object_id($entity)];
2466
    }
2467
2468
    /**
2469
     * Processes an entity instance to extract their identifier values.
2470
     *
2471
     * @param object $entity The entity instance.
2472
     *
2473
     * @return mixed A scalar value.
2474
     *
2475
     * @throws ORMInvalidArgumentException
2476
     */
2477 70
    public function getSingleIdentifierValue($entity)
2478
    {
2479 70
        $class     = $this->em->getClassMetadata(get_class($entity));
2480 70
        $persister = $this->getEntityPersister($class->getClassName());
2481
2482 70
        if ($class->isIdentifierComposite()) {
2483
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2484
        }
2485
2486 70
        $values = $this->isInIdentityMap($entity)
2487 58
            ? $this->getEntityIdentifier($entity)
2488 70
            : $persister->getIdentifier($entity);
2489
2490 70
        return $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...
2491
    }
2492
2493
    /**
2494
     * Tries to find an entity with the given identifier in the identity map of
2495
     * this UnitOfWork.
2496
     *
2497
     * @param mixed|mixed[] $id            The entity identifier to look for.
2498
     * @param string        $rootClassName The name of the root class of the mapped entity hierarchy.
2499
     *
2500
     * @return object|bool Returns the entity with the specified identifier if it exists in
2501
     *                     this UnitOfWork, FALSE otherwise.
2502
     */
2503 539
    public function tryGetById($id, $rootClassName)
2504
    {
2505 539
        $idHash = implode(' ', (array) $id);
2506
2507 539
        return $this->identityMap[$rootClassName][$idHash] ?? false;
2508
    }
2509
2510
    /**
2511
     * Schedules an entity for dirty-checking at commit-time.
2512
     *
2513
     * @param object $entity The entity to schedule for dirty-checking.
2514
     */
2515 5
    public function scheduleForSynchronization($entity)
2516
    {
2517 5
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->getRootClassName();
2518
2519 5
        $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity;
2520 5
    }
2521
2522
    /**
2523
     * Checks whether the UnitOfWork has any pending insertions.
2524
     *
2525
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2526
     */
2527
    public function hasPendingInsertions()
2528
    {
2529
        return ! empty($this->entityInsertions);
2530
    }
2531
2532
    /**
2533
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2534
     * number of entities in the identity map.
2535
     *
2536
     * @return int
2537
     */
2538 1
    public function size()
2539
    {
2540 1
        return array_sum(array_map('count', $this->identityMap));
2541
    }
2542
2543
    /**
2544
     * Gets the EntityPersister for an Entity.
2545
     *
2546
     * @param string $entityName The name of the Entity.
2547
     *
2548
     * @return EntityPersister
2549
     */
2550 1083
    public function getEntityPersister($entityName)
2551
    {
2552 1083
        if (isset($this->entityPersisters[$entityName])) {
2553 1026
            return $this->entityPersisters[$entityName];
2554
        }
2555
2556 1083
        $class = $this->em->getClassMetadata($entityName);
2557
2558
        switch (true) {
2559 1083
            case ($class->inheritanceType === InheritanceType::NONE):
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2560 1040
                $persister = new BasicEntityPersister($this->em, $class);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...ersister::__construct(). ( Ignorable by Annotation )

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

2560
                $persister = new BasicEntityPersister($this->em, /** @scrutinizer ignore-type */ $class);
Loading history...
2561 1040
                break;
2562
2563 385
            case ($class->inheritanceType === InheritanceType::SINGLE_TABLE):
2564 223
                $persister = new SingleTablePersister($this->em, $class);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...ersister::__construct(). ( Ignorable by Annotation )

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

2564
                $persister = new SingleTablePersister($this->em, /** @scrutinizer ignore-type */ $class);
Loading history...
2565 223
                break;
2566
2567 355
            case ($class->inheritanceType === InheritanceType::JOINED):
2568 355
                $persister = new JoinedSubclassPersister($this->em, $class);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...ersister::__construct(). ( Ignorable by Annotation )

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

2568
                $persister = new JoinedSubclassPersister($this->em, /** @scrutinizer ignore-type */ $class);
Loading history...
2569 355
                break;
2570
2571
            default:
2572
                throw new \RuntimeException('No persister found for entity.');
2573
        }
2574
2575 1083
        if ($this->hasCache && $class->getCache()) {
0 ignored issues
show
Bug introduced by
The method getCache() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

2575
        if ($this->hasCache && $class->/** @scrutinizer ignore-call */ getCache()) {

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...
2576 131
            $persister = $this->em->getConfiguration()
2577 131
                ->getSecondLevelCacheConfiguration()
2578 131
                ->getCacheFactory()
2579 131
                ->buildCachedEntityPersister($this->em, $persister, $class);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $metadata of Doctrine\ORM\Cache\Cache...CachedEntityPersister(). ( Ignorable by Annotation )

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

2579
                ->buildCachedEntityPersister($this->em, $persister, /** @scrutinizer ignore-type */ $class);
Loading history...
2580
        }
2581
2582 1083
        $this->entityPersisters[$entityName] = $persister;
2583
2584 1083
        return $this->entityPersisters[$entityName];
2585
    }
2586
2587
    /**
2588
     * Gets a collection persister for a collection-valued association.
2589
     *
2590
     * @return CollectionPersister
2591
     */
2592 566
    public function getCollectionPersister(ToManyAssociationMetadata $association)
2593
    {
2594 566
        $role = $association->getCache()
2595 78
            ? sprintf('%s::%s', $association->getSourceEntity(), $association->getName())
2596 566
            : get_class($association);
2597
2598 566
        if (isset($this->collectionPersisters[$role])) {
2599 431
            return $this->collectionPersisters[$role];
2600
        }
2601
2602 566
        $persister = $association instanceof OneToManyAssociationMetadata
2603 403
            ? new OneToManyPersister($this->em)
2604 566
            : new ManyToManyPersister($this->em);
2605
2606 566
        if ($this->hasCache && $association->getCache()) {
2607 77
            $persister = $this->em->getConfiguration()
2608 77
                ->getSecondLevelCacheConfiguration()
2609 77
                ->getCacheFactory()
2610 77
                ->buildCachedCollectionPersister($this->em, $persister, $association);
2611
        }
2612
2613 566
        $this->collectionPersisters[$role] = $persister;
2614
2615 566
        return $this->collectionPersisters[$role];
2616
    }
2617
2618
    /**
2619
     * INTERNAL:
2620
     * Registers an entity as managed.
2621
     *
2622
     * @param object  $entity The entity.
2623
     * @param mixed[] $id     Map containing identifier field names as key and its associated values.
2624
     * @param mixed[] $data   The original entity data.
2625
     */
2626 291
    public function registerManaged($entity, array $id, array $data)
2627
    {
2628 291
        $isProxy = $entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized();
2629 291
        $oid     = spl_object_id($entity);
2630
2631 291
        $this->entityIdentifiers[$oid]  = $id;
2632 291
        $this->entityStates[$oid]       = self::STATE_MANAGED;
2633 291
        $this->originalEntityData[$oid] = $data;
2634
2635 291
        $this->addToIdentityMap($entity);
2636
2637 285
        if ($entity instanceof NotifyPropertyChanged && ! $isProxy) {
2638 1
            $entity->addPropertyChangedListener($this);
2639
        }
2640 285
    }
2641
2642
    /**
2643
     * INTERNAL:
2644
     * Clears the property changeset of the entity with the given OID.
2645
     *
2646
     * @param string $oid The entity's OID.
2647
     */
2648
    public function clearEntityChangeSet($oid)
2649
    {
2650
        unset($this->entityChangeSets[$oid]);
2651
    }
2652
2653
    /* PropertyChangedListener implementation */
2654
2655
    /**
2656
     * Notifies this UnitOfWork of a property change in an entity.
2657
     *
2658
     * @param object $entity       The entity that owns the property.
2659
     * @param string $propertyName The name of the property that changed.
2660
     * @param mixed  $oldValue     The old value of the property.
2661
     * @param mixed  $newValue     The new value of the property.
2662
     */
2663 3
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
2664
    {
2665 3
        $class = $this->em->getClassMetadata(get_class($entity));
2666
2667 3
        if (! $class->getProperty($propertyName)) {
2668
            return; // ignore non-persistent fields
2669
        }
2670
2671 3
        $oid = spl_object_id($entity);
2672
2673
        // Update changeset and mark entity for synchronization
2674 3
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2675
2676 3
        if (! isset($this->scheduledForSynchronization[$class->getRootClassName()][$oid])) {
2677 3
            $this->scheduleForSynchronization($entity);
2678
        }
2679 3
    }
2680
2681
    /**
2682
     * Gets the currently scheduled entity insertions in this UnitOfWork.
2683
     *
2684
     * @return object[]
2685
     */
2686 2
    public function getScheduledEntityInsertions()
2687
    {
2688 2
        return $this->entityInsertions;
2689
    }
2690
2691
    /**
2692
     * Gets the currently scheduled entity updates in this UnitOfWork.
2693
     *
2694
     * @return object[]
2695
     */
2696 3
    public function getScheduledEntityUpdates()
2697
    {
2698 3
        return $this->entityUpdates;
2699
    }
2700
2701
    /**
2702
     * Gets the currently scheduled entity deletions in this UnitOfWork.
2703
     *
2704
     * @return object[]
2705
     */
2706 1
    public function getScheduledEntityDeletions()
2707
    {
2708 1
        return $this->entityDeletions;
2709
    }
2710
2711
    /**
2712
     * Gets the currently scheduled complete collection deletions
2713
     *
2714
     * @return Collection[]|object[][]
2715
     */
2716 1
    public function getScheduledCollectionDeletions()
2717
    {
2718 1
        return $this->collectionDeletions;
2719
    }
2720
2721
    /**
2722
     * Gets the currently scheduled collection inserts, updates and deletes.
2723
     *
2724
     * @return Collection[]|object[][]
2725
     */
2726
    public function getScheduledCollectionUpdates()
2727
    {
2728
        return $this->collectionUpdates;
2729
    }
2730
2731
    /**
2732
     * Helper method to initialize a lazy loading proxy or persistent collection.
2733
     *
2734
     * @param object $obj
2735
     */
2736 2
    public function initializeObject($obj)
2737
    {
2738 2
        if ($obj instanceof GhostObjectInterface) {
2739 1
            $obj->initializeProxy();
2740
2741 1
            return;
2742
        }
2743
2744 1
        if ($obj instanceof PersistentCollection) {
2745 1
            $obj->initialize();
2746
        }
2747 1
    }
2748
2749
    /**
2750
     * Helper method to show an object as string.
2751
     *
2752
     * @param object $obj
2753
     *
2754
     * @return string
2755
     */
2756
    private static function objToStr($obj)
2757
    {
2758
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_id($obj);
2759
    }
2760
2761
    /**
2762
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
2763
     *
2764
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
2765
     * on this object that might be necessary to perform a correct update.
2766
     *
2767
     * @param object $object
2768
     *
2769
     * @throws ORMInvalidArgumentException
2770
     */
2771 6
    public function markReadOnly($object)
2772
    {
2773 6
        if (! is_object($object) || ! $this->isInIdentityMap($object)) {
2774 1
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2775
        }
2776
2777 5
        $this->readOnlyObjects[spl_object_id($object)] = true;
2778 5
    }
2779
2780
    /**
2781
     * Is this entity read only?
2782
     *
2783
     * @param object $object
2784
     *
2785
     * @return bool
2786
     *
2787
     * @throws ORMInvalidArgumentException
2788
     */
2789 3
    public function isReadOnly($object)
2790
    {
2791 3
        if (! is_object($object)) {
2792
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2793
        }
2794
2795 3
        return isset($this->readOnlyObjects[spl_object_id($object)]);
2796
    }
2797
2798
    /**
2799
     * Perform whatever processing is encapsulated here after completion of the transaction.
2800
     */
2801
    private function afterTransactionComplete()
2802
    {
2803 999
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
2804 95
            $persister->afterTransactionComplete();
2805 999
        });
2806 999
    }
2807
2808
    /**
2809
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
2810
     */
2811
    private function afterTransactionRolledBack()
2812
    {
2813 10
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
2814
            $persister->afterTransactionRolledBack();
2815 10
        });
2816 10
    }
2817
2818
    /**
2819
     * Performs an action after the transaction.
2820
     */
2821 1004
    private function performCallbackOnCachedPersister(callable $callback)
2822
    {
2823 1004
        if (! $this->hasCache) {
2824 909
            return;
2825
        }
2826
2827 95
        foreach (array_merge($this->entityPersisters, $this->collectionPersisters) as $persister) {
2828 95
            if ($persister instanceof CachedPersister) {
2829 95
                $callback($persister);
2830
            }
2831
        }
2832 95
    }
2833
2834 1009
    private function dispatchOnFlushEvent()
2835
    {
2836 1009
        if ($this->eventManager->hasListeners(Events::onFlush)) {
2837 4
            $this->eventManager->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
2838
        }
2839 1009
    }
2840
2841 1004
    private function dispatchPostFlushEvent()
2842
    {
2843 1004
        if ($this->eventManager->hasListeners(Events::postFlush)) {
2844 5
            $this->eventManager->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
2845
        }
2846 1003
    }
2847
2848
    /**
2849
     * Verifies if two given entities actually are the same based on identifier comparison
2850
     *
2851
     * @param object $entity1
2852
     * @param object $entity2
2853
     *
2854
     * @return bool
2855
     */
2856 17
    private function isIdentifierEquals($entity1, $entity2)
2857
    {
2858 17
        if ($entity1 === $entity2) {
2859
            return true;
2860
        }
2861
2862 17
        $class     = $this->em->getClassMetadata(get_class($entity1));
2863 17
        $persister = $this->getEntityPersister($class->getClassName());
2864
2865 17
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
2866 11
            return false;
2867
        }
2868
2869 6
        $identifierFlattener = $this->em->getIdentifierFlattener();
2870
2871 6
        $oid1 = spl_object_id($entity1);
2872 6
        $oid2 = spl_object_id($entity2);
2873
2874 6
        $id1 = $this->entityIdentifiers[$oid1]
2875 6
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity1));
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Ide...er::flattenIdentifier(). ( Ignorable by Annotation )

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

2875
            ?? $identifierFlattener->flattenIdentifier(/** @scrutinizer ignore-type */ $class, $persister->getIdentifier($entity1));
Loading history...
2876 6
        $id2 = $this->entityIdentifiers[$oid2]
2877 6
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity2));
2878
2879 6
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
2880
    }
2881
2882
    /**
2883
     * @throws ORMInvalidArgumentException
2884
     */
2885 1006
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
2886
    {
2887 1006
        $entitiesNeedingCascadePersist = array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
2888
2889 1006
        $this->nonCascadedNewDetectedEntities = [];
2890
2891 1006
        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...
2892 4
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
2893 4
                array_values($entitiesNeedingCascadePersist)
2894
            );
2895
        }
2896 1004
    }
2897
2898
    /**
2899
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
2900
     * Unit of work able to fire deferred events, related to loading events here.
2901
     *
2902
     * @internal should be called internally from object hydrators
2903
     */
2904 872
    public function hydrationComplete()
2905
    {
2906 872
        $this->hydrationCompleteHandler->hydrationComplete();
2907 872
    }
2908
}
2909