Failed Conditions
Pull Request — master (#7448)
by Ilya
09:52
created

testSavingSingleEntityWithIdentityColumnForcesInsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 17
nc 1
nop 0
dl 0
loc 33
rs 9.7
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
104
    /** Operational tests */
105
    public function testSavingSingleEntityWithIdentityColumnForcesInsert() : void
106
    {
107
        // Setup fake persister and id generator for identity generation
108
        $userPersister = new EntityPersisterMock($this->emMock, $this->emMock->getClassMetadata(ForumUser::class));
109
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
110
        $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

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

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

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

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

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