Failed Conditions
Pull Request — master (#7448)
by Ilya
10:20
created

UnitOfWorkTest.php$1 ➔ preFlush()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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\Event\PreFlushEventArgs;
14
use Doctrine\ORM\Events;
15
use Doctrine\ORM\Exception\CommitInsideCommit;
16
use Doctrine\ORM\Mapping\ClassMetadata;
17
use Doctrine\ORM\Mapping\ClassMetadataBuildingContext;
18
use Doctrine\ORM\Mapping\ClassMetadataFactory;
19
use Doctrine\ORM\Mapping\GeneratorType;
20
use Doctrine\ORM\ORMInvalidArgumentException;
21
use Doctrine\ORM\Reflection\RuntimeReflectionService;
22
use Doctrine\ORM\UnitOfWork;
23
use Doctrine\Tests\Mocks\ConnectionMock;
24
use Doctrine\Tests\Mocks\DriverMock;
25
use Doctrine\Tests\Mocks\EntityManagerMock;
26
use Doctrine\Tests\Mocks\EntityPersisterMock;
27
use Doctrine\Tests\Mocks\UnitOfWorkMock;
28
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
29
use Doctrine\Tests\Models\Forum\ForumAvatar;
30
use Doctrine\Tests\Models\Forum\ForumUser;
31
use Doctrine\Tests\Models\GeoNames\City;
32
use Doctrine\Tests\Models\GeoNames\Country;
33
use Doctrine\Tests\OrmTestCase;
34
use InvalidArgumentException;
35
use PHPUnit_Framework_MockObject_MockObject;
36
use RuntimeException;
37
use stdClass;
38
use function count;
39
use function get_class;
40
use function random_int;
41
use function serialize;
42
use function uniqid;
43
use function unserialize;
44
45
/**
46
 * UnitOfWork tests.
47
 */
48
class UnitOfWorkTest extends OrmTestCase
49
{
50
    /**
51
     * SUT
52
     *
53
     * @var UnitOfWorkMock
54
     */
55
    private $unitOfWork;
56
57
    /**
58
     * Provides a sequence mock to the UnitOfWork
59
     *
60
     * @var ConnectionMock
61
     */
62
    private $connectionMock;
63
64
    /**
65
     * The EntityManager mock that provides the mock persisters
66
     *
67
     * @var EntityManagerMock
68
     */
69
    private $emMock;
70
71
    /** @var EventManager */
72
    private $eventManager;
73
74
    /** @var ClassMetadataBuildingContext|PHPUnit_Framework_MockObject_MockObject */
75
    private $metadataBuildingContext;
76
77
    protected function setUp() : void
78
    {
79
        parent::setUp();
80
81
        $this->metadataBuildingContext = new ClassMetadataBuildingContext(
82
            $this->createMock(ClassMetadataFactory::class),
83
            new RuntimeReflectionService()
84
        );
85
86
        $this->eventManager   = new EventManager();
87
        $this->connectionMock = new ConnectionMock([], new DriverMock(), null, $this->eventManager);
88
        $this->emMock         = EntityManagerMock::create($this->connectionMock, null, $this->eventManager);
89
        $this->unitOfWork     = new UnitOfWorkMock($this->emMock);
90
91
        $this->emMock->setUnitOfWork($this->unitOfWork);
92
    }
93
94
    public function testRegisterRemovedOnNewEntityIsIgnored() : void
95
    {
96
        $user           = new ForumUser();
97
        $user->username = 'romanb';
98
        self::assertFalse($this->unitOfWork->isScheduledForDelete($user));
99
        $this->unitOfWork->scheduleForDelete($user);
100
        self::assertFalse($this->unitOfWork->isScheduledForDelete($user));
101
    }
102
103
    /** Operational tests */
104
    public function testSavingSingleEntityWithIdentityColumnForcesInsert() : void
105
    {
106
        // Setup fake persister and id generator for identity generation
107
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
108
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
109
        $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

109
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
110
111
        // Test
112
        $user           = new ForumUser();
113
        $user->username = 'romanb';
114
        $this->unitOfWork->persist($user);
115
116
        // Check
117
        self::assertCount(0, $userPersister->getInserts());
118
        self::assertCount(0, $userPersister->getUpdates());
119
        self::assertCount(0, $userPersister->getDeletes());
120
        self::assertFalse($this->unitOfWork->isInIdentityMap($user));
121
        // should no longer be scheduled for insert
122
        self::assertTrue($this->unitOfWork->isScheduledForInsert($user));
123
124
        // Now lets check whether a subsequent commit() does anything
125
        $userPersister->reset();
126
127
        // Test
128
        $this->unitOfWork->commit();
129
130
        // Check.
131
        self::assertCount(1, $userPersister->getInserts());
132
        self::assertCount(0, $userPersister->getUpdates());
133
        self::assertCount(0, $userPersister->getDeletes());
134
135
        // should have an id
136
        self::assertInternalType('numeric', $user->id);
137
    }
138
139
    /**
140
     * Tests a scenario where a save() operation is cascaded from a ForumUser
141
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
142
     */
143
    public function testCascadedIdentityColumnInsert() : void
144
    {
145
        // Setup fake persister and id generator for identity generation
146
        //ForumUser
147
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
148
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
149
        $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

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

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

306
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
307
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
308
309
        // Create a test user
310
        $user       = new ForumUser();
311
        $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...
312
        $this->unitOfWork->persist($user);
313
        $this->unitOfWork->commit();
314
315
        // Schedule user for update without changes
316
        $this->unitOfWork->scheduleForUpdate($user);
317
318
        self::assertNotEmpty($this->unitOfWork->getScheduledEntityUpdates());
319
320
        // This commit should not raise an E_NOTICE
321
        $this->unitOfWork->commit();
322
323
        self::assertEmpty($this->unitOfWork->getScheduledEntityUpdates());
324
    }
325
326
    /**
327
     * @group DDC-1984
328
     */
329
    public function testLockWithoutEntityThrowsException() : void
330
    {
331
        $this->expectException(InvalidArgumentException::class);
332
        $this->unitOfWork->lock(null, null, null);
333
    }
334
335
    /**
336
     * @param mixed $invalidValue
337
     *
338
     * @group DDC-3490
339
     * @dataProvider invalidAssociationValuesDataProvider
340
     */
341
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue) : void
342
    {
343
        $this->unitOfWork->setEntityPersister(
344
            ForumUser::class,
345
            new EntityPersisterMock(
346
                $this->emMock,
347
                $this->emMock->getClassMetadata(ForumUser::class)
348
            )
349
        );
350
351
        $user           = new ForumUser();
352
        $user->username = 'John';
353
        $user->avatar   = $invalidValue;
354
355
        $this->expectException(ORMInvalidArgumentException::class);
356
357
        $this->unitOfWork->persist($user);
358
    }
359
360
    /**
361
     * @param mixed $invalidValue
362
     *
363
     * @group DDC-3490
364
     * @dataProvider invalidAssociationValuesDataProvider
365
     */
366
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue) : void
367
    {
368
        $metadata = $this->emMock->getClassMetadata(ForumUser::class);
369
370
        $this->unitOfWork->setEntityPersister(
371
            ForumUser::class,
372
            new EntityPersisterMock($this->emMock, $metadata)
373
        );
374
375
        $user = new ForumUser();
376
377
        $this->unitOfWork->persist($user);
378
379
        $user->username = 'John';
380
        $user->avatar   = $invalidValue;
381
382
        $this->expectException(ORMInvalidArgumentException::class);
383
384
        $this->unitOfWork->computeChangeSet($metadata, $user);
385
    }
386
387
    /**
388
     * @group DDC-3619
389
     * @group 1338
390
     */
391
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected() : void
392
    {
393
        $entity     = new ForumUser();
394
        $entity->id = 123;
395
396
        $this->unitOfWork->registerManaged($entity, ['id' => 123], []);
397
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity));
398
399
        $this->unitOfWork->remove($entity);
400
        self::assertFalse($this->unitOfWork->isInIdentityMap($entity));
401
402
        $this->unitOfWork->persist($entity);
403
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity));
404
    }
405
406
    /**
407
     * @group 5849
408
     * @group 5850
409
     */
410
    public function testPersistedEntityAndClearManager() : void
411
    {
412
        $entity1 = new City(123, 'London');
413
        $entity2 = new Country(456, 'United Kingdom');
414
415
        $this->unitOfWork->persist($entity1);
416
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity1));
417
418
        $this->unitOfWork->persist($entity2);
419
        self::assertTrue($this->unitOfWork->isInIdentityMap($entity2));
420
421
        $this->unitOfWork->clear();
422
423
        self::assertFalse($this->unitOfWork->isInIdentityMap($entity1));
424
        self::assertFalse($this->unitOfWork->isInIdentityMap($entity2));
425
426
        self::assertFalse($this->unitOfWork->isScheduledForInsert($entity1));
427
        self::assertFalse($this->unitOfWork->isScheduledForInsert($entity2));
428
    }
429
430
    /**
431
     * @group #5579
432
     */
433
    public function testEntityChangeSetIsClearedAfterFlush() : void
434
    {
435
        $entity1 = new NotifyChangedEntity();
436
        $entity2 = new NotifyChangedEntity();
437
438
        $entity1->setData('thedata');
439
        $entity2->setData('thedata');
440
441
        $this->unitOfWork->persist($entity1);
442
        $this->unitOfWork->persist($entity2);
443
        $this->unitOfWork->commit();
444
445
        self::assertEmpty($this->unitOfWork->getEntityChangeSet($entity1));
446
        self::assertEmpty($this->unitOfWork->getEntityChangeSet($entity2));
447
    }
448
449
    /**
450
     * Data Provider
451
     *
452
     * @return mixed[][]
453
     */
454
    public function invalidAssociationValuesDataProvider()
455
    {
456
        return [
457
            ['foo'],
458
            [['foo']],
459
            [''],
460
            [[]],
461
            [new stdClass()],
462
            [new ArrayCollection()],
463
        ];
464
    }
465
466
    /**
467
     * @param object $entity
468
     * @param string $idHash
469
     *
470
     * @dataProvider entitiesWithValidIdentifiersProvider
471
     */
472
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash) : void
473
    {
474
        $this->unitOfWork->persist($entity);
475
        $this->unitOfWork->addToIdentityMap($entity);
476
477
        self::assertSame($entity, $this->unitOfWork->getByIdHash($idHash, get_class($entity)));
478
    }
479
480
    public function entitiesWithValidIdentifiersProvider()
481
    {
482
        $emptyString = new EntityWithStringIdentifier();
483
484
        $emptyString->id = '';
485
486
        $nonEmptyString = new EntityWithStringIdentifier();
487
488
        $nonEmptyString->id = uniqid('id', true);
489
490
        $emptyStrings = new EntityWithCompositeStringIdentifier();
491
492
        $emptyStrings->id1 = '';
493
        $emptyStrings->id2 = '';
494
495
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
496
497
        $nonEmptyStrings->id1 = uniqid('id1', true);
498
        $nonEmptyStrings->id2 = uniqid('id2', true);
499
500
        $booleanTrue = new EntityWithBooleanIdentifier();
501
502
        $booleanTrue->id = true;
503
504
        $booleanFalse = new EntityWithBooleanIdentifier();
505
506
        $booleanFalse->id = false;
507
508
        return [
509
            'empty string, single field'     => [$emptyString, ''],
510
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
511
            'empty strings, two fields'      => [$emptyStrings, ' '],
512
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
513
            'boolean true'                   => [$booleanTrue, '1'],
514
            'boolean false'                  => [$booleanFalse, ''],
515
        ];
516
    }
517
518
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier() : void
519
    {
520
        $this->expectException(ORMInvalidArgumentException::class);
521
522
        $this->unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
523
    }
524
525
    /**
526
     * @param object $entity
527
     * @param array  $identifier
528
     *
529
     * @dataProvider entitiesWithInvalidIdentifiersProvider
530
     */
531
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier) : void
532
    {
533
        $this->expectException(ORMInvalidArgumentException::class);
534
535
        $this->unitOfWork->registerManaged($entity, $identifier, []);
536
    }
537
538
    public function entitiesWithInvalidIdentifiersProvider()
539
    {
540
        $firstNullString = new EntityWithCompositeStringIdentifier();
541
542
        $firstNullString->id2 = uniqid('id2', true);
543
544
        $secondNullString = new EntityWithCompositeStringIdentifier();
545
546
        $secondNullString->id1 = uniqid('id1', true);
547
548
        return [
549
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
550
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
551
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
552
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
553
        ];
554
    }
555
556
    /**
557
     * Unlike next test, this one demonstrates that the problem does
558
     * not necessarily reproduce if all the pieces are being flushed together.
559
     *
560
     * @group DDC-2922
561
     * @group #1521
562
     */
563
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughCascadedAssociationsFirst() : void
564
    {
565
        $persister1 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CascadePersistedEntity::class));
566
        $persister2 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithCascadingAssociation::class));
567
        $persister3 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
568
569
        $this->unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
570
        $this->unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
571
        $this->unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
572
573
        $cascadePersisted = new CascadePersistedEntity();
574
        $cascading        = new EntityWithCascadingAssociation();
575
        $nonCascading     = new EntityWithNonCascadingAssociation();
576
577
        // First we persist and flush a EntityWithCascadingAssociation with
578
        // the cascading association not set. Having the "cascading path" involve
579
        // a non-new object is important to show that the ORM should be considering
580
        // cascades across entity changesets in subsequent flushes.
581
        $cascading->cascaded    = $cascadePersisted;
582
        $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...
583
584
        $this->unitOfWork->persist($cascading);
585
        $this->unitOfWork->persist($nonCascading);
586
        $this->unitOfWork->commit();
587
588
        self::assertCount(1, $persister1->getInserts());
589
        self::assertCount(1, $persister2->getInserts());
590
        self::assertCount(1, $persister3->getInserts());
591
    }
592
593
    /**
594
     * This test exhibits the bug describe in the ticket, where an object that
595
     * ought to be reachable causes errors.
596
     *
597
     * @group DDC-2922
598
     * @group #1521
599
     */
600
    public function testNewAssociatedEntityPersistenceOfNewEntitiesThroughNonCascadedAssociationsFirst() : void
601
    {
602
        $persister1 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CascadePersistedEntity::class));
603
        $persister2 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithCascadingAssociation::class));
604
        $persister3 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
605
606
        $this->unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
607
        $this->unitOfWork->setEntityPersister(EntityWithCascadingAssociation::class, $persister2);
608
        $this->unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister3);
609
610
        $cascadePersisted = new CascadePersistedEntity();
611
        $cascading        = new EntityWithCascadingAssociation();
612
        $nonCascading     = new EntityWithNonCascadingAssociation();
613
614
        // First we persist and flush a EntityWithCascadingAssociation with
615
        // the cascading association not set. Having the "cascading path" involve
616
        // a non-new object is important to show that the ORM should be considering
617
        // cascades across entity changesets in subsequent flushes.
618
        $cascading->cascaded = null;
619
620
        $this->unitOfWork->persist($cascading);
621
        $this->unitOfWork->commit();
622
623
        self::assertCount(0, $persister1->getInserts());
624
        self::assertCount(1, $persister2->getInserts());
625
        self::assertCount(0, $persister3->getInserts());
626
627
        // Note that we have NOT directly persisted the CascadePersistedEntity,
628
        // and EntityWithNonCascadingAssociation does NOT have a configured
629
        // cascade-persist.
630
        $nonCascading->nonCascaded = $cascadePersisted;
631
632
        // However, EntityWithCascadingAssociation *does* have a cascade-persist
633
        // association, which ought to allow us to save the CascadePersistedEntity
634
        // anyway through that connection.
635
        $cascading->cascaded = $cascadePersisted;
636
637
        $this->unitOfWork->persist($nonCascading);
638
        $this->unitOfWork->commit();
639
640
        self::assertCount(1, $persister1->getInserts());
641
        self::assertCount(1, $persister2->getInserts());
642
        self::assertCount(1, $persister3->getInserts());
643
    }
644
645
    /**
646
     * This test exhibits the bug describe in the ticket, where an object that
647
     * ought to be reachable causes errors.
648
     *
649
     * @group DDC-2922
650
     * @group #1521
651
     */
652
    public function testPreviousDetectedIllegalNewNonCascadedEntitiesAreCleanedUpOnSubsequentCommits() : void
653
    {
654
        $persister1 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(CascadePersistedEntity::class));
655
        $persister2 = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(EntityWithNonCascadingAssociation::class));
656
657
        $this->unitOfWork->setEntityPersister(CascadePersistedEntity::class, $persister1);
658
        $this->unitOfWork->setEntityPersister(EntityWithNonCascadingAssociation::class, $persister2);
659
660
        $cascadePersisted = new CascadePersistedEntity();
661
        $nonCascading     = new EntityWithNonCascadingAssociation();
662
663
        // We explicitly cause the ORM to detect a non-persisted new entity in the association graph:
664
        $nonCascading->nonCascaded = $cascadePersisted;
665
666
        $this->unitOfWork->persist($nonCascading);
667
668
        try {
669
            $this->unitOfWork->commit();
670
671
            self::fail('An exception was supposed to be raised');
672
        } catch (ORMInvalidArgumentException $ignored) {
673
            self::assertEmpty($persister1->getInserts());
674
            self::assertEmpty($persister2->getInserts());
675
        }
676
677
        $this->unitOfWork->clear();
678
        $this->unitOfWork->persist(new CascadePersistedEntity());
679
        $this->unitOfWork->commit();
680
681
        // Persistence operations should just recover normally:
682
        self::assertCount(1, $persister1->getInserts());
683
        self::assertCount(0, $persister2->getInserts());
684
    }
685
686
    /**
687
     * @group DDC-3120
688
     */
689
    public function testCanInstantiateInternalPhpClassSubclass() : void
690
    {
691
        $classMetadata = new ClassMetadata(MyArrayObjectEntity::class, $this->metadataBuildingContext);
692
693
        self::assertInstanceOf(MyArrayObjectEntity::class, $this->unitOfWork->newInstance($classMetadata));
694
    }
695
696
    /**
697
     * @group DDC-3120
698
     */
699
    public function testCanInstantiateInternalPhpClassSubclassFromUnserializedMetadata() : void
700
    {
701
        /** @var ClassMetadata $classMetadata */
702
        $classMetadata = unserialize(
703
            serialize(
704
                new ClassMetadata(MyArrayObjectEntity::class, $this->metadataBuildingContext)
705
            )
706
        );
707
708
        $classMetadata->wakeupReflection(new RuntimeReflectionService());
709
710
        self::assertInstanceOf(MyArrayObjectEntity::class, $this->unitOfWork->newInstance($classMetadata));
711
    }
712
713
    public function testCallCommitInsideCommit() : void
714
    {
715
        $listener = new class()
716
        {
717
            public function preFlush(PreFlushEventArgs $eventArgs)
718
            {
719
                $eventArgs->getEntityManager()->getUnitOfWork()->commit();
720
            }
721
        };
722
723
        $this->expectException(CommitInsideCommit::class);
724
        $this->eventManager->addEventListener(Events::preFlush, $listener);
725
        $this->unitOfWork->commit();
726
    }
727
728
    public function testCommitWithExceptionClearsInProgressFlag() : void
729
    {
730
        $listener = new class()
731
        {
732
            public $listenerCallCounter = 0;
733
            public function preFlush(PreFlushEventArgs $eventArgs)
0 ignored issues
show
Unused Code introduced by
The parameter $eventArgs is not used and could be removed. ( Ignorable by Annotation )

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

733
            public function preFlush(/** @scrutinizer ignore-unused */ PreFlushEventArgs $eventArgs)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Coding Style introduced by
Expected 1 blank line before function; 0 found
Loading history...
734
            {
735
                $this->listenerCallCounter++;
736
                throw new RuntimeException();
737
            }
738
        };
739
740
        $this->eventManager->addEventListener(Events::preFlush, $listener);
741
        try {
742
            $this->unitOfWork->commit();
743
        } catch (RuntimeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
744
        }
745
746
        $this->eventManager->removeEventListener(Events::preFlush, $listener);
747
748
        try {
749
            $this->unitOfWork->commit();
750
        } catch (CommitInsideCommit $e) {
751
            self::fail('UnitOfWork::$commitInProgress flag must be reset in case of exception during commit process');
752
        }
753
754
        self::assertSame(1, $listener->listenerCallCounter);
755
    }
756
}
757
758
/**
759
 * @ORM\Entity
760
 */
761
class NotifyChangedEntity implements NotifyPropertyChanged
762
{
763
    private $listeners = [];
764
    /**
765
     * @ORM\Id
766
     * @ORM\Column(type="integer")
767
     * @ORM\GeneratedValue
768
     */
769
    private $id;
770
    /** @ORM\Column(type="string") */
771
    private $data;
772
773
    private $transient; // not persisted
774
775
    /** @ORM\OneToMany(targetEntity=NotifyChangedRelatedItem::class, mappedBy="owner") */
776
    private $items;
777
778
    public function __construct()
779
    {
780
        $this->items = new ArrayCollection();
781
    }
782
783
    public function getId()
784
    {
785
        return $this->id;
786
    }
787
788
    public function getItems()
789
    {
790
        return $this->items;
791
    }
792
793
    public function setTransient($value)
794
    {
795
        if ($value !== $this->transient) {
796
            $this->onPropertyChanged('transient', $this->transient, $value);
797
            $this->transient = $value;
798
        }
799
    }
800
801
    public function getData()
802
    {
803
        return $this->data;
804
    }
805
806
    public function setData($data)
807
    {
808
        if ($data !== $this->data) {
809
            $this->onPropertyChanged('data', $this->data, $data);
810
            $this->data = $data;
811
        }
812
    }
813
814
    public function addPropertyChangedListener(PropertyChangedListener $listener)
815
    {
816
        $this->listeners[] = $listener;
817
    }
818
819
    protected function onPropertyChanged($propName, $oldValue, $newValue)
820
    {
821
        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...
822
            foreach ($this->listeners as $listener) {
823
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
824
            }
825
        }
826
    }
827
}
828
829
/** @ORM\Entity */
830
class NotifyChangedRelatedItem
831
{
832
    /**
833
     * @ORM\Id
834
     * @ORM\Column(type="integer")
835
     * @ORM\GeneratedValue
836
     */
837
    private $id;
838
839
    /** @ORM\ManyToOne(targetEntity=NotifyChangedEntity::class, inversedBy="items") */
840
    private $owner;
841
842
    public function getId()
843
    {
844
        return $this->id;
845
    }
846
847
    public function getOwner()
848
    {
849
        return $this->owner;
850
    }
851
852
    public function setOwner($owner)
853
    {
854
        $this->owner = $owner;
855
    }
856
}
857
858
/** @ORM\Entity */
859
class VersionedAssignedIdentifierEntity
860
{
861
    /** @ORM\Id @ORM\Column(type="integer") */
862
    public $id;
863
    /** @ORM\Version @ORM\Column(type="integer") */
864
    public $version;
865
}
866
867
/** @ORM\Entity */
868
class EntityWithStringIdentifier
869
{
870
    /**
871
     * @ORM\Id @ORM\Column(type="string")
872
     *
873
     * @var string|null
874
     */
875
    public $id;
876
}
877
878
/** @ORM\Entity */
879
class EntityWithBooleanIdentifier
880
{
881
    /**
882
     * @ORM\Id @ORM\Column(type="boolean")
883
     *
884
     * @var bool|null
885
     */
886
    public $id;
887
}
888
889
/** @ORM\Entity */
890
class EntityWithCompositeStringIdentifier
891
{
892
    /**
893
     * @ORM\Id @ORM\Column(type="string")
894
     *
895
     * @var string|null
896
     */
897
    public $id1;
898
899
    /**
900
     * @ORM\Id @ORM\Column(type="string")
901
     *
902
     * @var string|null
903
     */
904
    public $id2;
905
}
906
907
/** @ORM\Entity */
908
class EntityWithRandomlyGeneratedField
909
{
910
    /** @ORM\Id @ORM\Column(type="string") */
911
    public $id;
912
913
    /** @ORM\Column(type="integer") */
914
    public $generatedField;
915
916
    public function __construct()
917
    {
918
        $this->id             = uniqid('id', true);
919
        $this->generatedField = random_int(0, 100000);
920
    }
921
}
922
923
/** @ORM\Entity */
924
class CascadePersistedEntity
925
{
926
    /** @ORM\Id @ORM\Column(type="string") @ORM\GeneratedValue(strategy="NONE") */
927
    private $id;
928
929
    public function __construct()
930
    {
931
        $this->id = uniqid(self::class, true);
932
    }
933
}
934
935
/** @ORM\Entity */
936
class EntityWithCascadingAssociation
937
{
938
    /** @ORM\Id @ORM\Column(type="string") @ORM\GeneratedValue(strategy="NONE") */
939
    private $id;
940
941
    /** @ORM\ManyToOne(targetEntity=CascadePersistedEntity::class, cascade={"persist"}) */
942
    public $cascaded;
943
944
    public function __construct()
945
    {
946
        $this->id = uniqid(self::class, true);
947
    }
948
}
949
950
/** @ORM\Entity */
951
class EntityWithNonCascadingAssociation
952
{
953
    /** @ORM\Id @ORM\Column(type="string") @ORM\GeneratedValue(strategy="NONE") */
954
    private $id;
955
956
    /** @ORM\ManyToOne(targetEntity=CascadePersistedEntity::class) */
957
    public $nonCascaded;
958
959
    public function __construct()
960
    {
961
        $this->id = uniqid(self::class, true);
962
    }
963
}
964
965
class MyArrayObjectEntity extends ArrayObject
966
{
967
}
968