Issues (1543)

tests/Doctrine/Tests/ORM/UnitOfWorkTest.php (2 issues)

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);
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);
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);
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);
150
        self::assertInternalType('numeric', $avatar->id);
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
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);
252
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
253
254
        // Create a test user
255
        $user       = new ForumUser();
256
        $user->name = 'Jasper';
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;
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