UnitOfWork::lock()   C
last analyzed

Complexity

Conditions 13
Paths 15

Size

Total Lines 50
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 13.6148

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 28
c 1
b 0
f 0
nc 15
nop 3
dl 0
loc 50
ccs 22
cts 26
cp 0.8462
crap 13.6148
rs 6.6166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

546
        foreach ($class->/** @scrutinizer ignore-call */ getPropertiesIterator() 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...
547 1021
            $value = $property->getValue($entity);
548
549 1021
            if ($property instanceof ToManyAssociationMetadata && $value !== null) {
550 773
                if ($value instanceof PersistentCollection && $value->getOwner() === $entity) {
551 186
                    continue;
552
                }
553
554 770
                $value = $property->wrap($entity, $value, $this->em);
555
556 770
                $property->setValue($entity, $value);
557
558 770
                $actualData[$name] = $value;
559
560 770
                continue;
561
            }
562
563 1021
            if (( ! $class->isIdentifier($name)
564 1021
                    || ! $class->getProperty($name) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\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

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

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

566
                    || $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...
567 1021
                ) && (! $class->isVersioned() || $name !== $class->versionProperty->getName())) {
0 ignored issues
show
Bug introduced by
The method isVersioned() does not exist on Doctrine\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

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

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

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

976
            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...
977 854
                continue;
978
            }
979
980 1007
            $persister->insert($entity);
981
982 1006
            if ($generationPlan->containsDeferred()) {
983
                // Entity has post-insert IDs
984 943
                $oid = spl_object_id($entity);
985 943
                $id  = $persister->getIdentifier($entity);
986
987 943
                $this->entityIdentifiers[$oid]  = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
988 943
                $this->entityStates[$oid]       = self::STATE_MANAGED;
989 943
                $this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid];
990
991 943
                $this->addToIdentityMap($entity);
992
            }
993
994 1006
            unset($this->entityInsertions[$oid]);
995
996 1006
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
997 53
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
998
999 53
                $this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke);
1000
            }
1001
        }
1002 1007
    }
1003
1004
    /**
1005
     * Executes all entity updates for entities of the specified type.
1006
     *
1007
     * @param ClassMetadata $class
1008
     */
1009 112
    private function executeUpdates($class)
1010
    {
1011 112
        $className        = $class->getClassName();
1012 112
        $persister        = $this->getEntityPersister($className);
1013 112
        $preUpdateInvoke  = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
1014 112
        $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
1015
1016 112
        foreach ($this->entityUpdates as $oid => $entity) {
1017 112
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
1018 71
                continue;
1019
            }
1020
1021 112
            if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
1022 9
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
1023
1024 9
                $this->recomputeSingleEntityChangeSet($class, $entity);
1025
            }
1026
1027 112
            if (! empty($this->entityChangeSets[$oid])) {
1028 82
                $persister->update($entity);
1029
            }
1030
1031 108
            unset($this->entityUpdates[$oid]);
1032
1033 108
            if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
1034 7
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
1035
            }
1036
        }
1037 108
    }
1038
1039
    /**
1040
     * Executes all entity deletions for entities of the specified type.
1041
     *
1042
     * @param ClassMetadata $class
1043
     */
1044 59
    private function executeDeletions($class)
1045
    {
1046 59
        $className = $class->getClassName();
1047 59
        $persister = $this->getEntityPersister($className);
1048 59
        $invoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1049
1050 59
        foreach ($this->entityDeletions as $oid => $entity) {
1051 59
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
1052 24
                continue;
1053
            }
1054
1055 59
            $persister->delete($entity);
1056
1057
            unset(
1058 59
                $this->entityDeletions[$oid],
1059 59
                $this->entityIdentifiers[$oid],
1060 59
                $this->originalEntityData[$oid],
1061 59
                $this->entityStates[$oid]
1062
            );
1063
1064
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1065
            // is obtained by a new entity because the old one went out of scope.
1066 59
            if (! $class->isIdentifierComposite()) {
1067 56
                $property = $class->getProperty($class->getSingleIdentifierFieldName());
1068
1069 56
                if ($property instanceof FieldMetadata && $property->hasValueGenerator()) {
1070 49
                    $property->setValue($entity, null);
1071
                }
1072
            }
1073
1074 59
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1075 6
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
1076
1077 6
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, $eventArgs, $invoke);
1078
            }
1079
        }
1080 58
    }
1081
1082
    /**
1083
     * Gets the commit order.
1084
     *
1085
     * @return ClassMetadata[]
1086
     */
1087 1011
    private function getCommitOrder()
1088
    {
1089 1011
        $calc = new Internal\CommitOrderCalculator();
1090
1091
        // See if there are any new classes in the changeset, that are not in the
1092
        // commit order graph yet (don't have a node).
1093
        // We have to inspect changeSet to be able to correctly build dependencies.
1094
        // It is not possible to use IdentityMap here because post inserted ids
1095
        // are not yet available.
1096 1011
        $newNodes = [];
1097
1098 1011
        foreach (array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) {
1099 1011
            $class = $this->em->getClassMetadata(get_class($entity));
1100
1101 1011
            if ($calc->hasNode($class->getClassName())) {
1102 635
                continue;
1103
            }
1104
1105 1011
            $calc->addNode($class->getClassName(), $class);
1106
1107 1011
            $newNodes[] = $class;
1108
        }
1109
1110
        // Calculate dependencies for new nodes
1111 1011
        while ($class = array_pop($newNodes)) {
1112 1011
            foreach ($class->getPropertiesIterator() as $property) {
1113 1011
                if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
1114 1011
                    continue;
1115
                }
1116
1117 834
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1118
1119 834
                if (! $calc->hasNode($targetClass->getClassName())) {
1120 637
                    $calc->addNode($targetClass->getClassName(), $targetClass);
1121
1122 637
                    $newNodes[] = $targetClass;
1123
                }
1124
1125 834
                $weight = ! array_filter(
1126 834
                    $property->getJoinColumns(),
1127
                    static function (JoinColumnMetadata $joinColumn) {
1128 834
                        return $joinColumn->isNullable();
1129 834
                    }
1130
                );
1131
1132 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

1132
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), /** @scrutinizer ignore-type */ $weight);
Loading history...
1133
1134
                // If the target class has mapped subclasses, these share the same dependency.
1135 834
                if (! $targetClass->getSubClasses()) {
0 ignored issues
show
Bug introduced by
The method getSubClasses() does not exist on Doctrine\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

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

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

1417
        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...
1418 5
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not exist on Doctrine\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

1418
            || ! $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...
1419 6
            || ! $class->getProperty($class->getSingleIdentifierFieldName())->hasValueGenerator()
1420
        ) {
1421
            // Check for a version field, if available, to avoid a db lookup.
1422 5
            if ($class->isVersioned()) {
1423 1
                return $class->versionProperty->getValue($entity)
0 ignored issues
show
Bug introduced by
Accessing versionProperty on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1424
                    ? self::STATE_DETACHED
1425 1
                    : self::STATE_NEW;
1426
            }
1427
1428
            // Last try before db lookup: check the identity map.
1429 4
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
1430 1
                return self::STATE_DETACHED;
1431
            }
1432
1433
            // db lookup
1434 4
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
1435
                return self::STATE_DETACHED;
1436
            }
1437
1438 4
            return self::STATE_NEW;
1439
        }
1440
1441 1
        if ($class->isIdentifierComposite()
1442 1
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
1443 1
            || ! $class->getValueGenerationPlan()->containsDeferred()) {
0 ignored issues
show
Bug introduced by
The method getValueGenerationPlan() does not exist on Doctrine\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

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

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

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

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

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

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