Failed Conditions
Push — master ( 0345f7...a16dc6 )
by Guilherme
110:10 queued 103:07
created

testChangeTrackingNotifyIndividualCommit()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 22
nc 1
nop 0
dl 0
loc 33
rs 8.8571
c 0
b 0
f 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->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 exist on Doctrine\Common\Collections\ArrayCollection. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

192
        $this->assertTrue($entity->getItems()->/** @scrutinizer ignore-call */ isDirty());

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
    public function testChangeTrackingNotifyIndividualCommit()
200
    {
201
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata("Doctrine\Tests\ORM\NotifyChangedEntity"));
202
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedEntity', $persister);
203
        $itemPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata("Doctrine\Tests\ORM\NotifyChangedRelatedItem"));
204
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedRelatedItem', $itemPersister);
205
206
        $entity = new NotifyChangedEntity;
207
        $entity->setData('thedata');
208
209
        $entity2 = new NotifyChangedEntity;
210
        $entity2->setData('thedata');
211
212
        $this->_unitOfWork->persist($entity);
213
        $this->_unitOfWork->persist($entity2);
214
        $this->_unitOfWork->commit($entity);
215
        $this->_unitOfWork->commit();
216
217
        $this->assertEquals(2, count($persister->getInserts()));
218
219
        $persister->reset();
220
221
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
222
223
        $entity->setData('newdata');
224
        $entity2->setData('newdata');
225
226
        $this->_unitOfWork->commit($entity);
227
228
        $this->assertTrue($this->_unitOfWork->isScheduledForDirtyCheck($entity2));
229
        $this->assertEquals(array('data' => array('thedata', 'newdata')), $this->_unitOfWork->getEntityChangeSet($entity2));
230
        $this->assertFalse($this->_unitOfWork->isScheduledForDirtyCheck($entity));
231
        $this->assertEquals(array(), $this->_unitOfWork->getEntityChangeSet($entity));
232
    }
233
234 View Code Duplication
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier()
235
    {
236
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(VersionedAssignedIdentifierEntity::class));
237
        $this->_unitOfWork->setEntityPersister(VersionedAssignedIdentifierEntity::class, $persister);
238
239
        $e = new VersionedAssignedIdentifierEntity();
240
        $e->id = 42;
241
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($e));
242
        $this->assertFalse($persister->isExistsCalled());
243
    }
244
245
    public function testGetEntityStateWithAssignedIdentity()
246
    {
247
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CmsPhonenumber::class));
248
        $this->_unitOfWork->setEntityPersister(CmsPhonenumber::class, $persister);
249
250
        $ph = new CmsPhonenumber();
251
        $ph->phonenumber = '12345';
252
253
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($ph));
254
        $this->assertTrue($persister->isExistsCalled());
255
256
        $persister->reset();
257
258
        // if the entity is already managed the exists() check should be skipped
259
        $this->_unitOfWork->registerManaged($ph, ['phonenumber' => '12345'], []);
260
        $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($ph));
261
        $this->assertFalse($persister->isExistsCalled());
262
        $ph2 = new CmsPhonenumber();
263
        $ph2->phonenumber = '12345';
264
        $this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_unitOfWork->getEntityState($ph2));
265
        $this->assertFalse($persister->isExistsCalled());
266
    }
267
268
    /**
269
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
270
     */
271
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges()
272
    {
273
        // Setup fake persister and id generator
274
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
275
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
276
        $this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
277
278
        // Create a test user
279
        $user = new ForumUser();
280
        $user->name = 'Jasper';
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Doctrine\Tests\Models\Forum\ForumUser.
Loading history...
281
        $this->_unitOfWork->persist($user);
282
        $this->_unitOfWork->commit();
283
284
        // Schedule user for update without changes
285
        $this->_unitOfWork->scheduleForUpdate($user);
286
287
        self::assertNotEmpty($this->_unitOfWork->getScheduledEntityUpdates());
288
289
        // This commit should not raise an E_NOTICE
290
        $this->_unitOfWork->commit();
291
292
        self::assertEmpty($this->_unitOfWork->getScheduledEntityUpdates());
293
    }
294
295
    /**
296
     * @group DDC-1984
297
     */
298
    public function testLockWithoutEntityThrowsException()
299
    {
300
        $this->expectException(\InvalidArgumentException::class);
301
        $this->_unitOfWork->lock(null, null, null);
302
    }
303
304
    /**
305
     * @group DDC-3490
306
     *
307
     * @dataProvider invalidAssociationValuesDataProvider
308
     *
309
     * @param mixed $invalidValue
310
     */
311 View Code Duplication
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
312
    {
313
        $this->_unitOfWork->setEntityPersister(
314
            ForumUser::class,
315
            new EntityPersisterMock(
316
                $this->_emMock,
317
                $this->_emMock->getClassMetadata(ForumUser::class)
318
            )
319
        );
320
321
        $user           = new ForumUser();
322
        $user->username = 'John';
323
        $user->avatar   = $invalidValue;
324
325
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
326
327
        $this->_unitOfWork->persist($user);
328
    }
329
330
    /**
331
     * @group DDC-3490
332
     *
333
     * @dataProvider invalidAssociationValuesDataProvider
334
     *
335
     * @param mixed $invalidValue
336
     */
337
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
338
    {
339
        $metadata = $this->_emMock->getClassMetadata(ForumUser::class);
340
341
        $this->_unitOfWork->setEntityPersister(
342
            ForumUser::class,
343
            new EntityPersisterMock($this->_emMock, $metadata)
344
        );
345
346
        $user = new ForumUser();
347
348
        $this->_unitOfWork->persist($user);
349
350
        $user->username = 'John';
351
        $user->avatar   = $invalidValue;
352
353
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
354
355
        $this->_unitOfWork->computeChangeSet($metadata, $user);
356
    }
357
358
    /**
359
     * @group DDC-3619
360
     * @group 1338
361
     */
362
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected()
363
    {
364
        $entity     = new ForumUser();
365
        $entity->id = 123;
366
367
        $this->_unitOfWork->registerManaged($entity, ['id' => 123], []);
368
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
369
370
        $this->_unitOfWork->remove($entity);
371
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
372
373
        $this->_unitOfWork->persist($entity);
374
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
375
    }
376
377
    /**
378
     * @group 5849
379
     * @group 5850
380
     */
381
    public function testPersistedEntityAndClearManager()
382
    {
383
        $entity1 = new City(123, 'London');
384
        $entity2 = new Country(456, 'United Kingdom');
385
386
        $this->_unitOfWork->persist($entity1);
387
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
388
389
        $this->_unitOfWork->persist($entity2);
390
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
391
392
        $this->_unitOfWork->clear(Country::class);
393
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
394
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity2));
395
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($entity1));
396
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity2));
397
    }
398
399
    /**
400
     * @group #5579
401
     */
402
    public function testEntityChangeSetIsNotClearedAfterFlushOnSingleEntity() : void
403
    {
404
        $entity1 = new NotifyChangedEntity;
405
        $entity2 = new NotifyChangedEntity;
406
407
        $entity1->setData('thedata');
408
        $entity2->setData('thedata');
409
410
        $this->_unitOfWork->persist($entity1);
411
        $this->_unitOfWork->persist($entity2);
412
413
        $this->_unitOfWork->commit($entity1);
414
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity1));
415
        self::assertCount(1, $this->_unitOfWork->getEntityChangeSet($entity2));
416
    }
417
418
    /**
419
     * @group #5579
420
     */
421
    public function testEntityChangeSetIsNotClearedAfterFlushOnArrayOfEntities() : void
422
    {
423
        $entity1 = new NotifyChangedEntity;
424
        $entity2 = new NotifyChangedEntity;
425
        $entity3 = new NotifyChangedEntity;
426
427
        $entity1->setData('thedata');
428
        $entity2->setData('thedata');
429
        $entity3->setData('thedata');
430
431
        $this->_unitOfWork->persist($entity1);
432
        $this->_unitOfWork->persist($entity2);
433
        $this->_unitOfWork->persist($entity3);
434
435
        $this->_unitOfWork->commit([$entity1, $entity3]);
436
437
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity1));
438
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity3));
439
        self::assertCount(1, $this->_unitOfWork->getEntityChangeSet($entity2));
440
    }
441
442
    /**
443
     * Data Provider
444
     *
445
     * @return mixed[][]
446
     */
447
    public function invalidAssociationValuesDataProvider()
448
    {
449
        return [
450
            ['foo'],
451
            [['foo']],
452
            [''],
453
            [[]],
454
            [new stdClass()],
455
            [new ArrayCollection()],
456
        ];
457
    }
458
459
    /**
460
     * @dataProvider entitiesWithValidIdentifiersProvider
461
     *
462
     * @param object $entity
463
     * @param string $idHash
464
     *
465
     * @return void
466
     */
467
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash)
468
    {
469
        $this->_unitOfWork->persist($entity);
470
        $this->_unitOfWork->addToIdentityMap($entity);
471
472
        self::assertSame($entity, $this->_unitOfWork->getByIdHash($idHash, get_class($entity)));
473
    }
474
475
    public function entitiesWithValidIdentifiersProvider()
476
    {
477
        $emptyString = new EntityWithStringIdentifier();
478
479
        $emptyString->id = '';
480
481
        $nonEmptyString = new EntityWithStringIdentifier();
482
483
        $nonEmptyString->id = uniqid('id', true);
484
485
        $emptyStrings = new EntityWithCompositeStringIdentifier();
486
487
        $emptyStrings->id1 = '';
488
        $emptyStrings->id2 = '';
489
490
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
491
492
        $nonEmptyStrings->id1 = uniqid('id1', true);
493
        $nonEmptyStrings->id2 = uniqid('id2', true);
494
495
        $booleanTrue = new EntityWithBooleanIdentifier();
496
497
        $booleanTrue->id = true;
498
499
        $booleanFalse = new EntityWithBooleanIdentifier();
500
501
        $booleanFalse->id = false;
502
503
        return [
504
            'empty string, single field'     => [$emptyString, ''],
505
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
506
            'empty strings, two fields'      => [$emptyStrings, ' '],
507
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
508
            'boolean true'                   => [$booleanTrue, '1'],
509
            'boolean false'                  => [$booleanFalse, ''],
510
        ];
511
    }
512
513
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier()
514
    {
515
        $this->expectException(ORMInvalidArgumentException::class);
516
517
        $this->_unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
518
    }
519
520
    /**
521
     * @dataProvider entitiesWithInvalidIdentifiersProvider
522
     *
523
     * @param object $entity
524
     * @param array  $identifier
525
     *
526
     * @return void
527
     */
528
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier)
529
    {
530
        $this->expectException(ORMInvalidArgumentException::class);
531
532
        $this->_unitOfWork->registerManaged($entity, $identifier, []);
533
    }
534
535
536
    public function entitiesWithInvalidIdentifiersProvider()
537
    {
538
        $firstNullString  = new EntityWithCompositeStringIdentifier();
539
540
        $firstNullString->id2 = uniqid('id2', true);
541
542
        $secondNullString = new EntityWithCompositeStringIdentifier();
543
544
        $secondNullString->id1 = uniqid('id1', true);
545
546
        return [
547
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
548
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
549
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
550
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
551
        ];
552
    }
553
554
    /**
555
     * @group 5689
556
     * @group 1465
557
     */
558
    public function testObjectHashesOfMergedEntitiesAreNotUsedInOriginalEntityDataMap()
559
    {
560
        $user       = new CmsUser();
561
        $user->name = 'ocramius';
562
        $mergedUser = $this->_unitOfWork->merge($user);
563
564
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($user), 'No original data was stored');
565
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($mergedUser), 'No original data was stored');
566
567
568
        $user       = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $user is dead and can be removed.
Loading history...
569
        $mergedUser = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $mergedUser is dead and can be removed.
Loading history...
570
571
        // force garbage collection of $user (frees the used object hashes, which may be recycled)
572
        gc_collect_cycles();
573
574
        $newUser       = new CmsUser();
575
        $newUser->name = 'ocramius';
576
577
        $this->_unitOfWork->persist($newUser);
578
579
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($newUser), 'No original data was stored');
580
    }
581
582
    /**
583
     * @group DDC-1955
584
     * @group 5570
585
     * @group 6174
586
     */
587
    public function testMergeWithNewEntityWillPersistItAndTriggerPrePersistListenersWithMergedEntityData()
588
    {
589
        $entity = new EntityWithRandomlyGeneratedField();
590
591
        $generatedFieldValue = $entity->generatedField;
592
593
        $this
594
            ->eventManager
595
            ->expects(self::any())
596
            ->method('hasListeners')
597
            ->willReturnCallback(function ($eventName) {
598
                return $eventName === Events::prePersist;
599
            });
600
        $this
601
            ->eventManager
602
            ->expects(self::once())
603
            ->method('dispatchEvent')
604
            ->with(
605
                self::anything(),
0 ignored issues
show
Bug introduced by
self::anything() of type PHPUnit\Framework\Constraint\IsAnything is incompatible with the type array expected by parameter $arguments of PHPUnit\Framework\MockOb...nvocationMocker::with(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

605
                /** @scrutinizer ignore-type */ self::anything(),
Loading history...
606
                self::callback(function (LifecycleEventArgs $args) use ($entity, $generatedFieldValue) {
0 ignored issues
show
Bug introduced by
self::callback(function(...) { /* ... */ }) of type PHPUnit\Framework\Constraint\Callback is incompatible with the type array expected by parameter $arguments of PHPUnit\Framework\MockOb...nvocationMocker::with(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

606
                /** @scrutinizer ignore-type */ self::callback(function (LifecycleEventArgs $args) use ($entity, $generatedFieldValue) {
Loading history...
607
                    /* @var $object EntityWithRandomlyGeneratedField */
608
                    $object = $args->getObject();
609
610
                    self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
611
                    self::assertNotSame($entity, $object);
612
                    self::assertSame($generatedFieldValue, $object->generatedField);
613
614
                    return true;
615
                })
616
            );
617
618
        /* @var $object EntityWithRandomlyGeneratedField */
619
        $object = $this->_unitOfWork->merge($entity);
620
621
        self::assertNotSame($object, $entity);
622
        self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
623
        self::assertSame($object->generatedField, $entity->generatedField);
624
    }
625
626
    /**
627
     * @group DDC-1955
628
     * @group 5570
629
     * @group 6174
630
     */
631
    public function testMergeWithExistingEntityWillNotPersistItNorTriggerPrePersistListeners()
632
    {
633
        $persistedEntity = new EntityWithRandomlyGeneratedField();
634
        $mergedEntity    = new EntityWithRandomlyGeneratedField();
635
636
        $mergedEntity->id = $persistedEntity->id;
637
        $mergedEntity->generatedField = random_int(
638
            $persistedEntity->generatedField + 1,
639
            $persistedEntity->generatedField + 1000
640
        );
641
642
        $this
643
            ->eventManager
644
            ->expects(self::any())
645
            ->method('hasListeners')
646
            ->willReturnCallback(function ($eventName) {
647
                return $eventName === Events::prePersist;
648
            });
649
        $this->eventManager->expects(self::never())->method('dispatchEvent');
650
651
        $this->_unitOfWork->registerManaged(
652
            $persistedEntity,
653
            ['id' => $persistedEntity->id],
654
            ['generatedField' => $persistedEntity->generatedField]
655
        );
656
657
        /* @var $merged EntityWithRandomlyGeneratedField */
658
        $merged = $this->_unitOfWork->merge($mergedEntity);
659
660
        self::assertSame($merged, $persistedEntity);
661
        self::assertSame($persistedEntity->generatedField, $mergedEntity->generatedField);
662
    }
663
664
    /**
665
     * Unlike next test, this one demonstrates that the problem does
666
     * not necessarily reproduce if all the pieces are being flushed together.
667
     *
668
     * @group DDC-2922
669
     * @group #1521
670
     */
671
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughCascadedAssociationsFirst()
672
    {
673
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
674
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithCascadingAssociation::class));
675
        $persister3 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
676
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
677
        $this->_unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
678
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
679
680
        $cascadePersisted = new CascadePersistedEntity();
681
        $cascading        = new EntityWithCascadingAssociation();
682
        $nonCascading     = new EntityWithNonCascadingAssociation();
683
684
        // First we persist and flush a EntityWithCascadingAssociation with
685
        // the cascading association not set. Having the "cascading path" involve
686
        // a non-new object is important to show that the ORM should be considering
687
        // cascades across entity changesets in subsequent flushes.
688
        $cascading->cascaded = $cascadePersisted;
689
        $nonCascading->cascaded = $cascadePersisted;
0 ignored issues
show
Bug introduced by
The property cascaded does not seem to exist on Doctrine\Tests\ORM\Entit...NonCascadingAssociation.
Loading history...
690
691
        $this->_unitOfWork->persist($cascading);
692
        $this->_unitOfWork->persist($nonCascading);
693
694
        $this->_unitOfWork->commit();
695
696
        $this->assertCount(1, $persister1->getInserts());
697
        $this->assertCount(1, $persister2->getInserts());
698
        $this->assertCount(1, $persister3->getInserts());
699
    }
700
701
    /**
702
     * This test exhibits the bug describe in the ticket, where an object that
703
     * ought to be reachable causes errors.
704
     *
705
     * @group DDC-2922
706
     * @group #1521
707
     */
708
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughNonCascadedAssociationsFirst()
709
    {
710
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
711
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithCascadingAssociation::class));
712
        $persister3 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
713
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
714
        $this->_unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
715
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
716
717
        $cascadePersisted = new CascadePersistedEntity();
718
        $cascading        = new EntityWithCascadingAssociation();
719
        $nonCascading     = new EntityWithNonCascadingAssociation();
720
721
        // First we persist and flush a EntityWithCascadingAssociation with
722
        // the cascading association not set. Having the "cascading path" involve
723
        // a non-new object is important to show that the ORM should be considering
724
        // cascades across entity changesets in subsequent flushes.
725
        $cascading->cascaded = null;
726
727
        $this->_unitOfWork->persist($cascading);
728
        $this->_unitOfWork->commit();
729
730
        self::assertCount(0, $persister1->getInserts());
731
        self::assertCount(1, $persister2->getInserts());
732
        self::assertCount(0, $persister3->getInserts());
733
734
        // Note that we have NOT directly persisted the CascadePersistedEntity,
735
        // and EntityWithNonCascadingAssociation does NOT have a configured
736
        // cascade-persist.
737
        $nonCascading->nonCascaded = $cascadePersisted;
738
739
        // However, EntityWithCascadingAssociation *does* have a cascade-persist
740
        // association, which ought to allow us to save the CascadePersistedEntity
741
        // anyway through that connection.
742
        $cascading->cascaded = $cascadePersisted;
743
744
        $this->_unitOfWork->persist($nonCascading);
745
        $this->_unitOfWork->commit();
746
747
        self::assertCount(1, $persister1->getInserts());
748
        self::assertCount(1, $persister2->getInserts());
749
        self::assertCount(1, $persister3->getInserts());
750
    }
751
752
753
    /**
754
     * This test exhibits the bug describe in the ticket, where an object that
755
     * ought to be reachable causes errors.
756
     *
757
     * @group DDC-2922
758
     * @group #1521
759
     */
760
    public function testPreviousDetectedIllegalNewNonCascadedEntitiesAreCleanedUpOnSubsequentCommits()
761
    {
762
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
763
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
764
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
765
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister2);
766
767
        $cascadePersisted = new CascadePersistedEntity();
768
        $nonCascading     = new EntityWithNonCascadingAssociation();
769
770
        // We explicitly cause the ORM to detect a non-persisted new entity in the association graph:
771
        $nonCascading->nonCascaded = $cascadePersisted;
772
773
        $this->_unitOfWork->persist($nonCascading);
774
775
        try {
776
            $this->_unitOfWork->commit();
777
778
            self::fail('An exception was supposed to be raised');
779
        } catch (ORMInvalidArgumentException $ignored) {
780
            self::assertEmpty($persister1->getInserts());
781
            self::assertEmpty($persister2->getInserts());
782
        }
783
784
        $this->_unitOfWork->clear();
785
        $this->_unitOfWork->persist(new CascadePersistedEntity());
786
        $this->_unitOfWork->commit();
787
788
        // Persistence operations should just recover normally:
789
        self::assertCount(1, $persister1->getInserts());
790
        self::assertCount(0, $persister2->getInserts());
791
    }
792
}
793
794
/**
795
 * @Entity
796
 */
797
class NotifyChangedEntity implements NotifyPropertyChanged
798
{
799
    private $_listeners = [];
800
    /**
801
     * @Id
802
     * @Column(type="integer")
803
     * @GeneratedValue
804
     */
805
    private $id;
806
    /**
807
     * @Column(type="string")
808
     */
809
    private $data;
810
811
    private $transient; // not persisted
812
813
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
814
    private $items;
815
816
    public function  __construct() {
817
        $this->items = new ArrayCollection;
818
    }
819
820
    public function getId() {
821
        return $this->id;
822
    }
823
824
    public function getItems() {
825
        return $this->items;
826
    }
827
828
    public function setTransient($value) {
829
        if ($value != $this->transient) {
830
            $this->_onPropertyChanged('transient', $this->transient, $value);
831
            $this->transient = $value;
832
        }
833
    }
834
835
    public function getData() {
836
        return $this->data;
837
    }
838
839
    public function setData($data) {
840
        if ($data != $this->data) {
841
            $this->_onPropertyChanged('data', $this->data, $data);
842
            $this->data = $data;
843
        }
844
    }
845
846
    public function addPropertyChangedListener(PropertyChangedListener $listener)
847
    {
848
        $this->_listeners[] = $listener;
849
    }
850
851
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
852
        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...
853
            foreach ($this->_listeners as $listener) {
854
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
855
            }
856
        }
857
    }
858
}
859
860
/** @Entity */
861
class NotifyChangedRelatedItem
862
{
863
    /**
864
     * @Id
865
     * @Column(type="integer")
866
     * @GeneratedValue
867
     */
868
    private $id;
869
870
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
871
    private $owner;
872
873
    public function getId() {
874
        return $this->id;
875
    }
876
877
    public function getOwner() {
878
        return $this->owner;
879
    }
880
881
    public function setOwner($owner) {
882
        $this->owner = $owner;
883
    }
884
}
885
886
/** @Entity */
887
class VersionedAssignedIdentifierEntity
888
{
889
    /**
890
     * @Id @Column(type="integer")
891
     */
892
    public $id;
893
    /**
894
     * @Version @Column(type="integer")
895
     */
896
    public $version;
897
}
898
899
/** @Entity */
900
class EntityWithStringIdentifier
901
{
902
    /**
903
     * @Id @Column(type="string")
904
     *
905
     * @var string|null
906
     */
907
    public $id;
908
}
909
910
/** @Entity */
911
class EntityWithBooleanIdentifier
912
{
913
    /**
914
     * @Id @Column(type="boolean")
915
     *
916
     * @var bool|null
917
     */
918
    public $id;
919
}
920
921
/** @Entity */
922
class EntityWithCompositeStringIdentifier
923
{
924
    /**
925
     * @Id @Column(type="string")
926
     *
927
     * @var string|null
928
     */
929
    public $id1;
930
931
    /**
932
     * @Id @Column(type="string")
933
     *
934
     * @var string|null
935
     */
936
    public $id2;
937
}
938
939
/** @Entity */
940
class EntityWithRandomlyGeneratedField
941
{
942
    /** @Id @Column(type="string") */
943
    public $id;
944
945
    /**
946
     * @Column(type="integer")
947
     */
948
    public $generatedField;
949
950
    public function __construct()
951
    {
952
        $this->id             = uniqid('id', true);
953
        $this->generatedField = random_int(0, 100000);
954
    }
955
}
956
957
/** @Entity */
958
class CascadePersistedEntity
959
{
960
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
961
    private $id;
962
963
    public function __construct()
964
    {
965
        $this->id = uniqid(self::class, true);
966
    }
967
}
968
969
/** @Entity */
970
class EntityWithCascadingAssociation
971
{
972
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
973
    private $id;
974
975
    /** @ManyToOne(targetEntity=CascadePersistedEntity::class, cascade={"persist"}) */
976
    public $cascaded;
977
978
    public function __construct()
979
    {
980
        $this->id = uniqid(self::class, true);
981
    }
982
}
983
984
/** @Entity */
985
class EntityWithNonCascadingAssociation
986
{
987
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
988
    private $id;
989
990
    /** @ManyToOne(targetEntity=CascadePersistedEntity::class) */
991
    public $nonCascaded;
992
993
    public function __construct()
994
    {
995
        $this->id = uniqid(self::class, true);
996
    }
997
}
998