Completed
Pull Request — master (#7725)
by Guilherme
08:45
created

UnitOfWork::scheduleExtraUpdate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 2
rs 10
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\Exception\UnexpectedAssociationValue;
21
use Doctrine\ORM\Internal\HydrationCompleteHandler;
22
use Doctrine\ORM\Mapping\AssociationMetadata;
23
use Doctrine\ORM\Mapping\ChangeTrackingPolicy;
24
use Doctrine\ORM\Mapping\ClassMetadata;
25
use Doctrine\ORM\Mapping\FetchMode;
26
use Doctrine\ORM\Mapping\FieldMetadata;
27
use Doctrine\ORM\Mapping\GeneratorType;
28
use Doctrine\ORM\Mapping\InheritanceType;
29
use Doctrine\ORM\Mapping\JoinColumnMetadata;
30
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
31
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
32
use Doctrine\ORM\Mapping\OneToOneAssociationMetadata;
33
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
34
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
35
use Doctrine\ORM\Mapping\VersionFieldMetadata;
36
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
37
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
38
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
39
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
40
use Doctrine\ORM\Persisters\Entity\EntityPersister;
41
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
42
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
43
use Doctrine\ORM\Utility\NormalizeIdentifier;
44
use Exception;
45
use InvalidArgumentException;
46
use ProxyManager\Proxy\GhostObjectInterface;
47
use RuntimeException;
48
use Throwable;
49
use UnexpectedValueException;
50
use function array_combine;
51
use function array_diff_key;
52
use function array_filter;
53
use function array_key_exists;
54
use function array_map;
55
use function array_merge;
56
use function array_pop;
57
use function array_reverse;
58
use function array_sum;
59
use function array_values;
60
use function current;
61
use function get_class;
62
use function implode;
63
use function in_array;
64
use function is_array;
65
use function is_object;
66
use function method_exists;
67
use function spl_object_id;
68
use function sprintf;
69
70
/**
71
 * The UnitOfWork is responsible for tracking changes to objects during an
72
 * "object-level" transaction and for writing out changes to the database
73
 * in the correct order.
74
 *
75
 * {@internal This class contains highly performance-sensitive code. }}
76
 */
77
class UnitOfWork implements PropertyChangedListener
78
{
79
    /**
80
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
81
     */
82
    public const STATE_MANAGED = 1;
83
84
    /**
85
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
86
     * and is not (yet) managed by an EntityManager.
87
     */
88
    public const STATE_NEW = 2;
89
90
    /**
91
     * A detached entity is an instance with persistent state and identity that is not
92
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
93
     */
94
    public const STATE_DETACHED = 3;
95
96
    /**
97
     * A removed entity instance is an instance with a persistent identity,
98
     * associated with an EntityManager, whose persistent state will be deleted
99
     * on commit.
100
     */
101
    public const STATE_REMOVED = 4;
102
103
    /**
104
     * Hint used to collect all primary keys of associated entities during hydration
105
     * and execute it in a dedicated query afterwards
106
     *
107
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
108
     */
109
    public const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
110
111
    /**
112
     * The identity map that holds references to all managed entities that have
113
     * an identity. The entities are grouped by their class name.
114
     * Since all classes in a hierarchy must share the same identifier set,
115
     * we always take the root class name of the hierarchy.
116
     *
117
     * @var object[]
118
     */
119
    private $identityMap = [];
120
121
    /**
122
     * Map of all identifiers of managed entities.
123
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
124
     * Values are maps of entity identifiers, where its key is the column name and the value is the raw value.
125
     *
126
     * @var mixed[][]
127
     */
128
    private $entityIdentifiers = [];
129
130
    /**
131
     * Map of the original entity data of managed entities.
132
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
133
     * Values are maps of entity data, where its key is the field name and the value is the converted
134
     * (convertToPHPValue) value.
135
     * This structure is used for calculating changesets at commit time.
136
     *
137
     * Internal: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
138
     *           A value will only really be copied if the value in the entity is modified by the user.
139
     *
140
     * @var mixed[][]
141
     */
142
    private $originalEntityData = [];
143
144
    /**
145
     * Map of entity changes. Keys are object ids (spl_object_id).
146
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
147
     *
148
     * @var mixed[][]
149
     */
150
    private $entityChangeSets = [];
151
152
    /**
153
     * The (cached) states of any known entities.
154
     * Keys are object ids (spl_object_id).
155
     *
156
     * @var int[]
157
     */
158
    private $entityStates = [];
159
160
    /**
161
     * Map of entities that are scheduled for dirty checking at commit time.
162
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
163
     * Keys are object ids (spl_object_id).
164
     *
165
     * @var object[][]
166
     */
167
    private $scheduledForSynchronization = [];
168
169
    /**
170
     * A list of all pending entity insertions.
171
     *
172
     * @var object[]
173
     */
174
    private $entityInsertions = [];
175
176
    /**
177
     * A list of all pending entity updates.
178
     *
179
     * @var object[]
180
     */
181
    private $entityUpdates = [];
182
183
    /**
184
     * Any pending extra updates that have been scheduled by persisters.
185
     *
186
     * @var object[]
187
     */
188
    private $extraUpdates = [];
189
190
    /**
191
     * A list of all pending entity deletions.
192
     *
193
     * @var object[]
194
     */
195
    private $entityDeletions = [];
196
197
    /**
198
     * New entities that were discovered through relationships that were not
199
     * marked as cascade-persist. During flush, this array is populated and
200
     * then pruned of any entities that were discovered through a valid
201
     * cascade-persist path. (Leftovers cause an error.)
202
     *
203
     * Keys are OIDs, payload is a two-item array describing the association
204
     * and the entity.
205
     *
206
     * @var object[][]|array[][] indexed by respective object spl_object_id()
207
     */
208
    private $nonCascadedNewDetectedEntities = [];
209
210
    /**
211
     * All pending collection deletions.
212
     *
213
     * @var Collection[]|object[][]
214
     */
215
    private $collectionDeletions = [];
216
217
    /**
218
     * All pending collection updates.
219
     *
220
     * @var Collection[]|object[][]
221
     */
222
    private $collectionUpdates = [];
223
224
    /**
225
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
226
     * At the end of the UnitOfWork all these collections will make new snapshots
227
     * of their data.
228
     *
229
     * @var Collection[]|object[][]
230
     */
231
    private $visitedCollections = [];
232
233
    /**
234
     * The EntityManager that "owns" this UnitOfWork instance.
235
     *
236
     * @var EntityManagerInterface
237
     */
238
    private $em;
239
240
    /**
241
     * The entity persister instances used to persist entity instances.
242
     *
243
     * @var EntityPersister[]
244
     */
245
    private $entityPersisters = [];
246
247
    /**
248
     * The collection persister instances used to persist collections.
249
     *
250
     * @var CollectionPersister[]
251
     */
252
    private $collectionPersisters = [];
253
254
    /**
255
     * The EventManager used for dispatching events.
256
     *
257
     * @var EventManager
258
     */
259
    private $eventManager;
260
261
    /**
262
     * The ListenersInvoker used for dispatching events.
263
     *
264
     * @var ListenersInvoker
265
     */
266
    private $listenersInvoker;
267
268
    /** @var Instantiator */
269
    private $instantiator;
270
271
    /**
272
     * Orphaned entities that are scheduled for removal.
273
     *
274
     * @var object[]
275
     */
276
    private $orphanRemovals = [];
277
278
    /**
279
     * Read-Only objects are never evaluated
280
     *
281
     * @var object[]
282
     */
283
    private $readOnlyObjects = [];
284
285
    /**
286
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
287
     *
288
     * @var mixed[][][]
289
     */
290
    private $eagerLoadingEntities = [];
291
292
    /** @var bool */
293
    protected $hasCache = false;
294
295
    /**
296
     * Helper for handling completion of hydration
297
     *
298
     * @var HydrationCompleteHandler
299
     */
300
    private $hydrationCompleteHandler;
301
302
    /** @var NormalizeIdentifier */
303
    private $normalizeIdentifier;
304
305
    /**
306
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
307
     */
308 2244
    public function __construct(EntityManagerInterface $em)
309
    {
310 2244
        $this->em                       = $em;
311 2244
        $this->eventManager             = $em->getEventManager();
312 2244
        $this->listenersInvoker         = new ListenersInvoker($em);
313 2244
        $this->hasCache                 = $em->getConfiguration()->isSecondLevelCacheEnabled();
314 2244
        $this->instantiator             = new Instantiator();
315 2244
        $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
316 2244
        $this->normalizeIdentifier      = new NormalizeIdentifier();
317 2244
    }
318
319
    /**
320
     * Commits the UnitOfWork, executing all operations that have been postponed
321
     * up to this point. The state of all managed entities will be synchronized with
322
     * the database.
323
     *
324
     * The operations are executed in the following order:
325
     *
326
     * 1) All entity insertions
327
     * 2) All entity updates
328
     * 3) All collection deletions
329
     * 4) All collection updates
330
     * 5) All entity deletions
331
     *
332
     * @throws Exception
333
     */
334 1020
    public function commit()
335
    {
336
        // Raise preFlush
337 1020
        if ($this->eventManager->hasListeners(Events::preFlush)) {
338 2
            $this->eventManager->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
339
        }
340
341 1020
        $this->computeChangeSets();
342
343 1018
        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...
344 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...
345 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...
346 37
                $this->collectionUpdates ||
347 34
                $this->collectionDeletions ||
348 1018
                $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...
349 22
            $this->dispatchOnFlushEvent();
350 22
            $this->dispatchPostFlushEvent();
351
352 22
            $this->postCommitCleanup();
353
354 22
            return; // Nothing to do.
355
        }
356
357 1013
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
358
359 1011
        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...
360 15
            foreach ($this->orphanRemovals as $orphan) {
361 15
                $this->remove($orphan);
362
            }
363
        }
364
365 1011
        $this->dispatchOnFlushEvent();
366
367
        // Now we need a commit order to maintain referential integrity
368 1011
        $commitOrder = $this->getCommitOrder();
369
370 1011
        $conn = $this->em->getConnection();
371 1011
        $conn->beginTransaction();
372
373
        try {
374
            // Collection deletions (deletions of complete collections)
375 1011
            foreach ($this->collectionDeletions as $collectionToDelete) {
376 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
377
            }
378
379 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...
380 1007
                foreach ($commitOrder as $class) {
381 1007
                    $this->executeInserts($class);
382
                }
383
            }
384
385 1010
            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...
386 111
                foreach ($commitOrder as $class) {
387 111
                    $this->executeUpdates($class);
388
                }
389
            }
390
391
            // Extra updates that were requested by persisters.
392 1006
            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...
393 29
                $this->executeExtraUpdates();
394
            }
395
396
            // Collection updates (deleteRows, updateRows, insertRows)
397 1006
            foreach ($this->collectionUpdates as $collectionToUpdate) {
398 526
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
399
            }
400
401
            // Entity deletions come last and need to be in reverse commit order
402 1006
            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...
403 60
                foreach (array_reverse($commitOrder) as $committedEntityName) {
404 60
                    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...
405 33
                        break; // just a performance optimisation
406
                    }
407
408 60
                    $this->executeDeletions($committedEntityName);
409
                }
410
            }
411
412 1006
            $conn->commit();
413 10
        } catch (Throwable $e) {
414 10
            $this->em->close();
415 10
            $conn->rollBack();
416
417 10
            $this->afterTransactionRolledBack();
418
419 10
            throw $e;
420
        }
421
422 1006
        $this->afterTransactionComplete();
423
424
        // Take new snapshots from visited collections
425 1006
        foreach ($this->visitedCollections as $coll) {
426 525
            $coll->takeSnapshot();
427
        }
428
429 1006
        $this->dispatchPostFlushEvent();
430
431 1005
        $this->postCommitCleanup();
432 1005
    }
433
434 1010
    private function postCommitCleanup() : void
435
    {
436 1010
        $this->entityInsertions            =
437 1010
        $this->entityUpdates               =
438 1010
        $this->entityDeletions             =
439 1010
        $this->extraUpdates                =
440 1010
        $this->entityChangeSets            =
441 1010
        $this->collectionUpdates           =
442 1010
        $this->collectionDeletions         =
443 1010
        $this->visitedCollections          =
444 1010
        $this->scheduledForSynchronization =
445 1010
        $this->orphanRemovals              = [];
446 1010
    }
447
448
    /**
449
     * Computes the changesets of all entities scheduled for insertion.
450
     */
451 1020
    private function computeScheduleInsertsChangeSets()
452
    {
453 1020
        foreach ($this->entityInsertions as $entity) {
454 1011
            $class = $this->em->getClassMetadata(get_class($entity));
455
456 1011
            $this->computeChangeSet($class, $entity);
457
        }
458 1018
    }
459
460
    /**
461
     * Executes any extra updates that have been scheduled.
462
     */
463 29
    private function executeExtraUpdates()
464
    {
465 29
        foreach ($this->extraUpdates as $oid => $update) {
466 29
            [$entity, $changeset] = $update;
467
468 29
            $this->entityChangeSets[$oid] = $changeset;
469
470 29
            $this->getEntityPersister(get_class($entity))->update($entity);
471
        }
472
473 29
        $this->extraUpdates = [];
474 29
    }
475
476
    /**
477
     * Gets the changeset for an entity.
478
     *
479
     * @param object $entity
480
     *
481
     * @return mixed[]
482
     */
483
    public function & getEntityChangeSet($entity)
484
    {
485 1006
        $oid  = spl_object_id($entity);
486 1006
        $data = [];
487
488 1006
        if (! isset($this->entityChangeSets[$oid])) {
489 2
            return $data;
490
        }
491
492 1006
        return $this->entityChangeSets[$oid];
493
    }
494
495
    /**
496
     * Computes the changes that happened to a single entity.
497
     *
498
     * Modifies/populates the following properties:
499
     *
500
     * {@link originalEntityData}
501
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
502
     * then it was not fetched from the database and therefore we have no original
503
     * entity data yet. All of the current entity data is stored as the original entity data.
504
     *
505
     * {@link entityChangeSets}
506
     * The changes detected on all properties of the entity are stored there.
507
     * A change is a tuple array where the first entry is the old value and the second
508
     * entry is the new value of the property. Changesets are used by persisters
509
     * to INSERT/UPDATE the persistent entity state.
510
     *
511
     * {@link entityUpdates}
512
     * If the entity is already fully MANAGED (has been fetched from the database before)
513
     * and any changes to its properties are detected, then a reference to the entity is stored
514
     * there to mark it for an update.
515
     *
516
     * {@link collectionDeletions}
517
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
518
     * then this collection is marked for deletion.
519
     *
520
     * @internal Don't call from the outside.
521
     *
522
     * @param ClassMetadata $class  The class descriptor of the entity.
523
     * @param object        $entity The entity for which to compute the changes.
524
     *
525
     * @ignore
526
     */
527 1021
    public function computeChangeSet(ClassMetadata $class, $entity)
528
    {
529 1021
        $oid = spl_object_id($entity);
530
531 1021
        if (isset($this->readOnlyObjects[$oid])) {
532 2
            return;
533
        }
534
535 1021
        if ($class->inheritanceType !== InheritanceType::NONE) {
536 333
            $class = $this->em->getClassMetadata(get_class($entity));
537
        }
538
539 1021
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
540
541 1021
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
542 130
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
543
        }
544
545 1021
        $actualData = [];
546
547 1021
        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

547
        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...
548 1021
            $value = $property->getValue($entity);
549
550 1021
            if ($property instanceof ToManyAssociationMetadata && $value !== null) {
551 773
                if ($value instanceof PersistentCollection && $value->getOwner() === $entity) {
552 186
                    continue;
553
                }
554
555 770
                $value = $property->wrap($entity, $value, $this->em);
556
557 770
                $property->setValue($entity, $value);
558
559 770
                $actualData[$name] = $value;
560
561 770
                continue;
562
            }
563
564 1021
            if (( ! $class->isIdentifier($name)
565 1021
                    || ! $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

565
                    || ! $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...
566 1021
                    || ! $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

566
                    || ! $class->getProperty($name)->/** @scrutinizer ignore-call */ hasValueGenerator()
Loading history...
567 1021
                    || $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

567
                    || $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...
568 1021
                ) && (! $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

568
                ) && (! $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...
569 988
                $actualData[$name] = $value;
570
            }
571
        }
572
573 1021
        if (! isset($this->originalEntityData[$oid])) {
574
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
575
            // These result in an INSERT.
576 1017
            $this->originalEntityData[$oid] = $actualData;
577 1017
            $changeSet                      = [];
578
579 1017
            foreach ($actualData as $propName => $actualValue) {
580 995
                $property = $class->getProperty($propName);
581
582 995
                if (($property instanceof FieldMetadata) ||
583 995
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
584 980
                    $changeSet[$propName] = [null, $actualValue];
585
                }
586
            }
587
588 1017
            $this->entityChangeSets[$oid] = $changeSet;
589
        } else {
590
            // Entity is "fully" MANAGED: it was already fully persisted before
591
            // and we have a copy of the original data
592 257
            $originalData           = $this->originalEntityData[$oid];
593 257
            $isChangeTrackingNotify = $class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY;
594 257
            $changeSet              = $isChangeTrackingNotify && isset($this->entityChangeSets[$oid])
595
                ? $this->entityChangeSets[$oid]
596 257
                : [];
597
598 257
            foreach ($actualData as $propName => $actualValue) {
599
                // skip field, its a partially omitted one!
600 245
                if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
601 40
                    continue;
602
                }
603
604 245
                $orgValue = $originalData[$propName];
605
606
                // skip if value haven't changed
607 245
                if ($orgValue === $actualValue) {
608 228
                    continue;
609
                }
610
611 109
                $property = $class->getProperty($propName);
612
613
                // Persistent collection was exchanged with the "originally"
614
                // created one. This can only mean it was cloned and replaced
615
                // on another entity.
616 109
                if ($actualValue instanceof PersistentCollection) {
617 8
                    $owner = $actualValue->getOwner();
618
619 8
                    if ($owner === null) { // cloned
620
                        $actualValue->setOwner($entity, $property);
0 ignored issues
show
Bug introduced by
It seems like $property can also be of type null; however, parameter $association of Doctrine\ORM\PersistentCollection::setOwner() does only seem to accept Doctrine\ORM\Mapping\ToManyAssociationMetadata, 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

620
                        $actualValue->setOwner($entity, /** @scrutinizer ignore-type */ $property);
Loading history...
621 8
                    } elseif ($owner !== $entity) { // no clone, we have to fix
622
                        if (! $actualValue->isInitialized()) {
623
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
624
                        }
625
626
                        $newValue = clone $actualValue;
627
628
                        $newValue->setOwner($entity, $property);
629
630
                        $property->setValue($entity, $newValue);
631
                    }
632
                }
633
634
                switch (true) {
635 109
                    case $property instanceof FieldMetadata:
636 58
                        if ($isChangeTrackingNotify) {
637
                            // Continue inside switch behaves as break.
638
                            // We are required to use continue 2, since we need to continue to next $actualData item
639
                            continue 2;
640
                        }
641
642 58
                        $changeSet[$propName] = [$orgValue, $actualValue];
643 58
                        break;
644
645 57
                    case $property instanceof ToOneAssociationMetadata:
646 46
                        if ($property->isOwningSide()) {
647 20
                            $changeSet[$propName] = [$orgValue, $actualValue];
648
                        }
649
650 46
                        if ($orgValue !== null && $property->isOrphanRemoval()) {
651 4
                            $this->scheduleOrphanRemoval($orgValue);
652
                        }
653
654 46
                        break;
655
656 12
                    case $property instanceof ToManyAssociationMetadata:
657
                        // Check if original value exists
658 9
                        if ($orgValue instanceof PersistentCollection) {
659
                            // A PersistentCollection was de-referenced, so delete it.
660 8
                            if (! $this->isCollectionScheduledForDeletion($orgValue)) {
661 8
                                $this->scheduleCollectionDeletion($orgValue);
662
663 8
                                $changeSet[$propName] = $orgValue; // Signal changeset, to-many associations will be ignored
664
                            }
665
                        }
666
667 9
                        break;
668
669
                    default:
670
                        // Do nothing
671
                }
672
            }
673
674 257
            if ($changeSet) {
675 84
                $this->entityChangeSets[$oid]   = $changeSet;
676 84
                $this->originalEntityData[$oid] = $actualData;
677 84
                $this->entityUpdates[$oid]      = $entity;
678
            }
679
        }
680
681
        // Look for changes in associations of the entity
682 1021
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
683 1021
            if (! $property instanceof AssociationMetadata) {
684 1021
                continue;
685
            }
686
687 886
            $value = $property->getValue($entity);
688
689 886
            if ($value === null) {
690 625
                continue;
691
            }
692
693 862
            $this->computeAssociationChanges($property, $value);
694
695 854
            if ($property instanceof ManyToManyAssociationMetadata &&
696 854
                $value instanceof PersistentCollection &&
697 854
                ! isset($this->entityChangeSets[$oid]) &&
698 854
                $property->isOwningSide() &&
699 854
                $value->isDirty()) {
700 31
                $this->entityChangeSets[$oid]   = [];
701 31
                $this->originalEntityData[$oid] = $actualData;
702 31
                $this->entityUpdates[$oid]      = $entity;
703
            }
704
        }
705 1013
    }
706
707
    /**
708
     * Computes all the changes that have been done to entities and collections
709
     * since the last commit and stores these changes in the _entityChangeSet map
710
     * temporarily for access by the persisters, until the UoW commit is finished.
711
     */
712 1020
    public function computeChangeSets()
713
    {
714
        // Compute changes for INSERTed entities first. This must always happen.
715 1020
        $this->computeScheduleInsertsChangeSets();
716
717
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
718 1018
        foreach ($this->identityMap as $className => $entities) {
719 446
            $class = $this->em->getClassMetadata($className);
720
721
            // Skip class if instances are read-only
722 446
            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

722
            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...
723 1
                continue;
724
            }
725
726
            // If change tracking is explicit or happens through notification, then only compute
727
            // changes on entities of that type that are explicitly marked for synchronization.
728
            switch (true) {
729 445
                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...
730 442
                    $entitiesToProcess = $entities;
731 442
                    break;
732
733 4
                case isset($this->scheduledForSynchronization[$className]):
734 4
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
735 4
                    break;
736
737
                default:
738 1
                    $entitiesToProcess = [];
739
            }
740
741 445
            foreach ($entitiesToProcess as $entity) {
742
                // Ignore uninitialized proxy objects
743 425
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
744 38
                    continue;
745
                }
746
747
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
748 423
                $oid = spl_object_id($entity);
749
750 423
                if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
751 257
                    $this->computeChangeSet($class, $entity);
752
                }
753
            }
754
        }
755 1018
    }
756
757
    /**
758
     * Computes the changes of an association.
759
     *
760
     * @param AssociationMetadata $association The association mapping.
761
     * @param mixed               $value       The value of the association.
762
     *
763
     * @throws ORMInvalidArgumentException
764
     * @throws ORMException
765
     */
766 862
    private function computeAssociationChanges(AssociationMetadata $association, $value)
767
    {
768 862
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
769 31
            return;
770
        }
771
772 861
        if ($value instanceof PersistentCollection && $value->isDirty()) {
773 529
            $coid = spl_object_id($value);
774
775 529
            $this->collectionUpdates[$coid]  = $value;
776 529
            $this->visitedCollections[$coid] = $value;
777
        }
778
779
        // Look through the entities, and in any of their associations,
780
        // for transient (new) entities, recursively. ("Persistence by reachability")
781
        // Unwrap. Uninitialized collections will simply be empty.
782 861
        $unwrappedValue = $association instanceof ToOneAssociationMetadata ? [$value] : $value->unwrap();
783 861
        $targetEntity   = $association->getTargetEntity();
784 861
        $targetClass    = $this->em->getClassMetadata($targetEntity);
785
786 861
        foreach ($unwrappedValue as $key => $entry) {
787 718
            if (! ($entry instanceof $targetEntity)) {
788 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $association, $entry);
789
            }
790
791 710
            $state = $this->getEntityState($entry, self::STATE_NEW);
792
793 710
            if (! ($entry instanceof $targetEntity)) {
794
                throw UnexpectedAssociationValue::create(
795
                    $association->getSourceEntity(),
796
                    $association->getName(),
797
                    get_class($entry),
798
                    $targetEntity
799
                );
800
            }
801
802
            switch ($state) {
803 710
                case self::STATE_NEW:
804 41
                    if (! in_array('persist', $association->getCascade(), true)) {
805 5
                        $this->nonCascadedNewDetectedEntities[spl_object_id($entry)] = [$association, $entry];
806
807 5
                        break;
808
                    }
809
810 37
                    $this->persistNew($targetClass, $entry);
811 37
                    $this->computeChangeSet($targetClass, $entry);
812
813 37
                    break;
814
815 703
                case self::STATE_REMOVED:
816
                    // Consume the $value as array (it's either an array or an ArrayAccess)
817
                    // and remove the element from Collection.
818 4
                    if ($association instanceof ToManyAssociationMetadata) {
819 3
                        unset($value[$key]);
820
                    }
821 4
                    break;
822
823 703
                case self::STATE_DETACHED:
824
                    // Can actually not happen right now as we assume STATE_NEW,
825
                    // so the exception will be raised from the DBAL layer (constraint violation).
826
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($association, $entry);
827
                    break;
828
829
                default:
830
                    // MANAGED associated entities are already taken into account
831
                    // during changeset calculation anyway, since they are in the identity map.
832
            }
833
        }
834 853
    }
835
836
    /**
837
     * @param ClassMetadata $class
838
     * @param object        $entity
839
     */
840 1032
    private function persistNew($class, $entity)
841
    {
842 1032
        $oid    = spl_object_id($entity);
843 1032
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
844
845 1032
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
846 132
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
847
        }
848
849 1032
        $generationPlan = $class->getValueGenerationPlan();
850 1032
        $persister      = $this->getEntityPersister($class->getClassName());
851 1032
        $generationPlan->executeImmediate($this->em, $entity);
852
853 1032
        if (! $generationPlan->containsDeferred()) {
854 221
            $id = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
855
856
            // Some identifiers may be foreign keys to new entities.
857
            // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
858 221
            if (! $this->hasMissingIdsWhichAreForeignKeys($class, $id)) {
859 221
                $this->entityIdentifiers[$oid] = $id;
860
            }
861
        }
862
863 1032
        $this->entityStates[$oid] = self::STATE_MANAGED;
864
865 1032
        $this->scheduleForInsert($entity);
866 1032
    }
867
868
    /**
869
     * @param mixed[] $idValue
870
     */
871 221
    private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
872
    {
873 221
        foreach ($idValue as $idField => $idFieldValue) {
874 221
            if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist on Doctrine\ORM\Mapping\ClassMetadata.
Loading history...
875
                return true;
876
            }
877
        }
878
879 221
        return false;
880
    }
881
882
    /**
883
     * INTERNAL:
884
     * Computes the changeset of an individual entity, independently of the
885
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
886
     *
887
     * The passed entity must be a managed entity. If the entity already has a change set
888
     * because this method is invoked during a commit cycle then the change sets are added.
889
     * whereby changes detected in this method prevail.
890
     *
891
     * @param ClassMetadata $class  The class descriptor of the entity.
892
     * @param object        $entity The entity for which to (re)calculate the change set.
893
     *
894
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
895
     * @throws RuntimeException
896
     *
897
     * @ignore
898
     */
899 15
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) : void
900
    {
901 15
        $oid = spl_object_id($entity);
902
903 15
        if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) {
904
            throw ORMInvalidArgumentException::entityNotManaged($entity);
905
        }
906
907
        // skip if change tracking is "NOTIFY"
908 15
        if ($class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) {
909
            return;
910
        }
911
912 15
        if ($class->inheritanceType !== InheritanceType::NONE) {
913 3
            $class = $this->em->getClassMetadata(get_class($entity));
914
        }
915
916 15
        $actualData = [];
917
918 15
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
919
            switch (true) {
920 15
                case $property instanceof VersionFieldMetadata:
921
                    // Ignore version field
922
                    break;
923
924 15
                case $property instanceof FieldMetadata:
925 15
                    if (! $property->isPrimaryKey()
926 15
                        || ! $property->getValueGenerator()
927 15
                        || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY) {
928 15
                        $actualData[$name] = $property->getValue($entity);
929
                    }
930
931 15
                    break;
932
933 11
                case $property instanceof ToOneAssociationMetadata:
934 9
                    $actualData[$name] = $property->getValue($entity);
935 9
                    break;
936
            }
937
        }
938
939 15
        if (! isset($this->originalEntityData[$oid])) {
940
            throw new RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
941
        }
942
943 15
        $originalData = $this->originalEntityData[$oid];
944 15
        $changeSet    = [];
945
946 15
        foreach ($actualData as $propName => $actualValue) {
947 15
            $orgValue = $originalData[$propName] ?? null;
948
949 15
            if ($orgValue !== $actualValue) {
950 7
                $changeSet[$propName] = [$orgValue, $actualValue];
951
            }
952
        }
953
954 15
        if ($changeSet) {
955 7
            if (isset($this->entityChangeSets[$oid])) {
956 6
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
957 1
            } elseif (! isset($this->entityInsertions[$oid])) {
958 1
                $this->entityChangeSets[$oid] = $changeSet;
959 1
                $this->entityUpdates[$oid]    = $entity;
960
            }
961 7
            $this->originalEntityData[$oid] = $actualData;
962
        }
963 15
    }
964
965
    /**
966
     * Executes all entity insertions for entities of the specified type.
967
     */
968 1007
    private function executeInserts(ClassMetadata $class) : void
969
    {
970 1007
        $className      = $class->getClassName();
971 1007
        $persister      = $this->getEntityPersister($className);
972 1007
        $invoke         = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
973 1007
        $generationPlan = $class->getValueGenerationPlan();
974
975 1007
        $insertionsForClass = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $insertionsForClass is dead and can be removed.
Loading history...
976
977 1007
        foreach ($this->entityInsertions as $oid => $entity) {
978 1007
            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

978
            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...
979 854
                continue;
980
            }
981
982 1007
            $persister->insert($entity);
983
984 1006
            if ($generationPlan->containsDeferred()) {
985
                // Entity has post-insert IDs
986 943
                $oid = spl_object_id($entity);
987 943
                $id  = $persister->getIdentifier($entity);
988
989 943
                $this->entityIdentifiers[$oid]  = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
990 943
                $this->entityStates[$oid]       = self::STATE_MANAGED;
991 943
                $this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid];
992
993 943
                $this->addToIdentityMap($entity);
994
            }
995
996 1006
            unset($this->entityInsertions[$oid]);
997
998 1006
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
999 128
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
1000
1001 128
                $this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke);
1002
            }
1003
        }
1004 1007
    }
1005
1006
    /**
1007
     * @param object $entity
1008
     */
1009
    private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
0 ignored issues
show
Unused Code introduced by
The method addToEntityIdentifiersAndEntityMap() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
introduced by
Class UnitOfWork contains unused private method addToEntityIdentifiersAndEntityMap().
Loading history...
introduced by
There must be exactly 1 whitespace between closing parenthesis and return type colon.
Loading history...
1010
    {
1011
        $identifier = [];
1012
1013
        foreach ($class->getIdentifierFieldNames() as $idField) {
1014
            $value = $class->getFieldValue($entity, $idField);
0 ignored issues
show
Bug introduced by
The method getFieldValue() does not exist on Doctrine\ORM\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

1014
            /** @scrutinizer ignore-call */ 
1015
            $value = $class->getFieldValue($entity, $idField);

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...
1015
1016
            if (isset($class->associationMappings[$idField])) {
0 ignored issues
show
Bug introduced by
The property associationMappings does not seem to exist on Doctrine\ORM\Mapping\ClassMetadata.
Loading history...
1017
                // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
1018
                $value = $this->getSingleIdentifierValue($value);
1019
            }
1020
1021
            $identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
1022
        }
1023
1024
        $this->entityStates[$oid]      = self::STATE_MANAGED;
1025
        $this->entityIdentifiers[$oid] = $identifier;
1026
1027
        $this->addToIdentityMap($entity);
1028
    }
1029
1030
    /**
1031
     * Executes all entity updates for entities of the specified type.
1032
     *
1033
     * @param ClassMetadata $class
1034
     */
1035 111
    private function executeUpdates($class)
1036
    {
1037 111
        $className        = $class->getClassName();
1038 111
        $persister        = $this->getEntityPersister($className);
1039 111
        $preUpdateInvoke  = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1040 111
        $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1041
1042 111
        foreach ($this->entityUpdates as $oid => $entity) {
1043 111
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
1044 71
                continue;
1045
            }
1046
1047 111
            if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
1048 12
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1049
1050 12
                $this->recomputeSingleEntityChangeSet($class, $entity);
1051
            }
1052
1053 111
            if (! empty($this->entityChangeSets[$oid])) {
1054 81
                $persister->update($entity);
1055
            }
1056
1057 107
            unset($this->entityUpdates[$oid]);
1058
1059 107
            if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
1060 10
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1061
            }
1062
        }
1063 107
    }
1064
1065
    /**
1066
     * Executes all entity deletions for entities of the specified type.
1067
     *
1068
     * @param ClassMetadata $class
1069
     */
1070 60
    private function executeDeletions($class)
1071
    {
1072 60
        $className = $class->getClassName();
1073 60
        $persister = $this->getEntityPersister($className);
1074 60
        $invoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1075
1076 60
        foreach ($this->entityDeletions as $oid => $entity) {
1077 60
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
1078 24
                continue;
1079
            }
1080
1081 60
            $persister->delete($entity);
1082
1083
            unset(
1084 60
                $this->entityDeletions[$oid],
1085 60
                $this->entityIdentifiers[$oid],
1086 60
                $this->originalEntityData[$oid],
1087 60
                $this->entityStates[$oid]
1088
            );
1089
1090
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1091
            // is obtained by a new entity because the old one went out of scope.
1092 60
            if (! $class->isIdentifierComposite()) {
1093 57
                $property = $class->getProperty($class->getSingleIdentifierFieldName());
1094
1095 57
                if ($property instanceof FieldMetadata && $property->hasValueGenerator()) {
1096 50
                    $property->setValue($entity, null);
1097
                }
1098
            }
1099
1100 60
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1101 9
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
1102
1103 9
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, $eventArgs, $invoke);
1104
            }
1105
        }
1106 59
    }
1107
1108
    /**
1109
     * Gets the commit order.
1110
     *
1111
     * @return ClassMetadata[]
1112
     */
1113 1011
    private function getCommitOrder()
1114
    {
1115 1011
        $calc = new Internal\CommitOrderCalculator();
1116
1117
        // See if there are any new classes in the changeset, that are not in the
1118
        // commit order graph yet (don't have a node).
1119
        // We have to inspect changeSet to be able to correctly build dependencies.
1120
        // It is not possible to use IdentityMap here because post inserted ids
1121
        // are not yet available.
1122 1011
        $newNodes = [];
1123
1124 1011
        foreach (array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) {
1125 1011
            $class = $this->em->getClassMetadata(get_class($entity));
1126
1127 1011
            if ($calc->hasNode($class->getClassName())) {
1128 635
                continue;
1129
            }
1130
1131 1011
            $calc->addNode($class->getClassName(), $class);
1132
1133 1011
            $newNodes[] = $class;
1134
        }
1135
1136
        // Calculate dependencies for new nodes
1137 1011
        while ($class = array_pop($newNodes)) {
1138 1011
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
1139 1011
                if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
1140 1011
                    continue;
1141
                }
1142
1143 834
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1144
1145 834
                if (! $calc->hasNode($targetClass->getClassName())) {
1146 637
                    $calc->addNode($targetClass->getClassName(), $targetClass);
1147
1148 637
                    $newNodes[] = $targetClass;
1149
                }
1150
1151 834
                $weight = ! array_filter(
1152 834
                    $property->getJoinColumns(),
1153
                    static function (JoinColumnMetadata $joinColumn) {
1154 834
                        return $joinColumn->isNullable();
1155 834
                    }
1156
                );
1157
1158 834
                $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

1158
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), /** @scrutinizer ignore-type */ $weight);
Loading history...
1159
1160
                // If the target class has mapped subclasses, these share the same dependency.
1161 834
                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

1161
                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...
1162 829
                    continue;
1163
                }
1164
1165 225
                foreach ($targetClass->getSubClasses() as $subClassName) {
1166 225
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1167
1168 225
                    if (! $calc->hasNode($subClassName)) {
1169 199
                        $calc->addNode($targetSubClass->getClassName(), $targetSubClass);
1170
1171 199
                        $newNodes[] = $targetSubClass;
1172
                    }
1173
1174 225
                    $calc->addDependency($targetSubClass->getClassName(), $class->getClassName(), 1);
1175
                }
1176
            }
1177
        }
1178
1179 1011
        return $calc->sort();
1180
    }
1181
1182
    /**
1183
     * Schedules an entity for insertion into the database.
1184
     * If the entity already has an identifier, it will be added to the identity map.
1185
     *
1186
     * @param object $entity The entity to schedule for insertion.
1187
     *
1188
     * @throws ORMInvalidArgumentException
1189
     * @throws InvalidArgumentException
1190
     */
1191 1033
    public function scheduleForInsert($entity)
1192
    {
1193 1033
        $oid = spl_object_id($entity);
1194
1195 1033
        if (isset($this->entityUpdates[$oid])) {
1196
            throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
1197
        }
1198
1199 1033
        if (isset($this->entityDeletions[$oid])) {
1200 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1201
        }
1202 1033
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1203 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1204
        }
1205
1206 1033
        if (isset($this->entityInsertions[$oid])) {
1207 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1208
        }
1209
1210 1033
        $this->entityInsertions[$oid] = $entity;
1211
1212 1033
        if (isset($this->entityIdentifiers[$oid])) {
1213 221
            $this->addToIdentityMap($entity);
1214
        }
1215
1216 1033
        if ($entity instanceof NotifyPropertyChanged) {
1217 5
            $entity->addPropertyChangedListener($this);
1218
        }
1219 1033
    }
1220
1221
    /**
1222
     * Checks whether an entity is scheduled for insertion.
1223
     *
1224
     * @param object $entity
1225
     *
1226
     * @return bool
1227
     */
1228 621
    public function isScheduledForInsert($entity)
1229
    {
1230 621
        return isset($this->entityInsertions[spl_object_id($entity)]);
1231
    }
1232
1233
    /**
1234
     * Schedules an entity for being updated.
1235
     *
1236
     * @param object $entity The entity to schedule for being updated.
1237
     *
1238
     * @throws ORMInvalidArgumentException
1239
     */
1240 1
    public function scheduleForUpdate($entity) : void
1241
    {
1242 1
        $oid = spl_object_id($entity);
1243
1244 1
        if (! isset($this->entityIdentifiers[$oid])) {
1245
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update');
1246
        }
1247
1248 1
        if (isset($this->entityDeletions[$oid])) {
1249
            throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update');
1250
        }
1251
1252 1
        if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1253 1
            $this->entityUpdates[$oid] = $entity;
1254
        }
1255 1
    }
1256
1257
    /**
1258
     * INTERNAL:
1259
     * Schedules an extra update that will be executed immediately after the
1260
     * regular entity updates within the currently running commit cycle.
1261
     *
1262
     * Extra updates for entities are stored as (entity, changeset) tuples.
1263
     *
1264
     * @param object  $entity    The entity for which to schedule an extra update.
1265
     * @param mixed[] $changeset The changeset of the entity (what to update).
1266
     *
1267
     * @ignore
1268
     */
1269 29
    public function scheduleExtraUpdate($entity, array $changeset) : void
1270
    {
1271 29
        $oid         = spl_object_id($entity);
1272 29
        $extraUpdate = [$entity, $changeset];
1273
1274 29
        if (isset($this->extraUpdates[$oid])) {
1275 1
            [$unused, $changeset2] = $this->extraUpdates[$oid];
1276
1277 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1278
        }
1279
1280 29
        $this->extraUpdates[$oid] = $extraUpdate;
1281 29
    }
1282
1283
    /**
1284
     * Checks whether an entity is registered as dirty in the unit of work.
1285
     * Note: Is not very useful currently as dirty entities are only registered
1286
     * at commit time.
1287
     *
1288
     * @param object $entity
1289
     */
1290
    public function isScheduledForUpdate($entity) : bool
1291
    {
1292
        return isset($this->entityUpdates[spl_object_id($entity)]);
1293
    }
1294
1295
    /**
1296
     * Checks whether an entity is registered to be checked in the unit of work.
1297
     *
1298
     * @param object $entity
1299
     */
1300 2
    public function isScheduledForDirtyCheck($entity) : bool
1301
    {
1302 2
        $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

1302
        $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...
1303
1304 2
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]);
1305
    }
1306
1307
    /**
1308
     * INTERNAL:
1309
     * Schedules an entity for deletion.
1310
     *
1311
     * @param object $entity
1312
     */
1313 63
    public function scheduleForDelete($entity)
1314
    {
1315 63
        $oid = spl_object_id($entity);
1316
1317 63
        if (isset($this->entityInsertions[$oid])) {
1318 1
            if ($this->isInIdentityMap($entity)) {
1319
                $this->removeFromIdentityMap($entity);
1320
            }
1321
1322 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1323
1324 1
            return; // entity has not been persisted yet, so nothing more to do.
1325
        }
1326
1327 63
        if (! $this->isInIdentityMap($entity)) {
1328 1
            return;
1329
        }
1330
1331 62
        $this->removeFromIdentityMap($entity);
1332
1333 62
        unset($this->entityUpdates[$oid]);
1334
1335 62
        if (! isset($this->entityDeletions[$oid])) {
1336 62
            $this->entityDeletions[$oid] = $entity;
1337 62
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1338
        }
1339 62
    }
1340
1341
    /**
1342
     * Checks whether an entity is registered as removed/deleted with the unit
1343
     * of work.
1344
     *
1345
     * @param object $entity
1346
     *
1347
     * @return bool
1348
     */
1349 13
    public function isScheduledForDelete($entity)
1350
    {
1351 13
        return isset($this->entityDeletions[spl_object_id($entity)]);
1352
    }
1353
1354
    /**
1355
     * Checks whether an entity is scheduled for insertion, update or deletion.
1356
     *
1357
     * @param object $entity
1358
     *
1359
     * @return bool
1360
     */
1361
    public function isEntityScheduled($entity)
1362
    {
1363
        $oid = spl_object_id($entity);
1364
1365
        return isset($this->entityInsertions[$oid])
1366
            || isset($this->entityUpdates[$oid])
1367
            || isset($this->entityDeletions[$oid]);
1368
    }
1369
1370
    /**
1371
     * INTERNAL:
1372
     * Registers an entity in the identity map.
1373
     * Note that entities in a hierarchy are registered with the class name of
1374
     * the root entity.
1375
     *
1376
     * @param object $entity The entity to register.
1377
     *
1378
     * @return bool  TRUE if the registration was successful, FALSE if the identity of
1379
     *               the entity in question is already managed.
1380
     *
1381
     * @throws ORMInvalidArgumentException
1382
     *
1383
     * @ignore
1384
     */
1385 1089
    public function addToIdentityMap($entity)
1386
    {
1387 1089
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1388 1089
        $identifier    = $this->entityIdentifiers[spl_object_id($entity)];
1389
1390 1089
        if (empty($identifier) || in_array(null, $identifier, true)) {
1391 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->getClassName(), $entity);
1392
        }
1393
1394 1083
        $idHash    = implode(' ', $identifier);
1395 1083
        $className = $classMetadata->getRootClassName();
1396
1397 1083
        if (isset($this->identityMap[$className][$idHash])) {
1398 31
            return false;
1399
        }
1400
1401 1083
        $this->identityMap[$className][$idHash] = $entity;
1402
1403 1083
        return true;
1404
    }
1405
1406
    /**
1407
     * Gets the state of an entity with regard to the current unit of work.
1408
     *
1409
     * @param object   $entity
1410
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1411
     *                         This parameter can be set to improve performance of entity state detection
1412
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1413
     *                         is either known or does not matter for the caller of the method.
1414
     *
1415
     * @return int The entity state.
1416
     */
1417 1041
    public function getEntityState($entity, $assume = null)
1418
    {
1419 1041
        $oid = spl_object_id($entity);
1420
1421 1041
        if (isset($this->entityStates[$oid])) {
1422 751
            return $this->entityStates[$oid];
1423
        }
1424
1425 1036
        if ($assume !== null) {
1426 1033
            return $assume;
1427
        }
1428
1429
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1430
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1431
        // the UoW does not hold references to such objects and the object hash can be reused.
1432
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1433 8
        $class     = $this->em->getClassMetadata(get_class($entity));
1434 8
        $persister = $this->getEntityPersister($class->getClassName());
1435 8
        $id        = $persister->getIdentifier($entity);
1436
1437 8
        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...
1438 3
            return self::STATE_NEW;
1439
        }
1440
1441 6
        $flatId = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
1442
1443 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

1443
        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...
1444 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

1444
            || ! $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...
1445 6
            || ! $class->getProperty($class->getSingleIdentifierFieldName())->hasValueGenerator()
1446
        ) {
1447
            // Check for a version field, if available, to avoid a db lookup.
1448 5
            if ($class->isVersioned()) {
1449 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...
1450
                    ? self::STATE_DETACHED
1451 1
                    : self::STATE_NEW;
1452
            }
1453
1454
            // Last try before db lookup: check the identity map.
1455 4
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
1456 1
                return self::STATE_DETACHED;
1457
            }
1458
1459
            // db lookup
1460 4
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
1461
                return self::STATE_DETACHED;
1462
            }
1463
1464 4
            return self::STATE_NEW;
1465
        }
1466
1467 1
        if ($class->isIdentifierComposite()
1468 1
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
1469 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

1469
            || ! $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...
1470
            // if we have a pre insert generator we can't be sure that having an id
1471
            // really means that the entity exists. We have to verify this through
1472
            // the last resort: a db lookup
1473
1474
            // Last try before db lookup: check the identity map.
1475
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
1476
                return self::STATE_DETACHED;
1477
            }
1478
1479
            // db lookup
1480
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
1481
                return self::STATE_DETACHED;
1482
            }
1483
1484
            return self::STATE_NEW;
1485
        }
1486
1487 1
        return self::STATE_DETACHED;
1488
    }
1489
1490
    /**
1491
     * INTERNAL:
1492
     * Removes an entity from the identity map. This effectively detaches the
1493
     * entity from the persistence management of Doctrine.
1494
     *
1495
     * @param object $entity
1496
     *
1497
     * @return bool
1498
     *
1499
     * @throws ORMInvalidArgumentException
1500
     *
1501
     * @ignore
1502
     */
1503 62
    public function removeFromIdentityMap($entity)
1504
    {
1505 62
        $oid           = spl_object_id($entity);
1506 62
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1507 62
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1508
1509 62
        if ($idHash === '') {
1510
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
1511
        }
1512
1513 62
        $className = $classMetadata->getRootClassName();
1514
1515 62
        if (isset($this->identityMap[$className][$idHash])) {
1516 62
            unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]);
1517
1518 62
            return true;
1519
        }
1520
1521
        return false;
1522
    }
1523
1524
    /**
1525
     * INTERNAL:
1526
     * Gets an entity in the identity map by its identifier hash.
1527
     *
1528
     * @param string $idHash
1529
     * @param string $rootClassName
1530
     *
1531
     * @return object
1532
     *
1533
     * @ignore
1534
     */
1535 6
    public function getByIdHash($idHash, $rootClassName)
1536
    {
1537 6
        return $this->identityMap[$rootClassName][$idHash];
1538
    }
1539
1540
    /**
1541
     * INTERNAL:
1542
     * Tries to get an entity by its identifier hash. If no entity is found for
1543
     * the given hash, FALSE is returned.
1544
     *
1545
     * @param mixed  $idHash        (must be possible to cast it to string)
1546
     * @param string $rootClassName
1547
     *
1548
     * @return object|bool The found entity or FALSE.
1549
     *
1550
     * @ignore
1551
     */
1552
    public function tryGetByIdHash($idHash, $rootClassName)
1553
    {
1554
        $stringIdHash = (string) $idHash;
1555
1556
        return $this->identityMap[$rootClassName][$stringIdHash] ?? false;
1557
    }
1558
1559
    /**
1560
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1561
     *
1562
     * @param object $entity
1563
     *
1564
     * @return bool
1565
     */
1566 147
    public function isInIdentityMap($entity)
1567
    {
1568 147
        $oid = spl_object_id($entity);
1569
1570 147
        if (empty($this->entityIdentifiers[$oid])) {
1571 23
            return false;
1572
        }
1573
1574 133
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1575 133
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1576
1577 133
        return isset($this->identityMap[$classMetadata->getRootClassName()][$idHash]);
1578
    }
1579
1580
    /**
1581
     * INTERNAL:
1582
     * Checks whether an identifier hash exists in the identity map.
1583
     *
1584
     * @param string $idHash
1585
     * @param string $rootClassName
1586
     *
1587
     * @return bool
1588
     *
1589
     * @ignore
1590
     */
1591
    public function containsIdHash($idHash, $rootClassName)
1592
    {
1593
        return isset($this->identityMap[$rootClassName][$idHash]);
1594
    }
1595
1596
    /**
1597
     * Persists an entity as part of the current unit of work.
1598
     *
1599
     * @param object $entity The entity to persist.
1600
     */
1601 1033
    public function persist($entity)
1602
    {
1603 1033
        $visited = [];
1604
1605 1033
        $this->doPersist($entity, $visited);
1606 1026
    }
1607
1608
    /**
1609
     * Persists an entity as part of the current unit of work.
1610
     *
1611
     * This method is internally called during persist() cascades as it tracks
1612
     * the already visited entities to prevent infinite recursions.
1613
     *
1614
     * @param object   $entity  The entity to persist.
1615
     * @param object[] $visited The already visited entities.
1616
     *
1617
     * @throws ORMInvalidArgumentException
1618
     * @throws UnexpectedValueException
1619
     */
1620 1033
    private function doPersist($entity, array &$visited)
1621
    {
1622 1033
        $oid = spl_object_id($entity);
1623
1624 1033
        if (isset($visited[$oid])) {
1625 109
            return; // Prevent infinite recursion
1626
        }
1627
1628 1033
        $visited[$oid] = $entity; // Mark visited
1629
1630 1033
        $class = $this->em->getClassMetadata(get_class($entity));
1631
1632
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1633
        // If we would detect DETACHED here we would throw an exception anyway with the same
1634
        // consequences (not recoverable/programming error), so just assuming NEW here
1635
        // lets us avoid some database lookups for entities with natural identifiers.
1636 1033
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1637
1638
        switch ($entityState) {
1639 1033
            case self::STATE_MANAGED:
1640
                // Nothing to do, except if policy is "deferred explicit"
1641 220
                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...
1642 3
                    $this->scheduleForSynchronization($entity);
1643
                }
1644 220
                break;
1645
1646 1033
            case self::STATE_NEW:
1647 1032
                $this->persistNew($class, $entity);
1648 1032
                break;
1649
1650 1
            case self::STATE_REMOVED:
1651
                // Entity becomes managed again
1652 1
                unset($this->entityDeletions[$oid]);
1653 1
                $this->addToIdentityMap($entity);
1654
1655 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1656 1
                break;
1657
1658
            case self::STATE_DETACHED:
1659
                // Can actually not happen right now since we assume STATE_NEW.
1660
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted');
1661
1662
            default:
1663
                throw new UnexpectedValueException(
1664
                    sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity))
1665
                );
1666
        }
1667
1668 1033
        $this->cascadePersist($entity, $visited);
1669 1026
    }
1670
1671
    /**
1672
     * Deletes an entity as part of the current unit of work.
1673
     *
1674
     * @param object $entity The entity to remove.
1675
     */
1676 62
    public function remove($entity)
1677
    {
1678 62
        $visited = [];
1679
1680 62
        $this->doRemove($entity, $visited);
1681 62
    }
1682
1683
    /**
1684
     * Deletes an entity as part of the current unit of work.
1685
     *
1686
     * This method is internally called during delete() cascades as it tracks
1687
     * the already visited entities to prevent infinite recursions.
1688
     *
1689
     * @param object   $entity  The entity to delete.
1690
     * @param object[] $visited The map of the already visited entities.
1691
     *
1692
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1693
     * @throws UnexpectedValueException
1694
     */
1695 62
    private function doRemove($entity, array &$visited)
1696
    {
1697 62
        $oid = spl_object_id($entity);
1698
1699 62
        if (isset($visited[$oid])) {
1700 1
            return; // Prevent infinite recursion
1701
        }
1702
1703 62
        $visited[$oid] = $entity; // mark visited
1704
1705
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1706
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1707 62
        $this->cascadeRemove($entity, $visited);
1708
1709 62
        $class       = $this->em->getClassMetadata(get_class($entity));
1710 62
        $entityState = $this->getEntityState($entity);
1711
1712
        switch ($entityState) {
1713 62
            case self::STATE_NEW:
1714 62
            case self::STATE_REMOVED:
1715
                // nothing to do
1716 2
                break;
1717
1718 62
            case self::STATE_MANAGED:
1719 62
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1720
1721 62
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1722 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1723
                }
1724
1725 62
                $this->scheduleForDelete($entity);
1726 62
                break;
1727
1728
            case self::STATE_DETACHED:
1729
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed');
1730
            default:
1731
                throw new UnexpectedValueException(
1732
                    sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity))
1733
                );
1734
        }
1735 62
    }
1736
1737
    /**
1738
     * Refreshes the state of the given entity from the database, overwriting
1739
     * any local, unpersisted changes.
1740
     *
1741
     * @param object $entity The entity to refresh.
1742
     *
1743
     * @throws InvalidArgumentException If the entity is not MANAGED.
1744
     */
1745 15
    public function refresh($entity)
1746
    {
1747 15
        $visited = [];
1748
1749 15
        $this->doRefresh($entity, $visited);
1750 15
    }
1751
1752
    /**
1753
     * Executes a refresh operation on an entity.
1754
     *
1755
     * @param object   $entity  The entity to refresh.
1756
     * @param object[] $visited The already visited entities during cascades.
1757
     *
1758
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
1759
     */
1760 15
    private function doRefresh($entity, array &$visited)
1761
    {
1762 15
        $oid = spl_object_id($entity);
1763
1764 15
        if (isset($visited[$oid])) {
1765
            return; // Prevent infinite recursion
1766
        }
1767
1768 15
        $visited[$oid] = $entity; // mark visited
1769
1770 15
        $class = $this->em->getClassMetadata(get_class($entity));
1771
1772 15
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1773
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1774
        }
1775
1776 15
        $this->getEntityPersister($class->getClassName())->refresh(
1777 15
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->ge...ntityIdentifiers[$oid]) can also be of type false; however, parameter $id of Doctrine\ORM\Persisters\...ityPersister::refresh() does only seem to accept array, 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

1777
            /** @scrutinizer ignore-type */ array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
Loading history...
1778 15
            $entity
1779
        );
1780
1781 15
        $this->cascadeRefresh($entity, $visited);
1782 15
    }
1783
1784
    /**
1785
     * Cascades a refresh operation to associated entities.
1786
     *
1787
     * @param object   $entity
1788
     * @param object[] $visited
1789
     */
1790 15
    private function cascadeRefresh($entity, array &$visited)
1791
    {
1792 15
        $class = $this->em->getClassMetadata(get_class($entity));
1793
1794 15
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1795 15
            if (! ($association instanceof AssociationMetadata && in_array('refresh', $association->getCascade(), true))) {
1796 15
                continue;
1797
            }
1798
1799 4
            $relatedEntities = $association->getValue($entity);
1800
1801
            switch (true) {
1802 4
                case $relatedEntities instanceof PersistentCollection:
1803
                    // Unwrap so that foreach() does not initialize
1804 4
                    $relatedEntities = $relatedEntities->unwrap();
1805
                    // break; is commented intentionally!
1806
1807
                case $relatedEntities instanceof Collection:
1808
                case is_array($relatedEntities):
1809 4
                    foreach ($relatedEntities as $relatedEntity) {
1810
                        $this->doRefresh($relatedEntity, $visited);
1811
                    }
1812 4
                    break;
1813
1814
                case $relatedEntities !== null:
1815
                    $this->doRefresh($relatedEntities, $visited);
1816
                    break;
1817
1818
                default:
1819
                    // Do nothing
1820
            }
1821
        }
1822 15
    }
1823
1824
    /**
1825
     * Cascades the save operation to associated entities.
1826
     *
1827
     * @param object   $entity
1828
     * @param object[] $visited
1829
     *
1830
     * @throws ORMInvalidArgumentException
1831
     */
1832 1033
    private function cascadePersist($entity, array &$visited)
1833
    {
1834 1033
        $class = $this->em->getClassMetadata(get_class($entity));
1835
1836 1033
        if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1837
            // nothing to do - proxy is not initialized, therefore we don't do anything with it
1838 1
            return;
1839
        }
1840
1841 1033
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1842 1033
            if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade(), true))) {
1843 1033
                continue;
1844
            }
1845
1846
            /** @var AssociationMetadata $association */
1847 647
            $relatedEntities = $association->getValue($entity);
1848 647
            $targetEntity    = $association->getTargetEntity();
1849
1850
            switch (true) {
1851 647
                case $relatedEntities instanceof PersistentCollection:
1852
                    // Unwrap so that foreach() does not initialize
1853 13
                    $relatedEntities = $relatedEntities->unwrap();
1854
                    // break; is commented intentionally!
1855
1856 647
                case $relatedEntities instanceof Collection:
1857 584
                case is_array($relatedEntities):
1858 542
                    if (! ($association instanceof ToManyAssociationMetadata)) {
1859 3
                        throw ORMInvalidArgumentException::invalidAssociation(
1860 3
                            $this->em->getClassMetadata($targetEntity),
1861 3
                            $association,
1862 3
                            $relatedEntities
1863
                        );
1864
                    }
1865
1866 539
                    foreach ($relatedEntities as $relatedEntity) {
1867 283
                        $this->doPersist($relatedEntity, $visited);
1868
                    }
1869
1870 539
                    break;
1871
1872 573
                case $relatedEntities !== null:
1873 241
                    if (! $relatedEntities instanceof $targetEntity) {
1874 4
                        throw ORMInvalidArgumentException::invalidAssociation(
1875 4
                            $this->em->getClassMetadata($targetEntity),
1876 4
                            $association,
1877 4
                            $relatedEntities
1878
                        );
1879
                    }
1880
1881 237
                    $this->doPersist($relatedEntities, $visited);
1882 237
                    break;
1883
1884
                default:
1885
                    // Do nothing
1886
            }
1887
        }
1888 1026
    }
1889
1890
    /**
1891
     * Cascades the delete operation to associated entities.
1892
     *
1893
     * @param object   $entity
1894
     * @param object[] $visited
1895
     */
1896 62
    private function cascadeRemove($entity, array &$visited)
1897
    {
1898 62
        $entitiesToCascade = [];
1899 62
        $class             = $this->em->getClassMetadata(get_class($entity));
1900
1901 62
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1902 62
            if (! ($association instanceof AssociationMetadata && in_array('remove', $association->getCascade(), true))) {
1903 62
                continue;
1904
            }
1905
1906 25
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1907 6
                $entity->initializeProxy();
1908
            }
1909
1910 25
            $relatedEntities = $association->getValue($entity);
1911
1912
            switch (true) {
1913 25
                case $relatedEntities instanceof Collection:
1914 18
                case is_array($relatedEntities):
1915
                    // If its a PersistentCollection initialization is intended! No unwrap!
1916 20
                    foreach ($relatedEntities as $relatedEntity) {
1917 10
                        $entitiesToCascade[] = $relatedEntity;
1918
                    }
1919 20
                    break;
1920
1921 18
                case $relatedEntities !== null:
1922 7
                    $entitiesToCascade[] = $relatedEntities;
1923 7
                    break;
1924
1925
                default:
1926
                    // Do nothing
1927
            }
1928
        }
1929
1930 62
        foreach ($entitiesToCascade as $relatedEntity) {
1931 16
            $this->doRemove($relatedEntity, $visited);
1932
        }
1933 62
    }
1934
1935
    /**
1936
     * Acquire a lock on the given entity.
1937
     *
1938
     * @param object $entity
1939
     * @param int    $lockMode
1940
     * @param int    $lockVersion
1941
     *
1942
     * @throws ORMInvalidArgumentException
1943
     * @throws TransactionRequiredException
1944
     * @throws OptimisticLockException
1945
     * @throws InvalidArgumentException
1946
     */
1947 10
    public function lock($entity, $lockMode, $lockVersion = null)
1948
    {
1949 10
        if ($entity === null) {
1950 1
            throw new InvalidArgumentException('No entity passed to UnitOfWork#lock().');
1951
        }
1952
1953 9
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1954 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1955
        }
1956
1957 8
        $class = $this->em->getClassMetadata(get_class($entity));
1958
1959
        switch (true) {
1960 8
            case $lockMode === LockMode::OPTIMISTIC:
1961 6
                if (! $class->isVersioned()) {
1962 1
                    throw OptimisticLockException::notVersioned($class->getClassName());
1963
                }
1964
1965 5
                if ($lockVersion === null) {
1966 1
                    return;
1967
                }
1968
1969 4
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1970 1
                    $entity->initializeProxy();
1971
                }
1972
1973 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...
1974
1975 4
                if ($entityVersion !== $lockVersion) {
1976 2
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
1977
                }
1978
1979 2
                break;
1980
1981 2
            case $lockMode === LockMode::NONE:
1982 2
            case $lockMode === LockMode::PESSIMISTIC_READ:
1983 1
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1984 2
                if (! $this->em->getConnection()->isTransactionActive()) {
1985 2
                    throw TransactionRequiredException::transactionRequired();
1986
                }
1987
1988
                $oid = spl_object_id($entity);
1989
1990
                $this->getEntityPersister($class->getClassName())->lock(
1991
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->ge...ntityIdentifiers[$oid]) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...EntityPersister::lock() does only seem to accept array, 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

1991
                    /** @scrutinizer ignore-type */ array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
Loading history...
1992
                    $lockMode
1993
                );
1994
                break;
1995
1996
            default:
1997
                // Do nothing
1998
        }
1999 2
    }
2000
2001
    /**
2002
     * Clears the UnitOfWork.
2003
     */
2004 1219
    public function clear()
2005
    {
2006 1219
        $this->entityPersisters               =
2007 1219
        $this->collectionPersisters           =
2008 1219
        $this->eagerLoadingEntities           =
2009 1219
        $this->identityMap                    =
2010 1219
        $this->entityIdentifiers              =
2011 1219
        $this->originalEntityData             =
2012 1219
        $this->entityChangeSets               =
2013 1219
        $this->entityStates                   =
2014 1219
        $this->scheduledForSynchronization    =
2015 1219
        $this->entityInsertions               =
2016 1219
        $this->entityUpdates                  =
2017 1219
        $this->entityDeletions                =
2018 1219
        $this->collectionDeletions            =
2019 1219
        $this->collectionUpdates              =
2020 1219
        $this->extraUpdates                   =
2021 1219
        $this->readOnlyObjects                =
2022 1219
        $this->visitedCollections             =
2023 1219
        $this->nonCascadedNewDetectedEntities =
2024 1219
        $this->orphanRemovals                 = [];
2025 1219
    }
2026
2027
    /**
2028
     * INTERNAL:
2029
     * Schedules an orphaned entity for removal. The remove() operation will be
2030
     * invoked on that entity at the beginning of the next commit of this
2031
     * UnitOfWork.
2032
     *
2033
     * @param object $entity
2034
     *
2035
     * @ignore
2036
     */
2037 16
    public function scheduleOrphanRemoval($entity)
2038
    {
2039 16
        $this->orphanRemovals[spl_object_id($entity)] = $entity;
2040 16
    }
2041
2042
    /**
2043
     * INTERNAL:
2044
     * Cancels a previously scheduled orphan removal.
2045
     *
2046
     * @param object $entity
2047
     *
2048
     * @ignore
2049
     */
2050 111
    public function cancelOrphanRemoval($entity)
2051
    {
2052 111
        unset($this->orphanRemovals[spl_object_id($entity)]);
2053 111
    }
2054
2055
    /**
2056
     * INTERNAL:
2057
     * Schedules a complete collection for removal when this UnitOfWork commits.
2058
     */
2059 22
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2060
    {
2061 22
        $coid = spl_object_id($coll);
2062
2063
        // TODO: if $coll is already scheduled for recreation ... what to do?
2064
        // Just remove $coll from the scheduled recreations?
2065 22
        unset($this->collectionUpdates[$coid]);
2066
2067 22
        $this->collectionDeletions[$coid] = $coll;
2068 22
    }
2069
2070
    /**
2071
     * @return bool
2072
     */
2073 8
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2074
    {
2075 8
        return isset($this->collectionDeletions[spl_object_id($coll)]);
2076
    }
2077
2078
    /**
2079
     * INTERNAL:
2080
     * Creates a new instance of the mapped class, without invoking the constructor.
2081
     * This is only meant to be used internally, and should not be consumed by end users.
2082
     *
2083
     * @return EntityManagerAware|object
2084
     *
2085
     * @ignore
2086
     */
2087 665
    public function newInstance(ClassMetadata $class)
2088
    {
2089 665
        $entity = $this->instantiator->instantiate($class->getClassName());
2090
2091 665
        if ($entity instanceof EntityManagerAware) {
2092 5
            $entity->injectEntityManager($this->em, $class);
2093
        }
2094
2095 665
        return $entity;
2096
    }
2097
2098
    /**
2099
     * INTERNAL:
2100
     * Creates an entity. Used for reconstitution of persistent entities.
2101
     *
2102
     * {@internal Highly performance-sensitive method. }}
2103
     *
2104
     * @param string  $className The name of the entity class.
2105
     * @param mixed[] $data      The data for the entity.
2106
     * @param mixed[] $hints     Any hints to account for during reconstitution/lookup of the entity.
2107
     *
2108
     * @return object The managed entity instance.
2109
     *
2110
     * @ignore
2111
     * @todo Rename: getOrCreateEntity
2112
     */
2113 802
    public function createEntity($className, array $data, &$hints = [])
2114
    {
2115 802
        $class  = $this->em->getClassMetadata($className);
2116 802
        $id     = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data);
2117 802
        $idHash = implode(' ', $id);
2118
2119 802
        if (isset($this->identityMap[$class->getRootClassName()][$idHash])) {
2120 305
            $entity = $this->identityMap[$class->getRootClassName()][$idHash];
2121 305
            $oid    = spl_object_id($entity);
2122
2123 305
            if (isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])) {
2124 65
                $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
2125 65
                if ($unmanagedProxy !== $entity
2126 65
                    && $unmanagedProxy instanceof GhostObjectInterface
2127 65
                    && $this->isIdentifierEquals($unmanagedProxy, $entity)
2128
                ) {
2129
                    // We will hydrate the given un-managed proxy anyway:
2130
                    // continue work, but consider it the entity from now on
2131 5
                    $entity = $unmanagedProxy;
2132
                }
2133
            }
2134
2135 305
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
2136 21
                $entity->setProxyInitializer(null);
2137
2138 21
                if ($entity instanceof NotifyPropertyChanged) {
2139 21
                    $entity->addPropertyChangedListener($this);
2140
                }
2141
            } else {
2142 291
                if (! isset($hints[Query::HINT_REFRESH])
2143 291
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2144 229
                    return $entity;
2145
                }
2146
            }
2147
2148
            // inject EntityManager upon refresh.
2149 103
            if ($entity instanceof EntityManagerAware) {
2150 3
                $entity->injectEntityManager($this->em, $class);
2151
            }
2152
2153 103
            $this->originalEntityData[$oid] = $data;
2154
        } else {
2155 662
            $entity = $this->newInstance($class);
2156 662
            $oid    = spl_object_id($entity);
2157
2158 662
            $this->entityIdentifiers[$oid]  = $id;
2159 662
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2160 662
            $this->originalEntityData[$oid] = $data;
2161
2162 662
            $this->identityMap[$class->getRootClassName()][$idHash] = $entity;
2163
        }
2164
2165 695
        if ($entity instanceof NotifyPropertyChanged) {
2166 3
            $entity->addPropertyChangedListener($this);
2167
        }
2168
2169 695
        foreach ($data as $field => $value) {
2170 695
            $property = $class->getProperty($field);
2171
2172 695
            if ($property instanceof FieldMetadata) {
2173 693
                $property->setValue($entity, $value);
2174
            }
2175
        }
2176
2177
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2178 695
        unset($this->eagerLoadingEntities[$class->getRootClassName()][$idHash]);
2179
2180 695
        if (isset($this->eagerLoadingEntities[$class->getRootClassName()]) && ! $this->eagerLoadingEntities[$class->getRootClassName()]) {
2181
            unset($this->eagerLoadingEntities[$class->getRootClassName()]);
2182
        }
2183
2184
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2185 695
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2186 34
            return $entity;
2187
        }
2188
2189 661
        foreach ($class->getDeclaredPropertiesIterator() as $field => $association) {
2190 661
            if (! ($association instanceof AssociationMetadata)) {
2191 661
                continue;
2192
            }
2193
2194
            // Check if the association is not among the fetch-joined associations already.
2195 565
            if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
2196 243
                continue;
2197
            }
2198
2199 542
            $targetEntity = $association->getTargetEntity();
2200 542
            $targetClass  = $this->em->getClassMetadata($targetEntity);
2201
2202 542
            if ($association instanceof ToManyAssociationMetadata) {
2203
                // Ignore if its a cached collection
2204 463
                if (isset($hints[Query::HINT_CACHE_ENABLED]) &&
2205 463
                    $association->getValue($entity) instanceof PersistentCollection) {
2206
                    continue;
2207
                }
2208
2209 463
                $hasDataField = isset($data[$field]);
2210
2211
                // use the given collection
2212 463
                if ($hasDataField && $data[$field] instanceof PersistentCollection) {
2213
                    $data[$field]->setOwner($entity, $association);
2214
2215
                    $association->setValue($entity, $data[$field]);
2216
2217
                    $this->originalEntityData[$oid][$field] = $data[$field];
2218
2219
                    continue;
2220
                }
2221
2222
                // Inject collection
2223 463
                $pColl = $association->wrap($entity, $hasDataField ? $data[$field] : [], $this->em);
2224
2225 463
                $pColl->setInitialized($hasDataField);
2226
2227 463
                $association->setValue($entity, $pColl);
2228
2229 463
                if ($association->getFetchMode() === FetchMode::EAGER) {
2230 4
                    $this->loadCollection($pColl);
2231 4
                    $pColl->takeSnapshot();
2232
                }
2233
2234 463
                $this->originalEntityData[$oid][$field] = $pColl;
2235
2236 463
                continue;
2237
            }
2238
2239 469
            if (! $association->isOwningSide()) {
2240
                // use the given entity association
2241 66
                if (isset($data[$field]) && is_object($data[$field]) &&
2242 66
                    isset($this->entityStates[spl_object_id($data[$field])])) {
2243 3
                    $inverseAssociation = $targetClass->getProperty($association->getMappedBy());
2244
2245 3
                    $association->setValue($entity, $data[$field]);
2246 3
                    $inverseAssociation->setValue($data[$field], $entity);
2247
2248 3
                    $this->originalEntityData[$oid][$field] = $data[$field];
2249
2250 3
                    continue;
2251
                }
2252
2253
                // Inverse side of x-to-one can never be lazy
2254 63
                $persister = $this->getEntityPersister($targetEntity);
2255
2256 63
                $association->setValue($entity, $persister->loadToOneEntity($association, $entity));
2257
2258 63
                continue;
2259
            }
2260
2261
            // use the entity association
2262 469
            if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
2263 38
                $association->setValue($entity, $data[$field]);
2264
2265 38
                $this->originalEntityData[$oid][$field] = $data[$field];
2266
2267 38
                continue;
2268
            }
2269
2270 462
            $associatedId = [];
2271
2272
            // TODO: Is this even computed right in all cases of composite keys?
2273 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

2273
            foreach ($association->/** @scrutinizer ignore-call */ getJoinColumns() as $joinColumn) {
Loading history...
2274
                /** @var JoinColumnMetadata $joinColumn */
2275 462
                $joinColumnName  = $joinColumn->getColumnName();
2276 462
                $joinColumnValue = $data[$joinColumnName] ?? null;
2277 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...
2278
2279 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...
2280
                    // the missing key is part of target's entity primary key
2281 266
                    $associatedId = [];
2282
2283 266
                    continue;
2284
                }
2285
2286 284
                $associatedId[$targetField] = $joinColumnValue;
2287
            }
2288
2289 462
            if (! $associatedId) {
2290
                // Foreign key is NULL
2291 266
                $association->setValue($entity, null);
2292 266
                $this->originalEntityData[$oid][$field] = null;
2293
2294 266
                continue;
2295
            }
2296
2297
            // @todo guilhermeblanco Can we remove the need of this somehow?
2298 284
            if (! isset($hints['fetchMode'][$class->getClassName()][$field])) {
2299 281
                $hints['fetchMode'][$class->getClassName()][$field] = $association->getFetchMode();
2300
            }
2301
2302
            // Foreign key is set
2303
            // Check identity map first
2304
            // FIXME: Can break easily with composite keys if join column values are in
2305
            //        wrong order. The correct order is the one in ClassMetadata#identifier.
2306 284
            $relatedIdHash = implode(' ', $associatedId);
2307
2308
            switch (true) {
2309 284
                case isset($this->identityMap[$targetClass->getRootClassName()][$relatedIdHash]):
2310 168
                    $newValue = $this->identityMap[$targetClass->getRootClassName()][$relatedIdHash];
2311
2312
                    // If this is an uninitialized proxy, we are deferring eager loads,
2313
                    // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2314
                    // then we can append this entity for eager loading!
2315 168
                    if (! $targetClass->isIdentifierComposite() &&
2316 168
                        $newValue instanceof GhostObjectInterface &&
2317 168
                        isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2318 168
                        $hints['fetchMode'][$class->getClassName()][$field] === FetchMode::EAGER &&
2319 168
                        ! $newValue->isProxyInitialized()
2320
                    ) {
2321
                        $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($associatedId);
2322
                    }
2323
2324 168
                    break;
2325
2326 190
                case $targetClass->getSubClasses():
2327
                    // If it might be a subtype, it can not be lazy. There isn't even
2328
                    // a way to solve this with deferred eager loading, which means putting
2329
                    // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2330 29
                    $persister = $this->getEntityPersister($targetEntity);
2331 29
                    $newValue  = $persister->loadToOneEntity($association, $entity, $associatedId);
2332 29
                    break;
2333
2334
                default:
2335
                    // Proxies do not carry any kind of original entity data until they're fully loaded/initialized
2336 163
                    $managedData = [];
2337
2338 163
                    $normalizedAssociatedId = $this->normalizeIdentifier->__invoke(
2339 163
                        $this->em,
2340 163
                        $targetClass,
2341 163
                        $associatedId
2342
                    );
2343
2344
                    switch (true) {
2345
                        // We are negating the condition here. Other cases will assume it is valid!
2346 163
                        case $hints['fetchMode'][$class->getClassName()][$field] !== FetchMode::EAGER:
2347 156
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
2348 156
                            break;
2349
2350
                        // Deferred eager load only works for single identifier classes
2351 7
                        case isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite():
2352
                            // TODO: Is there a faster approach?
2353 7
                            $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($normalizedAssociatedId);
2354
2355 7
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
2356 7
                            break;
2357
2358
                        default:
2359
                            // TODO: This is very imperformant, ignore it?
2360
                            $newValue = $this->em->find($targetEntity, $normalizedAssociatedId);
2361
                            // Needed to re-assign original entity data for freshly loaded entity
2362
                            $managedData = $this->originalEntityData[spl_object_id($newValue)];
2363
                            break;
2364
                    }
2365
2366
                    // @TODO using `$associatedId` here seems to be risky.
2367 163
                    $this->registerManaged($newValue, $associatedId, $managedData);
2368
2369 163
                    break;
2370
            }
2371
2372 284
            $this->originalEntityData[$oid][$field] = $newValue;
2373 284
            $association->setValue($entity, $newValue);
2374
2375 284
            if ($association->getInversedBy()
2376 284
                && $association instanceof OneToOneAssociationMetadata
2377
                // @TODO refactor this
2378
                // we don't want to set any values in un-initialized proxies
2379
                && ! (
2380 56
                    $newValue instanceof GhostObjectInterface
2381 284
                    && ! $newValue->isProxyInitialized()
2382
                )
2383
            ) {
2384 19
                $inverseAssociation = $targetClass->getProperty($association->getInversedBy());
2385
2386 19
                $inverseAssociation->setValue($newValue, $entity);
2387
            }
2388
        }
2389
2390
        // defer invoking of postLoad event to hydration complete step
2391 661
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2392
2393 661
        return $entity;
2394
    }
2395
2396 862
    public function triggerEagerLoads()
2397
    {
2398 862
        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...
2399 862
            return;
2400
        }
2401
2402
        // avoid infinite recursion
2403 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2404 7
        $this->eagerLoadingEntities = [];
2405
2406 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2407 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...
2408
                continue;
2409
            }
2410
2411 7
            $class = $this->em->getClassMetadata($entityName);
2412
2413 7
            $this->getEntityPersister($entityName)->loadAll(
2414 7
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
It seems like array_combine($class->id...ay(array_values($ids))) can also be of type false; however, parameter $criteria of Doctrine\ORM\Persisters\...ityPersister::loadAll() does only seem to accept array, 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

2414
                /** @scrutinizer ignore-type */ array_combine($class->identifier, [array_values($ids)])
Loading history...
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...
2415
            );
2416
        }
2417 7
    }
2418
2419
    /**
2420
     * Initializes (loads) an uninitialized persistent collection of an entity.
2421
     *
2422
     * @param PersistentCollection $collection The collection to initialize.
2423
     *
2424
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2425
     */
2426 139
    public function loadCollection(PersistentCollection $collection)
2427
    {
2428 139
        $association = $collection->getMapping();
2429 139
        $persister   = $this->getEntityPersister($association->getTargetEntity());
2430
2431 139
        if ($association instanceof OneToManyAssociationMetadata) {
2432 74
            $persister->loadOneToManyCollection($association, $collection->getOwner(), $collection);
2433
        } else {
2434 75
            $persister->loadManyToManyCollection($association, $collection->getOwner(), $collection);
2435
        }
2436
2437 139
        $collection->setInitialized(true);
2438 139
    }
2439
2440
    /**
2441
     * Gets the identity map of the UnitOfWork.
2442
     *
2443
     * @return object[]
2444
     */
2445 1
    public function getIdentityMap()
2446
    {
2447 1
        return $this->identityMap;
2448
    }
2449
2450
    /**
2451
     * Gets the original data of an entity. The original data is the data that was
2452
     * present at the time the entity was reconstituted from the database.
2453
     *
2454
     * @param object $entity
2455
     *
2456
     * @return mixed[]
2457
     */
2458 118
    public function getOriginalEntityData($entity)
2459
    {
2460 118
        $oid = spl_object_id($entity);
2461
2462 118
        return $this->originalEntityData[$oid] ?? [];
2463
    }
2464
2465
    /**
2466
     * @param object  $entity
2467
     * @param mixed[] $data
2468
     *
2469
     * @ignore
2470
     */
2471
    public function setOriginalEntityData($entity, array $data)
2472
    {
2473
        $this->originalEntityData[spl_object_id($entity)] = $data;
2474
    }
2475
2476
    /**
2477
     * INTERNAL:
2478
     * Sets a property value of the original data array of an entity.
2479
     *
2480
     * @param string $oid
2481
     * @param string $property
2482
     * @param mixed  $value
2483
     *
2484
     * @ignore
2485
     */
2486 301
    public function setOriginalEntityProperty($oid, $property, $value)
2487
    {
2488 301
        $this->originalEntityData[$oid][$property] = $value;
2489 301
    }
2490
2491
    /**
2492
     * Gets the identifier of an entity.
2493
     * The returned value is always an array of identifier values. If the entity
2494
     * has a composite identifier then the identifier values are in the same
2495
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2496
     *
2497
     * @param object $entity
2498
     *
2499
     * @return mixed[] The identifier values.
2500
     */
2501 556
    public function getEntityIdentifier($entity)
2502
    {
2503 556
        return $this->entityIdentifiers[spl_object_id($entity)];
2504
    }
2505
2506
    /**
2507
     * Processes an entity instance to extract their identifier values.
2508
     *
2509
     * @param object $entity The entity instance.
2510
     *
2511
     * @return mixed A scalar value.
2512
     *
2513
     * @throws ORMInvalidArgumentException
2514
     */
2515 70
    public function getSingleIdentifierValue($entity)
2516
    {
2517 70
        $class     = $this->em->getClassMetadata(get_class($entity));
2518 70
        $persister = $this->getEntityPersister($class->getClassName());
2519
2520 70
        if ($class->isIdentifierComposite()) {
2521
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2522
        }
2523
2524 70
        $values = $this->isInIdentityMap($entity)
2525 58
            ? $this->getEntityIdentifier($entity)
2526 70
            : $persister->getIdentifier($entity);
2527
2528 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...
2529
    }
2530
2531
    /**
2532
     * Tries to find an entity with the given identifier in the identity map of
2533
     * this UnitOfWork.
2534
     *
2535
     * @param mixed|mixed[] $id            The entity identifier to look for.
2536
     * @param string        $rootClassName The name of the root class of the mapped entity hierarchy.
2537
     *
2538
     * @return object|bool Returns the entity with the specified identifier if it exists in
2539
     *                     this UnitOfWork, FALSE otherwise.
2540
     */
2541 538
    public function tryGetById($id, $rootClassName)
2542
    {
2543 538
        $idHash = implode(' ', (array) $id);
2544
2545 538
        return $this->identityMap[$rootClassName][$idHash] ?? false;
2546
    }
2547
2548
    /**
2549
     * Schedules an entity for dirty-checking at commit-time.
2550
     *
2551
     * @param object $entity The entity to schedule for dirty-checking.
2552
     */
2553 6
    public function scheduleForSynchronization($entity)
2554
    {
2555 6
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->getRootClassName();
2556
2557 6
        $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity;
2558 6
    }
2559
2560
    /**
2561
     * Checks whether the UnitOfWork has any pending insertions.
2562
     *
2563
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2564
     */
2565
    public function hasPendingInsertions()
2566
    {
2567
        return ! empty($this->entityInsertions);
2568
    }
2569
2570
    /**
2571
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2572
     * number of entities in the identity map.
2573
     *
2574
     * @return int
2575
     */
2576 1
    public function size()
2577
    {
2578 1
        return array_sum(array_map('count', $this->identityMap));
2579
    }
2580
2581
    /**
2582
     * Gets the EntityPersister for an Entity.
2583
     *
2584
     * @param string $entityName The name of the Entity.
2585
     *
2586
     * @return EntityPersister
2587
     */
2588 1084
    public function getEntityPersister($entityName)
2589
    {
2590 1084
        if (isset($this->entityPersisters[$entityName])) {
2591 1031
            return $this->entityPersisters[$entityName];
2592
        }
2593
2594 1084
        $class = $this->em->getClassMetadata($entityName);
2595
2596
        switch (true) {
2597 1084
            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...
2598 1041
                $persister = new BasicEntityPersister($this->em, $class);
2599 1041
                break;
2600
2601 385
            case $class->inheritanceType === InheritanceType::SINGLE_TABLE:
2602 223
                $persister = new SingleTablePersister($this->em, $class);
2603 223
                break;
2604
2605 355
            case $class->inheritanceType === InheritanceType::JOINED:
2606 355
                $persister = new JoinedSubclassPersister($this->em, $class);
2607 355
                break;
2608
2609
            default:
2610
                throw new RuntimeException('No persister found for entity.');
2611
        }
2612
2613 1084
        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

2613
        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...
2614 128
            $persister = $this->em->getConfiguration()
2615 128
                ->getSecondLevelCacheConfiguration()
2616 128
                ->getCacheFactory()
2617 128
                ->buildCachedEntityPersister($this->em, $persister, $class);
2618
        }
2619
2620 1084
        $this->entityPersisters[$entityName] = $persister;
2621
2622 1084
        return $this->entityPersisters[$entityName];
2623
    }
2624
2625
    /**
2626
     * Gets a collection persister for a collection-valued association.
2627
     *
2628
     * @return CollectionPersister
2629
     */
2630 563
    public function getCollectionPersister(ToManyAssociationMetadata $association)
2631
    {
2632 563
        $role = $association->getCache()
2633 78
            ? sprintf('%s::%s', $association->getSourceEntity(), $association->getName())
2634 563
            : get_class($association);
2635
2636 563
        if (isset($this->collectionPersisters[$role])) {
2637 431
            return $this->collectionPersisters[$role];
2638
        }
2639
2640 563
        $persister = $association instanceof OneToManyAssociationMetadata
2641 401
            ? new OneToManyPersister($this->em)
2642 563
            : new ManyToManyPersister($this->em);
2643
2644 563
        if ($this->hasCache && $association->getCache()) {
2645 77
            $persister = $this->em->getConfiguration()
2646 77
                ->getSecondLevelCacheConfiguration()
2647 77
                ->getCacheFactory()
2648 77
                ->buildCachedCollectionPersister($this->em, $persister, $association);
2649
        }
2650
2651 563
        $this->collectionPersisters[$role] = $persister;
2652
2653 563
        return $this->collectionPersisters[$role];
2654
    }
2655
2656
    /**
2657
     * INTERNAL:
2658
     * Registers an entity as managed.
2659
     *
2660
     * @param object  $entity The entity.
2661
     * @param mixed[] $id     Map containing identifier field names as key and its associated values.
2662
     * @param mixed[] $data   The original entity data.
2663
     */
2664 279
    public function registerManaged($entity, array $id, array $data)
2665
    {
2666 279
        $isProxy = $entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized();
2667 279
        $oid     = spl_object_id($entity);
2668
2669 279
        $this->entityIdentifiers[$oid]  = $id;
2670 279
        $this->entityStates[$oid]       = self::STATE_MANAGED;
2671 279
        $this->originalEntityData[$oid] = $data;
2672
2673 279
        $this->addToIdentityMap($entity);
2674
2675 273
        if ($entity instanceof NotifyPropertyChanged && ! $isProxy) {
2676 1
            $entity->addPropertyChangedListener($this);
2677
        }
2678 273
    }
2679
2680
    /**
2681
     * INTERNAL:
2682
     * Clears the property changeset of the entity with the given OID.
2683
     *
2684
     * @param string $oid The entity's OID.
2685
     */
2686
    public function clearEntityChangeSet($oid)
2687
    {
2688
        unset($this->entityChangeSets[$oid]);
2689
    }
2690
2691
    /* PropertyChangedListener implementation */
2692
2693
    /**
2694
     * Notifies this UnitOfWork of a property change in an entity.
2695
     *
2696
     * @param object $entity       The entity that owns the property.
2697
     * @param string $propertyName The name of the property that changed.
2698
     * @param mixed  $oldValue     The old value of the property.
2699
     * @param mixed  $newValue     The new value of the property.
2700
     */
2701 3
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
2702
    {
2703 3
        $class = $this->em->getClassMetadata(get_class($entity));
2704
2705 3
        if (! $class->getProperty($propertyName)) {
2706
            return; // ignore non-persistent fields
2707
        }
2708
2709 3
        $oid = spl_object_id($entity);
2710
2711
        // Update changeset and mark entity for synchronization
2712 3
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2713
2714 3
        if (! isset($this->scheduledForSynchronization[$class->getRootClassName()][$oid])) {
2715 3
            $this->scheduleForSynchronization($entity);
2716
        }
2717 3
    }
2718
2719
    /**
2720
     * Gets the currently scheduled entity insertions in this UnitOfWork.
2721
     *
2722
     * @return object[]
2723
     */
2724 2
    public function getScheduledEntityInsertions()
2725
    {
2726 2
        return $this->entityInsertions;
2727
    }
2728
2729
    /**
2730
     * Gets the currently scheduled entity updates in this UnitOfWork.
2731
     *
2732
     * @return object[]
2733
     */
2734 3
    public function getScheduledEntityUpdates()
2735
    {
2736 3
        return $this->entityUpdates;
2737
    }
2738
2739
    /**
2740
     * Gets the currently scheduled entity deletions in this UnitOfWork.
2741
     *
2742
     * @return object[]
2743
     */
2744 1
    public function getScheduledEntityDeletions()
2745
    {
2746 1
        return $this->entityDeletions;
2747
    }
2748
2749
    /**
2750
     * Gets the currently scheduled complete collection deletions
2751
     *
2752
     * @return Collection[]|object[][]
2753
     */
2754 1
    public function getScheduledCollectionDeletions()
2755
    {
2756 1
        return $this->collectionDeletions;
2757
    }
2758
2759
    /**
2760
     * Gets the currently scheduled collection inserts, updates and deletes.
2761
     *
2762
     * @return Collection[]|object[][]
2763
     */
2764
    public function getScheduledCollectionUpdates()
2765
    {
2766
        return $this->collectionUpdates;
2767
    }
2768
2769
    /**
2770
     * Helper method to initialize a lazy loading proxy or persistent collection.
2771
     *
2772
     * @param object $obj
2773
     */
2774 2
    public function initializeObject($obj)
2775
    {
2776 2
        if ($obj instanceof GhostObjectInterface) {
2777 1
            $obj->initializeProxy();
2778
2779 1
            return;
2780
        }
2781
2782 1
        if ($obj instanceof PersistentCollection) {
2783 1
            $obj->initialize();
2784
        }
2785 1
    }
2786
2787
    /**
2788
     * Helper method to show an object as string.
2789
     *
2790
     * @param object $obj
2791
     *
2792
     * @return string
2793
     */
2794
    private static function objToStr($obj)
2795
    {
2796
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_id($obj);
2797
    }
2798
2799
    /**
2800
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
2801
     *
2802
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
2803
     * on this object that might be necessary to perform a correct update.
2804
     *
2805
     * @param object $object
2806
     *
2807
     * @throws ORMInvalidArgumentException
2808
     */
2809 6
    public function markReadOnly($object)
2810
    {
2811 6
        if (! is_object($object) || ! $this->isInIdentityMap($object)) {
2812 1
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2813
        }
2814
2815 5
        $this->readOnlyObjects[spl_object_id($object)] = true;
2816 5
    }
2817
2818
    /**
2819
     * Is this entity read only?
2820
     *
2821
     * @param object $object
2822
     *
2823
     * @return bool
2824
     *
2825
     * @throws ORMInvalidArgumentException
2826
     */
2827 3
    public function isReadOnly($object)
2828
    {
2829 3
        if (! is_object($object)) {
2830
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2831
        }
2832
2833 3
        return isset($this->readOnlyObjects[spl_object_id($object)]);
2834
    }
2835
2836
    /**
2837
     * Perform whatever processing is encapsulated here after completion of the transaction.
2838
     */
2839 1006
    private function afterTransactionComplete()
2840
    {
2841
        $this->performCallbackOnCachedPersister(static function (CachedPersister $persister) {
2842 95
            $persister->afterTransactionComplete();
2843 1006
        });
2844 1006
    }
2845
2846
    /**
2847
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
2848
     */
2849 10
    private function afterTransactionRolledBack()
2850
    {
2851
        $this->performCallbackOnCachedPersister(static function (CachedPersister $persister) {
2852
            $persister->afterTransactionRolledBack();
2853 10
        });
2854 10
    }
2855
2856
    /**
2857
     * Performs an action after the transaction.
2858
     */
2859 1011
    private function performCallbackOnCachedPersister(callable $callback)
2860
    {
2861 1011
        if (! $this->hasCache) {
2862 916
            return;
2863
        }
2864
2865 95
        foreach (array_merge($this->entityPersisters, $this->collectionPersisters) as $persister) {
2866 95
            if ($persister instanceof CachedPersister) {
2867 95
                $callback($persister);
2868
            }
2869
        }
2870 95
    }
2871
2872 1016
    private function dispatchOnFlushEvent()
2873
    {
2874 1016
        if ($this->eventManager->hasListeners(Events::onFlush)) {
2875 4
            $this->eventManager->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
2876
        }
2877 1016
    }
2878
2879 1011
    private function dispatchPostFlushEvent()
2880
    {
2881 1011
        if ($this->eventManager->hasListeners(Events::postFlush)) {
2882 5
            $this->eventManager->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
2883
        }
2884 1010
    }
2885
2886
    /**
2887
     * Verifies if two given entities actually are the same based on identifier comparison
2888
     *
2889
     * @param object $entity1
2890
     * @param object $entity2
2891
     *
2892
     * @return bool
2893
     */
2894 17
    private function isIdentifierEquals($entity1, $entity2)
2895
    {
2896 17
        if ($entity1 === $entity2) {
2897
            return true;
2898
        }
2899
2900 17
        $class     = $this->em->getClassMetadata(get_class($entity1));
2901 17
        $persister = $this->getEntityPersister($class->getClassName());
2902
2903 17
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
2904 11
            return false;
2905
        }
2906
2907 6
        $identifierFlattener = $this->em->getIdentifierFlattener();
2908
2909 6
        $oid1 = spl_object_id($entity1);
2910 6
        $oid2 = spl_object_id($entity2);
2911
2912 6
        $id1 = $this->entityIdentifiers[$oid1]
2913 6
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity1));
2914 6
        $id2 = $this->entityIdentifiers[$oid2]
2915 6
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity2));
2916
2917 6
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
2918
    }
2919
2920
    /**
2921
     * @throws ORMInvalidArgumentException
2922
     */
2923 1013
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
2924
    {
2925 1013
        $entitiesNeedingCascadePersist = array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
2926
2927 1013
        $this->nonCascadedNewDetectedEntities = [];
2928
2929 1013
        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...
2930 4
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
2931 4
                array_values($entitiesNeedingCascadePersist)
2932
            );
2933
        }
2934 1011
    }
2935
2936
    /**
2937
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
2938
     * Unit of work able to fire deferred events, related to loading events here.
2939
     *
2940
     * @internal should be called internally from object hydrators
2941
     */
2942 878
    public function hydrationComplete()
2943
    {
2944 878
        $this->hydrationCompleteHandler->hydrationComplete();
2945 878
    }
2946
}
2947