Failed Conditions
Pull Request — develop (#6873)
by
unknown
112:44 queued 47:41
created

testPersistedEntityAndClearManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

261
        $userPersister->setMockIdGeneratorType(/** @scrutinizer ignore-type */ GeneratorType::IDENTITY);
Loading history...
262
        $this->unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
263
264
        // Create a test user
265
        $user = new ForumUser();
266
        $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...
267
        $this->unitOfWork->persist($user);
268
        $this->unitOfWork->commit();
269
270
        // Schedule user for update without changes
271
        $this->unitOfWork->scheduleForUpdate($user);
272
273
        self::assertNotEmpty($this->unitOfWork->getScheduledEntityUpdates());
274
275
        // This commit should not raise an E_NOTICE
276
        $this->unitOfWork->commit();
277
278
        self::assertEmpty($this->unitOfWork->getScheduledEntityUpdates());
279
    }
280
281
    /**
282
     * @group DDC-1984
283
     */
284
    public function testLockWithoutEntityThrowsException()
285
    {
286
        $this->expectException(\InvalidArgumentException::class);
287
        $this->unitOfWork->lock(null, null, null);
288
    }
289
290
    /**
291
     * @group DDC-3490
292
     *
293
     * @dataProvider invalidAssociationValuesDataProvider
294
     *
295
     * @param mixed $invalidValue
296
     */
297
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
298
    {
299
        $this->unitOfWork->setEntityPersister(
300
            ForumUser::class,
301
            new EntityPersisterMock(
302
                $this->emMock,
303
                $this->emMock->getClassMetadata(ForumUser::class)
304
            )
305
        );
306
307
        $user           = new ForumUser();
308
        $user->username = 'John';
309
        $user->avatar   = $invalidValue;
310
311
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
312
313
        $this->unitOfWork->persist($user);
314
    }
315
316
    /**
317
     * @group DDC-3490
318
     *
319
     * @dataProvider invalidAssociationValuesDataProvider
320
     *
321
     * @param mixed $invalidValue
322
     */
323
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
324
    {
325
        $metadata = $this->emMock->getClassMetadata(ForumUser::class);
326
327
        $this->unitOfWork->setEntityPersister(
328
            ForumUser::class,
329
            new EntityPersisterMock($this->emMock, $metadata)
330
        );
331
332
        $user = new ForumUser();
333
334
        $this->unitOfWork->persist($user);
335
336
        $user->username = 'John';
337
        $user->avatar   = $invalidValue;
338
339
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
340
341
        $this->unitOfWork->computeChangeSet($metadata, $user);
0 ignored issues
show
Bug introduced by
$metadata of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\UnitOfWork::computeChangeSet(). ( Ignorable by Annotation )

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

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