Failed Conditions
Pull Request — master (#5926)
by Luís
20:39
created

testUnitOfWorkStoresAlsoNonUtf8Identifiers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
rs 9.4285
cc 1
eloc 13
nc 1
nop 0
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
Bug introduced by
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
Bug introduced by
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
Documentation introduced by
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, serialize([''])],
426
            'non-empty string, single field' => [$nonEmptyString, serialize([$nonEmptyString->id])],
427
            'empty strings, two fields'      => [$emptyStrings, serialize(['', ''])],
428
            'non-empty strings, two fields'  => [
429
                $nonEmptyStrings,
430
                serialize([$nonEmptyStrings->id1, $nonEmptyStrings->id2])
431
            ],
432
            'boolean true'                   => [$booleanTrue, serialize(['1'])],
433
            'boolean false'                  => [$booleanFalse, serialize([''])],
434
        ];
435
    }
436
437
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier()
438
    {
439
        $this->expectException(ORMInvalidArgumentException::class);
440
441
        $this->_unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
442
    }
443
444
    /**
445
     * @dataProvider entitiesWithInvalidIdentifiersProvider
446
     *
447
     * @param object $entity
448
     * @param array  $identifier
449
     *
450
     * @return void
451
     */
452
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier)
453
    {
454
        $this->expectException(ORMInvalidArgumentException::class);
455
456
        $this->_unitOfWork->registerManaged($entity, $identifier, []);
457
    }
458
459
460
    public function entitiesWithInvalidIdentifiersProvider()
461
    {
462
        $firstNullString  = new EntityWithCompositeStringIdentifier();
463
464
        $firstNullString->id2 = uniqid('id2', true);
465
466
        $secondNullString = new EntityWithCompositeStringIdentifier();
467
468
        $secondNullString->id1 = uniqid('id1', true);
469
470
        return [
471
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
472
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
473
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
474
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
475
        ];
476
    }
477
478
    /**
479
     * @group 5689
480
     * @group 1465
481
     */
482
    public function testObjectHashesOfMergedEntitiesAreNotUsedInOriginalEntityDataMap()
483
    {
484
        $user       = new CmsUser();
485
        $user->name = 'ocramius';
486
        $mergedUser = $this->_unitOfWork->merge($user);
487
488
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($user), 'No original data was stored');
489
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($mergedUser), 'No original data was stored');
490
491
492
        $user       = null;
0 ignored issues
show
Unused Code introduced by
$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...
493
        $mergedUser = null;
0 ignored issues
show
Unused Code introduced by
$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...
494
495
        // force garbage collection of $user (frees the used object hashes, which may be recycled)
496
        gc_collect_cycles();
497
498
        $newUser       = new CmsUser();
499
        $newUser->name = 'ocramius';
500
501
        $this->_unitOfWork->persist($newUser);
502
503
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($newUser), 'No original data was stored');
504
    }
505
506
    /**
507
     * @group DDC-1955
508
     * @group 5570
509
     * @group 6174
510
     */
511
    public function testMergeWithNewEntityWillPersistItAndTriggerPrePersistListenersWithMergedEntityData()
512
    {
513
        $entity = new EntityWithRandomlyGeneratedField();
514
515
        $generatedFieldValue = $entity->generatedField;
516
517
        $this
0 ignored issues
show
Bug introduced by
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...
518
            ->eventManager
519
            ->expects(self::any())
520
            ->method('hasListeners')
521
            ->willReturnCallback(function ($eventName) {
522
                return $eventName === Events::prePersist;
523
            });
524
        $this
0 ignored issues
show
Bug introduced by
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...
525
            ->eventManager
526
            ->expects(self::once())
527
            ->method('dispatchEvent')
528
            ->with(
529
                self::anything(),
530
                self::callback(function (LifecycleEventArgs $args) use ($entity, $generatedFieldValue) {
531
                    /* @var $object EntityWithRandomlyGeneratedField */
532
                    $object = $args->getObject();
533
534
                    self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
535
                    self::assertNotSame($entity, $object);
536
                    self::assertSame($generatedFieldValue, $object->generatedField);
537
538
                    return true;
539
                })
540
            );
541
542
        /* @var $object EntityWithRandomlyGeneratedField */
543
        $object = $this->_unitOfWork->merge($entity);
544
545
        self::assertNotSame($object, $entity);
546
        self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
547
        self::assertSame($object->generatedField, $entity->generatedField);
548
    }
549
550
    /**
551
     * @group DDC-1955
552
     * @group 5570
553
     * @group 6174
554
     */
555
    public function testMergeWithExistingEntityWillNotPersistItNorTriggerPrePersistListeners()
556
    {
557
        $persistedEntity = new EntityWithRandomlyGeneratedField();
558
        $mergedEntity    = new EntityWithRandomlyGeneratedField();
559
560
        $mergedEntity->id = $persistedEntity->id;
561
        $mergedEntity->generatedField = mt_rand(
562
            $persistedEntity->generatedField + 1,
563
            $persistedEntity->generatedField + 1000
564
        );
565
566
        $this
0 ignored issues
show
Bug introduced by
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...
567
            ->eventManager
568
            ->expects(self::any())
569
            ->method('hasListeners')
570
            ->willReturnCallback(function ($eventName) {
571
                return $eventName === Events::prePersist;
572
            });
573
        $this->eventManager->expects(self::never())->method('dispatchEvent');
0 ignored issues
show
Bug introduced by
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...
574
575
        $this->_unitOfWork->registerManaged(
576
            $persistedEntity,
577
            ['id' => $persistedEntity->id],
578
            ['generatedField' => $persistedEntity->generatedField]
579
        );
580
581
        /* @var $merged EntityWithRandomlyGeneratedField */
582
        $merged = $this->_unitOfWork->merge($mergedEntity);
583
584
        self::assertSame($merged, $persistedEntity);
585
        self::assertSame($persistedEntity->generatedField, $mergedEntity->generatedField);
586
    }
587
588
    /**
589
     * @group 5923
590
     */
591
    public function testUnitOfWorkDoesNotConcatenateIdentifierWithAmbiguousPatterns()
592
    {
593
        $entity1 = new EntityWithCompositeStringIdentifier();
594
        $entity2 = new EntityWithCompositeStringIdentifier();
595
596
        $entity1->id1 = '';
597
        $entity1->id2 = ' ';
598
599
        $entity2->id1 = ' ';
600
        $entity2->id2 = '';
601
602
        $this->_unitOfWork->persist($entity1);
603
        $this->_unitOfWork->persist($entity2);
604
        $this->_unitOfWork->addToIdentityMap($entity1);
605
        $this->_unitOfWork->addToIdentityMap($entity2);
606
607
        self::assertSame(
608
            $entity1,
609
            $this->_unitOfWork->tryGetById(['id1' => '', 'id2' => ' '],  EntityWithCompositeStringIdentifier::class)
610
        );
611
        self::assertSame(
612
            $entity2,
613
            $this->_unitOfWork->tryGetById(['id1' => ' ', 'id2' => ''],  EntityWithCompositeStringIdentifier::class)
614
        );
615
    }
616
617
    /**
618
     * @group 5923
619
     */
620
    public function testUnitOfWorkStoresAlsoNonUtf8Identifiers()
621
    {
622
        $entity1 = new EntityWithStringIdentifier();
623
        $entity2 = new EntityWithStringIdentifier();
624
625
        $entity1->id = chr(65533);
626
        $entity2->id = chr(65534);
627
628
        $this->_unitOfWork->persist($entity1);
629
        $this->_unitOfWork->persist($entity2);
630
631
        self::assertSame(
632
            $entity1,
633
            $this->_unitOfWork->tryGetById(['id' => $entity1->id], EntityWithStringIdentifier::class)
634
        );
635
        self::assertSame(
636
            $entity2,
637
            $this->_unitOfWork->tryGetById(['id' => $entity2->id], EntityWithStringIdentifier::class)
638
        );
639
    }
640
641
    /**
642
     * @group 5923
643
     *
644
     * Note: this test is introduced for BC compliance only. If we expect consumers to always
645
     *       pass in the keys of an identifier, then we should change this test so it fails,
646
     *       but we'd have to document the BC break.
647
     */
648
    public function testUnitOfWorkCanFetchByIdentifierWithFlatListOfIdentifierValues()
649
    {
650
        $entity = new EntityWithStringIdentifier();
651
652
        $entity->id = uniqid('id', true);
653
654
        $this->_unitOfWork->persist($entity);
655
656
        self::assertSame($entity, $this->_unitOfWork->tryGetById([$entity->id], EntityWithStringIdentifier::class));
657
    }
658
}
659
660
/**
661
 * @Entity
662
 */
663
class NotifyChangedEntity implements NotifyPropertyChanged
664
{
665
    private $_listeners = [];
666
    /**
667
     * @Id
668
     * @Column(type="integer")
669
     * @GeneratedValue
670
     */
671
    private $id;
672
    /**
673
     * @Column(type="string")
674
     */
675
    private $data;
676
677
    private $transient; // not persisted
678
679
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
680
    private $items;
681
682
    public function  __construct() {
683
        $this->items = new ArrayCollection;
684
    }
685
686
    public function getId() {
687
        return $this->id;
688
    }
689
690
    public function getItems() {
691
        return $this->items;
692
    }
693
694
    public function setTransient($value) {
695
        if ($value != $this->transient) {
696
            $this->_onPropertyChanged('transient', $this->transient, $value);
697
            $this->transient = $value;
698
        }
699
    }
700
701
    public function getData() {
702
        return $this->data;
703
    }
704
705
    public function setData($data) {
706
        if ($data != $this->data) {
707
            $this->_onPropertyChanged('data', $this->data, $data);
708
            $this->data = $data;
709
        }
710
    }
711
712
    public function addPropertyChangedListener(PropertyChangedListener $listener)
713
    {
714
        $this->_listeners[] = $listener;
715
    }
716
717
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
718
        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...
719
            foreach ($this->_listeners as $listener) {
720
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
721
            }
722
        }
723
    }
724
}
725
726
/** @Entity */
727
class NotifyChangedRelatedItem
728
{
729
    /**
730
     * @Id
731
     * @Column(type="integer")
732
     * @GeneratedValue
733
     */
734
    private $id;
735
736
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
737
    private $owner;
738
739
    public function getId() {
740
        return $this->id;
741
    }
742
743
    public function getOwner() {
744
        return $this->owner;
745
    }
746
747
    public function setOwner($owner) {
748
        $this->owner = $owner;
749
    }
750
}
751
752
/** @Entity */
753
class VersionedAssignedIdentifierEntity
754
{
755
    /**
756
     * @Id @Column(type="integer")
757
     */
758
    public $id;
759
    /**
760
     * @Version @Column(type="integer")
761
     */
762
    public $version;
763
}
764
765
/** @Entity */
766
class EntityWithStringIdentifier
767
{
768
    /**
769
     * @Id @Column(type="string")
770
     *
771
     * @var string|null
772
     */
773
    public $id;
774
}
775
776
/** @Entity */
777
class EntityWithBooleanIdentifier
778
{
779
    /**
780
     * @Id @Column(type="boolean")
781
     *
782
     * @var bool|null
783
     */
784
    public $id;
785
}
786
787
/** @Entity */
788
class EntityWithCompositeStringIdentifier
789
{
790
    /**
791
     * @Id @Column(type="string")
792
     *
793
     * @var string|null
794
     */
795
    public $id1;
796
797
    /**
798
     * @Id @Column(type="string")
799
     *
800
     * @var string|null
801
     */
802
    public $id2;
803
}
804
805
/** @Entity */
806
class EntityWithRandomlyGeneratedField
807
{
808
    /** @Id @Column(type="string") */
809
    public $id;
810
811
    /**
812
     * @Column(type="integer")
813
     */
814
    public $generatedField;
815
816
    public function __construct()
817
    {
818
        $this->id             = uniqid('id', true);
819
        $this->generatedField = mt_rand(0, 100000);
820
    }
821
}
822