Passed
Pull Request — 2.6 (#7586)
by
unknown
06:35
created

testPersistedEntityAndClearManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

193
        $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...
194
        $this->_unitOfWork->commit();
195
        $updates = $itemPersister->getUpdates();
196
        $this->assertEquals(1, count($updates));
197
        $this->assertTrue($updates[0] === $item);
198
    }
199
200
    public function testChangeTrackingNotifyIndividualCommit()
201
    {
202
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata("Doctrine\Tests\ORM\NotifyChangedEntity"));
203
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedEntity', $persister);
204
        $itemPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata("Doctrine\Tests\ORM\NotifyChangedRelatedItem"));
205
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedRelatedItem', $itemPersister);
206
207
        $entity = new NotifyChangedEntity;
208
        $entity->setData('thedata');
209
210
        $entity2 = new NotifyChangedEntity;
211
        $entity2->setData('thedata');
212
213
        $this->_unitOfWork->persist($entity);
214
        $this->_unitOfWork->persist($entity2);
215
        $this->_unitOfWork->commit($entity);
216
        $this->_unitOfWork->commit();
217
218
        $this->assertEquals(2, count($persister->getInserts()));
219
220
        $persister->reset();
221
222
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
223
224
        $entity->setData('newdata');
225
        $entity2->setData('newdata');
226
227
        $this->_unitOfWork->commit($entity);
228
229
        $this->assertTrue($this->_unitOfWork->isScheduledForDirtyCheck($entity2));
230
        $this->assertEquals(array('data' => array('thedata', 'newdata')), $this->_unitOfWork->getEntityChangeSet($entity2));
231
        $this->assertFalse($this->_unitOfWork->isScheduledForDirtyCheck($entity));
232
        $this->assertEquals(array(), $this->_unitOfWork->getEntityChangeSet($entity));
233
    }
234
235
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier()
236
    {
237
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(VersionedAssignedIdentifierEntity::class));
238
        $this->_unitOfWork->setEntityPersister(VersionedAssignedIdentifierEntity::class, $persister);
239
240
        $e = new VersionedAssignedIdentifierEntity();
241
        $e->id = 42;
242
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($e));
243
        $this->assertFalse($persister->isExistsCalled());
244
    }
245
246
    public function testGetEntityStateWithAssignedIdentity()
247
    {
248
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CmsPhonenumber::class));
249
        $this->_unitOfWork->setEntityPersister(CmsPhonenumber::class, $persister);
250
251
        $ph = new CmsPhonenumber();
252
        $ph->phonenumber = '12345';
253
254
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($ph));
255
        $this->assertTrue($persister->isExistsCalled());
256
257
        $persister->reset();
258
259
        // if the entity is already managed the exists() check should be skipped
260
        $this->_unitOfWork->registerManaged($ph, ['phonenumber' => '12345'], []);
261
        $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($ph));
262
        $this->assertFalse($persister->isExistsCalled());
263
        $ph2 = new CmsPhonenumber();
264
        $ph2->phonenumber = '12345';
265
        $this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_unitOfWork->getEntityState($ph2));
266
        $this->assertFalse($persister->isExistsCalled());
267
    }
268
269
    /**
270
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
271
     */
272
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges()
273
    {
274
        // Setup fake persister and id generator
275
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
276
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
277
        $this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
278
279
        // Create a test user
280
        $user = new ForumUser();
281
        $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...
282
        $this->_unitOfWork->persist($user);
283
        $this->_unitOfWork->commit();
284
285
        // Schedule user for update without changes
286
        $this->_unitOfWork->scheduleForUpdate($user);
287
288
        self::assertNotEmpty($this->_unitOfWork->getScheduledEntityUpdates());
289
290
        // This commit should not raise an E_NOTICE
291
        $this->_unitOfWork->commit();
292
293
        self::assertEmpty($this->_unitOfWork->getScheduledEntityUpdates());
294
    }
295
296
    /**
297
     * @group DDC-1984
298
     */
299
    public function testLockWithoutEntityThrowsException()
300
    {
301
        $this->expectException(\InvalidArgumentException::class);
302
        $this->_unitOfWork->lock(null, null, null);
303
    }
304
305
    /**
306
     * @group DDC-3490
307
     *
308
     * @dataProvider invalidAssociationValuesDataProvider
309
     *
310
     * @param mixed $invalidValue
311
     */
312
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
313
    {
314
        $this->_unitOfWork->setEntityPersister(
315
            ForumUser::class,
316
            new EntityPersisterMock(
317
                $this->_emMock,
318
                $this->_emMock->getClassMetadata(ForumUser::class)
319
            )
320
        );
321
322
        $user           = new ForumUser();
323
        $user->username = 'John';
324
        $user->avatar   = $invalidValue;
325
326
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
327
328
        $this->_unitOfWork->persist($user);
329
    }
330
331
    /**
332
     * @group DDC-3490
333
     *
334
     * @dataProvider invalidAssociationValuesDataProvider
335
     *
336
     * @param mixed $invalidValue
337
     */
338
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
339
    {
340
        $metadata = $this->_emMock->getClassMetadata(ForumUser::class);
341
342
        $this->_unitOfWork->setEntityPersister(
343
            ForumUser::class,
344
            new EntityPersisterMock($this->_emMock, $metadata)
345
        );
346
347
        $user = new ForumUser();
348
349
        $this->_unitOfWork->persist($user);
350
351
        $user->username = 'John';
352
        $user->avatar   = $invalidValue;
353
354
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
355
356
        $this->_unitOfWork->computeChangeSet($metadata, $user);
357
    }
358
359
    /**
360
     * @group DDC-3619
361
     * @group 1338
362
     */
363
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected()
364
    {
365
        $entity     = new ForumUser();
366
        $entity->id = 123;
367
368
        $this->_unitOfWork->registerManaged($entity, ['id' => 123], []);
369
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
370
371
        $this->_unitOfWork->remove($entity);
372
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
373
374
        $this->_unitOfWork->persist($entity);
375
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
376
    }
377
378
    /**
379
     * @group 5849
380
     * @group 5850
381
     */
382
    public function testPersistedEntityAndClearManager()
383
    {
384
        $entity1 = new City(123, 'London');
385
        $entity2 = new Country(456, 'United Kingdom');
386
387
        $this->_unitOfWork->persist($entity1);
388
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
389
390
        $this->_unitOfWork->persist($entity2);
391
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
392
393
        $this->_unitOfWork->clear(Country::class);
394
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
395
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity2));
396
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($entity1));
397
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity2));
398
    }
399
400
    /**
401
     * @group #5579
402
     */
403
    public function testEntityChangeSetIsNotClearedAfterFlushOnSingleEntity() : void
404
    {
405
        $entity1 = new NotifyChangedEntity;
406
        $entity2 = new NotifyChangedEntity;
407
408
        $entity1->setData('thedata');
409
        $entity2->setData('thedata');
410
411
        $this->_unitOfWork->persist($entity1);
412
        $this->_unitOfWork->persist($entity2);
413
414
        $this->_unitOfWork->commit($entity1);
415
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity1));
416
        self::assertCount(1, $this->_unitOfWork->getEntityChangeSet($entity2));
417
    }
418
419
    /**
420
     * @group #5579
421
     */
422
    public function testEntityChangeSetIsNotClearedAfterFlushOnArrayOfEntities() : void
423
    {
424
        $entity1 = new NotifyChangedEntity;
425
        $entity2 = new NotifyChangedEntity;
426
        $entity3 = new NotifyChangedEntity;
427
428
        $entity1->setData('thedata');
429
        $entity2->setData('thedata');
430
        $entity3->setData('thedata');
431
432
        $this->_unitOfWork->persist($entity1);
433
        $this->_unitOfWork->persist($entity2);
434
        $this->_unitOfWork->persist($entity3);
435
436
        $this->_unitOfWork->commit([$entity1, $entity3]);
437
438
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity1));
439
        self::assertEmpty($this->_unitOfWork->getEntityChangeSet($entity3));
440
        self::assertCount(1, $this->_unitOfWork->getEntityChangeSet($entity2));
441
    }
442
443
    /**
444
     * Data Provider
445
     *
446
     * @return mixed[][]
447
     */
448
    public function invalidAssociationValuesDataProvider()
449
    {
450
        return [
451
            ['foo'],
452
            [['foo']],
453
            [''],
454
            [[]],
455
            [new stdClass()],
456
            [new ArrayCollection()],
457
        ];
458
    }
459
460
    /**
461
     * @dataProvider entitiesWithValidIdentifiersProvider
462
     *
463
     * @param object $entity
464
     * @param string $idHash
465
     *
466
     * @return void
467
     */
468
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash)
469
    {
470
        $this->_unitOfWork->persist($entity);
471
        $this->_unitOfWork->addToIdentityMap($entity);
472
473
        self::assertSame($entity, $this->_unitOfWork->getByIdHash($idHash, get_class($entity)));
474
    }
475
476
    public function entitiesWithValidIdentifiersProvider()
477
    {
478
        $emptyString = new EntityWithStringIdentifier();
479
480
        $emptyString->id = '';
481
482
        $nonEmptyString = new EntityWithStringIdentifier();
483
484
        $nonEmptyString->id = uniqid('id', true);
485
486
        $emptyStrings = new EntityWithCompositeStringIdentifier();
487
488
        $emptyStrings->id1 = '';
489
        $emptyStrings->id2 = '';
490
491
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
492
493
        $nonEmptyStrings->id1 = uniqid('id1', true);
494
        $nonEmptyStrings->id2 = uniqid('id2', true);
495
496
        $booleanTrue = new EntityWithBooleanIdentifier();
497
498
        $booleanTrue->id = true;
499
500
        $booleanFalse = new EntityWithBooleanIdentifier();
501
502
        $booleanFalse->id = false;
503
504
        return [
505
            'empty string, single field'     => [$emptyString, ''],
506
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
507
            'empty strings, two fields'      => [$emptyStrings, ' '],
508
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
509
            'boolean true'                   => [$booleanTrue, '1'],
510
            'boolean false'                  => [$booleanFalse, ''],
511
        ];
512
    }
513
514
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier()
515
    {
516
        $this->expectException(ORMInvalidArgumentException::class);
517
518
        $this->_unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
519
    }
520
521
    /**
522
     * @dataProvider entitiesWithInvalidIdentifiersProvider
523
     *
524
     * @param object $entity
525
     * @param array  $identifier
526
     *
527
     * @return void
528
     */
529
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier)
530
    {
531
        $this->expectException(ORMInvalidArgumentException::class);
532
533
        $this->_unitOfWork->registerManaged($entity, $identifier, []);
534
    }
535
536
537
    public function entitiesWithInvalidIdentifiersProvider()
538
    {
539
        $firstNullString  = new EntityWithCompositeStringIdentifier();
540
541
        $firstNullString->id2 = uniqid('id2', true);
542
543
        $secondNullString = new EntityWithCompositeStringIdentifier();
544
545
        $secondNullString->id1 = uniqid('id1', true);
546
547
        return [
548
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
549
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
550
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
551
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
552
        ];
553
    }
554
555
    /**
556
     * @group 5689
557
     * @group 1465
558
     */
559
    public function testObjectHashesOfMergedEntitiesAreNotUsedInOriginalEntityDataMap()
560
    {
561
        $user       = new CmsUser();
562
        $user->name = 'ocramius';
563
        $mergedUser = $this->_unitOfWork->merge($user);
564
565
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($user), 'No original data was stored');
566
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($mergedUser), 'No original data was stored');
567
568
569
        $user       = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $user is dead and can be removed.
Loading history...
570
        $mergedUser = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $mergedUser is dead and can be removed.
Loading history...
571
572
        // force garbage collection of $user (frees the used object hashes, which may be recycled)
573
        gc_collect_cycles();
574
575
        $newUser       = new CmsUser();
576
        $newUser->name = 'ocramius';
577
578
        $this->_unitOfWork->persist($newUser);
579
580
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($newUser), 'No original data was stored');
581
    }
582
583
    /**
584
     * @group DDC-1955
585
     * @group 5570
586
     * @group 6174
587
     */
588
    public function testMergeWithNewEntityWillPersistItAndTriggerPrePersistListenersWithMergedEntityData()
589
    {
590
        $entity = new EntityWithRandomlyGeneratedField();
591
592
        $generatedFieldValue = $entity->generatedField;
593
594
        $this
595
            ->eventManager
596
            ->expects(self::any())
597
            ->method('hasListeners')
598
            ->willReturnCallback(function ($eventName) {
599
                return $eventName === Events::prePersist;
600
            });
601
        $this
602
            ->eventManager
603
            ->expects(self::once())
604
            ->method('dispatchEvent')
605
            ->with(
606
                self::anything(),
607
                self::callback(function (LifecycleEventArgs $args) use ($entity, $generatedFieldValue) {
608
                    /* @var $object EntityWithRandomlyGeneratedField */
609
                    $object = $args->getObject();
610
611
                    self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
612
                    self::assertNotSame($entity, $object);
613
                    self::assertSame($generatedFieldValue, $object->generatedField);
614
615
                    return true;
616
                })
617
            );
618
619
        /* @var $object EntityWithRandomlyGeneratedField */
620
        $object = $this->_unitOfWork->merge($entity);
621
622
        self::assertNotSame($object, $entity);
623
        self::assertInstanceOf(EntityWithRandomlyGeneratedField::class, $object);
624
        self::assertSame($object->generatedField, $entity->generatedField);
625
    }
626
627
    /**
628
     * @group DDC-1955
629
     * @group 5570
630
     * @group 6174
631
     */
632
    public function testMergeWithExistingEntityWillNotPersistItNorTriggerPrePersistListeners()
633
    {
634
        $persistedEntity = new EntityWithRandomlyGeneratedField();
635
        $mergedEntity    = new EntityWithRandomlyGeneratedField();
636
637
        $mergedEntity->id = $persistedEntity->id;
638
        $mergedEntity->generatedField = random_int(
639
            $persistedEntity->generatedField + 1,
640
            $persistedEntity->generatedField + 1000
641
        );
642
643
        $this
644
            ->eventManager
645
            ->expects(self::any())
646
            ->method('hasListeners')
647
            ->willReturnCallback(function ($eventName) {
648
                return $eventName === Events::prePersist;
649
            });
650
        $this->eventManager->expects(self::never())->method('dispatchEvent');
651
652
        $this->_unitOfWork->registerManaged(
653
            $persistedEntity,
654
            ['id' => $persistedEntity->id],
655
            ['generatedField' => $persistedEntity->generatedField]
656
        );
657
658
        /* @var $merged EntityWithRandomlyGeneratedField */
659
        $merged = $this->_unitOfWork->merge($mergedEntity);
660
661
        self::assertSame($merged, $persistedEntity);
662
        self::assertSame($persistedEntity->generatedField, $mergedEntity->generatedField);
663
    }
664
665
    /**
666
     * Unlike next test, this one demonstrates that the problem does
667
     * not necessarily reproduce if all the pieces are being flushed together.
668
     *
669
     * @group DDC-2922
670
     * @group #1521
671
     */
672
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughCascadedAssociationsFirst()
673
    {
674
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
675
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithCascadingAssociation::class));
676
        $persister3 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
677
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
678
        $this->_unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
679
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
680
681
        $cascadePersisted = new CascadePersistedEntity();
682
        $cascading        = new EntityWithCascadingAssociation();
683
        $nonCascading     = new EntityWithNonCascadingAssociation();
684
685
        // First we persist and flush a EntityWithCascadingAssociation with
686
        // the cascading association not set. Having the "cascading path" involve
687
        // a non-new object is important to show that the ORM should be considering
688
        // cascades across entity changesets in subsequent flushes.
689
        $cascading->cascaded = $cascadePersisted;
690
        $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...
691
692
        $this->_unitOfWork->persist($cascading);
693
        $this->_unitOfWork->persist($nonCascading);
694
695
        $this->_unitOfWork->commit();
696
697
        $this->assertCount(1, $persister1->getInserts());
698
        $this->assertCount(1, $persister2->getInserts());
699
        $this->assertCount(1, $persister3->getInserts());
700
    }
701
702
    /**
703
     * This test exhibits the bug describe in the ticket, where an object that
704
     * ought to be reachable causes errors.
705
     *
706
     * @group DDC-2922
707
     * @group #1521
708
     */
709
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughNonCascadedAssociationsFirst()
710
    {
711
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
712
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithCascadingAssociation::class));
713
        $persister3 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
714
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
715
        $this->_unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
716
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
717
718
        $cascadePersisted = new CascadePersistedEntity();
719
        $cascading        = new EntityWithCascadingAssociation();
720
        $nonCascading     = new EntityWithNonCascadingAssociation();
721
722
        // First we persist and flush a EntityWithCascadingAssociation with
723
        // the cascading association not set. Having the "cascading path" involve
724
        // a non-new object is important to show that the ORM should be considering
725
        // cascades across entity changesets in subsequent flushes.
726
        $cascading->cascaded = null;
727
728
        $this->_unitOfWork->persist($cascading);
729
        $this->_unitOfWork->commit();
730
731
        self::assertCount(0, $persister1->getInserts());
732
        self::assertCount(1, $persister2->getInserts());
733
        self::assertCount(0, $persister3->getInserts());
734
735
        // Note that we have NOT directly persisted the CascadePersistedEntity,
736
        // and EntityWithNonCascadingAssociation does NOT have a configured
737
        // cascade-persist.
738
        $nonCascading->nonCascaded = $cascadePersisted;
739
740
        // However, EntityWithCascadingAssociation *does* have a cascade-persist
741
        // association, which ought to allow us to save the CascadePersistedEntity
742
        // anyway through that connection.
743
        $cascading->cascaded = $cascadePersisted;
744
745
        $this->_unitOfWork->persist($nonCascading);
746
        $this->_unitOfWork->commit();
747
748
        self::assertCount(1, $persister1->getInserts());
749
        self::assertCount(1, $persister2->getInserts());
750
        self::assertCount(1, $persister3->getInserts());
751
    }
752
753
754
    /**
755
     * This test exhibits the bug describe in the ticket, where an object that
756
     * ought to be reachable causes errors.
757
     *
758
     * @group DDC-2922
759
     * @group #1521
760
     */
761
    public function testPreviousDetectedIllegalNewNonCascadedEntitiesAreCleanedUpOnSubsequentCommits()
762
    {
763
        $persister1 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(CascadePersistedEntity::class));
764
        $persister2 = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
765
        $this->_unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
766
        $this->_unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister2);
767
768
        $cascadePersisted = new CascadePersistedEntity();
769
        $nonCascading     = new EntityWithNonCascadingAssociation();
770
771
        // We explicitly cause the ORM to detect a non-persisted new entity in the association graph:
772
        $nonCascading->nonCascaded = $cascadePersisted;
773
774
        $this->_unitOfWork->persist($nonCascading);
775
776
        try {
777
            $this->_unitOfWork->commit();
778
779
            self::fail('An exception was supposed to be raised');
780
        } catch (ORMInvalidArgumentException $ignored) {
781
            self::assertEmpty($persister1->getInserts());
782
            self::assertEmpty($persister2->getInserts());
783
        }
784
785
        $this->_unitOfWork->clear();
786
        $this->_unitOfWork->persist(new CascadePersistedEntity());
787
        $this->_unitOfWork->commit();
788
789
        // Persistence operations should just recover normally:
790
        self::assertCount(1, $persister1->getInserts());
791
        self::assertCount(0, $persister2->getInserts());
792
    }
793
794
    /**
795
     * @group #7583
796
     * @throws \Exception
797
     */
798
    public function testUpdateEntityWithObjectProperty(): void
799
    {
800
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(Flight::class));
801
        $this->_unitOfWork->setEntityPersister(Flight::class, $persister);
802
803
        $departure = '2019-01-26 14:20:00';
804
805
        $flight = new Flight('Novosibirsk', 'Moscow', $departure);
806
        $this->_unitOfWork->persist($flight);
807
        $this->_unitOfWork->commit();
808
809
        $persister->reset();
810
811
        //set equal value
812
        $flight->setDeparture(new DateTime($departure));
813
        $this->_unitOfWork->persist($flight);
814
        $this->_unitOfWork->commit();
815
816
        self::assertCount(0, $persister->getUpdates());
817
    }
818
}
819
820
/**
821
 * @Entity
822
 */
823
class NotifyChangedEntity implements NotifyPropertyChanged
824
{
825
    private $_listeners = [];
826
    /**
827
     * @Id
828
     * @Column(type="integer")
829
     * @GeneratedValue
830
     */
831
    private $id;
832
    /**
833
     * @Column(type="string")
834
     */
835
    private $data;
836
837
    private $transient; // not persisted
838
839
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
840
    private $items;
841
842
    public function  __construct() {
843
        $this->items = new ArrayCollection;
844
    }
845
846
    public function getId() {
847
        return $this->id;
848
    }
849
850
    public function getItems() {
851
        return $this->items;
852
    }
853
854
    public function setTransient($value) {
855
        if ($value != $this->transient) {
856
            $this->_onPropertyChanged('transient', $this->transient, $value);
857
            $this->transient = $value;
858
        }
859
    }
860
861
    public function getData() {
862
        return $this->data;
863
    }
864
865
    public function setData($data) {
866
        if ($data != $this->data) {
867
            $this->_onPropertyChanged('data', $this->data, $data);
868
            $this->data = $data;
869
        }
870
    }
871
872
    public function addPropertyChangedListener(PropertyChangedListener $listener)
873
    {
874
        $this->_listeners[] = $listener;
875
    }
876
877
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
878
        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...
879
            foreach ($this->_listeners as $listener) {
880
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
881
            }
882
        }
883
    }
884
}
885
886
/** @Entity */
887
class NotifyChangedRelatedItem
888
{
889
    /**
890
     * @Id
891
     * @Column(type="integer")
892
     * @GeneratedValue
893
     */
894
    private $id;
895
896
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
897
    private $owner;
898
899
    public function getId() {
900
        return $this->id;
901
    }
902
903
    public function getOwner() {
904
        return $this->owner;
905
    }
906
907
    public function setOwner($owner) {
908
        $this->owner = $owner;
909
    }
910
}
911
912
/** @Entity */
913
class VersionedAssignedIdentifierEntity
914
{
915
    /**
916
     * @Id @Column(type="integer")
917
     */
918
    public $id;
919
    /**
920
     * @Version @Column(type="integer")
921
     */
922
    public $version;
923
}
924
925
/** @Entity */
926
class EntityWithStringIdentifier
927
{
928
    /**
929
     * @Id @Column(type="string")
930
     *
931
     * @var string|null
932
     */
933
    public $id;
934
}
935
936
/** @Entity */
937
class EntityWithBooleanIdentifier
938
{
939
    /**
940
     * @Id @Column(type="boolean")
941
     *
942
     * @var bool|null
943
     */
944
    public $id;
945
}
946
947
/** @Entity */
948
class EntityWithCompositeStringIdentifier
949
{
950
    /**
951
     * @Id @Column(type="string")
952
     *
953
     * @var string|null
954
     */
955
    public $id1;
956
957
    /**
958
     * @Id @Column(type="string")
959
     *
960
     * @var string|null
961
     */
962
    public $id2;
963
}
964
965
/** @Entity */
966
class EntityWithRandomlyGeneratedField
967
{
968
    /** @Id @Column(type="string") */
969
    public $id;
970
971
    /**
972
     * @Column(type="integer")
973
     */
974
    public $generatedField;
975
976
    public function __construct()
977
    {
978
        $this->id             = uniqid('id', true);
979
        $this->generatedField = random_int(0, 100000);
980
    }
981
}
982
983
/** @Entity */
984
class CascadePersistedEntity
985
{
986
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
987
    private $id;
988
989
    public function __construct()
990
    {
991
        $this->id = uniqid(self::class, true);
992
    }
993
}
994
995
/** @Entity */
996
class EntityWithCascadingAssociation
997
{
998
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
999
    private $id;
1000
1001
    /** @ManyToOne(targetEntity=CascadePersistedEntity::class, cascade={"persist"}) */
1002
    public $cascaded;
1003
1004
    public function __construct()
1005
    {
1006
        $this->id = uniqid(self::class, true);
1007
    }
1008
}
1009
1010
/** @Entity */
1011
class EntityWithNonCascadingAssociation
1012
{
1013
    /** @Id @Column(type="string") @GeneratedValue(strategy="NONE") */
1014
    private $id;
1015
1016
    /** @ManyToOne(targetEntity=CascadePersistedEntity::class) */
1017
    public $nonCascaded;
1018
1019
    public function __construct()
1020
    {
1021
        $this->id = uniqid(self::class, true);
1022
    }
1023
}
1024
1025
/**
1026
 * @Entity
1027
 */
1028
class Flight
1029
{
1030
    /**
1031
     * @var string
1032
     *
1033
     * @Id
1034
     * @Column(type="string")
1035
     */
1036
    private $leavingFrom;
1037
1038
    /**
1039
     * @var string
1040
     *
1041
     * @Id
1042
     * @Column(type="string")
1043
     */
1044
    private $goingTo;
1045
1046
    /**
1047
     * @var DateTime
1048
     *
1049
     * @Column(type="datetime")
1050
     */
1051
    private $departure;
1052
1053
    /**
1054
     * Flight constructor.
1055
     *
1056
     * @param string $leavingFrom
1057
     * @param string $goingTo
1058
     * @param string $departure
1059
     *
1060
     * @throws \Exception
1061
     */
1062
    public function __construct(string $leavingFrom, string $goingTo, string $departure)
1063
    {
1064
        $this->goingTo = $goingTo;
1065
        $this->leavingFrom = $leavingFrom;
1066
        $this->departure = new DateTime($departure);
1067
    }
1068
1069
    /**
1070
     * @param DateTime $departure
1071
     */
1072
    public function setDeparture(DateTime $departure)
1073
    {
1074
        $this->departure = $departure;
1075
    }
1076
}
1077