Completed
Pull Request — master (#6017)
by Jeremy
09:24
created

UnitOfWorkTest::testCascadedIdentityColumnInsert()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
c 0
b 0
f 0
rs 8.8571
cc 1
eloc 21
nc 1
nop 0
1
<?php
2
3
namespace Doctrine\Tests\ORM;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\Common\NotifyPropertyChanged;
7
use Doctrine\Common\PropertyChangedListener;
8
use Doctrine\ORM\Mapping\ClassMetadata;
9
use Doctrine\ORM\ORMInvalidArgumentException;
10
use Doctrine\ORM\UnitOfWork;
11
use Doctrine\Tests\Mocks\ConnectionMock;
12
use Doctrine\Tests\Mocks\DriverMock;
13
use Doctrine\Tests\Mocks\EntityManagerMock;
14
use Doctrine\Tests\Mocks\EntityPersisterMock;
15
use Doctrine\Tests\Mocks\UnitOfWorkMock;
16
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
17
use Doctrine\Tests\Models\CMS\CmsUser;
18
use Doctrine\Tests\Models\Forum\ForumAvatar;
19
use Doctrine\Tests\Models\Forum\ForumUser;
20
use Doctrine\Tests\Models\GeoNames\City;
21
use Doctrine\Tests\Models\GeoNames\Country;
22
use Doctrine\Tests\OrmTestCase;
23
use stdClass;
24
25
/**
26
 * UnitOfWork tests.
27
 */
28
class UnitOfWorkTest extends OrmTestCase
29
{
30
    /**
31
     * SUT
32
     *
33
     * @var UnitOfWorkMock
34
     */
35
    private $_unitOfWork;
36
37
    /**
38
     * Provides a sequence mock to the UnitOfWork
39
     *
40
     * @var ConnectionMock
41
     */
42
    private $_connectionMock;
43
44
    /**
45
     * The EntityManager mock that provides the mock persisters
46
     *
47
     * @var EntityManagerMock
48
     */
49
    private $_emMock;
50
51
    protected function setUp() {
52
        parent::setUp();
53
        $this->_connectionMock = new ConnectionMock(array(), new DriverMock());
54
        $this->_emMock = EntityManagerMock::create($this->_connectionMock);
55
        // SUT
56
        $this->_unitOfWork = new UnitOfWorkMock($this->_emMock);
57
        $this->_emMock->setUnitOfWork($this->_unitOfWork);
58
    }
59
60
    protected function tearDown() {
61
    }
62
63
    public function testRegisterRemovedOnNewEntityIsIgnored()
64
    {
65
        $user = new ForumUser();
66
        $user->username = 'romanb';
67
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
68
        $this->_unitOfWork->scheduleForDelete($user);
69
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
70
    }
71
72
73
    /* Operational tests */
74
75
    public function testSavingSingleEntityWithIdentityColumnForcesInsert()
76
    {
77
        // Setup fake persister and id generator for identity generation
78
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
79
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
80
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
81
82
        // Test
83
        $user = new ForumUser();
84
        $user->username = 'romanb';
85
        $this->_unitOfWork->persist($user);
86
87
        // Check
88
        $this->assertEquals(0, count($userPersister->getInserts()));
89
        $this->assertEquals(0, count($userPersister->getUpdates()));
90
        $this->assertEquals(0, count($userPersister->getDeletes()));
91
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($user));
92
        // should no longer be scheduled for insert
93
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($user));
94
95
        // Now lets check whether a subsequent commit() does anything
96
        $userPersister->reset();
97
98
        // Test
99
        $this->_unitOfWork->commit();
100
101
        // Check.
102
        $this->assertEquals(1, count($userPersister->getInserts()));
103
        $this->assertEquals(0, count($userPersister->getUpdates()));
104
        $this->assertEquals(0, count($userPersister->getDeletes()));
105
106
        // should have an id
107
        $this->assertTrue(is_numeric($user->id));
108
    }
109
110
    /**
111
     * Tests a scenario where a save() operation is cascaded from a ForumUser
112
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
113
     */
114
    public function testCascadedIdentityColumnInsert()
115
    {
116
        // Setup fake persister and id generator for identity generation
117
        //ForumUser
118
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
119
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
120
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
121
        // ForumAvatar
122
        $avatarPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumAvatar'));
123
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumAvatar', $avatarPersister);
124
        $avatarPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
125
126
        // Test
127
        $user = new ForumUser();
128
        $user->username = 'romanb';
129
        $avatar = new ForumAvatar();
130
        $user->avatar = $avatar;
131
        $this->_unitOfWork->persist($user); // save cascaded to avatar
132
133
        $this->_unitOfWork->commit();
134
135
        $this->assertTrue(is_numeric($user->id));
136
        $this->assertTrue(is_numeric($avatar->id));
137
138
        $this->assertEquals(1, count($userPersister->getInserts()));
139
        $this->assertEquals(0, count($userPersister->getUpdates()));
140
        $this->assertEquals(0, count($userPersister->getDeletes()));
141
142
        $this->assertEquals(1, count($avatarPersister->getInserts()));
143
        $this->assertEquals(0, count($avatarPersister->getUpdates()));
144
        $this->assertEquals(0, count($avatarPersister->getDeletes()));
145
    }
146
147
    public function testChangeTrackingNotify()
148
    {
149
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedEntity'));
150
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedEntity', $persister);
151
        $itemPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedRelatedItem'));
152
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedRelatedItem', $itemPersister);
153
154
        $entity = new NotifyChangedEntity;
155
        $entity->setData('thedata');
156
        $this->_unitOfWork->persist($entity);
157
158
        $this->_unitOfWork->commit();
159
        $this->assertEquals(1, count($persister->getInserts()));
160
        $persister->reset();
161
162
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
163
164
        $entity->setData('newdata');
165
        $entity->setTransient('newtransientvalue');
166
167
        $this->assertTrue($this->_unitOfWork->isScheduledForDirtyCheck($entity));
168
169
        $this->assertEquals(array('data' => array('thedata', 'newdata')), $this->_unitOfWork->getEntityChangeSet($entity));
170
171
        $item = new NotifyChangedRelatedItem();
172
        $entity->getItems()->add($item);
173
        $item->setOwner($entity);
174
        $this->_unitOfWork->persist($item);
175
176
        $this->_unitOfWork->commit();
177
        $this->assertEquals(1, count($itemPersister->getInserts()));
178
        $persister->reset();
179
        $itemPersister->reset();
180
181
182
        $entity->getItems()->removeElement($item);
183
        $item->setOwner(null);
184
        $this->assertTrue($entity->getItems()->isDirty());
185
        $this->_unitOfWork->commit();
186
        $updates = $itemPersister->getUpdates();
187
        $this->assertEquals(1, count($updates));
188
        $this->assertTrue($updates[0] === $item);
189
    }
190
191
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier()
192
    {
193
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\VersionedAssignedIdentifierEntity'));
194
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\VersionedAssignedIdentifierEntity', $persister);
195
196
        $e = new VersionedAssignedIdentifierEntity();
197
        $e->id = 42;
198
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($e));
199
        $this->assertFalse($persister->isExistsCalled());
200
    }
201
202
    public function testGetEntityStateWithAssignedIdentity()
203
    {
204
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'));
205
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\CMS\CmsPhonenumber', $persister);
206
207
        $ph = new CmsPhonenumber();
208
        $ph->phonenumber = '12345';
209
210
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($ph));
211
        $this->assertTrue($persister->isExistsCalled());
212
213
        $persister->reset();
214
215
        // if the entity is already managed the exists() check should be skipped
216
        $this->_unitOfWork->registerManaged($ph, array('phonenumber' => '12345'), array());
217
        $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($ph));
218
        $this->assertFalse($persister->isExistsCalled());
219
        $ph2 = new CmsPhonenumber();
220
        $ph2->phonenumber = '12345';
221
        $this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_unitOfWork->getEntityState($ph2));
222
        $this->assertFalse($persister->isExistsCalled());
223
    }
224
225
    /**
226
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
227
     */
228
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges()
229
    {
230
        // Setup fake persister and id generator
231
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
232
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
233
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
234
235
        // Create a test user
236
        $user = new ForumUser();
237
        $user->name = 'Jasper';
238
        $this->_unitOfWork->persist($user);
239
        $this->_unitOfWork->commit();
240
241
        // Schedule user for update without changes
242
        $this->_unitOfWork->scheduleForUpdate($user);
243
244
        // This commit should not raise an E_NOTICE
245
        $this->_unitOfWork->commit();
246
    }
247
248
    /**
249
     * @group DDC-1984
250
     */
251
    public function testLockWithoutEntityThrowsException()
252
    {
253
        $this->expectException(\InvalidArgumentException::class);
254
        $this->_unitOfWork->lock(null, null, null);
255
    }
256
257
    /**
258
     * @group DDC-3490
259
     *
260
     * @dataProvider invalidAssociationValuesDataProvider
261
     *
262
     * @param mixed $invalidValue
263
     */
264
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
265
    {
266
        $this->_unitOfWork->setEntityPersister(
267
            'Doctrine\Tests\Models\Forum\ForumUser',
268
            new EntityPersisterMock(
269
                $this->_emMock,
270
                $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser')
271
            )
272
        );
273
274
        $user           = new ForumUser();
275
        $user->username = 'John';
276
        $user->avatar   = $invalidValue;
277
278
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
279
280
        $this->_unitOfWork->persist($user);
281
    }
282
283
    /**
284
     * @group DDC-3490
285
     *
286
     * @dataProvider invalidAssociationValuesDataProvider
287
     *
288
     * @param mixed $invalidValue
289
     */
290
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
291
    {
292
        $metadata = $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser');
293
294
        $this->_unitOfWork->setEntityPersister(
295
            'Doctrine\Tests\Models\Forum\ForumUser',
296
            new EntityPersisterMock($this->_emMock, $metadata)
297
        );
298
299
        $user = new ForumUser();
300
301
        $this->_unitOfWork->persist($user);
302
303
        $user->username = 'John';
304
        $user->avatar   = $invalidValue;
305
306
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
307
308
        $this->_unitOfWork->computeChangeSet($metadata, $user);
309
    }
310
311
    /**
312
     * @group DDC-3619
313
     * @group 1338
314
     */
315
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected()
316
    {
317
        $entity     = new ForumUser();
318
        $entity->id = 123;
319
320
        $this->_unitOfWork->registerManaged($entity, array('id' => 123), array());
321
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
322
323
        $this->_unitOfWork->remove($entity);
324
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
325
326
        $this->_unitOfWork->persist($entity);
327
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
328
    }
329
330
    /**
331
     * @group 5849
332
     * @group 5850
333
     */
334
    public function testPersistedEntityAndClearManager()
335
    {
336
        $entity1 = new City(123, 'London');
337
        $entity2 = new Country(456, 'United Kingdom');
338
339
        $this->_unitOfWork->persist($entity1);
340
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
341
342
        $this->_unitOfWork->persist($entity2);
343
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
344
345
        $this->_unitOfWork->clear(Country::class);
346
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
347
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity2));
348
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($entity1));
349
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity2));
350
    }
351
352
    public function testClearManagerWithObject()
353
    {
354
        $this->expectException(\InvalidArgumentException::class);
355
        $this->expectExceptionMessage('must be a string');
356
357
        $entity = new Country(456, 'United Kingdom');
358
359
        $this->_unitOfWork->persist($entity);
360
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
361
362
        $this->_unitOfWork->clear($entity);
363
    }
364
365
    public function testClearManagerWithNullValue()
366
    {
367
        $entity = new Country(456, 'United Kingdom');
368
369
        $this->_unitOfWork->persist($entity);
370
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
371
372
        $this->_unitOfWork->clear();
373
374
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
375
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity));
376
    }
377
378
    /**
379
     * Data Provider
380
     *
381
     * @return mixed[][]
382
     */
383
    public function invalidAssociationValuesDataProvider()
384
    {
385
        return [
386
            ['foo'],
387
            [['foo']],
388
            [''],
389
            [[]],
390
            [new stdClass()],
391
            [new ArrayCollection()],
392
        ];
393
    }
394
395
    /**
396
     * @dataProvider entitiesWithValidIdentifiersProvider
397
     *
398
     * @param object $entity
399
     * @param string $idHash
400
     *
401
     * @return void
402
     */
403
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash)
404
    {
405
        $this->_unitOfWork->persist($entity);
406
        $this->_unitOfWork->addToIdentityMap($entity);
407
408
        self::assertSame($entity, $this->_unitOfWork->getByIdHash($idHash, get_class($entity)));
409
    }
410
411
    public function entitiesWithValidIdentifiersProvider()
412
    {
413
        $emptyString = new EntityWithStringIdentifier();
414
415
        $emptyString->id = '';
416
417
        $nonEmptyString = new EntityWithStringIdentifier();
418
419
        $nonEmptyString->id = uniqid('id', true);
420
421
        $emptyStrings = new EntityWithCompositeStringIdentifier();
422
423
        $emptyStrings->id1 = '';
424
        $emptyStrings->id2 = '';
425
426
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
427
428
        $nonEmptyStrings->id1 = uniqid('id1', true);
429
        $nonEmptyStrings->id2 = uniqid('id2', true);
430
431
        $booleanTrue = new EntityWithBooleanIdentifier();
432
433
        $booleanTrue->id = true;
434
435
        $booleanFalse = new EntityWithBooleanIdentifier();
436
437
        $booleanFalse->id = false;
438
439
        return [
440
            'empty string, single field'     => [$emptyString, ''],
441
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
442
            'empty strings, two fields'      => [$emptyStrings, ' '],
443
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
444
            'boolean true'                   => [$booleanTrue, '1'],
445
            'boolean false'                  => [$booleanFalse, ''],
446
        ];
447
    }
448
449
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier()
450
    {
451
        $this->expectException(ORMInvalidArgumentException::class);
452
453
        $this->_unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
454
    }
455
456
    /**
457
     * @dataProvider entitiesWithInvalidIdentifiersProvider
458
     *
459
     * @param object $entity
460
     * @param array  $identifier
461
     *
462
     * @return void
463
     */
464
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier)
465
    {
466
        $this->expectException(ORMInvalidArgumentException::class);
467
468
        $this->_unitOfWork->registerManaged($entity, $identifier, []);
469
    }
470
471
472
    public function entitiesWithInvalidIdentifiersProvider()
473
    {
474
        $firstNullString  = new EntityWithCompositeStringIdentifier();
475
476
        $firstNullString->id2 = uniqid('id2', true);
477
478
        $secondNullString = new EntityWithCompositeStringIdentifier();
479
480
        $secondNullString->id1 = uniqid('id1', true);
481
482
        return [
483
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
484
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
485
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
486
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
487
        ];
488
    }
489
490
    /**
491
     * @group 5689
492
     * @group 1465
493
     */
494
    public function testObjectHashesOfMergedEntitiesAreNotUsedInOriginalEntityDataMap()
495
    {
496
        $user       = new CmsUser();
497
        $user->name = 'ocramius';
498
        $mergedUser = $this->_unitOfWork->merge($user);
499
500
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($user), 'No original data was stored');
501
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($mergedUser), 'No original data was stored');
502
503
504
        $user       = null;
505
        $mergedUser = null;
506
507
        // force garbage collection of $user (frees the used object hashes, which may be recycled)
508
        gc_collect_cycles();
509
510
        $newUser       = new CmsUser();
511
        $newUser->name = 'ocramius';
512
513
        $this->_unitOfWork->persist($newUser);
514
515
        self::assertSame([], $this->_unitOfWork->getOriginalEntityData($newUser), 'No original data was stored');
516
    }
517
}
518
519
/**
520
 * @Entity
521
 */
522
class NotifyChangedEntity implements NotifyPropertyChanged
523
{
524
    private $_listeners = array();
525
    /**
526
     * @Id
527
     * @Column(type="integer")
528
     * @GeneratedValue
529
     */
530
    private $id;
531
    /**
532
     * @Column(type="string")
533
     */
534
    private $data;
535
536
    private $transient; // not persisted
537
538
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
539
    private $items;
540
541
    public function  __construct() {
542
        $this->items = new ArrayCollection;
543
    }
544
545
    public function getId() {
546
        return $this->id;
547
    }
548
549
    public function getItems() {
550
        return $this->items;
551
    }
552
553
    public function setTransient($value) {
554
        if ($value != $this->transient) {
555
            $this->_onPropertyChanged('transient', $this->transient, $value);
556
            $this->transient = $value;
557
        }
558
    }
559
560
    public function getData() {
561
        return $this->data;
562
    }
563
564
    public function setData($data) {
565
        if ($data != $this->data) {
566
            $this->_onPropertyChanged('data', $this->data, $data);
567
            $this->data = $data;
568
        }
569
    }
570
571
    public function addPropertyChangedListener(PropertyChangedListener $listener)
572
    {
573
        $this->_listeners[] = $listener;
574
    }
575
576
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
577
        if ($this->_listeners) {
578
            foreach ($this->_listeners as $listener) {
579
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
580
            }
581
        }
582
    }
583
}
584
585
/** @Entity */
586
class NotifyChangedRelatedItem
587
{
588
    /**
589
     * @Id
590
     * @Column(type="integer")
591
     * @GeneratedValue
592
     */
593
    private $id;
594
595
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
596
    private $owner;
597
598
    public function getId() {
599
        return $this->id;
600
    }
601
602
    public function getOwner() {
603
        return $this->owner;
604
    }
605
606
    public function setOwner($owner) {
607
        $this->owner = $owner;
608
    }
609
}
610
611
/** @Entity */
612
class VersionedAssignedIdentifierEntity
613
{
614
    /**
615
     * @Id @Column(type="integer")
616
     */
617
    public $id;
618
    /**
619
     * @Version @Column(type="integer")
620
     */
621
    public $version;
622
}
623
624
/** @Entity */
625
class EntityWithStringIdentifier
626
{
627
    /**
628
     * @Id @Column(type="string")
629
     *
630
     * @var string|null
631
     */
632
    public $id;
633
}
634
635
/** @Entity */
636
class EntityWithBooleanIdentifier
637
{
638
    /**
639
     * @Id @Column(type="boolean")
640
     *
641
     * @var bool|null
642
     */
643
    public $id;
644
}
645
646
/** @Entity */
647
class EntityWithCompositeStringIdentifier
648
{
649
    /**
650
     * @Id @Column(type="string")
651
     *
652
     * @var string|null
653
     */
654
    public $id1;
655
656
    /**
657
     * @Id @Column(type="string")
658
     *
659
     * @var string|null
660
     */
661
    public $id2;
662
}
663