Failed Conditions
Push — master ( ddccd4...8ad3df )
by Marco
13:34
created

CascadePersistedEntity   A

Complexity

Total Complexity 1

Size/Duplication

Total Lines 10
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 0

Importance

Changes 0
Metric Value
wmc 1
lcom 0
cbo 0
dl 0
loc 10
rs 10
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
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->assertCount(1, $persister->getInserts());
167
168
        $persister->reset();
169
170
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
171
172
        $entity->setData('newdata');
173
        $entity->setTransient('newtransientvalue');
174
175
        $this->assertTrue($this->_unitOfWork->isScheduledForDirtyCheck($entity));
176
177
        $this->assertEquals(['data' => ['thedata', 'newdata']], $this->_unitOfWork->getEntityChangeSet($entity));
178
179
        $item = new NotifyChangedRelatedItem();
180
        $entity->getItems()->add($item);
181
        $item->setOwner($entity);
182
        $this->_unitOfWork->persist($item);
183
184
        $this->_unitOfWork->commit();
185
        $this->assertEquals(1, count($itemPersister->getInserts()));
186
        $persister->reset();
187
        $itemPersister->reset();
188
189
190
        $entity->getItems()->removeElement($item);
191
        $item->setOwner(null);
192
        $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...
193
        $this->_unitOfWork->commit();
194
        $updates = $itemPersister->getUpdates();
195
        $this->assertEquals(1, count($updates));
196
        $this->assertTrue($updates[0] === $item);
197
    }
198
199 View Code Duplication
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier()
200
    {
201
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(VersionedAssignedIdentifierEntity::class));
202
        $this->_unitOfWork->setEntityPersister(VersionedAssignedIdentifierEntity::class, $persister);
203
204
        $e = new VersionedAssignedIdentifierEntity();
205
        $e->id = 42;
206
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($e));
207
        $this->assertFalse($persister->isExistsCalled());
208
    }
209
210
    public function testGetEntityStateWithAssignedIdentity()
211
    {
212
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CmsPhonenumber::class));
213
        $this->_unitOfWork->setEntityPersister(CmsPhonenumber::class, $persister);
214
215
        $ph = new CmsPhonenumber();
216
        $ph->phonenumber = '12345';
217
218
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($ph));
219
        $this->assertTrue($persister->isExistsCalled());
220
221
        $persister->reset();
222
223
        // if the entity is already managed the exists() check should be skipped
224
        $this->_unitOfWork->registerManaged($ph, ['phonenumber' => '12345'], []);
225
        $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($ph));
226
        $this->assertFalse($persister->isExistsCalled());
227
        $ph2 = new CmsPhonenumber();
228
        $ph2->phonenumber = '12345';
229
        $this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_unitOfWork->getEntityState($ph2));
230
        $this->assertFalse($persister->isExistsCalled());
231
    }
232
233
    /**
234
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
235
     */
236
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges()
237
    {
238
        // Setup fake persister and id generator
239
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
240
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
241
        $this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
242
243
        // Create a test user
244
        $user = new ForumUser();
245
        $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...
246
        $this->_unitOfWork->persist($user);
247
        $this->_unitOfWork->commit();
248
249
        // Schedule user for update without changes
250
        $this->_unitOfWork->scheduleForUpdate($user);
251
252
        self::assertNotEmpty($this->_unitOfWork->getScheduledEntityUpdates());
253
254
        // This commit should not raise an E_NOTICE
255
        $this->_unitOfWork->commit();
256
257
        self::assertEmpty($this->_unitOfWork->getScheduledEntityUpdates());
258
    }
259
260
    /**
261
     * @group DDC-1984
262
     */
263
    public function testLockWithoutEntityThrowsException()
264
    {
265
        $this->expectException(\InvalidArgumentException::class);
266
        $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...
267
    }
268
269
    /**
270
     * @group DDC-3490
271
     *
272
     * @dataProvider invalidAssociationValuesDataProvider
273
     *
274
     * @param mixed $invalidValue
275
     */
276 View Code Duplication
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
277
    {
278
        $this->_unitOfWork->setEntityPersister(
279
            ForumUser::class,
280
            new EntityPersisterMock(
281
                $this->_emMock,
282
                $this->_emMock->getClassMetadata(ForumUser::class)
283
            )
284
        );
285
286
        $user           = new ForumUser();
287
        $user->username = 'John';
288
        $user->avatar   = $invalidValue;
289
290
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
291
292
        $this->_unitOfWork->persist($user);
293
    }
294
295
    /**
296
     * @group DDC-3490
297
     *
298
     * @dataProvider invalidAssociationValuesDataProvider
299
     *
300
     * @param mixed $invalidValue
301
     */
302
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
303
    {
304
        $metadata = $this->_emMock->getClassMetadata(ForumUser::class);
305
306
        $this->_unitOfWork->setEntityPersister(
307
            ForumUser::class,
308
            new EntityPersisterMock($this->_emMock, $metadata)
309
        );
310
311
        $user = new ForumUser();
312
313
        $this->_unitOfWork->persist($user);
314
315
        $user->username = 'John';
316
        $user->avatar   = $invalidValue;
317
318
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
319
320
        $this->_unitOfWork->computeChangeSet($metadata, $user);
321
    }
322
323
    /**
324
     * @group DDC-3619
325
     * @group 1338
326
     */
327
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected()
328
    {
329
        $entity     = new ForumUser();
330
        $entity->id = 123;
331
332
        $this->_unitOfWork->registerManaged($entity, ['id' => 123], []);
333
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
334
335
        $this->_unitOfWork->remove($entity);
336
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
337
338
        $this->_unitOfWork->persist($entity);
339
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
340
    }
341
342
    /**
343
     * @group 5849
344
     * @group 5850
345
     */
346
    public function testPersistedEntityAndClearManager()
347
    {
348
        $entity1 = new City(123, 'London');
349
        $entity2 = new Country(456, 'United Kingdom');
350
351
        $this->_unitOfWork->persist($entity1);
352
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
353
354
        $this->_unitOfWork->persist($entity2);
355
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
356
357
        $this->_unitOfWork->clear(Country::class);
358
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
359
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity2));
360
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($entity1));
361
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity2));
362
    }
363
364
    /**
365
     * @group #5579
366
     */
367
    public function testEntityChangeSetIsNotClearedAfterFlushOnSingleEntity() : void
368
    {
369
        $entity1 = new NotifyChangedEntity;
370
        $entity2 = new NotifyChangedEntity;
371
372
        $entity1->setData('thedata');
373
        $entity2->setData('thedata');
374
375
        $this->_unitOfWork->persist($entity1);
376
        $this->_unitOfWork->persist($entity2);
377
378
        $this->_unitOfWork->commit($entity1);
379
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity1));
380
        self::assertCount(1, $this->_unitOfWork->getEntityChangeSet($entity2));
381
    }
382
383
    /**
384
     * @group #5579
385
     */
386
    public function testEntityChangeSetIsNotClearedAfterFlushOnArrayOfEntities() : void
387
    {
388
        $entity1 = new NotifyChangedEntity;
389
        $entity2 = new NotifyChangedEntity;
390
        $entity3 = new NotifyChangedEntity;
391
392
        $entity1->setData('thedata');
393
        $entity2->setData('thedata');
394
        $entity3->setData('thedata');
395
396
        $this->_unitOfWork->persist($entity1);
397
        $this->_unitOfWork->persist($entity2);
398
        $this->_unitOfWork->persist($entity3);
399
400
        $this->_unitOfWork->commit([$entity1, $entity3]);
401
402
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity1));
403
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity3));
404
        self::assertCount(1, $this->_unitOfWork->getEntityChangeSet($entity2));
405
    }
406
407
    /**
408
     * Data Provider
409
     *
410
     * @return mixed[][]
411
     */
412
    public function invalidAssociationValuesDataProvider()
413
    {
414
        return [
415
            ['foo'],
416
            [['foo']],
417
            [''],
418
            [[]],
419
            [new stdClass()],
420
            [new ArrayCollection()],
421
        ];
422
    }
423
424
    /**
425
     * @dataProvider entitiesWithValidIdentifiersProvider
426
     *
427
     * @param object $entity
428
     * @param string $idHash
429
     *
430
     * @return void
431
     */
432
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash)
433
    {
434
        $this->_unitOfWork->persist($entity);
435
        $this->_unitOfWork->addToIdentityMap($entity);
436
437
        self::assertSame($entity, $this->_unitOfWork->getByIdHash($idHash, get_class($entity)));
438
    }
439
440
    public function entitiesWithValidIdentifiersProvider()
441
    {
442
        $emptyString = new EntityWithStringIdentifier();
443
444
        $emptyString->id = '';
445
446
        $nonEmptyString = new EntityWithStringIdentifier();
447
448
        $nonEmptyString->id = uniqid('id', true);
449
450
        $emptyStrings = new EntityWithCompositeStringIdentifier();
451
452
        $emptyStrings->id1 = '';
453
        $emptyStrings->id2 = '';
454
455
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
456
457
        $nonEmptyStrings->id1 = uniqid('id1', true);
458
        $nonEmptyStrings->id2 = uniqid('id2', true);
459
460
        $booleanTrue = new EntityWithBooleanIdentifier();
461
462
        $booleanTrue->id = true;
463
464
        $booleanFalse = new EntityWithBooleanIdentifier();
465
466
        $booleanFalse->id = false;
467
468
        return [
469
            'empty string, single field'     => [$emptyString, ''],
470
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
471
            'empty strings, two fields'      => [$emptyStrings, ' '],
472
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
473
            'boolean true'                   => [$booleanTrue, '1'],
474
            'boolean false'                  => [$booleanFalse, ''],
475
        ];
476
    }
477
478
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier()
479
    {
480
        $this->expectException(ORMInvalidArgumentException::class);
481
482
        $this->_unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
483
    }
484
485
    /**
486
     * @dataProvider entitiesWithInvalidIdentifiersProvider
487
     *
488
     * @param object $entity
489
     * @param array  $identifier
490
     *
491
     * @return void
492
     */
493
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier)
494
    {
495
        $this->expectException(ORMInvalidArgumentException::class);
496
497
        $this->_unitOfWork->registerManaged($entity, $identifier, []);
498
    }
499
500
501
    public function entitiesWithInvalidIdentifiersProvider()
502
    {
503
        $firstNullString  = new EntityWithCompositeStringIdentifier();
504
505
        $firstNullString->id2 = uniqid('id2', true);
506
507
        $secondNullString = new EntityWithCompositeStringIdentifier();
508
509
        $secondNullString->id1 = uniqid('id1', true);
510
511
        return [
512
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
513
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
514
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
515
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
516
        ];
517
    }
518
519
    /**
520
     * @group 5689
521
     * @group 1465
522
     */
523
    public function testObjectHashesOfMergedEntitiesAreNotUsedInOriginalEntityDataMap()
524
    {
525
        $user       = new CmsUser();
526
        $user->name = 'ocramius';
527
        $mergedUser = $this->_unitOfWork->merge($user);
528
529
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($user), 'No original data was stored');
530
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($mergedUser), 'No original data was stored');
531
532
533
        $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...
534
        $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...
535
536
        // force garbage collection of $user (frees the used object hashes, which may be recycled)
537
        gc_collect_cycles();
538
539
        $newUser       = new CmsUser();
540
        $newUser->name = 'ocramius';
541
542
        $this->_unitOfWork->persist($newUser);
543
544
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($newUser), 'No original data was stored');
545
    }
546
547
    /**
548
     * @group DDC-1955
549
     * @group 5570
550
     * @group 6174
551
     */
552
    public function testMergeWithNewEntityWillPersistItAndTriggerPrePersistListenersWithMergedEntityData()
553
    {
554
        $entity = new EntityWithRandomlyGeneratedField();
555
556
        $generatedFieldValue = $entity->generatedField;
557
558
        $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...
559
            ->eventManager
560
            ->expects(self::any())
561
            ->method('hasListeners')
562
            ->willReturnCallback(function ($eventName) {
563
                return $eventName === Events::prePersist;
564
            });
565
        $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...
566
            ->eventManager
567
            ->expects(self::once())
568
            ->method('dispatchEvent')
569
            ->with(
570
                self::anything(),
571
                self::callback(function (LifecycleEventArgs $args) use ($entity, $generatedFieldValue) {
572
                    /* @var $object EntityWithRandomlyGeneratedField */
573
                    $object = $args->getObject();
574
575
                    self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
576
                    self::assertNotSame($entity, $object);
577
                    self::assertSame($generatedFieldValue, $object->generatedField);
578
579
                    return true;
580
                })
581
            );
582
583
        /* @var $object EntityWithRandomlyGeneratedField */
584
        $object = $this->_unitOfWork->merge($entity);
585
586
        self::assertNotSame($object, $entity);
587
        self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
588
        self::assertSame($object->generatedField, $entity->generatedField);
589
    }
590
591
    /**
592
     * @group DDC-1955
593
     * @group 5570
594
     * @group 6174
595
     */
596
    public function testMergeWithExistingEntityWillNotPersistItNorTriggerPrePersistListeners()
597
    {
598
        $persistedEntity = new EntityWithRandomlyGeneratedField();
599
        $mergedEntity    = new EntityWithRandomlyGeneratedField();
600
601
        $mergedEntity->id = $persistedEntity->id;
602
        $mergedEntity->generatedField = mt_rand(
603
            $persistedEntity->generatedField + 1,
604
            $persistedEntity->generatedField + 1000
605
        );
606
607
        $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...
608
            ->eventManager
609
            ->expects(self::any())
610
            ->method('hasListeners')
611
            ->willReturnCallback(function ($eventName) {
612
                return $eventName === Events::prePersist;
613
            });
614
        $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...
615
616
        $this->_unitOfWork->registerManaged(
617
            $persistedEntity,
618
            ['id' => $persistedEntity->id],
619
            ['generatedField' => $persistedEntity->generatedField]
620
        );
621
622
        /* @var $merged EntityWithRandomlyGeneratedField */
623
        $merged = $this->_unitOfWork->merge($mergedEntity);
624
625
        self::assertSame($merged, $persistedEntity);
626
        self::assertSame($persistedEntity->generatedField, $mergedEntity->generatedField);
627
    }
628
629
    /**
630
     * Unlike next test, this one demonstrates that the problem does
631
     * not necessarily reproduce if all the pieces are being flushed together.
632
     *
633
     * @group DDC-2922
634
     * @group #1521
635
     */
636
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughCascadedAssociationsFirst()
637
    {
638
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
639
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithCascadingAssociation::class));
640
        $persister3 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
641
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
642
        $this->_unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
643
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
644
645
        $cascadePersisted = new CascadePersistedEntity();
646
        $cascading        = new EntityWithCascadingAssociation();
647
        $nonCascading     = new EntityWithNonCascadingAssociation();
648
649
        // First we persist and flush a EntityWithCascadingAssociation with
650
        // the cascading association not set. Having the "cascading path" involve
651
        // a non-new object is important to show that the ORM should be considering
652
        // cascades across entity changesets in subsequent flushes.
653
        $cascading->cascaded = $cascadePersisted;
654
        $nonCascading->cascaded = $cascadePersisted;
0 ignored issues
show
Bug introduced by
The property cascaded does not seem to exist. Did you mean nonCascaded?

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...
655
656
        $this->_unitOfWork->persist($cascading);
657
        $this->_unitOfWork->persist($nonCascading);
658
659
        $this->_unitOfWork->commit();
660
661
        $this->assertCount(1, $persister1->getInserts());
662
        $this->assertCount(1, $persister2->getInserts());
663
        $this->assertCount(1, $persister3->getInserts());
664
    }
665
666
    /**
667
     * This test exhibits the bug describe in the ticket, where an object that
668
     * ought to be reachable causes errors.
669
     *
670
     * @group DDC-2922
671
     * @group #1521
672
     */
673
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughNonCascadedAssociationsFirst()
674
    {
675
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
676
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithCascadingAssociation::class));
677
        $persister3 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
678
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
679
        $this->_unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
680
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
681
682
        $cascadePersisted = new CascadePersistedEntity();
683
        $cascading        = new EntityWithCascadingAssociation();
684
        $nonCascading     = new EntityWithNonCascadingAssociation();
685
686
        // First we persist and flush a EntityWithCascadingAssociation with
687
        // the cascading association not set. Having the "cascading path" involve
688
        // a non-new object is important to show that the ORM should be considering
689
        // cascades across entity changesets in subsequent flushes.
690
        $cascading->cascaded = null;
691
692
        $this->_unitOfWork->persist($cascading);
693
        $this->_unitOfWork->commit();
694
695
        self::assertCount(0, $persister1->getInserts());
696
        self::assertCount(1, $persister2->getInserts());
697
        self::assertCount(0, $persister3->getInserts());
698
699
        // Note that we have NOT directly persisted the CascadePersistedEntity,
700
        // and EntityWithNonCascadingAssociation does NOT have a configured
701
        // cascade-persist.
702
        $nonCascading->nonCascaded = $cascadePersisted;
703
704
        // However, EntityWithCascadingAssociation *does* have a cascade-persist
705
        // association, which ought to allow us to save the CascadePersistedEntity
706
        // anyway through that connection.
707
        $cascading->cascaded = $cascadePersisted;
708
709
        $this->_unitOfWork->persist($nonCascading);
710
        $this->_unitOfWork->commit();
711
712
        self::assertCount(1, $persister1->getInserts());
713
        self::assertCount(1, $persister2->getInserts());
714
        self::assertCount(1, $persister3->getInserts());
715
    }
716
717
718
    /**
719
     * This test exhibits the bug describe in the ticket, where an object that
720
     * ought to be reachable causes errors.
721
     *
722
     * @group DDC-2922
723
     * @group #1521
724
     */
725
    public function testPreviousDetectedIllegalNewNonCascadedEntitiesAreCleanedUpOnSubsequentCommits()
726
    {
727
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
728
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
729
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
730
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister2);
731
732
        $cascadePersisted = new CascadePersistedEntity();
733
        $nonCascading     = new EntityWithNonCascadingAssociation();
734
735
        // We explicitly cause the ORM to detect a non-persisted new entity in the association graph:
736
        $nonCascading->nonCascaded = $cascadePersisted;
737
738
        $this->_unitOfWork->persist($nonCascading);
739
740
        try {
741
            $this->_unitOfWork->commit();
742
743
            self::fail('An exception was supposed to be raised');
744
        } catch (ORMInvalidArgumentException $ignored) {
745
            self::assertEmpty($persister1->getInserts());
746
            self::assertEmpty($persister2->getInserts());
747
        }
748
749
        $this->_unitOfWork->clear();
750
        $this->_unitOfWork->persist(new CascadePersistedEntity());
751
        $this->_unitOfWork->commit();
752
753
        // Persistence operations should just recover normally:
754
        self::assertCount(1, $persister1->getInserts());
755
        self::assertCount(0, $persister2->getInserts());
756
    }
757
}
758
759
/**
760
 * @Entity
761
 */
762
class NotifyChangedEntity implements NotifyPropertyChanged
763
{
764
    private $_listeners = [];
765
    /**
766
     * @Id
767
     * @Column(type="integer")
768
     * @GeneratedValue
769
     */
770
    private $id;
771
    /**
772
     * @Column(type="string")
773
     */
774
    private $data;
775
776
    private $transient; // not persisted
777
778
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
779
    private $items;
780
781
    public function  __construct() {
782
        $this->items = new ArrayCollection;
783
    }
784
785
    public function getId() {
786
        return $this->id;
787
    }
788
789
    public function getItems() {
790
        return $this->items;
791
    }
792
793
    public function setTransient($value) {
794
        if ($value != $this->transient) {
795
            $this->_onPropertyChanged('transient', $this->transient, $value);
796
            $this->transient = $value;
797
        }
798
    }
799
800
    public function getData() {
801
        return $this->data;
802
    }
803
804
    public function setData($data) {
805
        if ($data != $this->data) {
806
            $this->_onPropertyChanged('data', $this->data, $data);
807
            $this->data = $data;
808
        }
809
    }
810
811
    public function addPropertyChangedListener(PropertyChangedListener $listener)
812
    {
813
        $this->_listeners[] = $listener;
814
    }
815
816
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
817
        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...
818
            foreach ($this->_listeners as $listener) {
819
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
820
            }
821
        }
822
    }
823
}
824
825
/** @Entity */
826
class NotifyChangedRelatedItem
827
{
828
    /**
829
     * @Id
830
     * @Column(type="integer")
831
     * @GeneratedValue
832
     */
833
    private $id;
834
835
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
836
    private $owner;
837
838
    public function getId() {
839
        return $this->id;
840
    }
841
842
    public function getOwner() {
843
        return $this->owner;
844
    }
845
846
    public function setOwner($owner) {
847
        $this->owner = $owner;
848
    }
849
}
850
851
/** @Entity */
852
class VersionedAssignedIdentifierEntity
853
{
854
    /**
855
     * @Id @Column(type="integer")
856
     */
857
    public $id;
858
    /**
859
     * @Version @Column(type="integer")
860
     */
861
    public $version;
862
}
863
864
/** @Entity */
865
class EntityWithStringIdentifier
866
{
867
    /**
868
     * @Id @Column(type="string")
869
     *
870
     * @var string|null
871
     */
872
    public $id;
873
}
874
875
/** @Entity */
876
class EntityWithBooleanIdentifier
877
{
878
    /**
879
     * @Id @Column(type="boolean")
880
     *
881
     * @var bool|null
882
     */
883
    public $id;
884
}
885
886
/** @Entity */
887
class EntityWithCompositeStringIdentifier
888
{
889
    /**
890
     * @Id @Column(type="string")
891
     *
892
     * @var string|null
893
     */
894
    public $id1;
895
896
    /**
897
     * @Id @Column(type="string")
898
     *
899
     * @var string|null
900
     */
901
    public $id2;
902
}
903
904
/** @Entity */
905
class EntityWithRandomlyGeneratedField
906
{
907
    /** @Id @Column(type="string") */
908
    public $id;
909
910
    /**
911
     * @Column(type="integer")
912
     */
913
    public $generatedField;
914
915
    public function __construct()
916
    {
917
        $this->id             = uniqid('id', true);
918
        $this->generatedField = mt_rand(0, 100000);
919
    }
920
}
921
922
/** @Entity */
923
class CascadePersistedEntity
924
{
925
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
926
    private $id;
927
928
    public function __construct()
929
    {
930
        $this->id = uniqid(self::class, true);
931
    }
932
}
933
934
/** @Entity */
935
class EntityWithCascadingAssociation
936
{
937
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
938
    private $id;
939
940
    /** @ManyToOne(targetEntity=CascadePersistedEntity::class, cascade={"persist"}) */
941
    public $cascaded;
942
943
    public function __construct()
944
    {
945
        $this->id = uniqid(self::class, true);
946
    }
947
}
948
949
/** @Entity */
950
class EntityWithNonCascadingAssociation
951
{
952
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
953
    private $id;
954
955
    /** @ManyToOne(targetEntity=CascadePersistedEntity::class) */
956
    public $nonCascaded;
957
958
    public function __construct()
959
    {
960
        $this->id = uniqid(self::class, true);
961
    }
962
}
963