UnitOfWorkTest   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 615
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 274
c 3
b 0
f 0
dl 0
loc 615
rs 10
wmc 26

25 Methods

Rating   Name   Duplication   Size   Complexity  
A testPersistedEntityAndClearManager() 0 18 1
A testRegisteringAManagedInstanceRequiresANonEmptyIdentifier() 0 5 1
A testRegisterRemovedOnNewEntityIsIgnored() 0 7 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 setUp() 0 10 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
A testSavingSingleEntityWithIdentityColumnForcesInsert() 0 33 1
A testCascadedIdentityColumnInsert() 0 31 1
A testChangeTrackingNotify() 0 47 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\GeneratorType;
15
use Doctrine\ORM\ORMInvalidArgumentException;
16
use Doctrine\ORM\Reflection\RuntimeReflectionService;
17
use Doctrine\ORM\UnitOfWork;
18
use Doctrine\Tests\Mocks\ConnectionMock;
19
use Doctrine\Tests\Mocks\DriverMock;
20
use Doctrine\Tests\Mocks\EntityManagerMock;
21
use Doctrine\Tests\Mocks\EntityPersisterMock;
22
use Doctrine\Tests\Mocks\UnitOfWorkMock;
23
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
24
use Doctrine\Tests\Models\Forum\ForumAvatar;
25
use Doctrine\Tests\Models\Forum\ForumUser;
26
use Doctrine\Tests\Models\GeoNames\City;
27
use Doctrine\Tests\Models\GeoNames\Country;
28
use Doctrine\Tests\OrmTestCase;
29
use InvalidArgumentException;
30
use PHPUnit_Framework_MockObject_MockObject;
31
use stdClass;
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
    protected function setUp() : void
68
    {
69
        parent::setUp();
70
71
        $this->eventManager   = $this->getMockBuilder(EventManager::class)->getMock();
72
        $this->connectionMock = new ConnectionMock([], new DriverMock(), null, $this->eventManager);
73
        $this->emMock         = EntityManagerMock::create($this->connectionMock, null, $this->eventManager);
74
        $this->unitOfWork     = new UnitOfWorkMock($this->emMock);
75
76
        $this->emMock->setUnitOfWork($this->unitOfWork);
77
    }
78
79
    public function testRegisterRemovedOnNewEntityIsIgnored() : void
80
    {
81
        $user           = new ForumUser();
82
        $user->username = 'romanb';
83
        self::assertFalse($this->unitOfWork->isScheduledForDelete($user));
84
        $this->unitOfWork->scheduleForDelete($user);
85
        self::assertFalse($this->unitOfWork->isScheduledForDelete($user));
86
    }
87
88
    /** Operational tests */
89
    public function testSavingSingleEntityWithIdentityColumnForcesInsert() : void
90
    {
91
        // Setup fake persister and id generator for identity generation
92
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
93
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
94
        $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

94
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
95
96
        // Test
97
        $user           = new ForumUser();
98
        $user->username = 'romanb';
99
        $this->unitOfWork->persist($user);
100
101
        // Check
102
        self::assertCount(0, $userPersister->getInserts());
103
        self::assertCount(0, $userPersister->getUpdates());
104
        self::assertCount(0, $userPersister->getDeletes());
105
        self::assertFalse($this->unitOfWork->isInIdentityMap($user));
106
        // should no longer be scheduled for insert
107
        self::assertTrue($this->unitOfWork->isScheduledForInsert($user));
108
109
        // Now lets check whether a subsequent commit() does anything
110
        $userPersister->reset();
111
112
        // Test
113
        $this->unitOfWork->commit();
114
115
        // Check.
116
        self::assertCount(1, $userPersister->getInserts());
117
        self::assertCount(0, $userPersister->getUpdates());
118
        self::assertCount(0, $userPersister->getDeletes());
119
120
        // should have an id
121
        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

121
        /** @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...
122
    }
123
124
    /**
125
     * Tests a scenario where a save() operation is cascaded from a ForumUser
126
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
127
     */
128
    public function testCascadedIdentityColumnInsert() : void
129
    {
130
        // Setup fake persister and id generator for identity generation
131
        //ForumUser
132
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
133
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
134
        $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

134
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
135
        // ForumAvatar
136
        $avatarPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumAvatar::class));
137
        $this->unitOfWork->setEntityPersister(ForumAvatar::class, $avatarPersister);
138
        $avatarPersister->setMockIdGeneratorType(GeneratorType::IDENTITY);
139
140
        // Test
141
        $user           = new ForumUser();
142
        $user->username = 'romanb';
143
        $avatar         = new ForumAvatar();
144
        $user->avatar   = $avatar;
145
        $this->unitOfWork->persist($user); // save cascaded to avatar
146
147
        $this->unitOfWork->commit();
148
149
        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

149
        /** @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...
150
        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

150
        /** @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...
151
152
        self::assertCount(1, $userPersister->getInserts());
153
        self::assertCount(0, $userPersister->getUpdates());
154
        self::assertCount(0, $userPersister->getDeletes());
155
156
        self::assertCount(1, $avatarPersister->getInserts());
157
        self::assertCount(0, $avatarPersister->getUpdates());
158
        self::assertCount(0, $avatarPersister->getDeletes());
159
    }
160
161
    public function testChangeTrackingNotify() : void
162
    {
163
        $persister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(NotifyChangedEntity::class));
164
        $this->unitOfWork->setEntityPersister(NotifyChangedEntity::class, $persister);
165
        $itemPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(NotifyChangedRelatedItem::class));
166
        $this->unitOfWork->setEntityPersister(NotifyChangedRelatedItem::class, $itemPersister);
167
168
        $entity = new NotifyChangedEntity();
169
        $entity->setData('thedata');
170
        $this->unitOfWork->persist($entity);
171
172
        $this->unitOfWork->commit();
173
        self::assertCount(1, $persister->getInserts());
174
        $persister->reset();
175
176
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity));
177
178
        $entity->setData('newdata');
179
        $entity->setTransient('newtransientvalue');
180
181
        self::assertTrue($this->unitOfWork->isScheduledForDirtyCheck($entity));
182
183
        self::assertEquals(
184
            [
185
                'data' => ['thedata', 'newdata'],
186
                'transient' => [null, 'newtransientvalue'],
187
            ],
188
            $this->unitOfWork->getEntityChangeSet($entity)
189
        );
190
191
        $item = new NotifyChangedRelatedItem();
192
        $entity->getItems()->add($item);
193
        $item->setOwner($entity);
194
        $this->unitOfWork->persist($item);
195
196
        $this->unitOfWork->commit();
197
        self::assertCount(1, $itemPersister->getInserts());
198
        $persister->reset();
199
        $itemPersister->reset();
200
201
        $entity->getItems()->removeElement($item);
202
        $item->setOwner(null);
203
        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

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

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