Failed Conditions
Pull Request — master (#7130)
by Michael
13:25 queued 18s
created

UnitOfWorkTest   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 660
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 660
rs 9.8348
c 0
b 0
f 0
wmc 26

25 Methods

Rating   Name   Duplication   Size   Complexity  
B testSavingSingleEntityWithIdentityColumnForcesInsert() 0 33 1
B testCascadedIdentityColumnInsert() 0 31 1
A testGetEntityStateOnVersionedEntityWithAssignedIdentifier() 0 9 1
A testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges() 0 22 1
A testGetEntityStateWithAssignedIdentity() 0 21 1
B testChangeTrackingNotifyIndividualCommit() 0 38 1
A testRegisterRemovedOnNewEntityIsIgnored() 0 7 1
A setUp() 0 15 1
A testChangeTrackingNotify() 0 47 1
A testPersistedEntityAndClearManager() 0 18 1
A testRegisteringAManagedInstanceRequiresANonEmptyIdentifier() 0 5 1
B testPreviousDetectedIllegalNewNonCascadedEntitiesAreCleanedUpOnSubsequentCommits() 0 32 2
A testEntityChangeSetIsClearedAfterFlush() 0 14 1
A entitiesWithInvalidIdentifiersProvider() 0 15 1
A testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue() 0 19 1
B testNewAssociatedEntityPersistenceOfNewEntitiesThroughCascadedAssociationsFirst() 0 28 1
A testAddToIdentityMapInvalidIdentifiers() 0 5 1
B testNewAssociatedEntityPersistenceOfNewEntitiesThroughNonCascadedAssociationsFirst() 0 43 1
A testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected() 0 13 1
A testCanInstantiateInternalPhpClassSubclass() 0 5 1
A testCanInstantiateInternalPhpClassSubclassFromUnserializedMetadata() 0 12 1
A testAddToIdentityMapValidIdentifiers() 0 6 1
A testRejectsPersistenceOfObjectsWithInvalidAssociationValue() 0 17 1
A invalidAssociationValuesDataProvider() 0 9 1
B entitiesWithValidIdentifiersProvider() 0 35 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Tests\ORM;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\EventManager;
9
use Doctrine\Common\NotifyPropertyChanged;
10
use Doctrine\Common\PropertyChangedListener;
11
use Doctrine\ORM\Annotation as ORM;
12
use Doctrine\ORM\Mapping\ClassMetadata;
13
use Doctrine\ORM\Mapping\ClassMetadataBuildingContext;
14
use Doctrine\ORM\Mapping\ClassMetadataFactory;
15
use Doctrine\ORM\Mapping\GeneratorType;
16
use Doctrine\ORM\ORMInvalidArgumentException;
17
use Doctrine\ORM\Reflection\RuntimeReflectionService;
18
use Doctrine\ORM\UnitOfWork;
19
use Doctrine\Tests\Mocks\ConnectionMock;
20
use Doctrine\Tests\Mocks\DriverMock;
21
use Doctrine\Tests\Mocks\EntityManagerMock;
22
use Doctrine\Tests\Mocks\EntityPersisterMock;
23
use Doctrine\Tests\Mocks\UnitOfWorkMock;
24
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
25
use Doctrine\Tests\Models\Forum\ForumAvatar;
26
use Doctrine\Tests\Models\Forum\ForumUser;
27
use Doctrine\Tests\Models\GeoNames\City;
28
use Doctrine\Tests\Models\GeoNames\Country;
29
use Doctrine\Tests\OrmTestCase;
30
use stdClass;
31
use function count;
32
use function get_class;
33
use function random_int;
34
use function serialize;
35
use function uniqid;
36
use function unserialize;
37
38
/**
39
 * UnitOfWork tests.
40
 */
41
class UnitOfWorkTest extends OrmTestCase
42
{
43
    /**
44
     * SUT
45
     *
46
     * @var UnitOfWorkMock
47
     */
48
    private $unitOfWork;
49
50
    /**
51
     * Provides a sequence mock to the UnitOfWork
52
     *
53
     * @var ConnectionMock
54
     */
55
    private $connectionMock;
56
57
    /**
58
     * The EntityManager mock that provides the mock persisters
59
     *
60
     * @var EntityManagerMock
61
     */
62
    private $emMock;
63
64
    /** @var EventManager|\PHPUnit_Framework_MockObject_MockObject */
65
    private $eventManager;
66
67
    /** @var ClassMetadataBuildingContext|\PHPUnit_Framework_MockObject_MockObject */
68
    private $metadataBuildingContext;
69
70
    protected function setUp() : void
71
    {
72
        parent::setUp();
73
74
        $this->metadataBuildingContext = new ClassMetadataBuildingContext(
75
            $this->createMock(ClassMetadataFactory::class),
76
            new RuntimeReflectionService()
77
        );
78
79
        $this->eventManager   = $this->getMockBuilder(EventManager::class)->getMock();
80
        $this->connectionMock = new ConnectionMock([], new DriverMock(), null, $this->eventManager);
81
        $this->emMock         = EntityManagerMock::create($this->connectionMock, null, $this->eventManager);
82
        $this->unitOfWork     = new UnitOfWorkMock($this->emMock);
83
84
        $this->emMock->setUnitOfWork($this->unitOfWork);
85
    }
86
87
    public function testRegisterRemovedOnNewEntityIsIgnored() : void
88
    {
89
        $user           = new ForumUser();
90
        $user->username = 'romanb';
91
        self::assertFalse($this->unitOfWork->isScheduledForDelete($user));
92
        $this->unitOfWork->scheduleForDelete($user);
93
        self::assertFalse($this->unitOfWork->isScheduledForDelete($user));
94
    }
95
96
97
    /** Operational tests */
98
    public function testSavingSingleEntityWithIdentityColumnForcesInsert() : void
99
    {
100
        // Setup fake persister and id generator for identity generation
101
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
102
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
103
        $userPersister->setMockIdGeneratorType(GeneratorType::IDENTITY);
0 ignored issues
show
Bug introduced by
Doctrine\ORM\Mapping\GeneratorType::IDENTITY of type string is incompatible with the type integer expected by parameter $genType of Doctrine\Tests\Mocks\Ent...etMockIdGeneratorType(). ( Ignorable by Annotation )

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

103
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
104
105
        // Test
106
        $user           = new ForumUser();
107
        $user->username = 'romanb';
108
        $this->unitOfWork->persist($user);
109
110
        // Check
111
        self::assertCount(0, $userPersister->getInserts());
112
        self::assertCount(0, $userPersister->getUpdates());
113
        self::assertCount(0, $userPersister->getDeletes());
114
        self::assertFalse($this->unitOfWork->isInIdentityMap($user));
115
        // should no longer be scheduled for insert
116
        self::assertTrue($this->unitOfWork->isScheduledForInsert($user));
117
118
        // Now lets check whether a subsequent commit() does anything
119
        $userPersister->reset();
120
121
        // Test
122
        $this->unitOfWork->commit();
123
124
        // Check.
125
        self::assertCount(1, $userPersister->getInserts());
126
        self::assertCount(0, $userPersister->getUpdates());
127
        self::assertCount(0, $userPersister->getDeletes());
128
129
        // should have an id
130
        self::assertInternalType('numeric', $user->id);
131
    }
132
133
    /**
134
     * Tests a scenario where a save() operation is cascaded from a ForumUser
135
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
136
     */
137
    public function testCascadedIdentityColumnInsert() : void
138
    {
139
        // Setup fake persister and id generator for identity generation
140
        //ForumUser
141
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
142
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
143
        $userPersister->setMockIdGeneratorType(GeneratorType::IDENTITY);
0 ignored issues
show
Bug introduced by
Doctrine\ORM\Mapping\GeneratorType::IDENTITY of type string is incompatible with the type integer expected by parameter $genType of Doctrine\Tests\Mocks\Ent...etMockIdGeneratorType(). ( Ignorable by Annotation )

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

143
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
144
        // ForumAvatar
145
        $avatarPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumAvatar::class));
146
        $this->unitOfWork->setEntityPersister(ForumAvatar::class, $avatarPersister);
147
        $avatarPersister->setMockIdGeneratorType(GeneratorType::IDENTITY);
148
149
        // Test
150
        $user           = new ForumUser();
151
        $user->username = 'romanb';
152
        $avatar         = new ForumAvatar();
153
        $user->avatar   = $avatar;
154
        $this->unitOfWork->persist($user); // save cascaded to avatar
155
156
        $this->unitOfWork->commit();
157
158
        self::assertInternalType('numeric', $user->id);
159
        self::assertInternalType('numeric', $avatar->id);
160
161
        self::assertCount(1, $userPersister->getInserts());
162
        self::assertCount(0, $userPersister->getUpdates());
163
        self::assertCount(0, $userPersister->getDeletes());
164
165
        self::assertCount(1, $avatarPersister->getInserts());
166
        self::assertCount(0, $avatarPersister->getUpdates());
167
        self::assertCount(0, $avatarPersister->getDeletes());
168
    }
169
170
    public function testChangeTrackingNotify() : void
171
    {
172
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(NotifyChangedEntity::class));
173
        $this->unitOfWork->setEntityPersister(NotifyChangedEntity::class, $persister);
174
        $itemPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(NotifyChangedRelatedItem::class));
175
        $this->unitOfWork->setEntityPersister(NotifyChangedRelatedItem::class, $itemPersister);
176
177
        $entity = new NotifyChangedEntity();
178
        $entity->setData('thedata');
179
        $this->unitOfWork->persist($entity);
180
181
        $this->unitOfWork->commit();
182
        self::assertCount(1, $persister->getInserts());
183
        $persister->reset();
184
185
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity));
186
187
        $entity->setData('newdata');
188
        $entity->setTransient('newtransientvalue');
189
190
        self::assertTrue($this->unitOfWork->isScheduledForDirtyCheck($entity));
191
192
        self::assertEquals(
193
            [
194
                'data' => ['thedata', 'newdata'],
195
                'transient' => [null, 'newtransientvalue'],
196
            ],
197
            $this->unitOfWork->getEntityChangeSet($entity)
198
        );
199
200
        $item = new NotifyChangedRelatedItem();
201
        $entity->getItems()->add($item);
202
        $item->setOwner($entity);
203
        $this->unitOfWork->persist($item);
204
205
        $this->unitOfWork->commit();
206
        self::assertCount(1, $itemPersister->getInserts());
207
        $persister->reset();
208
        $itemPersister->reset();
209
210
        $entity->getItems()->removeElement($item);
211
        $item->setOwner(null);
212
        self::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

212
        self::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...
213
        $this->unitOfWork->commit();
214
        $updates = $itemPersister->getUpdates();
215
        self::assertCount(1, $updates);
216
        self::assertSame($updates[0], $item);
217
    }
218
219
    public function testChangeTrackingNotifyIndividualCommit() : void
220
    {
221
        self::markTestIncomplete(
222
            '@guilhermeblanco, this test was added directly on master#a16dc65cd206aed67a01a19f01f6318192b826af and'
223
            . ' since we do not support committing individual entities I think it is invalid now...'
224
        );
225
226
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedEntity'));
227
        $this->unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedEntity', $persister);
228
        $itemPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedRelatedItem'));
229
        $this->unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedRelatedItem', $itemPersister);
230
231
        $entity = new NotifyChangedEntity();
232
        $entity->setData('thedata');
233
234
        $entity2 = new NotifyChangedEntity();
235
        $entity2->setData('thedata');
236
237
        $this->unitOfWork->persist($entity);
238
        $this->unitOfWork->persist($entity2);
239
        $this->unitOfWork->commit($entity);
240
        $this->unitOfWork->commit();
241
242
        self::assertEquals(2, count($persister->getInserts()));
243
244
        $persister->reset();
245
246
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity2));
247
248
        $entity->setData('newdata');
249
        $entity2->setData('newdata');
250
251
        $this->unitOfWork->commit($entity);
252
253
        self::assertTrue($this->unitOfWork->isScheduledForDirtyCheck($entity2));
254
        self::assertEquals(['data' => ['thedata', 'newdata']], $this->unitOfWork->getEntityChangeSet($entity2));
255
        self::assertFalse($this->unitOfWork->isScheduledForDirtyCheck($entity));
256
        self::assertEquals([], $this->unitOfWork->getEntityChangeSet($entity));
257
    }
258
259
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier() : void
260
    {
261
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(VersionedAssignedIdentifierEntity::class));
262
        $this->unitOfWork->setEntityPersister(VersionedAssignedIdentifierEntity::class, $persister);
263
264
        $e     = new VersionedAssignedIdentifierEntity();
265
        $e->id = 42;
266
        self::assertEquals(UnitOfWork::STATE_NEW, $this->unitOfWork->getEntityState($e));
267
        self::assertFalse($persister->isExistsCalled());
268
    }
269
270
    public function testGetEntityStateWithAssignedIdentity() : void
271
    {
272
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CmsPhonenumber::class));
273
        $this->unitOfWork->setEntityPersister(CmsPhonenumber::class, $persister);
274
275
        $ph              = new CmsPhonenumber();
276
        $ph->phonenumber = '12345';
277
278
        self::assertEquals(UnitOfWork::STATE_NEW, $this->unitOfWork->getEntityState($ph));
279
        self::assertTrue($persister->isExistsCalled());
280
281
        $persister->reset();
282
283
        // if the entity is already managed the exists() check should be skipped
284
        $this->unitOfWork->registerManaged($ph, ['phonenumber' => '12345'], []);
285
        self::assertEquals(UnitOfWork::STATE_MANAGED, $this->unitOfWork->getEntityState($ph));
286
        self::assertFalse($persister->isExistsCalled());
287
        $ph2              = new CmsPhonenumber();
288
        $ph2->phonenumber = '12345';
289
        self::assertEquals(UnitOfWork::STATE_DETACHED, $this->unitOfWork->getEntityState($ph2));
290
        self::assertFalse($persister->isExistsCalled());
291
    }
292
293
    /**
294
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
295
     */
296
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges() : void
297
    {
298
        // Setup fake persister and id generator
299
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
300
        $userPersister->setMockIdGeneratorType(GeneratorType::IDENTITY);
0 ignored issues
show
Bug introduced by
Doctrine\ORM\Mapping\GeneratorType::IDENTITY of type string is incompatible with the type integer expected by parameter $genType of Doctrine\Tests\Mocks\Ent...etMockIdGeneratorType(). ( Ignorable by Annotation )

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

300
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
301
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
302
303
        // Create a test user
304
        $user       = new ForumUser();
305
        $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...
306
        $this->unitOfWork->persist($user);
307
        $this->unitOfWork->commit();
308
309
        // Schedule user for update without changes
310
        $this->unitOfWork->scheduleForUpdate($user);
311
312
        self::assertNotEmpty($this->unitOfWork->getScheduledEntityUpdates());
313
314
        // This commit should not raise an E_NOTICE
315
        $this->unitOfWork->commit();
316
317
        self::assertEmpty($this->unitOfWork->getScheduledEntityUpdates());
318
    }
319
320
    /**
321
     * @group DDC-3490
322
     *
323
     * @dataProvider invalidAssociationValuesDataProvider
324
     *
325
     * @param mixed $invalidValue
326
     */
327
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue) : void
328
    {
329
        $this->unitOfWork->setEntityPersister(
330
            ForumUser::class,
331
            new EntityPersisterMock(
332
                $this->emMock,
333
                $this->emMock->getClassMetadata(ForumUser::class)
334
            )
335
        );
336
337
        $user           = new ForumUser();
338
        $user->username = 'John';
339
        $user->avatar   = $invalidValue;
340
341
        $this->expectException(ORMInvalidArgumentException::class);
342
343
        $this->unitOfWork->persist($user);
344
    }
345
346
    /**
347
     * @group DDC-3490
348
     *
349
     * @dataProvider invalidAssociationValuesDataProvider
350
     *
351
     * @param mixed $invalidValue
352
     */
353
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue) : void
354
    {
355
        $metadata = $this->emMock->getClassMetadata(ForumUser::class);
356
357
        $this->unitOfWork->setEntityPersister(
358
            ForumUser::class,
359
            new EntityPersisterMock($this->emMock, $metadata)
360
        );
361
362
        $user = new ForumUser();
363
364
        $this->unitOfWork->persist($user);
365
366
        $user->username = 'John';
367
        $user->avatar   = $invalidValue;
368
369
        $this->expectException(ORMInvalidArgumentException::class);
370
371
        $this->unitOfWork->computeChangeSet($metadata, $user);
372
    }
373
374
    /**
375
     * @group DDC-3619
376
     * @group 1338
377
     */
378
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected() : void
379
    {
380
        $entity     = new ForumUser();
381
        $entity->id = 123;
382
383
        $this->unitOfWork->registerManaged($entity, ['id' => 123], []);
384
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity));
385
386
        $this->unitOfWork->remove($entity);
387
        self::assertFalse($this->unitOfWork->isInIdentityMap($entity));
388
389
        $this->unitOfWork->persist($entity);
390
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity));
391
    }
392
393
    /**
394
     * @group 5849
395
     * @group 5850
396
     */
397
    public function testPersistedEntityAndClearManager() : void
398
    {
399
        $entity1 = new City(123, 'London');
400
        $entity2 = new Country(456, 'United Kingdom');
401
402
        $this->unitOfWork->persist($entity1);
403
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity1));
404
405
        $this->unitOfWork->persist($entity2);
406
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity2));
407
408
        $this->unitOfWork->clear();
409
410
        self::assertFalse($this->unitOfWork->isInIdentityMap($entity1));
411
        self::assertFalse($this->unitOfWork->isInIdentityMap($entity2));
412
413
        self::assertFalse($this->unitOfWork->isScheduledForInsert($entity1));
414
        self::assertFalse($this->unitOfWork->isScheduledForInsert($entity2));
415
    }
416
417
    /**
418
     * @group #5579
419
     */
420
    public function testEntityChangeSetIsClearedAfterFlush() : void
421
    {
422
        $entity1 = new NotifyChangedEntity();
423
        $entity2 = new NotifyChangedEntity();
424
425
        $entity1->setData('thedata');
426
        $entity2->setData('thedata');
427
428
        $this->unitOfWork->persist($entity1);
429
        $this->unitOfWork->persist($entity2);
430
        $this->unitOfWork->commit();
431
432
        self::assertEmpty($this->unitOfWork->getEntityChangeSet($entity1));
433
        self::assertEmpty($this->unitOfWork->getEntityChangeSet($entity2));
434
    }
435
436
    /**
437
     * Data Provider
438
     *
439
     * @return mixed[][]
440
     */
441
    public function invalidAssociationValuesDataProvider()
442
    {
443
        return [
444
            ['foo'],
445
            [['foo']],
446
            [''],
447
            [[]],
448
            [new stdClass()],
449
            [new ArrayCollection()],
450
        ];
451
    }
452
453
    /**
454
     * @dataProvider entitiesWithValidIdentifiersProvider
455
     *
456
     * @param object $entity
457
     * @param string $idHash
458
     *
459
     */
460
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash) : void
461
    {
462
        $this->unitOfWork->persist($entity);
463
        $this->unitOfWork->addToIdentityMap($entity);
464
465
        self::assertSame($entity, $this->unitOfWork->getByIdHash($idHash, get_class($entity)));
466
    }
467
468
    public function entitiesWithValidIdentifiersProvider()
469
    {
470
        $emptyString = new EntityWithStringIdentifier();
471
472
        $emptyString->id = '';
473
474
        $nonEmptyString = new EntityWithStringIdentifier();
475
476
        $nonEmptyString->id = uniqid('id', true);
477
478
        $emptyStrings = new EntityWithCompositeStringIdentifier();
479
480
        $emptyStrings->id1 = '';
481
        $emptyStrings->id2 = '';
482
483
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
484
485
        $nonEmptyStrings->id1 = uniqid('id1', true);
486
        $nonEmptyStrings->id2 = uniqid('id2', true);
487
488
        $booleanTrue = new EntityWithBooleanIdentifier();
489
490
        $booleanTrue->id = true;
491
492
        $booleanFalse = new EntityWithBooleanIdentifier();
493
494
        $booleanFalse->id = false;
495
496
        return [
497
            'empty string, single field'     => [$emptyString, ''],
498
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
499
            'empty strings, two fields'      => [$emptyStrings, ' '],
500
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
501
            'boolean true'                   => [$booleanTrue, '1'],
502
            'boolean false'                  => [$booleanFalse, ''],
503
        ];
504
    }
505
506
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier() : void
507
    {
508
        $this->expectException(ORMInvalidArgumentException::class);
509
510
        $this->unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
511
    }
512
513
    /**
514
     * @dataProvider entitiesWithInvalidIdentifiersProvider
515
     *
516
     * @param object $entity
517
     * @param array  $identifier
518
     *
519
     */
520
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier) : void
521
    {
522
        $this->expectException(ORMInvalidArgumentException::class);
523
524
        $this->unitOfWork->registerManaged($entity, $identifier, []);
525
    }
526
527
528
    public function entitiesWithInvalidIdentifiersProvider()
529
    {
530
        $firstNullString = new EntityWithCompositeStringIdentifier();
531
532
        $firstNullString->id2 = uniqid('id2', true);
533
534
        $secondNullString = new EntityWithCompositeStringIdentifier();
535
536
        $secondNullString->id1 = uniqid('id1', true);
537
538
        return [
539
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
540
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
541
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
542
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
543
        ];
544
    }
545
546
    /**
547
     * Unlike next test, this one demonstrates that the problem does
548
     * not necessarily reproduce if all the pieces are being flushed together.
549
     *
550
     * @group DDC-2922
551
     * @group #1521
552
     */
553
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughCascadedAssociationsFirst() : void
554
    {
555
        $persister1 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CascadePersistedEntity::class));
556
        $persister2 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithCascadingAssociation::class));
557
        $persister3 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
558
559
        $this->unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
560
        $this->unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
561
        $this->unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
562
563
        $cascadePersisted = new CascadePersistedEntity();
564
        $cascading        = new EntityWithCascadingAssociation();
565
        $nonCascading     = new EntityWithNonCascadingAssociation();
566
567
        // First we persist and flush a EntityWithCascadingAssociation with
568
        // the cascading association not set. Having the "cascading path" involve
569
        // a non-new object is important to show that the ORM should be considering
570
        // cascades across entity changesets in subsequent flushes.
571
        $cascading->cascaded    = $cascadePersisted;
572
        $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...
573
574
        $this->unitOfWork->persist($cascading);
575
        $this->unitOfWork->persist($nonCascading);
576
        $this->unitOfWork->commit();
577
578
        self::assertCount(1, $persister1->getInserts());
579
        self::assertCount(1, $persister2->getInserts());
580
        self::assertCount(1, $persister3->getInserts());
581
    }
582
583
    /**
584
     * This test exhibits the bug describe in the ticket, where an object that
585
     * ought to be reachable causes errors.
586
     *
587
     * @group DDC-2922
588
     * @group #1521
589
     */
590
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughNonCascadedAssociationsFirst() : void
591
    {
592
        $persister1 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CascadePersistedEntity::class));
593
        $persister2 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithCascadingAssociation::class));
594
        $persister3 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
595
596
        $this->unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
597
        $this->unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
598
        $this->unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
599
600
        $cascadePersisted = new CascadePersistedEntity();
601
        $cascading        = new EntityWithCascadingAssociation();
602
        $nonCascading     = new EntityWithNonCascadingAssociation();
603
604
        // First we persist and flush a EntityWithCascadingAssociation with
605
        // the cascading association not set. Having the "cascading path" involve
606
        // a non-new object is important to show that the ORM should be considering
607
        // cascades across entity changesets in subsequent flushes.
608
        $cascading->cascaded = null;
609
610
        $this->unitOfWork->persist($cascading);
611
        $this->unitOfWork->commit();
612
613
        self::assertCount(0, $persister1->getInserts());
614
        self::assertCount(1, $persister2->getInserts());
615
        self::assertCount(0, $persister3->getInserts());
616
617
        // Note that we have NOT directly persisted the CascadePersistedEntity,
618
        // and EntityWithNonCascadingAssociation does NOT have a configured
619
        // cascade-persist.
620
        $nonCascading->nonCascaded = $cascadePersisted;
621
622
        // However, EntityWithCascadingAssociation *does* have a cascade-persist
623
        // association, which ought to allow us to save the CascadePersistedEntity
624
        // anyway through that connection.
625
        $cascading->cascaded = $cascadePersisted;
626
627
        $this->unitOfWork->persist($nonCascading);
628
        $this->unitOfWork->commit();
629
630
        self::assertCount(1, $persister1->getInserts());
631
        self::assertCount(1, $persister2->getInserts());
632
        self::assertCount(1, $persister3->getInserts());
633
    }
634
635
    /**
636
     * This test exhibits the bug describe in the ticket, where an object that
637
     * ought to be reachable causes errors.
638
     *
639
     * @group DDC-2922
640
     * @group #1521
641
     */
642
    public function testPreviousDetectedIllegalNewNonCascadedEntitiesAreCleanedUpOnSubsequentCommits() : void
643
    {
644
        $persister1 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CascadePersistedEntity::class));
645
        $persister2 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
646
647
        $this->unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
648
        $this->unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister2);
649
650
        $cascadePersisted = new CascadePersistedEntity();
651
        $nonCascading     = new EntityWithNonCascadingAssociation();
652
653
        // We explicitly cause the ORM to detect a non-persisted new entity in the association graph:
654
        $nonCascading->nonCascaded = $cascadePersisted;
655
656
        $this->unitOfWork->persist($nonCascading);
657
658
        try {
659
            $this->unitOfWork->commit();
660
661
            self::fail('An exception was supposed to be raised');
662
        } catch (ORMInvalidArgumentException $ignored) {
663
            self::assertEmpty($persister1->getInserts());
664
            self::assertEmpty($persister2->getInserts());
665
        }
666
667
        $this->unitOfWork->clear();
668
        $this->unitOfWork->persist(new CascadePersistedEntity());
669
        $this->unitOfWork->commit();
670
671
        // Persistence operations should just recover normally:
672
        self::assertCount(1, $persister1->getInserts());
673
        self::assertCount(0, $persister2->getInserts());
674
    }
675
676
    /**
677
     * @group DDC-3120
678
     */
679
    public function testCanInstantiateInternalPhpClassSubclass() : void
680
    {
681
        $classMetadata = new ClassMetadata(MyArrayObjectEntity::class, $this->metadataBuildingContext);
682
683
        self::assertInstanceOf(MyArrayObjectEntity::class, $this->unitOfWork->newInstance($classMetadata));
684
    }
685
686
    /**
687
     * @group DDC-3120
688
     */
689
    public function testCanInstantiateInternalPhpClassSubclassFromUnserializedMetadata() : void
690
    {
691
        /** @var ClassMetadata $classMetadata */
692
        $classMetadata = unserialize(
693
            serialize(
694
                new ClassMetadata(MyArrayObjectEntity::class, $this->metadataBuildingContext)
695
            )
696
        );
697
698
        $classMetadata->wakeupReflection(new RuntimeReflectionService());
699
700
        self::assertInstanceOf(MyArrayObjectEntity::class, $this->unitOfWork->newInstance($classMetadata));
701
    }
702
}
703
704
/**
705
 * @ORM\Entity
706
 */
707
class NotifyChangedEntity implements NotifyPropertyChanged
708
{
709
    private $listeners = [];
710
    /**
711
     * @ORM\Id
712
     * @ORM\Column(type="integer")
713
     * @ORM\GeneratedValue
714
     */
715
    private $id;
716
    /** @ORM\Column(type="string") */
717
    private $data;
718
719
    private $transient; // not persisted
720
721
    /** @ORM\OneToMany(targetEntity=NotifyChangedRelatedItem::class, mappedBy="owner") */
722
    private $items;
723
724
    public function __construct()
725
    {
726
        $this->items = new ArrayCollection();
727
    }
728
729
    public function getId()
730
    {
731
        return $this->id;
732
    }
733
734
    public function getItems()
735
    {
736
        return $this->items;
737
    }
738
739
    public function setTransient($value)
740
    {
741
        if ($value !== $this->transient) {
742
            $this->onPropertyChanged('transient', $this->transient, $value);
743
            $this->transient = $value;
744
        }
745
    }
746
747
    public function getData()
748
    {
749
        return $this->data;
750
    }
751
752
    public function setData($data)
753
    {
754
        if ($data !== $this->data) {
755
            $this->onPropertyChanged('data', $this->data, $data);
756
            $this->data = $data;
757
        }
758
    }
759
760
    public function addPropertyChangedListener(PropertyChangedListener $listener)
761
    {
762
        $this->listeners[] = $listener;
763
    }
764
765
    protected function onPropertyChanged($propName, $oldValue, $newValue)
766
    {
767
        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...
768
            foreach ($this->listeners as $listener) {
769
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
770
            }
771
        }
772
    }
773
}
774
775
/** @ORM\Entity */
776
class NotifyChangedRelatedItem
777
{
778
    /**
779
     * @ORM\Id
780
     * @ORM\Column(type="integer")
781
     * @ORM\GeneratedValue
782
     */
783
    private $id;
784
785
    /** @ORM\ManyToOne(targetEntity=NotifyChangedEntity::class, inversedBy="items") */
786
    private $owner;
787
788
    public function getId()
789
    {
790
        return $this->id;
791
    }
792
793
    public function getOwner()
794
    {
795
        return $this->owner;
796
    }
797
798
    public function setOwner($owner)
799
    {
800
        $this->owner = $owner;
801
    }
802
}
803
804
/** @ORM\Entity */
805
class VersionedAssignedIdentifierEntity
806
{
807
    /** @ORM\Id @ORM\Column(type="integer") */
808
    public $id;
809
    /** @ORM\Version @ORM\Column(type="integer") */
810
    public $version;
811
}
812
813
/** @ORM\Entity */
814
class EntityWithStringIdentifier
815
{
816
    /**
817
     * @ORM\Id @ORM\Column(type="string")
818
     *
819
     * @var string|null
820
     */
821
    public $id;
822
}
823
824
/** @ORM\Entity */
825
class EntityWithBooleanIdentifier
826
{
827
    /**
828
     * @ORM\Id @ORM\Column(type="boolean")
829
     *
830
     * @var bool|null
831
     */
832
    public $id;
833
}
834
835
/** @ORM\Entity */
836
class EntityWithCompositeStringIdentifier
837
{
838
    /**
839
     * @ORM\Id @ORM\Column(type="string")
840
     *
841
     * @var string|null
842
     */
843
    public $id1;
844
845
    /**
846
     * @ORM\Id @ORM\Column(type="string")
847
     *
848
     * @var string|null
849
     */
850
    public $id2;
851
}
852
853
/** @ORM\Entity */
854
class EntityWithRandomlyGeneratedField
855
{
856
    /** @ORM\Id @ORM\Column(type="string") */
857
    public $id;
858
859
    /** @ORM\Column(type="integer") */
860
    public $generatedField;
861
862
    public function __construct()
863
    {
864
        $this->id             = uniqid('id', true);
865
        $this->generatedField = random_int(0, 100000);
866
    }
867
}
868
869
/** @ORM\Entity */
870
class CascadePersistedEntity
871
{
872
    /** @ORM\Id @ORM\Column(type="string") @ORM\GeneratedValue(strategy="NONE") */
873
    private $id;
874
875
    public function __construct()
876
    {
877
        $this->id = uniqid(self::class, true);
878
    }
879
}
880
881
/** @ORM\Entity */
882
class EntityWithCascadingAssociation
883
{
884
    /** @ORM\Id @ORM\Column(type="string") @ORM\GeneratedValue(strategy="NONE") */
885
    private $id;
886
887
    /** @ORM\ManyToOne(targetEntity=CascadePersistedEntity::class, cascade={"persist"}) */
888
    public $cascaded;
889
890
    public function __construct()
891
    {
892
        $this->id = uniqid(self::class, true);
893
    }
894
}
895
896
/** @ORM\Entity */
897
class EntityWithNonCascadingAssociation
898
{
899
    /** @ORM\Id @ORM\Column(type="string") @ORM\GeneratedValue(strategy="NONE") */
900
    private $id;
901
902
    /** @ORM\ManyToOne(targetEntity=CascadePersistedEntity::class) */
903
    public $nonCascaded;
904
905
    public function __construct()
906
    {
907
        $this->id = uniqid(self::class, true);
908
    }
909
}
910
911
class MyArrayObjectEntity extends \ArrayObject
912
{
913
}
914