Failed Conditions
Push — master ( 6971e7...b11528 )
by Guilherme
08:44
created

UnitOfWorkTest   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 623
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 278
dl 0
loc 623
rs 10
c 1
b 0
f 0
wmc 26

25 Methods

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

105
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
106
107
        // Test
108
        $user           = new ForumUser();
109
        $user->username = 'romanb';
110
        $this->unitOfWork->persist($user);
111
112
        // Check
113
        self::assertCount(0, $userPersister->getInserts());
114
        self::assertCount(0, $userPersister->getUpdates());
115
        self::assertCount(0, $userPersister->getDeletes());
116
        self::assertFalse($this->unitOfWork->isInIdentityMap($user));
117
        // should no longer be scheduled for insert
118
        self::assertTrue($this->unitOfWork->isScheduledForInsert($user));
119
120
        // Now lets check whether a subsequent commit() does anything
121
        $userPersister->reset();
122
123
        // Test
124
        $this->unitOfWork->commit();
125
126
        // Check.
127
        self::assertCount(1, $userPersister->getInserts());
128
        self::assertCount(0, $userPersister->getUpdates());
129
        self::assertCount(0, $userPersister->getDeletes());
130
131
        // should have an id
132
        self::assertInternalType('numeric', $user->id);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertInternalType() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/3369 ( Ignorable by Annotation )

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

132
        /** @scrutinizer ignore-deprecated */ self::assertInternalType('numeric', $user->id);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
133
    }
134
135
    /**
136
     * Tests a scenario where a save() operation is cascaded from a ForumUser
137
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
138
     */
139
    public function testCascadedIdentityColumnInsert() : void
140
    {
141
        // Setup fake persister and id generator for identity generation
142
        //ForumUser
143
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
144
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
145
        $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

145
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
146
        // ForumAvatar
147
        $avatarPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumAvatar::class));
148
        $this->unitOfWork->setEntityPersister(ForumAvatar::class, $avatarPersister);
149
        $avatarPersister->setMockIdGeneratorType(GeneratorType::IDENTITY);
150
151
        // Test
152
        $user           = new ForumUser();
153
        $user->username = 'romanb';
154
        $avatar         = new ForumAvatar();
155
        $user->avatar   = $avatar;
156
        $this->unitOfWork->persist($user); // save cascaded to avatar
157
158
        $this->unitOfWork->commit();
159
160
        self::assertInternalType('numeric', $user->id);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertInternalType() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/3369 ( Ignorable by Annotation )

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

160
        /** @scrutinizer ignore-deprecated */ self::assertInternalType('numeric', $user->id);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
161
        self::assertInternalType('numeric', $avatar->id);
0 ignored issues
show
Deprecated Code introduced by
The function PHPUnit\Framework\Assert::assertInternalType() has been deprecated: https://github.com/sebastianbergmann/phpunit/issues/3369 ( Ignorable by Annotation )

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

161
        /** @scrutinizer ignore-deprecated */ self::assertInternalType('numeric', $avatar->id);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
162
163
        self::assertCount(1, $userPersister->getInserts());
164
        self::assertCount(0, $userPersister->getUpdates());
165
        self::assertCount(0, $userPersister->getDeletes());
166
167
        self::assertCount(1, $avatarPersister->getInserts());
168
        self::assertCount(0, $avatarPersister->getUpdates());
169
        self::assertCount(0, $avatarPersister->getDeletes());
170
    }
171
172
    public function testChangeTrackingNotify() : void
173
    {
174
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(NotifyChangedEntity::class));
175
        $this->unitOfWork->setEntityPersister(NotifyChangedEntity::class, $persister);
176
        $itemPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(NotifyChangedRelatedItem::class));
177
        $this->unitOfWork->setEntityPersister(NotifyChangedRelatedItem::class, $itemPersister);
178
179
        $entity = new NotifyChangedEntity();
180
        $entity->setData('thedata');
181
        $this->unitOfWork->persist($entity);
182
183
        $this->unitOfWork->commit();
184
        self::assertCount(1, $persister->getInserts());
185
        $persister->reset();
186
187
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity));
188
189
        $entity->setData('newdata');
190
        $entity->setTransient('newtransientvalue');
191
192
        self::assertTrue($this->unitOfWork->isScheduledForDirtyCheck($entity));
193
194
        self::assertEquals(
195
            [
196
                'data' => ['thedata', 'newdata'],
197
                'transient' => [null, 'newtransientvalue'],
198
            ],
199
            $this->unitOfWork->getEntityChangeSet($entity)
200
        );
201
202
        $item = new NotifyChangedRelatedItem();
203
        $entity->getItems()->add($item);
204
        $item->setOwner($entity);
205
        $this->unitOfWork->persist($item);
206
207
        $this->unitOfWork->commit();
208
        self::assertCount(1, $itemPersister->getInserts());
209
        $persister->reset();
210
        $itemPersister->reset();
211
212
        $entity->getItems()->removeElement($item);
213
        $item->setOwner(null);
214
        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

214
        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...
215
        $this->unitOfWork->commit();
216
        $updates = $itemPersister->getUpdates();
217
        self::assertCount(1, $updates);
218
        self::assertSame($updates[0], $item);
219
    }
220
221
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier() : void
222
    {
223
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(VersionedAssignedIdentifierEntity::class));
224
        $this->unitOfWork->setEntityPersister(VersionedAssignedIdentifierEntity::class, $persister);
225
226
        $e     = new VersionedAssignedIdentifierEntity();
227
        $e->id = 42;
228
        self::assertEquals(UnitOfWork::STATE_NEW, $this->unitOfWork->getEntityState($e));
229
        self::assertFalse($persister->isExistsCalled());
230
    }
231
232
    public function testGetEntityStateWithAssignedIdentity() : void
233
    {
234
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CmsPhonenumber::class));
235
        $this->unitOfWork->setEntityPersister(CmsPhonenumber::class, $persister);
236
237
        $ph              = new CmsPhonenumber();
238
        $ph->phonenumber = '12345';
239
240
        self::assertEquals(UnitOfWork::STATE_NEW, $this->unitOfWork->getEntityState($ph));
241
        self::assertTrue($persister->isExistsCalled());
242
243
        $persister->reset();
244
245
        // if the entity is already managed the exists() check should be skipped
246
        $this->unitOfWork->registerManaged($ph, ['phonenumber' => '12345'], []);
247
        self::assertEquals(UnitOfWork::STATE_MANAGED, $this->unitOfWork->getEntityState($ph));
248
        self::assertFalse($persister->isExistsCalled());
249
        $ph2              = new CmsPhonenumber();
250
        $ph2->phonenumber = '12345';
251
        self::assertEquals(UnitOfWork::STATE_DETACHED, $this->unitOfWork->getEntityState($ph2));
252
        self::assertFalse($persister->isExistsCalled());
253
    }
254
255
    /**
256
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
257
     */
258
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges() : void
259
    {
260
        // Setup fake persister and id generator
261
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
262
        $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

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