Failed Conditions
Push — master ( 6744b4...2b8acb )
by Marco
60:45 queued 60:36
created

tests/Doctrine/Tests/ORM/UnitOfWorkTest.php (10 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Doctrine\Tests\ORM;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\Common\EventManager;
7
use Doctrine\Common\NotifyPropertyChanged;
8
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
9
use Doctrine\Common\PropertyChangedListener;
10
use Doctrine\ORM\Events;
11
use Doctrine\ORM\Mapping\ClassMetadata;
12
use Doctrine\ORM\ORMInvalidArgumentException;
13
use Doctrine\ORM\UnitOfWork;
14
use Doctrine\Tests\Mocks\ConnectionMock;
15
use Doctrine\Tests\Mocks\DriverMock;
16
use Doctrine\Tests\Mocks\EntityManagerMock;
17
use Doctrine\Tests\Mocks\EntityPersisterMock;
18
use Doctrine\Tests\Mocks\UnitOfWorkMock;
19
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
20
use Doctrine\Tests\Models\CMS\CmsUser;
21
use Doctrine\Tests\Models\Forum\ForumAvatar;
22
use Doctrine\Tests\Models\Forum\ForumUser;
23
use Doctrine\Tests\Models\GeoNames\City;
24
use Doctrine\Tests\Models\GeoNames\Country;
25
use Doctrine\Tests\OrmTestCase;
26
use stdClass;
27
28
/**
29
 * UnitOfWork tests.
30
 */
31
class UnitOfWorkTest extends OrmTestCase
32
{
33
    /**
34
     * SUT
35
     *
36
     * @var UnitOfWorkMock
37
     */
38
    private $_unitOfWork;
39
40
    /**
41
     * Provides a sequence mock to the UnitOfWork
42
     *
43
     * @var ConnectionMock
44
     */
45
    private $_connectionMock;
46
47
    /**
48
     * The EntityManager mock that provides the mock persisters
49
     *
50
     * @var EntityManagerMock
51
     */
52
    private $_emMock;
53
54
    /**
55
     * @var EventManager|\PHPUnit_Framework_MockObject_MockObject
56
     */
57
    private $eventManager;
58
59
    protected function setUp()
60
    {
61
        parent::setUp();
62
        $this->_connectionMock = new ConnectionMock([], new DriverMock());
63
        $this->eventManager = $this->getMockBuilder(EventManager::class)->getMock();
64
        $this->_emMock = EntityManagerMock::create($this->_connectionMock, null, $this->eventManager);
65
        // SUT
66
        $this->_unitOfWork = new UnitOfWorkMock($this->_emMock);
67
        $this->_emMock->setUnitOfWork($this->_unitOfWork);
68
    }
69
70
    public function testRegisterRemovedOnNewEntityIsIgnored()
71
    {
72
        $user = new ForumUser();
73
        $user->username = 'romanb';
74
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
75
        $this->_unitOfWork->scheduleForDelete($user);
76
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
77
    }
78
79
80
    /* Operational tests */
81
82
    public function testSavingSingleEntityWithIdentityColumnForcesInsert()
83
    {
84
        // Setup fake persister and id generator for identity generation
85
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
86
        $this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
87
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
88
89
        // Test
90
        $user = new ForumUser();
91
        $user->username = 'romanb';
92
        $this->_unitOfWork->persist($user);
93
94
        // Check
95
        $this->assertEquals(0, count($userPersister->getInserts()));
96
        $this->assertEquals(0, count($userPersister->getUpdates()));
97
        $this->assertEquals(0, count($userPersister->getDeletes()));
98
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($user));
99
        // should no longer be scheduled for insert
100
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($user));
101
102
        // Now lets check whether a subsequent commit() does anything
103
        $userPersister->reset();
104
105
        // Test
106
        $this->_unitOfWork->commit();
107
108
        // Check.
109
        $this->assertEquals(1, count($userPersister->getInserts()));
110
        $this->assertEquals(0, count($userPersister->getUpdates()));
111
        $this->assertEquals(0, count($userPersister->getDeletes()));
112
113
        // should have an id
114
        $this->assertTrue(is_numeric($user->id));
115
    }
116
117
    /**
118
     * Tests a scenario where a save() operation is cascaded from a ForumUser
119
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
120
     */
121
    public function testCascadedIdentityColumnInsert()
122
    {
123
        // Setup fake persister and id generator for identity generation
124
        //ForumUser
125
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
126
        $this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
127
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
128
        // ForumAvatar
129
        $avatarPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumAvatar::class));
130
        $this->_unitOfWork->setEntityPersister(ForumAvatar::class, $avatarPersister);
131
        $avatarPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
132
133
        // Test
134
        $user = new ForumUser();
135
        $user->username = 'romanb';
136
        $avatar = new ForumAvatar();
137
        $user->avatar = $avatar;
138
        $this->_unitOfWork->persist($user); // save cascaded to avatar
139
140
        $this->_unitOfWork->commit();
141
142
        $this->assertTrue(is_numeric($user->id));
143
        $this->assertTrue(is_numeric($avatar->id));
144
145
        $this->assertEquals(1, count($userPersister->getInserts()));
146
        $this->assertEquals(0, count($userPersister->getUpdates()));
147
        $this->assertEquals(0, count($userPersister->getDeletes()));
148
149
        $this->assertEquals(1, count($avatarPersister->getInserts()));
150
        $this->assertEquals(0, count($avatarPersister->getUpdates()));
151
        $this->assertEquals(0, count($avatarPersister->getDeletes()));
152
    }
153
154
    public function testChangeTrackingNotify()
155
    {
156
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(NotifyChangedEntity::class));
157
        $this->_unitOfWork->setEntityPersister(NotifyChangedEntity::class, $persister);
158
        $itemPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(NotifyChangedRelatedItem::class));
159
        $this->_unitOfWork->setEntityPersister(NotifyChangedRelatedItem::class, $itemPersister);
160
161
        $entity = new NotifyChangedEntity;
162
        $entity->setData('thedata');
163
        $this->_unitOfWork->persist($entity);
164
165
        $this->_unitOfWork->commit();
166
        $this->assertEquals(1, count($persister->getInserts()));
167
        $persister->reset();
168
169
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
170
171
        $entity->setData('newdata');
172
        $entity->setTransient('newtransientvalue');
173
174
        $this->assertTrue($this->_unitOfWork->isScheduledForDirtyCheck($entity));
175
176
        $this->assertEquals(['data' => ['thedata', 'newdata']], $this->_unitOfWork->getEntityChangeSet($entity));
177
178
        $item = new NotifyChangedRelatedItem();
179
        $entity->getItems()->add($item);
180
        $item->setOwner($entity);
181
        $this->_unitOfWork->persist($item);
182
183
        $this->_unitOfWork->commit();
184
        $this->assertEquals(1, count($itemPersister->getInserts()));
185
        $persister->reset();
186
        $itemPersister->reset();
187
188
189
        $entity->getItems()->removeElement($item);
190
        $item->setOwner(null);
191
        $this->assertTrue($entity->getItems()->isDirty());
0 ignored issues
show
The method isDirty() does not seem to exist on object<Doctrine\Common\C...ctions\ArrayCollection>.

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...
192
        $this->_unitOfWork->commit();
193
        $updates = $itemPersister->getUpdates();
194
        $this->assertEquals(1, count($updates));
195
        $this->assertTrue($updates[0] === $item);
196
    }
197
198 View Code Duplication
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier()
199
    {
200
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(VersionedAssignedIdentifierEntity::class));
201
        $this->_unitOfWork->setEntityPersister(VersionedAssignedIdentifierEntity::class, $persister);
202
203
        $e = new VersionedAssignedIdentifierEntity();
204
        $e->id = 42;
205
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($e));
206
        $this->assertFalse($persister->isExistsCalled());
207
    }
208
209
    public function testGetEntityStateWithAssignedIdentity()
210
    {
211
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CmsPhonenumber::class));
212
        $this->_unitOfWork->setEntityPersister(CmsPhonenumber::class, $persister);
213
214
        $ph = new CmsPhonenumber();
215
        $ph->phonenumber = '12345';
216
217
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($ph));
218
        $this->assertTrue($persister->isExistsCalled());
219
220
        $persister->reset();
221
222
        // if the entity is already managed the exists() check should be skipped
223
        $this->_unitOfWork->registerManaged($ph, ['phonenumber' => '12345'], []);
224
        $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($ph));
225
        $this->assertFalse($persister->isExistsCalled());
226
        $ph2 = new CmsPhonenumber();
227
        $ph2->phonenumber = '12345';
228
        $this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_unitOfWork->getEntityState($ph2));
229
        $this->assertFalse($persister->isExistsCalled());
230
    }
231
232
    /**
233
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
234
     */
235
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges()
236
    {
237
        // Setup fake persister and id generator
238
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
239
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
240
        $this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
241
242
        // Create a test user
243
        $user = new ForumUser();
244
        $user->name = 'Jasper';
0 ignored issues
show
The property name does not seem to exist in Doctrine\Tests\Models\Forum\ForumUser.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
245
        $this->_unitOfWork->persist($user);
246
        $this->_unitOfWork->commit();
247
248
        // Schedule user for update without changes
249
        $this->_unitOfWork->scheduleForUpdate($user);
250
251
        self::assertNotEmpty($this->_unitOfWork->getScheduledEntityUpdates());
252
253
        // This commit should not raise an E_NOTICE
254
        $this->_unitOfWork->commit();
255
256
        self::assertEmpty($this->_unitOfWork->getScheduledEntityUpdates());
257
    }
258
259
    /**
260
     * @group DDC-1984
261
     */
262
    public function testLockWithoutEntityThrowsException()
263
    {
264
        $this->expectException(\InvalidArgumentException::class);
265
        $this->_unitOfWork->lock(null, null, null);
0 ignored issues
show
null is of type null, but the function expects a object.

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...
266
    }
267
268
    /**
269
     * @group DDC-3490
270
     *
271
     * @dataProvider invalidAssociationValuesDataProvider
272
     *
273
     * @param mixed $invalidValue
274
     */
275 View Code Duplication
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
276
    {
277
        $this->_unitOfWork->setEntityPersister(
278
            ForumUser::class,
279
            new EntityPersisterMock(
280
                $this->_emMock,
281
                $this->_emMock->getClassMetadata(ForumUser::class)
282
            )
283
        );
284
285
        $user           = new ForumUser();
286
        $user->username = 'John';
287
        $user->avatar   = $invalidValue;
288
289
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
290
291
        $this->_unitOfWork->persist($user);
292
    }
293
294
    /**
295
     * @group DDC-3490
296
     *
297
     * @dataProvider invalidAssociationValuesDataProvider
298
     *
299
     * @param mixed $invalidValue
300
     */
301
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
302
    {
303
        $metadata = $this->_emMock->getClassMetadata(ForumUser::class);
304
305
        $this->_unitOfWork->setEntityPersister(
306
            ForumUser::class,
307
            new EntityPersisterMock($this->_emMock, $metadata)
308
        );
309
310
        $user = new ForumUser();
311
312
        $this->_unitOfWork->persist($user);
313
314
        $user->username = 'John';
315
        $user->avatar   = $invalidValue;
316
317
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
318
319
        $this->_unitOfWork->computeChangeSet($metadata, $user);
320
    }
321
322
    /**
323
     * @group DDC-3619
324
     * @group 1338
325
     */
326
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected()
327
    {
328
        $entity     = new ForumUser();
329
        $entity->id = 123;
330
331
        $this->_unitOfWork->registerManaged($entity, ['id' => 123], []);
332
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
333
334
        $this->_unitOfWork->remove($entity);
335
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
336
337
        $this->_unitOfWork->persist($entity);
338
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
339
    }
340
341
    /**
342
     * @group 5849
343
     * @group 5850
344
     */
345
    public function testPersistedEntityAndClearManager()
346
    {
347
        $entity1 = new City(123, 'London');
348
        $entity2 = new Country(456, 'United Kingdom');
349
350
        $this->_unitOfWork->persist($entity1);
351
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
352
353
        $this->_unitOfWork->persist($entity2);
354
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
355
356
        $this->_unitOfWork->clear(Country::class);
357
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
358
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity2));
359
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($entity1));
360
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity2));
361
    }
362
363
    /**
364
     * Data Provider
365
     *
366
     * @return mixed[][]
367
     */
368
    public function invalidAssociationValuesDataProvider()
369
    {
370
        return [
371
            ['foo'],
372
            [['foo']],
373
            [''],
374
            [[]],
375
            [new stdClass()],
376
            [new ArrayCollection()],
377
        ];
378
    }
379
380
    /**
381
     * @dataProvider entitiesWithValidIdentifiersProvider
382
     *
383
     * @param object $entity
384
     * @param string $idHash
385
     *
386
     * @return void
387
     */
388
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash)
389
    {
390
        $this->_unitOfWork->persist($entity);
391
        $this->_unitOfWork->addToIdentityMap($entity);
392
393
        self::assertSame($entity, $this->_unitOfWork->getByIdHash($idHash, get_class($entity)));
394
    }
395
396
    public function entitiesWithValidIdentifiersProvider()
397
    {
398
        $emptyString = new EntityWithStringIdentifier();
399
400
        $emptyString->id = '';
401
402
        $nonEmptyString = new EntityWithStringIdentifier();
403
404
        $nonEmptyString->id = uniqid('id', true);
405
406
        $emptyStrings = new EntityWithCompositeStringIdentifier();
407
408
        $emptyStrings->id1 = '';
409
        $emptyStrings->id2 = '';
410
411
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
412
413
        $nonEmptyStrings->id1 = uniqid('id1', true);
414
        $nonEmptyStrings->id2 = uniqid('id2', true);
415
416
        $booleanTrue = new EntityWithBooleanIdentifier();
417
418
        $booleanTrue->id = true;
419
420
        $booleanFalse = new EntityWithBooleanIdentifier();
421
422
        $booleanFalse->id = false;
423
424
        return [
425
            'empty string, single field'     => [$emptyString, ''],
426
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
427
            'empty strings, two fields'      => [$emptyStrings, ' '],
428
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
429
            'boolean true'                   => [$booleanTrue, '1'],
430
            'boolean false'                  => [$booleanFalse, ''],
431
        ];
432
    }
433
434
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier()
435
    {
436
        $this->expectException(ORMInvalidArgumentException::class);
437
438
        $this->_unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
439
    }
440
441
    /**
442
     * @dataProvider entitiesWithInvalidIdentifiersProvider
443
     *
444
     * @param object $entity
445
     * @param array  $identifier
446
     *
447
     * @return void
448
     */
449
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier)
450
    {
451
        $this->expectException(ORMInvalidArgumentException::class);
452
453
        $this->_unitOfWork->registerManaged($entity, $identifier, []);
454
    }
455
456
457
    public function entitiesWithInvalidIdentifiersProvider()
458
    {
459
        $firstNullString  = new EntityWithCompositeStringIdentifier();
460
461
        $firstNullString->id2 = uniqid('id2', true);
462
463
        $secondNullString = new EntityWithCompositeStringIdentifier();
464
465
        $secondNullString->id1 = uniqid('id1', true);
466
467
        return [
468
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
469
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
470
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
471
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
472
        ];
473
    }
474
475
    /**
476
     * @group 5689
477
     * @group 1465
478
     */
479
    public function testObjectHashesOfMergedEntitiesAreNotUsedInOriginalEntityDataMap()
480
    {
481
        $user       = new CmsUser();
482
        $user->name = 'ocramius';
483
        $mergedUser = $this->_unitOfWork->merge($user);
484
485
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($user), 'No original data was stored');
486
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($mergedUser), 'No original data was stored');
487
488
489
        $user       = null;
0 ignored issues
show
$user is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
490
        $mergedUser = null;
0 ignored issues
show
$mergedUser is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
491
492
        // force garbage collection of $user (frees the used object hashes, which may be recycled)
493
        gc_collect_cycles();
494
495
        $newUser       = new CmsUser();
496
        $newUser->name = 'ocramius';
497
498
        $this->_unitOfWork->persist($newUser);
499
500
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($newUser), 'No original data was stored');
501
    }
502
503
    /**
504
     * @group DDC-1955
505
     * @group 5570
506
     * @group 6174
507
     */
508
    public function testMergeWithNewEntityWillPersistItAndTriggerPrePersistListenersWithMergedEntityData()
509
    {
510
        $entity = new EntityWithRandomlyGeneratedField();
511
512
        $generatedFieldValue = $entity->generatedField;
513
514
        $this
0 ignored issues
show
The method expects does only exist in PHPUnit_Framework_MockObject_MockObject, but not in Doctrine\Common\EventManager.

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
            ->eventManager
516
            ->expects(self::any())
517
            ->method('hasListeners')
518
            ->willReturnCallback(function ($eventName) {
519
                return $eventName === Events::prePersist;
520
            });
521
        $this
0 ignored issues
show
The method expects does only exist in PHPUnit_Framework_MockObject_MockObject, but not in Doctrine\Common\EventManager.

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...
522
            ->eventManager
523
            ->expects(self::once())
524
            ->method('dispatchEvent')
525
            ->with(
526
                self::anything(),
527
                self::callback(function (LifecycleEventArgs $args) use ($entity, $generatedFieldValue) {
528
                    /* @var $object EntityWithRandomlyGeneratedField */
529
                    $object = $args->getObject();
530
531
                    self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
532
                    self::assertNotSame($entity, $object);
533
                    self::assertSame($generatedFieldValue, $object->generatedField);
534
535
                    return true;
536
                })
537
            );
538
539
        /* @var $object EntityWithRandomlyGeneratedField */
540
        $object = $this->_unitOfWork->merge($entity);
541
542
        self::assertNotSame($object, $entity);
543
        self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
544
        self::assertSame($object->generatedField, $entity->generatedField);
545
    }
546
547
    /**
548
     * @group DDC-1955
549
     * @group 5570
550
     * @group 6174
551
     */
552
    public function testMergeWithExistingEntityWillNotPersistItNorTriggerPrePersistListeners()
553
    {
554
        $persistedEntity = new EntityWithRandomlyGeneratedField();
555
        $mergedEntity    = new EntityWithRandomlyGeneratedField();
556
557
        $mergedEntity->id = $persistedEntity->id;
558
        $mergedEntity->generatedField = mt_rand(
559
            $persistedEntity->generatedField + 1,
560
            $persistedEntity->generatedField + 1000
561
        );
562
563
        $this
0 ignored issues
show
The method expects does only exist in PHPUnit_Framework_MockObject_MockObject, but not in Doctrine\Common\EventManager.

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...
564
            ->eventManager
565
            ->expects(self::any())
566
            ->method('hasListeners')
567
            ->willReturnCallback(function ($eventName) {
568
                return $eventName === Events::prePersist;
569
            });
570
        $this->eventManager->expects(self::never())->method('dispatchEvent');
0 ignored issues
show
The method expects does only exist in PHPUnit_Framework_MockObject_MockObject, but not in Doctrine\Common\EventManager.

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...
571
572
        $this->_unitOfWork->registerManaged(
573
            $persistedEntity,
574
            ['id' => $persistedEntity->id],
575
            ['generatedField' => $persistedEntity->generatedField]
576
        );
577
578
        /* @var $merged EntityWithRandomlyGeneratedField */
579
        $merged = $this->_unitOfWork->merge($mergedEntity);
580
581
        self::assertSame($merged, $persistedEntity);
582
        self::assertSame($persistedEntity->generatedField, $mergedEntity->generatedField);
583
    }
584
}
585
586
/**
587
 * @Entity
588
 */
589
class NotifyChangedEntity implements NotifyPropertyChanged
590
{
591
    private $_listeners = [];
592
    /**
593
     * @Id
594
     * @Column(type="integer")
595
     * @GeneratedValue
596
     */
597
    private $id;
598
    /**
599
     * @Column(type="string")
600
     */
601
    private $data;
602
603
    private $transient; // not persisted
604
605
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
606
    private $items;
607
608
    public function  __construct() {
609
        $this->items = new ArrayCollection;
610
    }
611
612
    public function getId() {
613
        return $this->id;
614
    }
615
616
    public function getItems() {
617
        return $this->items;
618
    }
619
620
    public function setTransient($value) {
621
        if ($value != $this->transient) {
622
            $this->_onPropertyChanged('transient', $this->transient, $value);
623
            $this->transient = $value;
624
        }
625
    }
626
627
    public function getData() {
628
        return $this->data;
629
    }
630
631
    public function setData($data) {
632
        if ($data != $this->data) {
633
            $this->_onPropertyChanged('data', $this->data, $data);
634
            $this->data = $data;
635
        }
636
    }
637
638
    public function addPropertyChangedListener(PropertyChangedListener $listener)
639
    {
640
        $this->_listeners[] = $listener;
641
    }
642
643
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
644
        if ($this->_listeners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_listeners 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...
645
            foreach ($this->_listeners as $listener) {
646
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
647
            }
648
        }
649
    }
650
}
651
652
/** @Entity */
653
class NotifyChangedRelatedItem
654
{
655
    /**
656
     * @Id
657
     * @Column(type="integer")
658
     * @GeneratedValue
659
     */
660
    private $id;
661
662
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
663
    private $owner;
664
665
    public function getId() {
666
        return $this->id;
667
    }
668
669
    public function getOwner() {
670
        return $this->owner;
671
    }
672
673
    public function setOwner($owner) {
674
        $this->owner = $owner;
675
    }
676
}
677
678
/** @Entity */
679
class VersionedAssignedIdentifierEntity
680
{
681
    /**
682
     * @Id @Column(type="integer")
683
     */
684
    public $id;
685
    /**
686
     * @Version @Column(type="integer")
687
     */
688
    public $version;
689
}
690
691
/** @Entity */
692
class EntityWithStringIdentifier
693
{
694
    /**
695
     * @Id @Column(type="string")
696
     *
697
     * @var string|null
698
     */
699
    public $id;
700
}
701
702
/** @Entity */
703
class EntityWithBooleanIdentifier
704
{
705
    /**
706
     * @Id @Column(type="boolean")
707
     *
708
     * @var bool|null
709
     */
710
    public $id;
711
}
712
713
/** @Entity */
714
class EntityWithCompositeStringIdentifier
715
{
716
    /**
717
     * @Id @Column(type="string")
718
     *
719
     * @var string|null
720
     */
721
    public $id1;
722
723
    /**
724
     * @Id @Column(type="string")
725
     *
726
     * @var string|null
727
     */
728
    public $id2;
729
}
730
731
/** @Entity */
732
class EntityWithRandomlyGeneratedField
733
{
734
    /** @Id @Column(type="string") */
735
    public $id;
736
737
    /**
738
     * @Column(type="integer")
739
     */
740
    public $generatedField;
741
742
    public function __construct()
743
    {
744
        $this->id             = uniqid('id', true);
745
        $this->generatedField = mt_rand(0, 100000);
746
    }
747
}
748