Completed
Pull Request — master (#6017)
by Jeremy
10:12
created

UnitOfWorkTest::setUp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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