Test Setup Failed
Push — develop ( 082d66...6f26e1 )
by Guilherme
63:04
created

UnitOfWork::cascadeMerge()   D

Complexity

Conditions 9
Paths 7

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 9.3752

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 10
cts 12
cp 0.8333
rs 4.909
c 0
b 0
f 0
cc 9
eloc 15
nc 7
nop 3
crap 9.3752
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Collections\Collection;
8
use Doctrine\Common\NotifyPropertyChanged;
9
use Doctrine\Common\PropertyChangedListener;
10
use Doctrine\DBAL\LockMode;
11
use Doctrine\Instantiator\Instantiator;
12
use Doctrine\ORM\Cache\Persister\CachedPersister;
13
use Doctrine\ORM\Event\LifecycleEventArgs;
14
use Doctrine\ORM\Event\ListenersInvoker;
15
use Doctrine\ORM\Event\OnFlushEventArgs;
16
use Doctrine\ORM\Event\PostFlushEventArgs;
17
use Doctrine\ORM\Event\PreFlushEventArgs;
18
use Doctrine\ORM\Event\PreUpdateEventArgs;
19
use Doctrine\ORM\Internal\HydrationCompleteHandler;
20
use Doctrine\ORM\Mapping\AssociationMetadata;
21
use Doctrine\ORM\Mapping\ChangeTrackingPolicy;
22
use Doctrine\ORM\Mapping\ClassMetadata;
23
use Doctrine\ORM\Mapping\FetchMode;
24
use Doctrine\ORM\Mapping\FieldMetadata;
25
use Doctrine\ORM\Mapping\GeneratorType;
26
use Doctrine\ORM\Mapping\InheritanceType;
27
use Doctrine\ORM\Mapping\JoinColumnMetadata;
28
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
29
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
30
use Doctrine\ORM\Mapping\OneToOneAssociationMetadata;
31
use Doctrine\ORM\Mapping\Property;
32
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
33
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
34
use Doctrine\ORM\Mapping\VersionFieldMetadata;
35
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
36
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
37
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
38
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
39
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
40
use Doctrine\ORM\Proxy\Proxy;
41
use Exception;
42
use InvalidArgumentException;
43
use UnexpectedValueException;
44
45
/**
46
 * The UnitOfWork is responsible for tracking changes to objects during an
47
 * "object-level" transaction and for writing out changes to the database
48
 * in the correct order.
49
 *
50
 * Internal note: This class contains highly performance-sensitive code.
51
 *
52
 * @since       2.0
53
 * @author      Benjamin Eberlei <[email protected]>
54
 * @author      Guilherme Blanco <[email protected]>
55
 * @author      Jonathan Wage <[email protected]>
56
 * @author      Roman Borschel <[email protected]>
57
 * @author      Rob Caiger <[email protected]>
58
 */
59
class UnitOfWork implements PropertyChangedListener
60
{
61
    /**
62
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
63
     */
64
    const STATE_MANAGED = 1;
65
66
    /**
67
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
68
     * and is not (yet) managed by an EntityManager.
69
     */
70
    const STATE_NEW = 2;
71
72
    /**
73
     * A detached entity is an instance with persistent state and identity that is not
74
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
75
     */
76
    const STATE_DETACHED = 3;
77
78
    /**
79
     * A removed entity instance is an instance with a persistent identity,
80
     * associated with an EntityManager, whose persistent state will be deleted
81
     * on commit.
82
     */
83
    const STATE_REMOVED = 4;
84
85
    /**
86
     * Hint used to collect all primary keys of associated entities during hydration
87
     * and execute it in a dedicated query afterwards
88
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
89
     */
90
    const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
91
92
    /**
93
     * The identity map that holds references to all managed entities that have
94
     * an identity. The entities are grouped by their class name.
95
     * Since all classes in a hierarchy must share the same identifier set,
96
     * we always take the root class name of the hierarchy.
97
     *
98
     * @var array
99
     */
100
    private $identityMap = [];
101
102
    /**
103
     * Map of all identifiers of managed entities.
104
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_hash).
105
     * Values are maps of entity identifiers, where its key is the column name and the value is the raw value.
106
     *
107
     * @var array
108
     */
109
    private $entityIdentifiers = [];
110
111
    /**
112
     * Map of the original entity data of managed entities.
113
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_hash).
114
     * Values are maps of entity data, where its key is the field name and the value is the converted
115
     * (convertToPHPValue) value.
116
     * This structure is used for calculating changesets at commit time.
117
     *
118
     * Internal: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
119
     *           A value will only really be copied if the value in the entity is modified by the user.
120
     *
121
     * @var array
122
     */
123
    private $originalEntityData = [];
124
125
    /**
126
     * Map of entity changes. Keys are object ids (spl_object_hash).
127
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
128
     *
129
     * @var array
130
     */
131
    private $entityChangeSets = [];
132
133
    /**
134
     * The (cached) states of any known entities.
135
     * Keys are object ids (spl_object_hash).
136
     *
137
     * @var array
138
     */
139
    private $entityStates = [];
140
141
    /**
142
     * Map of entities that are scheduled for dirty checking at commit time.
143
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
144
     * Keys are object ids (spl_object_hash).
145
     *
146
     * @var array
147
     */
148
    private $scheduledForSynchronization = [];
149
150
    /**
151
     * A list of all pending entity insertions.
152
     *
153
     * @var array
154
     */
155
    private $entityInsertions = [];
156
157
    /**
158
     * A list of all pending entity updates.
159
     *
160
     * @var array
161
     */
162
    private $entityUpdates = [];
163
164
    /**
165
     * Any pending extra updates that have been scheduled by persisters.
166
     *
167
     * @var array
168
     */
169
    private $extraUpdates = [];
170
171
    /**
172
     * A list of all pending entity deletions.
173
     *
174
     * @var array
175
     */
176
    private $entityDeletions = [];
177
178
    /**
179
     * All pending collection deletions.
180
     *
181
     * @var array
182
     */
183
    private $collectionDeletions = [];
184
185
    /**
186
     * All pending collection updates.
187
     *
188
     * @var array
189
     */
190
    private $collectionUpdates = [];
191
192
    /**
193
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
194
     * At the end of the UnitOfWork all these collections will make new snapshots
195
     * of their data.
196
     *
197
     * @var array
198
     */
199
    private $visitedCollections = [];
200
201
    /**
202
     * The EntityManager that "owns" this UnitOfWork instance.
203
     *
204
     * @var EntityManagerInterface
205
     */
206
    private $em;
207
208
    /**
209
     * The entity persister instances used to persist entity instances.
210
     *
211
     * @var array
212
     */
213
    private $entityPersisters = [];
214
215
    /**
216
     * The collection persister instances used to persist collections.
217
     *
218
     * @var array
219
     */
220
    private $collectionPersisters = [];
221
222
    /**
223
     * The EventManager used for dispatching events.
224
     *
225
     * @var \Doctrine\Common\EventManager
226
     */
227
    private $eventManager;
228
229
    /**
230
     * The ListenersInvoker used for dispatching events.
231
     *
232
     * @var \Doctrine\ORM\Event\ListenersInvoker
233
     */
234
    private $listenersInvoker;
235
236
    /**
237
     * @var Instantiator
238
     */
239
    private $instantiator;
240
241
    /**
242
     * Orphaned entities that are scheduled for removal.
243
     *
244
     * @var array
245
     */
246
    private $orphanRemovals = [];
247
248
    /**
249
     * Read-Only objects are never evaluated
250
     *
251
     * @var array
252
     */
253
    private $readOnlyObjects = [];
254
255
    /**
256
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
257
     *
258
     * @var array
259
     */
260
    private $eagerLoadingEntities = [];
261
262
    /**
263
     * @var boolean
264
     */
265
    protected $hasCache = false;
266
267
    /**
268
     * Helper for handling completion of hydration
269
     *
270
     * @var HydrationCompleteHandler
271
     */
272
    private $hydrationCompleteHandler;
273
274
    /**
275
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
276
     *
277
     * @param EntityManagerInterface $em
278
     */
279
    public function __construct(EntityManagerInterface $em)
280
    {
281
        $this->em                       = $em;
282
        $this->eventManager             = $em->getEventManager();
283
        $this->listenersInvoker         = new ListenersInvoker($em);
284
        $this->hasCache                 = $em->getConfiguration()->isSecondLevelCacheEnabled();
285
        $this->instantiator             = new Instantiator();
286
        $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
287
    }
288
289
    /**
290
     * Commits the UnitOfWork, executing all operations that have been postponed
291
     * up to this point. The state of all managed entities will be synchronized with
292 2290
     * the database.
293
     *
294 2290
     * The operations are executed in the following order:
295 2290
     *
296 2290
     * 1) All entity insertions
297 2290
     * 2) All entity updates
298 2290
     * 3) All collection deletions
299 2290
     * 4) All collection updates
300 2290
     * 5) All entity deletions
301 2290
     *
302
     * @return void
303
     *
304
     * @throws \Exception
305
     */
306
    public function commit()
307
    {
308
        // Raise preFlush
309
        if ($this->eventManager->hasListeners(Events::preFlush)) {
310
            $this->eventManager->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
311
        }
312
313
        $this->computeChangeSets();
314
315
        if ( ! ($this->entityInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
317
                $this->entityUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
318
                $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
319
                $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
321
            $this->dispatchOnFlushEvent();
322 1015
            $this->dispatchPostFlushEvent();
323
324
            return; // Nothing to do.
325 1015
        }
326 2
327
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
328
            foreach ($this->orphanRemovals as $orphan) {
329
                $this->remove($orphan);
330 1015
            }
331 1007
        }
332 16
333 15
        $this->dispatchOnFlushEvent();
334 1
335 1
        // Now we need a commit order to maintain referential integrity
336 1
        $commitOrder = $this->getCommitOrder();
337
338
        $conn = $this->em->getConnection();
339
        $conn->beginTransaction();
340 1012
341 165
        try {
342 129
            // Collection deletions (deletions of complete collections)
343 40
            foreach ($this->collectionDeletions as $collectionToDelete) {
344 37
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
345 1012
            }
346 25
347 25
            if ($this->entityInsertions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
348
                foreach ($commitOrder as $class) {
349 25
                    $this->executeInserts($class);
350
                }
351
            }
352 1008
353 16
            if ($this->entityUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
354 16
                foreach ($commitOrder as $class) {
355
                    $this->executeUpdates($class);
356
                }
357
            }
358 1008
359
            // Extra updates that were requested by persisters.
360
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
361 1008
                $this->executeExtraUpdates();
362
            }
363 1008
364 1008
            // Collection updates (deleteRows, updateRows, insertRows)
365
            foreach ($this->collectionUpdates as $collectionToUpdate) {
366
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
367
            }
368 1008
369 19
            // Entity deletions come last and need to be in reverse commit order
370
            if ($this->entityDeletions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
372 1008
                    $this->executeDeletions($commitOrder[$i]);
373 1004
                }
374 1004
            }
375
376
            $conn->commit();
377
        } catch (Exception $e) {
378 1007
            $this->em->close();
379 115
            $conn->rollBack();
380 115
381
            $this->afterTransactionRolledBack();
382
383
            throw $e;
384
        }
385 1003
386 40
        $this->afterTransactionComplete();
387
388
        // Take new snapshots from visited collections
389
        foreach ($this->visitedCollections as $coll) {
390 1003
            $coll->takeSnapshot();
391 531
        }
392
393
        $this->dispatchPostFlushEvent();
394
395 1003
        // Clear up
396 62
        $this->entityInsertions =
397 62
        $this->entityUpdates =
398
        $this->entityDeletions =
399
        $this->extraUpdates =
400
        $this->entityChangeSets =
401 1003
        $this->collectionUpdates =
402 11
        $this->collectionDeletions =
403 11
        $this->visitedCollections =
404 11
        $this->scheduledForSynchronization =
405
        $this->orphanRemovals = [];
406 11
    }
407
408 11
    /**
409
     * Computes the changesets of all entities scheduled for insertion.
410
     *
411 1003
     * @return void
412
     */
413
    private function computeScheduleInsertsChangeSets()
414 1003
    {
415 530
        foreach ($this->entityInsertions as $entity) {
416
            $class = $this->em->getClassMetadata(get_class($entity));
417
418 1003
            $this->computeChangeSet($class, $entity);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
419
        }
420
    }
421 1002
422 1002
    /**
423 1002
     * Executes any extra updates that have been scheduled.
424 1002
     */
425 1002
    private function executeExtraUpdates()
426 1002
    {
427 1002
        foreach ($this->extraUpdates as $oid => $update) {
428 1002
            list ($entity, $changeset) = $update;
429 1002
430 1002
            $this->entityChangeSets[$oid] = $changeset;
431 1002
432
//            echo 'Extra update: ';
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
433
//            \Doctrine\Common\Util\Debug::dump($changeset, 3);
434
435
            $this->getEntityPersister(get_class($entity))->update($entity);
436
        }
437
438 1014
        $this->extraUpdates = [];
439
    }
440 1014
441 1006
    /**
442
     * Gets the changeset for an entity.
443 1006
     *
444
     * @param object $entity
445 1012
     *
446
     * @return array
447
     */
448
    public function & getEntityChangeSet($entity)
449
    {
450
        $oid  = spl_object_hash($entity);
451
        $data = [];
452
453
        if (!isset($this->entityChangeSets[$oid])) {
454
            return $data;
455
        }
456
457
        return $this->entityChangeSets[$oid];
458
    }
459
460
    /**
461 16
     * Computes the changes that happened to a single entity.
462
     *
463 16
     * Modifies/populates the following properties:
464
     *
465 16
     * {@link originalEntityData}
466 1
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
467
     * then it was not fetched from the database and therefore we have no original
468
     * entity data yet. All of the current entity data is stored as the original entity data.
469 15
     *
470
     * {@link entityChangeSets}
471 15
     * The changes detected on all properties of the entity are stored there.
472 14
     * A change is a tuple array where the first entry is the old value and the second
473
     * entry is the new value of the property. Changesets are used by persisters
474
     * to INSERT/UPDATE the persistent entity state.
475
     *
476 15
     * {@link entityUpdates}
477
     * If the entity is already fully MANAGED (has been fetched from the database before)
478 15
     * and any changes to its properties are detected, then a reference to the entity is stored
479
     * there to mark it for an update.
480
     *
481
     * {@link collectionDeletions}
482
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
483 15
     * then this collection is marked for deletion.
484 2
     *
485
     * @ignore
486
     *
487
     * @internal Don't call from the outside.
488 13
     *
489
     * @param ClassMetadata $class  The class descriptor of the entity.
490 13
     * @param object        $entity The entity for which to compute the changes.
491 6
     *
492
     * @return void
493 12
     */
494
    public function computeChangeSet(ClassMetadata $class, $entity)
495
    {
496
        $oid = spl_object_hash($entity);
497
498 40
        if (isset($this->readOnlyObjects[$oid])) {
499
            return;
500 40
        }
501 40
502
        if ($class->inheritanceType !== InheritanceType::NONE) {
503 40
            $class = $this->em->getClassMetadata(get_class($entity));
504
        }
505
506
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
0 ignored issues
show
Bug introduced by
It seems like $class defined by $this->em->getClassMetadata(get_class($entity)) on line 503 can also be of type object<Doctrine\Common\P...\Mapping\ClassMetadata>; however, Doctrine\ORM\Event\Liste...:getSubscribedSystems() does only seem to accept object<Doctrine\ORM\Mapping\ClassMetadata>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
507
508 40
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
509
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
0 ignored issues
show
Bug introduced by
It seems like $class defined by $this->em->getClassMetadata(get_class($entity)) on line 503 can also be of type object<Doctrine\Common\P...\Mapping\ClassMetadata>; however, Doctrine\ORM\Event\ListenersInvoker::invoke() does only seem to accept object<Doctrine\ORM\Mapping\ClassMetadata>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
510
        }
511 40
512 40
        $actualData = [];
513
514
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator does only exist in Doctrine\ORM\Mapping\ClassMetadata, but not in Doctrine\Common\Persistence\Mapping\ClassMetadata.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
515
            $value = $property->getValue($entity);
516
517
            if ($property instanceof ToManyAssociationMetadata && $value !== null) {
518
                if ($value instanceof PersistentCollection && $value->getOwner() === $entity) {
519
                    continue;
520
                }
521 1006
522
                $value = $property->wrap($entity, $value, $this->em);
523 1006
524 1006
                $property->setValue($entity, $value);
525
526 1006
                $actualData[$name] = $value;
527 1
528
                continue;
529
            }
530 1006
531
            if (
532
                ( ! $class->isIdentifier($name)
533
                    || ! $class->getProperty($name) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getProperty does only exist in Doctrine\ORM\Mapping\ClassMetadata, but not in Doctrine\Common\Persistence\Mapping\ClassMetadata.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
534
                    || ! $class->getProperty($name)->hasValueGenerator()
535
                    || $class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
536
                ) && (! $class->isVersioned() || $name !== $class->versionProperty->getName())) {
0 ignored issues
show
Bug introduced by
The method isVersioned does only exist in Doctrine\ORM\Mapping\ClassMetadata, but not in Doctrine\Common\Persistence\Mapping\ClassMetadata.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
537
                $actualData[$name] = $value;
538
            }
539
        }
540
541
        if ( ! isset($this->originalEntityData[$oid])) {
542
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
543
            // These result in an INSERT.
544
            $this->originalEntityData[$oid] = $actualData;
545
            $changeSet = [];
546
547
            foreach ($actualData as $propName => $actualValue) {
548
                $property = $class->getProperty($propName);
549
550
                if (($property instanceof FieldMetadata) ||
551
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
552
                    $changeSet[$propName] = [null, $actualValue];
553
                }
554
            }
555
556
            $this->entityChangeSets[$oid] = $changeSet;
557
        } else {
558
            // Entity is "fully" MANAGED: it was already fully persisted before
559
            // and we have a copy of the original data
560
            $originalData           = $this->originalEntityData[$oid];
561
            $isChangeTrackingNotify = $class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY;
562
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
563
                ? $this->entityChangeSets[$oid]
564
                : [];
565
566
            foreach ($actualData as $propName => $actualValue) {
567 1016
                // skip field, its a partially omitted one!
568
                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
569 1016
                    continue;
570
                }
571 1016
572 2
                $orgValue = $originalData[$propName];
573
574
                // skip if value haven't changed
575 1016
                if ($orgValue === $actualValue) {
576 307
                    continue;
577
                }
578
579 1016
                $property = $class->getProperty($propName);
580
581 1016
                // Persistent collection was exchanged with the "originally"
582 137
                // created one. This can only mean it was cloned and replaced
583
                // on another entity.
584
                if ($actualValue instanceof PersistentCollection) {
585 1016
                    $owner = $actualValue->getOwner();
586
587 1016
                    if ($owner === null) { // cloned
588 1016
                        $actualValue->setOwner($entity, $property);
589
                    } else if ($owner !== $entity) { // no clone, we have to fix
590 1016
                        if (! $actualValue->isInitialized()) {
591 777
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
592 200
                        }
593 200
594
                        $newValue = clone $actualValue;
595
596 5
                        $newValue->setOwner($entity, $property);
597
598
                        $property->setValue($entity, $newValue);
599
                    }
600 772
                }
601 242
602
                switch (true) {
603
                    case ($property instanceof FieldMetadata):
604 772
                        if ($isChangeTrackingNotify) {
605
                            // Continue inside switch behaves as break.
606
                            // We are required to use continue 2, since we need to continue to next $actualData item
607 772
                            continue 2;
608 772
                        }
609
610 772
                        $changeSet[$propName] = [$orgValue, $actualValue];
611 772
                        break;
612
613 772
                    case ($property instanceof ToOneAssociationMetadata):
614
                        if ($property->isOwningSide()) {
615 772
                            $changeSet[$propName] = [$orgValue, $actualValue];
616
                        }
617 772
618
                        if ($orgValue !== null && $property->isOrphanRemoval()) {
619
                            $this->scheduleOrphanRemoval($orgValue);
620 1016
                        }
621 1016
622 1016
                        break;
623
624
                    case ($property instanceof ToManyAssociationMetadata):
625
                        // Check if original value exists
626 1016
                        if ($orgValue instanceof PersistentCollection) {
627
                            // A PersistentCollection was de-referenced, so delete it.
628
                            $coid = spl_object_hash($orgValue);
629 1012
630 1012
                            if (!isset($this->collectionDeletions[$coid])) {
631
                                $this->collectionDeletions[$coid] = $orgValue;
632 1012
                                $changeSet[$propName] = $orgValue; // Signal changeset, to-many associations will be ignored
633 996
                            }
634 945
                        }
635
636 945
                        break;
637
638
                    default:
639 894
                        // Do nothing
640
                }
641 894
            }
642 894
643
            if ($changeSet) {
644
                $this->entityChangeSets[$oid]   = $changeSet;
645
                $this->originalEntityData[$oid] = $actualData;
646 1012
                $this->entityUpdates[$oid]      = $entity;
647
            }
648
        }
649
650 263
        // Look for changes in associations of the entity
651 263
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
652 263
            if (! ($property instanceof AssociationMetadata) || ($value = $property->getValue($entity)) === null) {
653
                continue;
654 263
            }
655
656 263
            $this->computeAssociationChanges($property, $value);
657
658 248
            if ($property instanceof ManyToManyAssociationMetadata &&
659 7
                $value instanceof PersistentCollection &&
660
                ! isset($this->entityChangeSets[$oid]) &&
661
                $property->isOwningSide() &&
662 248
                $value->isDirty()) {
663
664
                $this->entityChangeSets[$oid]   = [];
665 248
                $this->originalEntityData[$oid] = $actualData;
666 232
                $this->entityUpdates[$oid]      = $entity;
667
            }
668
        }
669
    }
670 111
671 57
    /**
672
     * Computes all the changes that have been done to entities and collections
673
     * since the last commit and stores these changes in the _entityChangeSet map
674
     * temporarily for access by the persisters, until the UoW commit is finished.
675 57
     *
676
     * @return void
677 57
     */
678
    public function computeChangeSets()
679
    {
680 58
        // Compute changes for INSERTed entities first. This must always happen.
681
        $this->computeScheduleInsertsChangeSets();
682
683
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
684
        foreach ($this->identityMap as $className => $entities) {
685 58
            $class = $this->em->getClassMetadata($className);
686 8
687 8
            // Skip class if instances are read-only
688
            if ($class->isReadOnly()) {
0 ignored issues
show
Bug introduced by
The method isReadOnly() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
689 8
                continue;
690
            }
691
692
            // If change tracking is explicit or happens through notification, then only compute
693
            // changes on entities of that type that are explicitly marked for synchronization.
694
            switch (true) {
695
                case ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_IMPLICIT):
0 ignored issues
show
Bug introduced by
Accessing changeTrackingPolicy on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
696
                    $entitiesToProcess = $entities;
697
                    break;
698
699 58
                case (isset($this->scheduledForSynchronization[$className])):
700
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
701 8
                    break;
702
703 8
                default:
704
                    $entitiesToProcess = [];
705
706
            }
707 8
708 8
            foreach ($entitiesToProcess as $entity) {
709
                // Ignore uninitialized proxy objects
710 8
                if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
711
                    continue;
712
                }
713 50
714 49
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
715 21
                $oid = spl_object_hash($entity);
716
717
                if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
718 49
                    $this->computeChangeSet($class, $entity);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
719 50
                }
720
            }
721
        }
722
    }
723
724 263
    /**
725 84
     * Computes the changes of an association.
726 84
     *
727 84
     * @param AssociationMetadata $association The association mapping.
728
     * @param mixed               $value       The value of the association.
729
     *
730
     * @throws ORMInvalidArgumentException
731
     * @throws ORMException
732 1016
     *
733 894
     * @return void
734 639
     */
735
    private function computeAssociationChanges(AssociationMetadata $association, $value)
736
    {
737 865
        if ($value instanceof Proxy && ! $value->__isInitialized()) {
738
            return;
739 857
        }
740 857
741 857
        if ($value instanceof PersistentCollection && $value->isDirty()) {
742 857
            $coid = spl_object_hash($value);
743 857
744
            $this->collectionUpdates[$coid] = $value;
745 35
            $this->visitedCollections[$coid] = $value;
746 35
        }
747 857
748
        // Look through the entities, and in any of their associations,
749
        // for transient (new) entities, recursively. ("Persistence by reachability")
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
750 1008
        // Unwrap. Uninitialized collections will simply be empty.
751
        $unwrappedValue = ($association instanceof ToOneAssociationMetadata) ? [$value] : $value->unwrap();
752
        $targetEntity   = $association->getTargetEntity();
753
        $targetClass    = $this->em->getClassMetadata($targetEntity);
754
755
        foreach ($unwrappedValue as $key => $entry) {
756
            if (! ($entry instanceof $targetEntity)) {
757
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $association, $entry);
0 ignored issues
show
Documentation introduced by
$targetClass is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
758
            }
759 1007
760
            $state = $this->getEntityState($entry, self::STATE_NEW);
761
762 1007
            if (! ($entry instanceof $targetEntity)) {
763
                throw ORMException::unexpectedAssociationValue(
764
                    $association->getSourceEntity(),
765 1005
                    $association->getName(),
766 447
                    get_class($entry),
767
                    $targetEntity
768
                );
769 447
            }
770 1
771
            switch ($state) {
772
                case self::STATE_NEW:
773
                    if ( ! in_array('persist', $association->getCascade())) {
774
                        throw ORMInvalidArgumentException::newEntityFoundThroughRelationship($association, $entry);
775
                    }
776 446
777 444
                    $this->persistNew($targetClass, $entry);
0 ignored issues
show
Documentation introduced by
$targetClass is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
778 444
                    $this->computeChangeSet($targetClass, $entry);
0 ignored issues
show
Documentation introduced by
$targetClass is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
779
                    break;
780 3
781 3
                case self::STATE_REMOVED:
782 3
                    // Consume the $value as array (it's either an array or an ArrayAccess)
783
                    // and remove the element from Collection.
784
                    if ($association instanceof ToManyAssociationMetadata) {
785 1
                        unset($value[$key]);
786
                    }
787
                    break;
788
789 446
                case self::STATE_DETACHED:
790
                    // Can actually not happen right now as we assume STATE_NEW,
791 426
                    // so the exception will be raised from the DBAL layer (constraint violation).
792 35
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($association, $entry);
793
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
794
795
                default:
796 425
                    // MANAGED associated entities are already taken into account
797
                    // during changeset calculation anyway, since they are in the identity map.
798 425
            }
799 446
        }
800
    }
801
802
    /**
803 1005
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
804
     * @param object                              $entity
805
     *
806
     * @return void
807
     */
808
    private function persistNew($class, $entity)
809
    {
810
        $oid    = spl_object_hash($entity);
811
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
812
813
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
814
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
815
        }
816 865
817
        $generationPlan = $class->getValueGenerationPlan();
818 865
        $persister = $this->getEntityPersister($class->getClassName());
819 28
        $generationPlan->executeImmediate($this->em, $entity);
820
821
        if (! $generationPlan->containsDeferred()) {
822 864
            $id = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
823 533
            $this->entityIdentifiers[$oid] = $id;
824
        }
825 533
826 533
        $this->entityStates[$oid] = self::STATE_MANAGED;
827
828
        $this->scheduleForInsert($entity);
829
    }
830
831
    /**
832 864
     * INTERNAL:
833 864
     * Computes the changeset of an individual entity, independently of the
834
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
835 864
     *
836 722
     * The passed entity must be a managed entity. If the entity already has a change set
837 6
     * because this method is invoked during a commit cycle then the change sets are added.
838
     * whereby changes detected in this method prevail.
839
     *
840 716
     * @ignore
841
     *
842 716
     * @param ClassMetadata $class  The class descriptor of the entity.
843
     * @param object        $entity The entity for which to (re)calculate the change set.
844
     *
845
     * @return void
846
     *
847 716
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
848 39
     * @throws \RuntimeException
849 4
     */
850
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) : void
851
    {
852 35
        $oid = spl_object_hash($entity);
853 35
854 35
        if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) {
855
            throw ORMInvalidArgumentException::entityNotManaged($entity);
856 710
        }
857
858
        // skip if change tracking is "NOTIFY"
859 4
        if ($class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) {
860 3
            return;
861
        }
862 4
863
        if ($class->inheritanceType !== InheritanceType::NONE) {
864 710
            $class = $this->em->getClassMetadata(get_class($entity));
865
        }
866
867
        $actualData = [];
868
869
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator does only exist in Doctrine\ORM\Mapping\ClassMetadata, but not in Doctrine\Common\Persistence\Mapping\ClassMetadata.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
870 713
            switch (true) {
871
                case ($property instanceof VersionFieldMetadata):
872
                    // Ignore version field
873
                    break;
874
875 856
                case ($property instanceof FieldMetadata):
876
                    if (! $property->isPrimaryKey()
877
                        || ! $property->getValueGenerator()
878
                        || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY) {
879
                        $actualData[$name] = $property->getValue($entity);
880
                    }
881
882
                    break;
883 1031
884
                case ($property instanceof ToOneAssociationMetadata):
885 1031
                    $actualData[$name] = $property->getValue($entity);
886 1031
                    break;
887
            }
888 1031
        }
889 139
890
        if ( ! isset($this->originalEntityData[$oid])) {
891
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
892 1031
        }
893
894 1031
        $originalData = $this->originalEntityData[$oid];
895 269
        $changeSet = [];
896
897 269
        foreach ($actualData as $propName => $actualValue) {
898 1
            $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
899
900 1
            if ($orgValue !== $actualValue) {
901
                $changeSet[$propName] = [$orgValue, $actualValue];
902
            }
903 269
        }
904
905
        if ($changeSet) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeSet 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...
906 1031
            if (isset($this->entityChangeSets[$oid])) {
907
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
908 1031
            } else if ( ! isset($this->entityInsertions[$oid])) {
909 1031
                $this->entityChangeSets[$oid] = $changeSet;
910
                $this->entityUpdates[$oid]    = $entity;
911
            }
912
            $this->originalEntityData[$oid] = $actualData;
913
        }
914
    }
915
916
    /**
917
     * Executes all entity insertions for entities of the specified type.
918
     *
919
     * @param ClassMetadata $class
920
     *
921
     * @return void
922
     */
923
    private function executeInserts(ClassMetadata $class) : void
924
    {
925
        $className      = $class->getClassName();
926
        $persister      = $this->getEntityPersister($className);
927
        $invoke         = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
928
        $generationPlan = $class->getValueGenerationPlan();
929 16
930
        foreach ($this->entityInsertions as $oid => $entity) {
931 16
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
932
                continue;
933 16
            }
934
935
            $persister->insert($entity);
936
937
            if ($generationPlan->containsDeferred()) {
938 16
                // Entity has post-insert IDs
939
                $oid = spl_object_hash($entity);
940
                $id  = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
941
942 16
                $this->entityIdentifiers[$oid] = $id;
943 3
                $this->entityStates[$oid] = self::STATE_MANAGED;
944
                $this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid];
945
946 16
                $this->addToIdentityMap($entity);
947
            }
948 16
949 16
            unset($this->entityInsertions[$oid]);
950 16
951 16
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
952 16
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
953
954
                $this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke);
955
            }
956 16
        }
957
    }
958
959
    /**
960 16
     * Executes all entity updates for entities of the specified type.
961 16
     *
962
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
963 16
     *
964 16
     * @return void
965
     */
966 16
    private function executeUpdates($class)
967 16
    {
968
        $className          = $class->getClassName();
969
        $persister          = $this->getEntityPersister($className);
970
        $preUpdateInvoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
971 16
        $postUpdateInvoke   = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
972 7
973 6
        foreach ($this->entityUpdates as $oid => $entity) {
974 1
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
975 1
                continue;
976 1
            }
977
978 7
            if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
979
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
980 16
981
                $this->recomputeSingleEntityChangeSet($class, $entity);
982
            }
983
984
            if ( ! empty($this->entityChangeSets[$oid])) {
985
//                echo 'Update: ';
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
986
//                \Doctrine\Common\Util\Debug::dump($this->entityChangeSets[$oid], 3);
987
988
                $persister->update($entity);
989 1004
            }
990
991 1004
            unset($this->entityUpdates[$oid]);
992 1004
993 1004
            if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) {
994 1004
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
995
            }
996 1004
        }
997
    }
998 1004
999 855
    /**
1000
     * Executes all entity deletions for entities of the specified type.
1001
     *
1002 1004
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1003
     *
1004 1004
     * @return void
1005
     */
1006 1004
    private function executeDeletions($class)
1007 1004
    {
1008
        $className  = $class->getClassName();
1009
        $persister  = $this->getEntityPersister($className);
1010
        $invoke     = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1011 1004
1012
        foreach ($this->entityDeletions as $oid => $entity) {
1013 1004
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1014
                continue;
1015 918
            }
1016 918
1017 918
            $persister->delete($entity);
1018 918
1019 918
            unset(
1020
                $this->entityDeletions[$oid],
1021 918
                $this->entityIdentifiers[$oid],
1022
                $this->originalEntityData[$oid],
1023 918
                $this->entityStates[$oid]
1024 918
            );
1025 918
1026
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1027 918
            // is obtained by a new entity because the old one went out of scope.
1028
            //$this->entityStates[$oid] = self::STATE_NEW;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1029
            if (! $class->isIdentifierComposite()) {
1030
                $property = $class->getProperty($class->getSingleIdentifierFieldName());
1031 1004
1032 135
                if ($property instanceof FieldMetadata && $property->hasValueGenerator()) {
1033
                    $property->setValue($entity, null);
1034 1004
                }
1035
            }
1036
1037
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1038
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
1039
1040
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, $eventArgs, $invoke);
1041
            }
1042
        }
1043 115
    }
1044
1045 115
    /**
1046 115
     * Gets the commit order.
1047 115
     *
1048 115
     * @param array|null $entityChangeSet
1049
     *
1050 115
     * @return array
1051 115
     */
1052 74
    private function getCommitOrder(array $entityChangeSet = null)
1053
    {
1054
        if ($entityChangeSet === null) {
1055 115
            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1056 13
        }
1057
1058 13
        $calc = new Internal\CommitOrderCalculator();
1059
1060
        // See if there are any new classes in the changeset, that are not in the
1061 115
        // commit order graph yet (don't have a node).
1062
        // We have to inspect changeSet to be able to correctly build dependencies.
1063
        // It is not possible to use IdentityMap here because post inserted ids
1064
        // are not yet available.
1065 81
        $newNodes = [];
1066
1067
        foreach ($entityChangeSet as $entity) {
1068 111
            $class = $this->em->getClassMetadata(get_class($entity));
1069
1070 111
            if ($calc->hasNode($class->getClassName())) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1071 111
                continue;
1072
            }
1073
1074 111
            $calc->addNode($class->getClassName(), $class);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1075
1076
            $newNodes[] = $class;
1077
        }
1078
1079
        // Calculate dependencies for new nodes
1080
        while ($class = array_pop($newNodes)) {
1081
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
1082
                if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
1083 62
                    continue;
1084
                }
1085 62
1086 62
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1087 62
1088
                if ( ! $calc->hasNode($targetClass->getClassName())) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1089 62
                    $calc->addNode($targetClass->getClassName(), $targetClass);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1090 62
1091 26
                    $newNodes[] = $targetClass;
1092
                }
1093
1094 62
                $weight = count(
1095
                    array_filter(
1096
                        $property->getJoinColumns(),
1097 62
                        function (JoinColumnMetadata $joinColumn) { return $joinColumn->isNullable(); }
1098 62
                    )
1099 62
                ) === 0;
1100 62
1101
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), $weight);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
Documentation introduced by
$weight is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1102
1103
                // If the target class has mapped subclasses, these share the same dependency.
1104
                if ( ! $targetClass->getSubClasses()) {
0 ignored issues
show
Bug introduced by
The method getSubClasses() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1105
                    continue;
1106 62
                }
1107 52
1108
                foreach ($targetClass->getSubClasses() as $subClassName) {
0 ignored issues
show
Bug introduced by
The method getSubClasses() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1109
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1110 62
1111 62
                    if ( ! $calc->hasNode($subClassName)) {
1112
                        $calc->addNode($targetSubClass->getClassName(), $targetSubClass);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1113
1114 61
                        $newNodes[] = $targetSubClass;
1115
                    }
1116
1117
                    $calc->addDependency($targetSubClass->getClassName(), $class->getClassName(), 1);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1118
                }
1119
            }
1120
        }
1121
1122
        return $calc->sort();
1123 1008
    }
1124
1125 1008
    /**
1126 1008
     * Schedules an entity for insertion into the database.
1127
     * If the entity already has an identifier, it will be added to the identity map.
1128
     *
1129 1008
     * @param object $entity The entity to schedule for insertion.
1130
     *
1131
     * @return void
1132
     *
1133
     * @throws ORMInvalidArgumentException
1134
     * @throws \InvalidArgumentException
1135
     */
1136 1008
    public function scheduleForInsert($entity)
1137
    {
1138 1008
        $oid = spl_object_hash($entity);
1139 1008
1140
        if (isset($this->entityUpdates[$oid])) {
1141 1008
            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1142 624
        }
1143
1144
        if (isset($this->entityDeletions[$oid])) {
1145 1008
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1146
        }
1147 1008
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1148
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1149
        }
1150
1151 1008
        if (isset($this->entityInsertions[$oid])) {
1152 1008
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1153 886
        }
1154 846
1155
        $this->entityInsertions[$oid] = $entity;
1156
1157 839
        if (isset($this->entityIdentifiers[$oid])) {
1158
            $this->addToIdentityMap($entity);
1159 839
        }
1160 649
1161
        if ($entity instanceof NotifyPropertyChanged) {
1162 649
            $entity->addPropertyChangedListener($this);
1163
        }
1164
    }
1165 839
1166
    /**
1167 839
     * Checks whether an entity is scheduled for insertion.
1168
     *
1169
     * @param object $entity
1170 839
     *
1171 832
     * @return boolean
1172
     */
1173
    public function isScheduledForInsert($entity)
1174 217
    {
1175 217
        return isset($this->entityInsertions[spl_object_hash($entity)]);
1176
    }
1177 217
1178 189
    /**
1179
     * Schedules an entity for being updated.
1180 189
     *
1181
     * @param object $entity The entity to schedule for being updated.
1182
     *
1183 217
     * @return void
1184
     *
1185
     * @throws ORMInvalidArgumentException
1186
     */
1187
    public function scheduleForUpdate($entity) : void
1188 1008
    {
1189
        $oid = spl_object_hash($entity);
1190
1191
        if ( ! isset($this->entityIdentifiers[$oid])) {
1192
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1193
        }
1194
1195
        if (isset($this->entityDeletions[$oid])) {
1196
            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1197
        }
1198
1199
        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1200
            $this->entityUpdates[$oid] = $entity;
1201
        }
1202 1032
    }
1203
1204 1032
    /**
1205
     * INTERNAL:
1206 1032
     * Schedules an extra update that will be executed immediately after the
1207
     * regular entity updates within the currently running commit cycle.
1208
     *
1209
     * Extra updates for entities are stored as (entity, changeset) tuples.
1210 1032
     *
1211 1
     * @ignore
1212
     *
1213 1032
     * @param object $entity    The entity for which to schedule an extra update.
1214 1
     * @param array  $changeset The changeset of the entity (what to update).
1215
     *
1216
     * @return void
1217 1032
     */
1218 1
    public function scheduleExtraUpdate($entity, array $changeset) : void
1219
    {
1220
        $oid         = spl_object_hash($entity);
1221 1032
        $extraUpdate = [$entity, $changeset];
1222
1223 1032
        if (isset($this->extraUpdates[$oid])) {
1224 269
            [$unused, $changeset2] = $this->extraUpdates[$oid];
0 ignored issues
show
Bug introduced by
The variable $unused does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $changeset2 does not exist. Did you mean $changeset?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1225
1226
            $extraUpdate = [$entity, $changeset + $changeset2];
0 ignored issues
show
Bug introduced by
The variable $changeset2 does not exist. Did you mean $changeset?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1227 1032
        }
1228 5
1229
        $this->extraUpdates[$oid] = $extraUpdate;
1230 1032
    }
1231
1232
    /**
1233
     * Checks whether an entity is registered as dirty in the unit of work.
1234
     * Note: Is not very useful currently as dirty entities are only registered
1235
     * at commit time.
1236
     *
1237
     * @param object $entity
1238
     *
1239 631
     * @return boolean
1240
     */
1241 631
    public function isScheduledForUpdate($entity) : bool
1242
    {
1243
        return isset($this->entityUpdates[spl_object_hash($entity)]);
1244
    }
1245
1246
    /**
1247
     * Checks whether an entity is registered to be checked in the unit of work.
1248
     *
1249
     * @param object $entity
1250
     *
1251
     * @return boolean
1252
     */
1253 1
    public function isScheduledForDirtyCheck($entity) : bool
1254
    {
1255 1
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->getRootClassName();
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1256
1257 1
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]);
1258
    }
1259
1260
    /**
1261 1
     * INTERNAL:
1262
     * Schedules an entity for deletion.
1263
     *
1264
     * @param object $entity
1265 1
     *
1266 1
     * @return void
1267
     */
1268 1
    public function scheduleForDelete($entity)
1269
    {
1270
        $oid = spl_object_hash($entity);
1271
1272
        if (isset($this->entityInsertions[$oid])) {
1273
            if ($this->isInIdentityMap($entity)) {
1274
                $this->removeFromIdentityMap($entity);
1275
            }
1276
1277
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1278
1279
            return; // entity has not been persisted yet, so nothing more to do.
1280
        }
1281
1282
        if ( ! $this->isInIdentityMap($entity)) {
1283
            return;
1284 40
        }
1285
1286 40
        $this->removeFromIdentityMap($entity);
1287 40
1288
        unset($this->entityUpdates[$oid]);
1289 40
1290 1
        if ( ! isset($this->entityDeletions[$oid])) {
1291
            $this->entityDeletions[$oid] = $entity;
1292 1
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1293
        }
1294
    }
1295 40
1296 40
    /**
1297
     * Checks whether an entity is registered as removed/deleted with the unit
1298
     * of work.
1299
     *
1300
     * @param object $entity
1301
     *
1302
     * @return boolean
1303
     */
1304
    public function isScheduledForDelete($entity)
1305
    {
1306
        return isset($this->entityDeletions[spl_object_hash($entity)]);
1307
    }
1308
1309
    /**
1310
     * Checks whether an entity is scheduled for insertion, update or deletion.
1311
     *
1312
     * @param object $entity
1313
     *
1314
     * @return boolean
1315
     */
1316
    public function isEntityScheduled($entity)
1317
    {
1318
        $oid = spl_object_hash($entity);
1319 1
1320
        return isset($this->entityInsertions[$oid])
1321 1
            || isset($this->entityUpdates[$oid])
1322
            || isset($this->entityDeletions[$oid]);
1323 1
    }
1324
1325
    /**
1326
     * INTERNAL:
1327
     * Registers an entity in the identity map.
1328
     * Note that entities in a hierarchy are registered with the class name of
1329
     * the root entity.
1330
     *
1331
     * @ignore
1332
     *
1333
     * @param object $entity The entity to register.
1334 65
     *
1335
     * @return boolean TRUE if the registration was successful, FALSE if the identity of
1336 65
     *                 the entity in question is already managed.
1337
     *
1338 65
     * @throws ORMInvalidArgumentException
1339 1
     */
1340
    public function addToIdentityMap($entity)
1341
    {
1342
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1343 1
        $identifier    = $this->entityIdentifiers[spl_object_hash($entity)];
1344
1345 1
        if (empty($identifier) || in_array(null, $identifier, true)) {
1346
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->getClassName(), $entity);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1347
        }
1348 65
1349 1
        $idHash    = implode(' ', $identifier);
1350
        $className = $classMetadata->getRootClassName();
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1351
1352 64
        if (isset($this->identityMap[$className][$idHash])) {
1353
            return false;
1354 64
        }
1355
1356 64
        $this->identityMap[$className][$idHash] = $entity;
1357 64
1358 64
        return true;
1359
    }
1360 64
1361
    /**
1362
     * Gets the state of an entity with regard to the current unit of work.
1363
     *
1364
     * @param object   $entity
1365
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1366
     *                         This parameter can be set to improve performance of entity state detection
1367
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1368
     *                         is either known or does not matter for the caller of the method.
1369
     *
1370 17
     * @return int The entity state.
1371
     */
1372 17
    public function getEntityState($entity, $assume = null)
1373
    {
1374
        $oid = spl_object_hash($entity);
1375
1376
        if (isset($this->entityStates[$oid])) {
1377
            return $this->entityStates[$oid];
1378
        }
1379
1380
        if ($assume !== null) {
1381
            return $assume;
1382
        }
1383
1384
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1385
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1386
        // the UoW does not hold references to such objects and the object hash can be reused.
1387
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1388
        $class     = $this->em->getClassMetadata(get_class($entity));
1389
        $persister = $this->getEntityPersister($class->getClassName());
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1390
        $id        = $persister->getIdentifier($entity);
1391
1392
        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...
1393
            return self::STATE_NEW;
1394
        }
1395
1396
        $flatId = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1397
1398
        if ($class->isIdentifierComposite()
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1399
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1400
            || ! $class->getProperty($class->getSingleIdentifierFieldName())->hasValueGenerator()
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1401
        ) {
1402
            // Check for a version field, if available, to avoid a db lookup.
1403
            if ($class->isVersioned()) {
0 ignored issues
show
Bug introduced by
The method isVersioned() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1404
                return $class->versionProperty->getValue($entity)
0 ignored issues
show
Bug introduced by
Accessing versionProperty on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1405
                    ? self::STATE_DETACHED
1406 1098
                    : self::STATE_NEW;
1407
            }
1408 1098
1409 1098
            // Last try before db lookup: check the identity map.
1410
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1411 1098
                return self::STATE_DETACHED;
1412 6
            }
1413
1414
            // db lookup
1415 1092
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1416 1092
                return self::STATE_DETACHED;
1417
            }
1418 1092
1419 83
            return self::STATE_NEW;
1420
        }
1421
1422 1092
        if ($class->isIdentifierComposite()
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1423
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1424 1092
            || ! $class->getValueGenerationPlan()->containsDeferred()) {
0 ignored issues
show
Bug introduced by
The method getValueGenerationPlan() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1425
            // if we have a pre insert generator we can't be sure that having an id
1426
            // really means that the entity exists. We have to verify this through
1427
            // the last resort: a db lookup
1428
1429
            // Last try before db lookup: check the identity map.
1430
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1431
                return self::STATE_DETACHED;
1432
            }
1433
1434
            // db lookup
1435
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1436
                return self::STATE_DETACHED;
1437
            }
1438 1045
1439
            return self::STATE_NEW;
1440 1045
        }
1441
1442 1045
        return self::STATE_DETACHED;
1443 783
    }
1444
1445
    /**
1446 1039
     * INTERNAL:
1447 1035
     * Removes an entity from the identity map. This effectively detaches the
1448
     * entity from the persistence management of Doctrine.
1449
     *
1450
     * @ignore
1451
     *
1452
     * @param object $entity
1453
     *
1454 13
     * @return boolean
1455 13
     *
1456
     * @throws ORMInvalidArgumentException
1457 13
     */
1458 5
    public function removeFromIdentityMap($entity)
1459
    {
1460
        $oid           = spl_object_hash($entity);
1461 10
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1462 1
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1463
1464
        if ($idHash === '') {
1465
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1466 10
        }
1467
1468 5
        $className = $classMetadata->getRootClassName();
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1469 1
1470
        if (isset($this->identityMap[$className][$idHash])) {
1471 1
            unset($this->identityMap[$className][$idHash]);
1472
            unset($this->readOnlyObjects[$oid]);
1473
1474
            //$this->entityStates[$oid] = self::STATE_DETACHED;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1475 4
1476 1
            return true;
1477
        }
1478
1479
        return false;
1480 4
    }
1481
1482
    /**
1483
     * INTERNAL:
1484 4
     * Gets an entity in the identity map by its identifier hash.
1485
     *
1486 5
     * @ignore
1487
     *
1488
     * @param string $idHash
1489
     * @param string $rootClassName
1490
     *
1491
     * @return object
1492
     */
1493
    public function getByIdHash($idHash, $rootClassName)
1494
    {
1495
        return $this->identityMap[$rootClassName][$idHash];
1496
    }
1497
1498
    /**
1499
     * INTERNAL:
1500
     * Tries to get an entity by its identifier hash. If no entity is found for
1501
     * the given hash, FALSE is returned.
1502
     *
1503
     * @ignore
1504 5
     *
1505
     * @param mixed  $idHash        (must be possible to cast it to string)
1506
     * @param string $rootClassName
1507
     *
1508
     * @return object|bool The found entity or FALSE.
1509
     */
1510
    public function tryGetByIdHash($idHash, $rootClassName)
1511
    {
1512
        $stringIdHash = (string) $idHash;
1513
1514
        return isset($this->identityMap[$rootClassName][$stringIdHash])
1515
            ? $this->identityMap[$rootClassName][$stringIdHash]
1516
            : false;
1517
    }
1518
1519
    /**
1520
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1521 76
     *
1522
     * @param object $entity
1523 76
     *
1524 76
     * @return boolean
1525 76
     */
1526
    public function isInIdentityMap($entity)
1527 76
    {
1528
        $oid = spl_object_hash($entity);
1529
1530
        if (empty($this->entityIdentifiers[$oid])) {
1531 76
            return false;
1532
        }
1533 76
1534 76
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1535 76
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1536
1537
        return isset($this->identityMap[$classMetadata->getRootClassName()][$idHash]);
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1538
    }
1539 76
1540
    /**
1541
     * INTERNAL:
1542
     * Checks whether an identifier hash exists in the identity map.
1543
     *
1544
     * @ignore
1545
     *
1546
     * @param string $idHash
1547
     * @param string $rootClassName
1548
     *
1549
     * @return boolean
1550
     */
1551
    public function containsIdHash($idHash, $rootClassName)
1552
    {
1553
        return isset($this->identityMap[$rootClassName][$idHash]);
1554
    }
1555
1556 6
    /**
1557
     * Persists an entity as part of the current unit of work.
1558 6
     *
1559
     * @param object $entity The entity to persist.
1560
     *
1561
     * @return void
1562
     */
1563
    public function persist($entity)
1564
    {
1565
        $visited = [];
1566
1567
        $this->doPersist($entity, $visited);
1568
    }
1569
1570
    /**
1571
     * Persists an entity as part of the current unit of work.
1572
     *
1573 34
     * This method is internally called during persist() cascades as it tracks
1574
     * the already visited entities to prevent infinite recursions.
1575 34
     *
1576
     * @param object $entity  The entity to persist.
1577 34
     * @param array  $visited The already visited entities.
1578 34
     *
1579 34
     * @return void
1580
     *
1581
     * @throws ORMInvalidArgumentException
1582
     * @throws UnexpectedValueException
1583
     */
1584
    private function doPersist($entity, array &$visited)
1585
    {
1586
        $oid = spl_object_hash($entity);
1587
1588
        if (isset($visited[$oid])) {
1589 212
            return; // Prevent infinite recursion
1590
        }
1591 212
1592
        $visited[$oid] = $entity; // Mark visited
1593 212
1594 31
        $class = $this->em->getClassMetadata(get_class($entity));
1595
1596
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1597 197
        // If we would detect DETACHED here we would throw an exception anyway with the same
1598 197
        // consequences (not recoverable/programming error), so just assuming NEW here
1599
        // lets us avoid some database lookups for entities with natural identifiers.
1600 197
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1601
1602
        switch ($entityState) {
1603
            case self::STATE_MANAGED:
1604 197
                // Nothing to do, except if policy is "deferred explicit"
1605
                if ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_EXPLICIT) {
0 ignored issues
show
Bug introduced by
Accessing changeTrackingPolicy on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1606
                    $this->scheduleForSynchronization($entity);
1607
                }
1608
                break;
1609
1610
            case self::STATE_NEW:
1611
                $this->persistNew($class, $entity);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1612
                break;
1613
1614
            case self::STATE_REMOVED:
1615
                // Entity becomes managed again
1616
                unset($this->entityDeletions[$oid]);
1617
                $this->addToIdentityMap($entity);
1618
1619
                $this->entityStates[$oid] = self::STATE_MANAGED;
1620
                break;
1621
1622
            case self::STATE_DETACHED:
1623
                // Can actually not happen right now since we assume STATE_NEW.
1624
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1625
1626
            default:
1627
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1628
        }
1629
1630 1028
        $this->cascadePersist($entity, $visited);
1631
    }
1632 1028
1633
    /**
1634 1028
     * Deletes an entity as part of the current unit of work.
1635 1021
     *
1636
     * @param object $entity The entity to remove.
1637
     *
1638
     * @return void
1639
     */
1640
    public function remove($entity)
1641
    {
1642
        $visited = [];
1643
1644
        $this->doRemove($entity, $visited);
1645
    }
1646
1647
    /**
1648
     * Deletes an entity as part of the current unit of work.
1649
     *
1650
     * This method is internally called during delete() cascades as it tracks
1651 1028
     * the already visited entities to prevent infinite recursions.
1652
     *
1653 1028
     * @param object $entity  The entity to delete.
1654
     * @param array  $visited The map of the already visited entities.
1655 1028
     *
1656 109
     * @return void
1657
     *
1658
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1659 1028
     * @throws UnexpectedValueException
1660
     */
1661 1028
    private function doRemove($entity, array &$visited)
1662
    {
1663
        $oid = spl_object_hash($entity);
1664
1665
        if (isset($visited[$oid])) {
1666
            return; // Prevent infinite recursion
1667 1028
        }
1668
1669
        $visited[$oid] = $entity; // mark visited
1670 1028
1671
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1672 234
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1673 2
        $this->cascadeRemove($entity, $visited);
1674
1675 234
        $class       = $this->em->getClassMetadata(get_class($entity));
1676
        $entityState = $this->getEntityState($entity);
1677 1028
1678 1027
        switch ($entityState) {
1679 1027
            case self::STATE_NEW:
1680
            case self::STATE_REMOVED:
1681 1
                // nothing to do
1682
                break;
1683 1
1684 1
            case self::STATE_MANAGED:
1685
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1686 1
1687 1
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1688
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1689
                }
1690
1691
                $this->scheduleForDelete($entity);
1692
                break;
1693
1694
            case self::STATE_DETACHED:
1695
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1696
            default:
1697 1028
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1698 1021
        }
1699
1700
    }
1701
1702
    /**
1703
     * Merges the state of the given detached entity into this UnitOfWork.
1704
     *
1705
     * @param object $entity
1706
     *
1707 64
     * @return object The managed copy of the entity.
1708
     *
1709 64
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1710
     *         attribute and the version check against the managed copy fails.
1711 64
     *
1712 64
     * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
1713
     */
1714
    public function merge($entity)
1715
    {
1716
        $visited = [];
1717
1718
        return $this->doMerge($entity, $visited);
1719
    }
1720
1721
    /**
1722
     * Executes a merge operation on an entity.
1723
     *
1724
     * @param object                   $entity
1725
     * @param array                    $visited
1726
     * @param null|object              $prevManagedCopy
1727
     * @param null|AssociationMetadata $association
1728 64
     *
1729
     * @return object The managed copy of the entity.
1730 64
     *
1731
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
1732 64
     *         attribute and the version check against the managed copy fails.
1733 1
     * @throws ORMInvalidArgumentException If the entity instance is NEW.
1734
     * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided
1735
     */
1736 64
    private function doMerge($entity, array &$visited, $prevManagedCopy = null, AssociationMetadata $association = null)
1737
    {
1738
        $oid = spl_object_hash($entity);
1739
1740 64
        if (isset($visited[$oid])) {
1741
            $managedCopy = $visited[$oid];
1742 64
1743 64
            if ($prevManagedCopy !== null) {
1744
                $this->updateAssociationWithMergedEntity($entity, $association, $prevManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $association defined by parameter $association on line 1736 can be null; however, Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
1745
            }
1746 64
1747 64
            return $managedCopy;
1748
        }
1749 2
1750
        $class     = $this->em->getClassMetadata(get_class($entity));
1751 64
        $persister = $this->getEntityPersister($class->getClassName());
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1752 64
1753
        // First we assume DETACHED, although it can still be NEW but we can avoid
1754 64
        // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1755 8
        // we need to fetch it from the db anyway in order to merge.
1756
        // MANAGED entities are ignored by the merge operation.
1757
        $managedCopy = $entity;
1758 64
1759 64
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1760
            // Try to look the entity up in the identity map.
1761
            $id = $persister->getIdentifier($entity);
1762
1763
            // If there is no ID, it is actually NEW.
1764
            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...
1765
                $managedCopy = $this->newInstance($class);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1766
1767 64
                $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1768
                $this->persistNew($class, $managedCopy);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1769
            } else {
1770
                $flatId      = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1771
                $managedCopy = $this->tryGetById($flatId, $class->getRootClassName());
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
Bug Compatibility introduced by
The expression $this->tryGetById($flatI...s->getRootClassName()); of type object|boolean adds the type boolean to the return on line 1823 which is incompatible with the return type documented by Doctrine\ORM\UnitOfWork::doMerge of type object.
Loading history...
1772
1773
                if ($managedCopy) {
1774
                    // We have the entity in-memory already, just make sure its not removed.
1775
                    if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
0 ignored issues
show
Bug introduced by
It seems like $managedCopy defined by $this->tryGetById($flatI...ss->getRootClassName()) on line 1771 can also be of type boolean; however, Doctrine\ORM\UnitOfWork::getEntityState() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1776
                        throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge");
0 ignored issues
show
Bug introduced by
It seems like $managedCopy defined by $this->tryGetById($flatI...ss->getRootClassName()) on line 1771 can also be of type boolean; however, Doctrine\ORM\ORMInvalidA...tion::entityIsRemoved() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1777
                    }
1778
                } else {
1779
                    // We need to fetch the managed copy in order to merge.
1780
                    $managedCopy = $this->em->find($class->getClassName(), $flatId);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1781 40
                }
1782
1783 40
                if ($managedCopy === null) {
1784
                    // If the identifier is ASSIGNED, it is NEW, otherwise an error
1785 40
                    // since the managed entity was not found.
1786
                    if (! $class->isIdentifierComposite()
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1787
                        && $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1788
                        && $class->getProperty($class->getSingleIdentifierFieldName())->hasValueGenerator()) {
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1789
                        throw EntityNotFoundException::fromClassNameAndIdentifier(
1790
                            $class->getClassName(),
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1791
                            $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id)
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1792
                        );
1793
                    }
1794
1795
                    $managedCopy = $this->newInstance($class);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1796
1797
                    $persister->setIdentifier($managedCopy, $id);
1798
1799
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1800
                    $this->persistNew($class, $managedCopy);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1801
                } else {
1802
                    $this->ensureVersionMatch($class, $entity, $managedCopy);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $managedCopy defined by $this->tryGetById($flatI...ss->getRootClassName()) on line 1771 can also be of type boolean; however, Doctrine\ORM\UnitOfWork::ensureVersionMatch() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1803 40
                    $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy defined by $this->tryGetById($flatI...ss->getRootClassName()) on line 1771 can also be of type boolean; however, Doctrine\ORM\UnitOfWork:...yStateIntoManagedCopy() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1804
                }
1805 40
            }
1806
1807 40
            $visited[$oid] = $managedCopy; // mark visited
1808 4
1809
            if ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_EXPLICIT) {
0 ignored issues
show
Bug introduced by
Accessing changeTrackingPolicy on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1810 4
                $this->scheduleForSynchronization($entity);
1811 4
            }
1812
        }
1813
1814 4
        if ($prevManagedCopy !== null) {
1815
            $this->updateAssociationWithMergedEntity($entity, $association, $prevManagedCopy, $managedCopy);
0 ignored issues
show
Bug introduced by
It seems like $association defined by parameter $association on line 1736 can be null; however, Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
Bug introduced by
It seems like $managedCopy defined by $this->tryGetById($flatI...ss->getRootClassName()) on line 1771 can also be of type boolean; however, Doctrine\ORM\UnitOfWork:...ationWithMergedEntity() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1816
        }
1817 40
1818
        // Mark the managed copy visited as well
1819
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1820
1821
        $this->cascadeMerge($entity, $managedCopy, $visited);
0 ignored issues
show
Bug introduced by
It seems like $managedCopy defined by $this->tryGetById($flatI...ss->getRootClassName()) on line 1771 can also be of type boolean; however, Doctrine\ORM\UnitOfWork::cascadeMerge() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1822
1823 40
        return $managedCopy;
1824
    }
1825 40
1826
    /**
1827 39
     * @param ClassMetadata $class
1828
     * @param object        $entity
1829
     * @param object        $managedCopy
1830 39
     *
1831 5
     * @return void
1832
     *
1833 5
     * @throws OptimisticLockException
1834
     */
1835 35
    private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy)
1836 3
    {
1837 35
        if (! ($class->isVersioned() && $this->isLoaded($managedCopy) && $this->isLoaded($entity))) {
1838
            return;
1839 35
        }
1840
1841 35
        $managedCopyVersion = $class->versionProperty->getValue($managedCopy);
1842
        $entityVersion      = $class->versionProperty->getValue($entity);
1843 14
1844 14
        // Throw exception if versions don't match.
1845
        if ($managedCopyVersion == $entityVersion) {
1846
            return;
1847
        }
1848 24
1849
        throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
1850
    }
1851 35
1852
    /**
1853
     * Tests if an entity is loaded - must either be a loaded proxy or not a proxy
1854 2
     *
1855 1
     * @param object $entity
1856 1
     *
1857 1
     * @return bool
1858
     */
1859
    private function isLoaded($entity)
1860
    {
1861 1
        return !($entity instanceof Proxy) || $entity->__isInitialized();
1862 1
    }
1863
1864 1
    /**
1865
     * Sets/adds associated managed copies into the previous entity's association field
1866
     *
1867
     * @param object              $entity
1868 38
     * @param AssociationMetadata $association
1869 4
     * @param object              $previousManagedCopy
1870 4
     * @param object              $managedCopy
1871
     *
1872
     * @return void
1873 4
     */
1874 1
    private function updateAssociationWithMergedEntity(
1875
        $entity,
1876
        AssociationMetadata $association,
1877
        $previousManagedCopy,
1878 37
        $managedCopy
1879
    )
1880 37
    {
1881 30
        if ($association instanceof ToOneAssociationMetadata) {
1882 4
            $association->setValue($previousManagedCopy, $managedCopy);
1883
1884
            return;
1885 30
        }
1886
1887
        /** @var array|Collection $value */
1888 37
        $value = $association->getValue($previousManagedCopy);
1889
1890
        $value[] = $managedCopy;
1891
1892
        if ($association instanceof OneToManyAssociationMetadata) {
1893 38
            $targetClass        = $this->em->getClassMetadata(get_class($entity));
1894 6
            $inverseAssociation = $targetClass->getProperty($association->getMappedBy());
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
1895
1896
            $inverseAssociation->setValue($managedCopy, $previousManagedCopy);
1897
        }
1898 38
    }
1899
1900 38
    /**
1901
     * Detaches an entity from the persistence management. It's persistence will
1902 38
     * no longer be managed by Doctrine.
1903
     *
1904
     * @param object $entity The entity to detach.
1905
     *
1906
     * @return void
1907
     */
1908
    public function detach($entity)
1909
    {
1910
        $visited = [];
1911
1912 38
        $this->doDetach($entity, $visited);
1913
    }
1914 38
1915
    /**
1916
     * Executes a detach operation on the given entity.
1917
     *
1918
     * @param object  $entity
1919
     * @param array   $visited
1920
     * @param boolean $noCascade if true, don't cascade detach operation.
1921
     *
1922
     * @return void
1923
     */
1924
    private function doDetach($entity, array &$visited, $noCascade = false)
1925
    {
1926
        $oid = spl_object_hash($entity);
1927 6
1928
        if (isset($visited[$oid])) {
1929 6
            return; // Prevent infinite recursion
1930 6
        }
1931
1932 6
        $visited[$oid] = $entity; // mark visited
1933 6
1934
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
1935 6
            case self::STATE_MANAGED:
1936
                if ($this->isInIdentityMap($entity)) {
1937
                    $this->removeFromIdentityMap($entity);
1938
                }
1939 1
1940 1
                unset(
1941
                    $this->entityInsertions[$oid],
1942 1
                    $this->entityUpdates[$oid],
1943 1
                    $this->entityDeletions[$oid],
1944
                    $this->entityIdentifiers[$oid],
1945 1
                    $this->entityStates[$oid],
1946
                    $this->originalEntityData[$oid]
1947 1
                );
1948
                break;
1949
            case self::STATE_NEW:
1950
            case self::STATE_DETACHED:
1951
                return;
1952
        }
1953
1954
        if ( ! $noCascade) {
1955
            $this->cascadeDetach($entity, $visited);
1956
        }
1957 12
    }
1958
1959 12
    /**
1960
     * Refreshes the state of the given entity from the database, overwriting
1961 12
     * any local, unpersisted changes.
1962 12
     *
1963
     * @param object $entity The entity to refresh.
1964
     *
1965
     * @return void
1966
     *
1967
     * @throws InvalidArgumentException If the entity is not MANAGED.
1968
     */
1969
    public function refresh($entity)
1970
    {
1971
        $visited = [];
1972
1973 15
        $this->doRefresh($entity, $visited);
1974
    }
1975 15
1976
    /**
1977 15
     * Executes a refresh operation on an entity.
1978
     *
1979
     * @param object $entity  The entity to refresh.
1980
     * @param array  $visited The already visited entities during cascades.
1981 15
     *
1982
     * @return void
1983 15
     *
1984 15
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
1985 13
     */
1986 12
    private function doRefresh($entity, array &$visited)
1987
    {
1988
        $oid = spl_object_hash($entity);
1989
1990 13
        if (isset($visited[$oid])) {
1991 13
            return; // Prevent infinite recursion
1992 13
        }
1993 13
1994 13
        $visited[$oid] = $entity; // mark visited
1995 13
1996
        $class = $this->em->getClassMetadata(get_class($entity));
1997 13
1998 3
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1999 3
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2000 3
        }
2001
2002
        $this->getEntityPersister($class->getClassName())->refresh(
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2003 13
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2004 13
            $entity
2005
        );
2006 13
2007
        $this->cascadeRefresh($entity, $visited);
2008
    }
2009
2010
    /**
2011
     * Cascades a refresh operation to associated entities.
2012
     *
2013
     * @param object $entity
2014
     * @param array  $visited
2015
     *
2016
     * @return void
2017
     */
2018 16
    private function cascadeRefresh($entity, array &$visited)
2019
    {
2020 16
        $class = $this->em->getClassMetadata(get_class($entity));
2021
2022 16
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2023 16
            if (! ($association instanceof AssociationMetadata && in_array('refresh', $association->getCascade()))) {
2024
                continue;
2025
            }
2026
2027
            $relatedEntities = $association->getValue($entity);
2028
2029
            switch (true) {
2030
                case ($relatedEntities instanceof PersistentCollection):
2031
                    // Unwrap so that foreach() does not initialize
2032
                    $relatedEntities = $relatedEntities->unwrap();
2033
                    // break; is commented intentionally!
2034
2035 16
                case ($relatedEntities instanceof Collection):
2036
                case (is_array($relatedEntities)):
2037 16
                    foreach ($relatedEntities as $relatedEntity) {
2038
                        $this->doRefresh($relatedEntity, $visited);
2039 16
                    }
2040
                    break;
2041
2042
                case ($relatedEntities !== null):
2043 16
                    $this->doRefresh($relatedEntities, $visited);
2044
                    break;
2045 16
2046
                default:
2047 16
                    // Do nothing
2048
            }
2049
        }
2050
    }
2051 16
2052 16
    /**
2053
     * Cascades a detach operation to associated entities.
2054
     *
2055
     * @param object $entity
2056 16
     * @param array  $visited
2057 16
     *
2058
     * @return void
2059
     */
2060
    private function cascadeDetach($entity, array &$visited)
2061
    {
2062
        $class = $this->em->getClassMetadata(get_class($entity));
2063
2064
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2065
            if (! ($association instanceof AssociationMetadata && in_array('detach', $association->getCascade()))) {
2066
                continue;
2067 16
            }
2068
2069 16
            $relatedEntities = $association->getValue($entity);
2070
2071 16
            switch (true) {
2072 16
                case ($relatedEntities instanceof PersistentCollection):
2073
                    // Unwrap so that foreach() does not initialize
2074
                    $relatedEntities = $relatedEntities->unwrap();
2075
                    // break; is commented intentionally!
2076 16
2077 5
                case ($relatedEntities instanceof Collection):
2078
                case (is_array($relatedEntities)):
2079
                    foreach ($relatedEntities as $relatedEntity) {
2080 5
                        $this->doDetach($relatedEntity, $visited);
2081
                    }
2082 5
                    break;
2083
2084
                case ($relatedEntities !== null):
2085
                    $this->doDetach($relatedEntities, $visited);
2086
                    break;
2087 5
2088
                default:
2089
                    // Do nothing
2090 5
            }
2091
        }
2092
    }
2093
2094
    /**
2095
     * Cascades a merge operation to associated entities.
2096 5
     *
2097
     * @param object $entity
2098
     * @param object $managedCopy
2099
     * @param array  $visited
2100 16
     *
2101
     * @return void
2102
     */
2103
    private function cascadeMerge($entity, $managedCopy, array &$visited)
2104
    {
2105
        $class = $this->em->getClassMetadata(get_class($entity));
2106
2107
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2108
            if (! ($association instanceof AssociationMetadata && in_array('merge', $association->getCascade()))) {
2109
                continue;
2110 13
            }
2111
2112 13
            /** @var AssociationMetadata $association */
2113
            $relatedEntities = $association->getValue($entity);
2114 13
2115 13
            if ($relatedEntities instanceof Collection) {
2116
                if ($relatedEntities === $association->getValue($managedCopy)) {
2117
                    continue;
2118
                }
2119 13
2120 3
                if ($relatedEntities instanceof PersistentCollection) {
2121
                    // Unwrap so that foreach() does not initialize
2122
                    $relatedEntities = $relatedEntities->unwrap();
2123 3
                }
2124
2125 2
                foreach ($relatedEntities as $relatedEntity) {
2126
                    $this->doMerge($relatedEntity, $visited, $managedCopy, $association);
2127
                }
2128
            } else if ($relatedEntities !== null) {
2129
                $this->doMerge($relatedEntities, $visited, $managedCopy, $association);
2130 3
            }
2131 1
        }
2132
    }
2133 3
2134
    /**
2135
     * Cascades the save operation to associated entities.
2136
     *
2137
     * @param object $entity
2138
     * @param array  $visited
2139 3
     *
2140
     * @return void
2141
     */
2142
    private function cascadePersist($entity, array &$visited)
2143 13
    {
2144
        $class = $this->em->getClassMetadata(get_class($entity));
2145
2146
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2147
            if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade()))) {
2148
                continue;
2149
            }
2150
2151
            /** @var AssociationMetadata $association */
2152
            $relatedEntities = $association->getValue($entity);
2153
            $targetEntity    = $association->getTargetEntity();
2154 38
2155
            switch (true) {
2156 38
                case ($relatedEntities instanceof PersistentCollection):
2157
                    // Unwrap so that foreach() does not initialize
2158 38
                    $relatedEntities = $relatedEntities->unwrap();
2159 38
                    // break; is commented intentionally!
2160
2161
                case ($relatedEntities instanceof Collection):
2162
                case (is_array($relatedEntities)):
2163 38
                    if (! ($association instanceof ToManyAssociationMetadata)) {
2164 15
                        throw ORMInvalidArgumentException::invalidAssociation(
2165
                            $this->em->getClassMetadata($targetEntity),
0 ignored issues
show
Documentation introduced by
$this->em->getClassMetadata($targetEntity) is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2166 15
                            $association,
2167 9
                            $relatedEntities
2168 1
                        );
2169
                    }
2170
2171 8
                    foreach ($relatedEntities as $relatedEntity) {
2172
                        $this->doPersist($relatedEntity, $visited);
2173 5
                    }
2174
2175
                    break;
2176 8
2177 8
                case ($relatedEntities !== null):
2178
                    if (! $relatedEntities instanceof $targetEntity) {
2179 7
                        throw ORMInvalidArgumentException::invalidAssociation(
2180 14
                            $this->em->getClassMetadata($targetEntity),
0 ignored issues
show
Documentation introduced by
$this->em->getClassMetadata($targetEntity) is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2181
                            $association,
2182
                            $relatedEntities
2183 38
                        );
2184
                    }
2185
2186
                    $this->doPersist($relatedEntities, $visited);
2187
                    break;
2188
2189
                default:
2190
                    // Do nothing
2191
            }
2192
        }
2193 1028
    }
2194
2195 1028
    /**
2196
     * Cascades the delete operation to associated entities.
2197 1028
     *
2198 1028
     * @param object $entity
2199
     * @param array  $visited
2200
     *
2201
     * @return void
2202 1028
     */
2203 650
    private function cascadeRemove($entity, array &$visited)
2204
    {
2205
        $entitiesToCascade = [];
2206 650
        $class             = $this->em->getClassMetadata(get_class($entity));
2207
2208 21
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2209
            if (! ($association instanceof AssociationMetadata && in_array('remove', $association->getCascade()))) {
2210
                continue;
2211
            }
2212 589
2213 554
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2214 3
                $entity->__load();
2215 3
            }
2216
2217
            $relatedEntities = $association->getValue($entity);
2218
2219
            switch (true) {
2220
                case ($relatedEntities instanceof Collection):
2221 551
                case (is_array($relatedEntities)):
2222 282
                    // If its a PersistentCollection initialization is intended! No unwrap!
2223
                    foreach ($relatedEntities as $relatedEntity) {
2224
                        $entitiesToCascade[] = $relatedEntity;
2225 551
                    }
2226
                    break;
2227 579
2228 246
                case ($relatedEntities !== null):
2229 4
                    $entitiesToCascade[] = $relatedEntities;
2230 4
                    break;
2231
2232
                default:
2233
                    // Do nothing
2234
            }
2235
        }
2236 242
2237 242
        foreach ($entitiesToCascade as $relatedEntity) {
2238
            $this->doRemove($relatedEntity, $visited);
2239 644
        }
2240
    }
2241
2242
    /**
2243 1021
     * Acquire a lock on the given entity.
2244
     *
2245
     * @param object $entity
2246
     * @param int    $lockMode
2247
     * @param int    $lockVersion
2248
     *
2249
     * @return void
2250
     *
2251
     * @throws ORMInvalidArgumentException
2252
     * @throws TransactionRequiredException
2253 64
     * @throws OptimisticLockException
2254
     */
2255 64
    public function lock($entity, $lockMode, $lockVersion = null)
2256
    {
2257 64
        if ($entity === null) {
2258 64
            throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock().");
2259
        }
2260
2261
        if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2262 64
            throw ORMInvalidArgumentException::entityNotManaged($entity);
2263
        }
2264 64
2265 26
        $class = $this->em->getClassMetadata(get_class($entity));
2266 6
2267
        switch (true) {
2268
            case LockMode::OPTIMISTIC === $lockMode:
2269 26
                if ( ! $class->isVersioned()) {
0 ignored issues
show
Bug introduced by
The method isVersioned() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2270
                    throw OptimisticLockException::notVersioned($class->getClassName());
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2271
                }
2272 26
2273 19
                if ($lockVersion === null) {
2274
                    return;
2275 20
                }
2276 10
2277
                if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2278 20
                    $entity->__load();
2279
                }
2280 19
2281 7
                $entityVersion = $class->versionProperty->getValue($entity);
0 ignored issues
show
Bug introduced by
Accessing versionProperty on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2282 7
2283
                if ($entityVersion != $lockVersion) {
2284 26
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
2285
                }
2286
2287
                break;
2288
2289 64
            case LockMode::NONE === $lockMode:
2290 16
            case LockMode::PESSIMISTIC_READ === $lockMode:
2291
            case LockMode::PESSIMISTIC_WRITE === $lockMode:
2292 64
                if (!$this->em->getConnection()->isTransactionActive()) {
2293
                    throw TransactionRequiredException::transactionRequired();
2294
                }
2295
2296
                $oid = spl_object_hash($entity);
2297
2298
                $this->getEntityPersister($class->getClassName())->lock(
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2299
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2300
                    $lockMode
2301
                );
2302
                break;
2303
2304
            default:
2305
                // Do nothing
2306
        }
2307 11
    }
2308
2309 11
    /**
2310 1
     * Clears the UnitOfWork.
2311
     *
2312
     * @return void
2313 10
     */
2314 1
    public function clear()
2315
    {
2316
        $this->entityPersisters =
2317 9
        $this->collectionPersisters =
2318
        $this->eagerLoadingEntities =
2319
        $this->identityMap =
2320 9
        $this->entityIdentifiers =
2321 6
        $this->originalEntityData =
2322 2
        $this->entityChangeSets =
2323
        $this->entityStates =
2324
        $this->scheduledForSynchronization =
2325 4
        $this->entityInsertions =
2326
        $this->entityUpdates =
2327
        $this->entityDeletions =
2328
        $this->collectionDeletions =
2329 4
        $this->collectionUpdates =
2330 1
        $this->extraUpdates =
2331
        $this->readOnlyObjects =
2332
        $this->visitedCollections =
2333 4
        $this->orphanRemovals = [];
2334
2335 4
        if ($this->eventManager->hasListeners(Events::onClear)) {
2336 2
            $this->eventManager->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em));
2337
        }
2338
    }
2339 2
2340
    /**
2341 3
     * INTERNAL:
2342 3
     * Schedules an orphaned entity for removal. The remove() operation will be
2343 1
     * invoked on that entity at the beginning of the next commit of this
2344 3
     * UnitOfWork.
2345 2
     *
2346
     * @ignore
2347
     *
2348 1
     * @param object $entity
2349
     *
2350 1
     * @return void
2351 1
     */
2352
    public function scheduleOrphanRemoval($entity)
2353
    {
2354 1
        $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2355
    }
2356
2357
    /**
2358
     * INTERNAL:
2359 3
     * Cancels a previously scheduled orphan removal.
2360
     *
2361
     * @ignore
2362
     *
2363
     * @param object $entity
2364
     *
2365
     * @return void
2366 1008
     */
2367
    public function cancelOrphanRemoval($entity)
2368 1008
    {
2369
        unset($this->orphanRemovals[spl_object_hash($entity)]);
2370
    }
2371
2372
    /**
2373
     * INTERNAL:
2374
     * Schedules a complete collection for removal when this UnitOfWork commits.
2375
     *
2376
     * @param PersistentCollection $coll
2377
     *
2378 1218
     * @return void
2379
     */
2380 1218
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2381 1217
    {
2382 1217
        $coid = spl_object_hash($coll);
2383 1217
2384 1217
        // TODO: if $coll is already scheduled for recreation ... what to do?
2385 1217
        // Just remove $coll from the scheduled recreations?
2386 1217
        unset($this->collectionUpdates[$coid]);
2387 1217
2388 1217
        $this->collectionDeletions[$coid] = $coll;
2389 1217
    }
2390 1217
2391 1217
    /**
2392 1217
     * @param PersistentCollection $coll
2393 1217
     *
2394 1217
     * @return bool
2395 1217
     */
2396
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2397 3
    {
2398 3
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2399
    }
2400
2401 1218
    /**
2402 7
     * INTERNAL:
2403
     * Creates a new instance of the mapped class, without invoking the constructor.
2404 1218
     * This is only meant to be used internally, and should not be consumed by end users.
2405
     *
2406
     * @ignore
2407
     *
2408
     * @param ClassMetadata $class
2409
     *
2410
     * @return EntityManagerAware|object
2411
     */
2412
    public function newInstance(ClassMetadata $class)
2413
    {
2414
        $entity = $this->instantiator->instantiate($class->getClassName());
2415
2416
        if ($entity instanceof EntityManagerAware) {
2417
            $entity->injectEntityManager($this->em, $class);
2418 17
        }
2419
2420 17
        return $entity;
2421 17
    }
2422
2423
    /**
2424
     * INTERNAL:
2425
     * Creates an entity. Used for reconstitution of persistent entities.
2426
     *
2427
     * Internal note: Highly performance-sensitive method.
2428
     *
2429
     * @ignore
2430
     *
2431
     * @param string $className The name of the entity class.
2432
     * @param array  $data      The data for the entity.
2433 112
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the entity.
2434
     *
2435 112
     * @return object The managed entity instance.
2436 112
     *
2437
     * @todo Rename: getOrCreateEntity
2438
     */
2439
    public function createEntity($className, array $data, &$hints = [])
2440
    {
2441
        $class  = $this->em->getClassMetadata($className);
2442
        $id     = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2443
        $idHash = implode(' ', $id);
2444
        //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]);
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2445
2446 13
        if (isset($this->identityMap[$class->getRootClassName()][$idHash])) {
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2447
            $entity = $this->identityMap[$class->getRootClassName()][$idHash];
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2448 13
            $oid = spl_object_hash($entity);
2449
2450
            if (
2451
                isset($hints[Query::HINT_REFRESH])
2452 13
                && isset($hints[Query::HINT_REFRESH_ENTITY])
2453
                && ($unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]) !== $entity
2454 13
                && $unmanagedProxy instanceof Proxy
2455 13
                && $this->isIdentifierEquals($unmanagedProxy, $entity)
2456
            ) {
2457
                // DDC-1238 - we have a managed instance, but it isn't the provided one.
2458
                // Therefore we clear its identifier. Also, we must re-fetch metadata since the
2459
                // refreshed object may be anything
2460
                foreach ($class->identifier as $fieldName) {
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2461
                    $property = $class->getProperty($fieldName);
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2462
2463
                    $property->setValue($unmanagedProxy, null);
2464
                }
2465
2466
                return $unmanagedProxy;
2467
            }
2468
2469
            if ($entity instanceof Proxy && ! $entity->__isInitialized()) {
2470
                $entity->__setInitialized(true);
2471
2472 668
                $overrideLocalValues = true;
2473
2474 668
                if ($entity instanceof NotifyPropertyChanged) {
2475
                    $entity->addPropertyChangedListener($this);
2476 668
                }
2477 4
            } else {
2478
                $overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
2479
2480 668
                // If only a specific entity is set to refresh, check that it's the one
2481
                if (isset($hints[Query::HINT_REFRESH_ENTITY])) {
2482
                    $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity;
2483
                }
2484
            }
2485
2486
            if ($overrideLocalValues) {
2487
                // inject EntityManager upon refresh.
2488
                if ($entity instanceof EntityManagerAware) {
2489
                    $entity->injectEntityManager($this->em, $class);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2490
                }
2491
2492
                $this->originalEntityData[$oid] = $data;
2493
            }
2494
        } else {
2495
            $entity = $this->newInstance($class);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2496
            $oid    = spl_object_hash($entity);
2497
2498
            $this->entityIdentifiers[$oid]  = $id;
2499 806
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2500
            $this->originalEntityData[$oid] = $data;
2501 806
2502
            $this->identityMap[$class->getRootClassName()][$idHash] = $entity;
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2503
2504 806
            if ($entity instanceof NotifyPropertyChanged) {
2505 806
                $entity->addPropertyChangedListener($this);
2506
            }
2507 806
2508 310
            $overrideLocalValues = true;
2509 310
        }
2510
2511
        if ( ! $overrideLocalValues) {
2512 310
            return $entity;
2513 310
        }
2514 310
2515 310
        foreach ($data as $field => $value) {
2516 310
            $property = $class->getProperty($field);
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2517
2518
            if ($property instanceof FieldMetadata) {
2519
                $property->setValue($entity, $value);
2520
            }
2521
        }
2522 2
2523 2
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2524
        unset($this->eagerLoadingEntities[$class->getRootClassName()][$idHash]);
2525
2526 2
        if (isset($this->eagerLoadingEntities[$class->getRootClassName()]) && ! $this->eagerLoadingEntities[$class->getRootClassName()]) {
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2527
            unset($this->eagerLoadingEntities[$class->getRootClassName()]);
2528
        }
2529 308
2530 21
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2531
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2532 21
            return $entity;
2533
        }
2534 21
2535 21
        foreach ($class->getDeclaredPropertiesIterator() as $field => $association) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2536
            if (! ($association instanceof AssociationMetadata)) {
2537
                continue;
2538 289
            }
2539
2540
            // Check if the association is not among the fetch-joined associations already.
2541 289
            if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2542 71
                continue;
2543
            }
2544
2545
            $targetEntity = $association->getTargetEntity();
2546 308
            $targetClass  = $this->em->getClassMetadata($targetEntity);
2547
2548 111
            if ($association instanceof ToManyAssociationMetadata) {
2549 3
                // Ignore if its a cached collection
2550
                if (isset($hints[Query::HINT_CACHE_ENABLED]) &&
2551
                    $association->getValue($entity) instanceof PersistentCollection) {
2552 308
                    continue;
2553
                }
2554
2555 665
                $hasDataField = isset($data[$field]);
2556 665
2557
                // use the given collection
2558 665
                if ($hasDataField && $data[$field] instanceof PersistentCollection) {
2559 665
                    $data[$field]->setOwner($entity, $association);
2560 665
2561
                    $association->setValue($entity, $data[$field]);
2562 665
2563
                    $this->originalEntityData[$oid][$field] = $data[$field];
2564 665
2565 2
                    continue;
2566
                }
2567
2568 665
                // Inject collection
2569
                $pColl = $association->wrap($entity, $hasDataField ? $data[$field] : [], $this->em);
2570
2571 805
                $pColl->setInitialized($hasDataField);
2572 219
2573
                $association->setValue($entity, $pColl);
2574
2575 702
                if ($association->getFetchMode() === FetchMode::EAGER) {
2576 702
                    $this->loadCollection($pColl);
2577 702
                    $pColl->takeSnapshot();
2578
                }
2579
2580
                $this->originalEntityData[$oid][$field] = $pColl;
2581
2582 702
                continue;
2583
            }
2584 702
2585
            if (! $association->isOwningSide()) {
2586
                // use the given entity association
2587
                if (isset($data[$field]) && is_object($data[$field]) &&
2588
                    isset($this->entityStates[spl_object_hash($data[$field])])) {
2589 702
                    $inverseAssociation = $targetClass->getProperty($association->getMappedBy());
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2590 33
2591
                    $association->setValue($entity, $data[$field]);
2592
                    $inverseAssociation->setValue($data[$field], $entity);
2593 669
2594
                    $this->originalEntityData[$oid][$field] = $data[$field];
2595 586
2596 260
                    continue;
2597
                }
2598
2599 564
                // Inverse side of x-to-one can never be lazy
2600
                $persister = $this->getEntityPersister($targetEntity);
2601
2602 564
                $association->setValue($entity, $persister->loadToOneEntity($association, $entity));
0 ignored issues
show
Compatibility introduced by
$association of type object<Doctrine\ORM\Mapping\AssociationMetadata> is not a sub-type of object<Doctrine\ORM\Mapp...OneAssociationMetadata>. It seems like you assume a child class of the class Doctrine\ORM\Mapping\AssociationMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
2603 485
2604
                continue;
2605
            }
2606 64
2607
            // use the entity association
2608 2
            if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
2609
                $association->setValue($entity, $data[$field]);
2610 2
2611 2
                $this->originalEntityData[$oid][$field] = $data[$field];
2612
2613 2
                continue;
2614
            }
2615
2616
            $associatedId = [];
2617 62
2618
            // TODO: Is this even computed right in all cases of composite keys?
2619 62
            foreach ($association->getJoinColumns() as $joinColumn) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\ORM\Mapping\AssociationMetadata as the method getJoinColumns() does only exist in the following sub-classes of Doctrine\ORM\Mapping\AssociationMetadata: Doctrine\ORM\Mapping\ManyToOneAssociationMetadata, Doctrine\ORM\Mapping\OneToOneAssociationMetadata, Doctrine\ORM\Mapping\ToOneAssociationMetadata. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
2620
                /** @var JoinColumnMetadata $joinColumn */
2621
                $joinColumnName = $joinColumn->getColumnName();
2622
                $joinColumnValue = isset($data[$joinColumnName]) ? $data[$joinColumnName] : null;
2623 485
                $targetField     = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2624 38
2625 38
                if ($joinColumnValue === null && in_array($targetField, $targetClass->identifier, true)) {
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2626
                    // the missing key is part of target's entity primary key
2627 38
                    $associatedId = [];
2628
2629
                    continue;
2630 478
                }
2631
2632
                $associatedId[$targetField] = $joinColumnValue;
2633 478
            }
2634 478
2635 478
            if (! $associatedId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $associatedId 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...
2636
                // Foreign key is NULL
2637 478
                $association->setValue($entity, null);
2638
                $this->originalEntityData[$oid][$field] = null;
2639 287
2640
                continue;
2641 287
            }
2642
2643
            // @todo guilhermeblanco Can we remove the need of this somehow?
2644 284
            if (!isset($hints['fetchMode'][$class->getClassName()][$field])) {
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2645
                $hints['fetchMode'][$class->getClassName()][$field] = $association->getFetchMode();
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2646
            }
2647 478
2648
            // Foreign key is set
2649 287
            // Check identity map first
2650 287
            // FIXME: Can break easily with composite keys if join column values are in
2651
            //        wrong order. The correct order is the one in ClassMetadata#identifier.
2652 287
            $relatedIdHash = implode(' ', $associatedId);
2653
2654
            switch (true) {
2655 284
                case (isset($this->identityMap[$targetClass->getRootClassName()][$relatedIdHash])):
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2656 281
                    $newValue = $this->identityMap[$targetClass->getRootClassName()][$relatedIdHash];
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2657
2658
                    // If this is an uninitialized proxy, we are deferring eager loads,
2659
                    // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2660
                    // then we can append this entity for eager loading!
2661
                    if (!$targetClass->isIdentifierComposite() &&
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2662
                        $newValue instanceof Proxy &&
2663 284
                        isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2664
                        $hints['fetchMode'][$class->getClassName()][$field] === FetchMode::EAGER &&
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2665
                        ! $newValue->__isInitialized()
2666 284
                    ) {
2667 166
2668
                        $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($associatedId);
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2669
                    }
2670
2671
                    break;
2672 166
2673 166
                case ($targetClass->getSubClasses()):
0 ignored issues
show
Bug introduced by
The method getSubClasses() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2674 166
                    // If it might be a subtype, it can not be lazy. There isn't even
2675 166
                    // a way to solve this with deferred eager loading, which means putting
2676 166
                    // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2677
                    $persister = $this->getEntityPersister($targetEntity);
2678
                    $newValue  = $persister->loadToOneEntity($association, $entity, $associatedId);
0 ignored issues
show
Compatibility introduced by
$association of type object<Doctrine\ORM\Mapping\AssociationMetadata> is not a sub-type of object<Doctrine\ORM\Mapp...OneAssociationMetadata>. It seems like you assume a child class of the class Doctrine\ORM\Mapping\AssociationMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
2679
                    break;
2680
2681 166
                default:
2682
                    // Proxies do not carry any kind of original entity data until they're fully loaded/initialized
2683 192
                    $managedData = [];
2684
2685
                    switch (true) {
2686
                        // We are negating the condition here. Other cases will assume it is valid!
2687 30
                        case ($hints['fetchMode'][$class->getClassName()][$field] !== FetchMode::EAGER):
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2688 30
                            $newValue = $this->em->getProxyFactory()->getProxy($targetEntity, $associatedId);
2689
                            break;
2690
2691
                        // Deferred eager load only works for single identifier classes
2692
                        case (isset($hints[self::HINT_DEFEREAGERLOAD]) && !$targetClass->isIdentifierComposite()):
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2693 163
                            // TODO: Is there a faster approach?
2694 157
                            $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($associatedId);
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2695 157
2696
                            $newValue = $this->em->getProxyFactory()->getProxy($targetEntity, $associatedId);
2697
                            break;
2698 6
2699
                        default:
2700 6
                            // TODO: This is very imperformant, ignore it?
2701
                            $newValue    = $this->em->find($targetEntity, $associatedId);
2702 6
                            // Needed to re-assign original entity data for freshly loaded entity
2703 6
                            $managedData = $this->originalEntityData[spl_object_hash($newValue)];
2704
                            break;
2705
                    }
2706
2707
                    $this->registerManaged($newValue, $associatedId, $managedData);
2708
2709
                    break;
2710
            }
2711
2712 163
            $this->originalEntityData[$oid][$field] = $newValue;
2713 163
            $association->setValue($entity, $newValue);
2714 163
2715
            if ($association->getInversedBy() && $association instanceof OneToOneAssociationMetadata) {
2716
                $inverseAssociation = $targetClass->getProperty($association->getInversedBy());
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2717 163
2718 163
                $inverseAssociation->setValue($newValue, $entity);
2719
            }
2720
        }
2721
2722 163
        if ($overrideLocalValues) {
2723
            // defer invoking of postLoad event to hydration complete step
2724 163
            $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2725
        }
2726
2727 284
        return $entity;
2728 284
    }
2729
2730 284
    /**
2731 49
     * @return void
2732 49
     */
2733
    public function triggerEagerLoads()
2734
    {
2735 284
        if ( ! $this->eagerLoadingEntities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->eagerLoadingEntities of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2736
            return;
2737
        }
2738
2739 486
        // avoid infinite recursion
2740
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2741
        $this->eagerLoadingEntities = [];
2742
2743
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2744 486
            if ( ! $ids) {
2745
                continue;
2746 3
            }
2747
2748 3
            $class = $this->em->getClassMetadata($entityName);
2749 3
2750
            $this->getEntityPersister($entityName)->loadAll(
2751 3
                array_combine($class->identifier, [array_values($ids)])
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2752
            );
2753
        }
2754
    }
2755 486
2756 486
    /**
2757 486
     * Initializes (loads) an uninitialized persistent collection of an entity.
2758
     *
2759 486
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2760 486
     *
2761
     * @return void
2762 486
     *
2763 4
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2764 4
     */
2765
    public function loadCollection(PersistentCollection $collection)
2766
    {
2767 486
        $association = $collection->getMapping();
2768 564
        $persister   = $this->getEntityPersister($association->getTargetEntity());
2769
2770
        if ($association instanceof OneToManyAssociationMetadata) {
2771
            $persister->loadOneToManyCollection($association, $collection->getOwner(), $collection);
2772 669
        } else {
2773
            $persister->loadManyToManyCollection($association, $collection->getOwner(), $collection);
0 ignored issues
show
Compatibility introduced by
$association of type object<Doctrine\ORM\Mapp...anyAssociationMetadata> is not a sub-type of object<Doctrine\ORM\Mapp...anyAssociationMetadata>. It seems like you assume a child class of the class Doctrine\ORM\Mapping\ToManyAssociationMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
2774 669
        }
2775
2776
        $collection->setInitialized(true);
2777 669
    }
2778
2779
    /**
2780
     * Gets the identity map of the UnitOfWork.
2781
     *
2782
     * @return array
2783 861
     */
2784
    public function getIdentityMap()
2785 861
    {
2786 861
        return $this->identityMap;
2787
    }
2788
2789
    /**
2790 6
     * Gets the original data of an entity. The original data is the data that was
2791 6
     * present at the time the entity was reconstituted from the database.
2792
     *
2793 6
     * @param object $entity
2794 6
     *
2795
     * @return array
2796
     */
2797
    public function getOriginalEntityData($entity)
2798 6
    {
2799
        $oid = spl_object_hash($entity);
2800 6
2801 6
        return isset($this->originalEntityData[$oid])
2802
            ? $this->originalEntityData[$oid]
2803
            : [];
2804 6
    }
2805
2806
    /**
2807
     * @ignore
2808
     *
2809
     * @param object $entity
2810
     * @param array  $data
2811
     *
2812
     * @return void
2813
     */
2814
    public function setOriginalEntityData($entity, array $data)
2815 142
    {
2816
        $this->originalEntityData[spl_object_hash($entity)] = $data;
2817 142
    }
2818 142
2819
    /**
2820 142
     * INTERNAL:
2821 142
     * Sets a property value of the original data array of an entity.
2822 76
     *
2823 76
     * @ignore
2824
     *
2825 80
     * @param string $oid
2826 80
     * @param string $property
2827 80
     * @param mixed  $value
2828
     *
2829
     * @return void
2830 142
     */
2831 142
    public function setOriginalEntityProperty($oid, $property, $value)
2832
    {
2833
        $this->originalEntityData[$oid][$property] = $value;
2834
    }
2835
2836
    /**
2837
     * Gets the identifier of an entity.
2838 2
     * The returned value is always an array of identifier values. If the entity
2839
     * has a composite identifier then the identifier values are in the same
2840 2
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2841
     *
2842
     * @param object $entity
2843
     *
2844
     * @return array The identifier values.
2845
     */
2846
    public function getEntityIdentifier($entity)
2847
    {
2848
        return $this->entityIdentifiers[spl_object_hash($entity)];
2849
    }
2850
2851 115
    /**
2852
     * Processes an entity instance to extract their identifier values.
2853 115
     *
2854
     * @param object $entity The entity instance.
2855 115
     *
2856 112
     * @return mixed A scalar value.
2857 115
     *
2858
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
2859
     */
2860
    public function getSingleIdentifierValue($entity)
2861
    {
2862
        $class     = $this->em->getClassMetadata(get_class($entity));
2863
        $persister = $this->getEntityPersister($class->getClassName());
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2864
2865
        if ($class->isIdentifierComposite()) {
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
2866
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2867
        }
2868
2869
        $values = $this->isInIdentityMap($entity)
2870
            ? $this->getEntityIdentifier($entity)
2871
            : $persister->getIdentifier($entity);
2872
2873
        return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null;
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2874
    }
2875
2876
    /**
2877
     * Tries to find an entity with the given identifier in the identity map of
2878
     * this UnitOfWork.
2879
     *
2880
     * @param mixed  $id            The entity identifier to look for.
2881
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
2882
     *
2883
     * @return object|bool Returns the entity with the specified identifier if it exists in
2884
     *                     this UnitOfWork, FALSE otherwise.
2885 313
     */
2886
    public function tryGetById($id, $rootClassName)
2887 313
    {
2888 313
        $idHash = implode(' ', (array) $id);
2889
2890
        return isset($this->identityMap[$rootClassName][$idHash])
2891
            ? $this->identityMap[$rootClassName][$idHash]
2892
            : false;
2893
    }
2894
2895
    /**
2896
     * Schedules an entity for dirty-checking at commit-time.
2897
     *
2898
     * @param object $entity The entity to schedule for dirty-checking.
2899
     *
2900 842
     * @return void
2901
     */
2902 842
    public function scheduleForSynchronization($entity)
2903
    {
2904
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->getRootClassName();
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2905
2906
        $this->scheduledForSynchronization[$rootClassName][spl_object_hash($entity)] = $entity;
2907
    }
2908
2909
    /**
2910
     * Checks whether the UnitOfWork has any pending insertions.
2911
     *
2912
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2913
     */
2914 126
    public function hasPendingInsertions()
2915
    {
2916 126
        return ! empty($this->entityInsertions);
2917
    }
2918 126
2919
    /**
2920
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2921
     * number of entities in the identity map.
2922 126
     *
2923 113
     * @return integer
2924 126
     */
2925
    public function size()
2926 126
    {
2927
        $countArray = array_map('count', $this->identityMap);
2928
2929
        return array_sum($countArray);
2930
    }
2931
2932
    /**
2933
     * Gets the EntityPersister for an Entity.
2934
     *
2935
     * @param string $entityName The name of the Entity.
2936
     *
2937
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
2938
     */
2939 522
    public function getEntityPersister($entityName)
2940
    {
2941 522
        if (isset($this->entityPersisters[$entityName])) {
2942
            return $this->entityPersisters[$entityName];
2943 522
        }
2944 79
2945 522
        $class = $this->em->getClassMetadata($entityName);
2946
2947
        switch (true) {
2948
            case ($class->inheritanceType === InheritanceType::NONE):
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2949
                $persister = new BasicEntityPersister($this->em, $class);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2950
                break;
2951
2952
            case ($class->inheritanceType === InheritanceType::SINGLE_TABLE):
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2953
                $persister = new SingleTablePersister($this->em, $class);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2954
                break;
2955
2956
            case ($class->inheritanceType === InheritanceType::JOINED):
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2957 5
                $persister = new JoinedSubclassPersister($this->em, $class);
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2958
                break;
2959 5
2960
            default:
2961 5
                throw new \RuntimeException('No persister found for entity.');
2962 5
        }
2963
2964
        if ($this->hasCache && $class->getCache()) {
0 ignored issues
show
Bug introduced by
The method getCache() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
2965
            $persister = $this->em->getConfiguration()
2966
                ->getSecondLevelCacheConfiguration()
2967
                ->getCacheFactory()
2968
                ->buildCachedEntityPersister($this->em, $persister, $class);
2969
        }
2970
2971
        $this->entityPersisters[$entityName] = $persister;
2972
2973
        return $this->entityPersisters[$entityName];
2974
    }
2975
2976
    /**
2977
     * Gets a collection persister for a collection-valued association.
2978
     *
2979
     * @param ToManyAssociationMetadata $association
2980 1
     *
2981
     * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister
2982 1
     */
2983
    public function getCollectionPersister(ToManyAssociationMetadata $association)
2984 1
    {
2985
        $role = $association->getCache()
2986
            ? sprintf('%s::%s', $association->getSourceEntity(), $association->getName())
2987
            : get_class($association);
2988
2989
        if (isset($this->collectionPersisters[$role])) {
2990
            return $this->collectionPersisters[$role];
2991
        }
2992
2993
        $persister = $association instanceof OneToManyAssociationMetadata
2994 1063
            ? new OneToManyPersister($this->em)
2995
            : new ManyToManyPersister($this->em);
2996 1063
2997 845
        if ($this->hasCache && $association->getCache()) {
2998
            $persister = $this->em->getConfiguration()
2999
                ->getSecondLevelCacheConfiguration()
3000 1063
                ->getCacheFactory()
3001
                ->buildCachedCollectionPersister($this->em, $persister, $association);
3002
        }
3003 1063
3004 1031
        $this->collectionPersisters[$role] = $persister;
3005 1031
3006
        return $this->collectionPersisters[$role];
3007 354
    }
3008 213
3009 213
    /**
3010
     * INTERNAL:
3011 333
     * Registers an entity as managed.
3012 333
     *
3013 333
     * @param object $entity The entity.
3014
     * @param array  $id     Map containing identifier field names as key and its associated values.
3015
     * @param array  $data   The original entity data.
3016
     *
3017
     * @return void
3018
     */
3019 1063
    public function registerManaged($entity, array $id, array $data)
3020 119
    {
3021 119
        $isProxy = $entity instanceof Proxy && ! $entity->__isInitialized();
3022 119
        $oid     = spl_object_hash($entity);
3023 119
3024
        $this->entityIdentifiers[$oid]  = $id;
3025
        $this->entityStates[$oid]       = self::STATE_MANAGED;
3026 1063
        $this->originalEntityData[$oid] = $data;
3027
3028 1063
        $this->addToIdentityMap($entity);
3029
3030
        if ($entity instanceof NotifyPropertyChanged && ! $isProxy) {
3031
            $entity->addPropertyChangedListener($this);
3032
        }
3033
    }
3034
3035
    /**
3036
     * INTERNAL:
3037
     * Clears the property changeset of the entity with the given OID.
3038 569
     *
3039
     * @param string $oid The entity's OID.
3040 569
     *
3041 76
     * @return void
3042 569
     */
3043
    public function clearEntityChangeSet($oid)
3044 569
    {
3045 448
        $this->entityChangeSets[$oid] = [];
3046
    }
3047
3048 569
    /* PropertyChangedListener implementation */
3049 403
3050 569
    /**
3051
     * Notifies this UnitOfWork of a property change in an entity.
3052 569
     *
3053 75
     * @param object $entity       The entity that owns the property.
3054 75
     * @param string $propertyName The name of the property that changed.
3055 75
     * @param mixed  $oldValue     The old value of the property.
3056 75
     * @param mixed  $newValue     The new value of the property.
3057
     *
3058
     * @return void
3059 569
     */
3060
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
3061 569
    {
3062
        $class = $this->em->getClassMetadata(get_class($entity));
3063
3064
        if (! $class->getProperty($propertyName)) {
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
3065
            return; // ignore non-persistent fields
3066
        }
3067
3068
        $oid = spl_object_hash($entity);
3069
3070
        // Update changeset and mark entity for synchronization
3071
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
3072
3073
        if ( ! isset($this->scheduledForSynchronization[$class->getRootClassName()][$oid])) {
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
3074 206
            $this->scheduleForSynchronization($entity);
3075
        }
3076 206
    }
3077
3078 206
    /**
3079 206
     * Gets the currently scheduled entity insertions in this UnitOfWork.
3080 206
     *
3081
     * @return array
3082 206
     */
3083
    public function getScheduledEntityInsertions()
3084 200
    {
3085 2
        return $this->entityInsertions;
3086
    }
3087 200
3088
    /**
3089
     * Gets the currently scheduled entity updates in this UnitOfWork.
3090
     *
3091
     * @return array
3092
     */
3093
    public function getScheduledEntityUpdates()
3094
    {
3095
        return $this->entityUpdates;
3096
    }
3097
3098
    /**
3099
     * Gets the currently scheduled entity deletions in this UnitOfWork.
3100
     *
3101
     * @return array
3102
     */
3103
    public function getScheduledEntityDeletions()
3104
    {
3105
        return $this->entityDeletions;
3106
    }
3107
3108
    /**
3109
     * Gets the currently scheduled complete collection deletions
3110
     *
3111
     * @return array
3112
     */
3113
    public function getScheduledCollectionDeletions()
3114 3
    {
3115
        return $this->collectionDeletions;
3116 3
    }
3117 3
3118 3
    /**
3119
     * Gets the currently scheduled collection inserts, updates and deletes.
3120 3
     *
3121 1
     * @return array
3122
     */
3123
    public function getScheduledCollectionUpdates()
3124
    {
3125 3
        return $this->collectionUpdates;
3126
    }
3127 3
3128 3
    /**
3129
     * Helper method to initialize a lazy loading proxy or persistent collection.
3130 3
     *
3131
     * @param object $obj
3132
     *
3133
     * @return void
3134
     */
3135
    public function initializeObject($obj)
3136
    {
3137 2
        if ($obj instanceof Proxy) {
3138
            $obj->__load();
3139 2
3140
            return;
3141
        }
3142
3143
        if ($obj instanceof PersistentCollection) {
3144
            $obj->initialize();
3145
        }
3146
    }
3147 2
3148
    /**
3149 2
     * Helper method to show an object as string.
3150
     *
3151
     * @param object $obj
3152
     *
3153
     * @return string
3154
     */
3155
    private static function objToStr($obj)
3156
    {
3157 1
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj).'@'.spl_object_hash($obj);
3158
    }
3159 1
3160
    /**
3161
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3162
     *
3163
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3164
     * on this object that might be necessary to perform a correct update.
3165
     *
3166
     * @param object $object
3167 1
     *
3168
     * @return void
3169 1
     *
3170
     * @throws ORMInvalidArgumentException
3171
     */
3172
    public function markReadOnly($object)
3173
    {
3174
        if ( ! is_object($object) || ! $this->isInIdentityMap($object)) {
3175
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3176
        }
3177
3178
        $this->readOnlyObjects[spl_object_hash($object)] = true;
3179
    }
3180
3181
    /**
3182
     * Is this entity read only?
3183
     *
3184
     * @param object $object
3185
     *
3186
     * @return bool
3187
     *
3188
     * @throws ORMInvalidArgumentException
3189 2
     */
3190
    public function isReadOnly($object)
3191 2
    {
3192 1
        if ( ! is_object($object)) {
3193
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3194 1
        }
3195
3196
        return isset($this->readOnlyObjects[spl_object_hash($object)]);
3197 1
    }
3198 1
3199
    /**
3200 1
     * Perform whatever processing is encapsulated here after completion of the transaction.
3201
     */
3202
    private function afterTransactionComplete()
3203
    {
3204
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3205
            $persister->afterTransactionComplete();
3206
        });
3207
    }
3208
3209 1
    /**
3210
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
3211 1
     */
3212
    private function afterTransactionRolledBack()
3213
    {
3214
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
3215
            $persister->afterTransactionRolledBack();
3216
        });
3217
    }
3218
3219
    /**
3220
     * Performs an action after the transaction.
3221
     *
3222
     * @param callable $callback
3223
     */
3224
    private function performCallbackOnCachedPersister(callable $callback)
3225
    {
3226 6
        if ( ! $this->hasCache) {
3227
            return;
3228 6
        }
3229 1
3230
        foreach (array_merge($this->entityPersisters, $this->collectionPersisters) as $persister) {
3231
            if ($persister instanceof CachedPersister) {
3232 5
                $callback($persister);
3233 5
            }
3234
        }
3235
    }
3236
3237
    private function dispatchOnFlushEvent()
3238
    {
3239
        if ($this->eventManager->hasListeners(Events::onFlush)) {
3240
            $this->eventManager->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
3241
        }
3242
    }
3243
3244 3
    private function dispatchPostFlushEvent()
3245
    {
3246 3
        if ($this->eventManager->hasListeners(Events::postFlush)) {
3247
            $this->eventManager->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
3248
        }
3249
    }
3250 3
3251
    /**
3252
     * Verifies if two given entities actually are the same based on identifier comparison
3253
     *
3254
     * @param object $entity1
3255
     * @param object $entity2
3256 1003
     *
3257
     * @return bool
3258
     */
3259 89
    private function isIdentifierEquals($entity1, $entity2)
3260 1003
    {
3261 1003
        if ($entity1 === $entity2) {
3262
            return true;
3263
        }
3264
3265
        $class     = $this->em->getClassMetadata(get_class($entity1));
3266
        $persister = $this->getEntityPersister($class->getClassName());
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
3267
3268 11
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
3269 3
            return false;
3270 11
        }
3271 11
3272
        $identifierFlattener = $this->em->getIdentifierFlattener();
3273
3274
        $oid1 = spl_object_hash($entity1);
3275
        $oid2 = spl_object_hash($entity2);
3276
3277
        $id1 = isset($this->entityIdentifiers[$oid1])
3278 1008
            ? $this->entityIdentifiers[$oid1]
3279
            : $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity1));
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3280 1008
        $id2 = isset($this->entityIdentifiers[$oid2])
3281 919
            ? $this->entityIdentifiers[$oid2]
3282
            : $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity2));
0 ignored issues
show
Documentation introduced by
$class is of type object<Doctrine\Common\P...\Mapping\ClassMetadata>, but the function expects a object<Doctrine\ORM\Mapping\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3283
3284 89
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
3285 89
    }
3286 89
3287
    /**
3288
     * @param object $entity
3289 89
     * @param object $managedCopy
3290
     *
3291 1012
     * @throws ORMException
3292
     * @throws OptimisticLockException
3293 1012
     * @throws TransactionRequiredException
3294 4
     */
3295
    private function mergeEntityStateIntoManagedCopy($entity, $managedCopy)
3296 1012
    {
3297
        if (! $this->isLoaded($entity)) {
3298 1007
            return;
3299
        }
3300 1007
3301 5
        if (! $this->isLoaded($managedCopy)) {
3302
            $managedCopy->__load();
3303 1006
        }
3304
3305
        $class = $this->em->getClassMetadata(get_class($entity));
3306
3307
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
3308
            switch (true) {
3309
                case ($property instanceof FieldMetadata):
3310
                    if (! $property->isPrimaryKey()) {
3311
                        $property->setValue($managedCopy, $property->getValue($entity));
3312
                    }
3313 14
3314
                    break;
3315 14
3316
                case ($property instanceof ToOneAssociationMetadata):
3317
                    $other = $property->getValue($entity);
3318
3319 14
                    if ($other === null) {
3320
                        $property->setValue($managedCopy, null);
3321 14
3322 11
                        // Just break out of switch statement to continue execution
3323
                        break;
3324
                    }
3325 3
3326 3
                    if ($other instanceof Proxy && ! $other->__isInitialized()) {
3327
                        // Do not merge fields marked lazy that have not been fetched.
3328 3
                        // Skip to next foreach element
3329 3
                        continue 2;
3330 3
                    }
3331 3
3332 3
                    if (! in_array('merge', $property->getCascade())) {
3333 3
                        if ($this->getEntityState($other) === self::STATE_DETACHED) {
3334
                            $targetEntity    = $property->getTargetEntity();
3335 3
                            $targetClass     = $this->em->getClassMetadata($targetEntity);
3336
                            $targetPersister = $this->getEntityPersister($targetEntity);
3337
                            $relatedId       = $targetPersister->getIdentifier($other);
3338
3339
                            if ($targetClass->getSubClasses()) {
0 ignored issues
show
Bug introduced by
The method getSubClasses() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
3340
                                $other = $this->em->find($targetClass->getClassName(), $relatedId);
0 ignored issues
show
Bug introduced by
The method getClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
3341
                            } else {
3342
                                $other = $this->em->getProxyFactory()->getProxy($targetEntity, $relatedId);
3343
3344
                                $this->registerManaged($other, $relatedId, []);
3345
                            }
3346 30
                        }
3347
3348 30
                        $property->setValue($managedCopy, $other);
3349
                    }
3350 30
3351 30
                    break;
3352
3353 30
                case ($property instanceof ToManyAssociationMetadata):
3354
                    $mergeCol = $property->getValue($entity);
3355 30
3356 30
                    if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
3357 30
                        // do not merge fields marked lazy that have not been fetched.
3358
                        // keep the lazy persistent collection of the managed copy.
3359
                        continue 2;
3360 28
                    }
3361
3362 28
                    $managedCol = $property->getValue($managedCopy);
3363 24
3364 24
                    if (! $managedCol) {
3365 11
                        $managedCol = $property->wrap($managedCopy, [], $this->em);
3366
3367 16
                        $property->setValue($managedCopy, $managedCol);
3368
                    }
3369 4
3370
                    if (in_array('merge', $property->getCascade())) {
3371
                        $managedCol->initialize();
3372 12
3373 6
                        // clear and set dirty a managed collection if its not also the same collection to merge from.
3374 3
                        if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
3375 3
                            $managedCol->unwrap()->clear();
3376
                            $managedCol->setDirty(true);
3377 3
3378 2
                            if ($property instanceof ManyToManyAssociationMetadata &&
3379
                                $property->isOwningSide() &&
3380 1
                                $class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) {
0 ignored issues
show
Bug introduced by
Accessing changeTrackingPolicy on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
3381 1
                                $this->scheduleForSynchronization($managedCopy);
3382
                            }
3383
                        }
3384 1
                    }
3385
3386
                    break;
3387
3388 20
                default:
3389
                    // Non-persistent properties are handled here. Copy them as-is
3390
                    $property->setValue($managedCopy, $property->getValue($entity));
3391
3392 16
                    break;
3393
            }
3394 16
3395
            if ($class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) {
0 ignored issues
show
Bug introduced by
Accessing changeTrackingPolicy on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
3396
                // Just treat all properties as changed, there is no other choice.
3397 5
                $this->propertyChanged($managedCopy, $property->getName(), null, $property->getValue($managedCopy));
3398
            }
3399
        }
3400 13
    }
3401
3402 13
    /**
3403 3
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
3404 3
     * Unit of work able to fire deferred events, related to loading events here.
3405 3
     *
3406 3
     * @internal should be called internally from object hydrators
3407
     */
3408 3
    public function hydrationComplete()
3409 3
    {
3410
        $this->hydrationCompleteHandler->hydrationComplete();
3411 3
    }
3412
}
3413