Completed
Pull Request — master (#5924)
by Marco
27:45 queued 19:39
created

testAddToIdentityMapValidIdentifiers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
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\Forum\ForumAvatar;
18
use Doctrine\Tests\Models\Forum\ForumUser;
19
use Doctrine\Tests\Models\GeoNames\City;
20
use Doctrine\Tests\Models\GeoNames\Country;
21
use Doctrine\Tests\OrmTestCase;
22
use stdClass;
23
24
/**
25
 * UnitOfWork tests.
26
 */
27
class UnitOfWorkTest extends OrmTestCase
28
{
29
    /**
30
     * SUT
31
     *
32
     * @var UnitOfWorkMock
33
     */
34
    private $_unitOfWork;
35
36
    /**
37
     * Provides a sequence mock to the UnitOfWork
38
     *
39
     * @var ConnectionMock
40
     */
41
    private $_connectionMock;
42
43
    /**
44
     * The EntityManager mock that provides the mock persisters
45
     *
46
     * @var EntityManagerMock
47
     */
48
    private $_emMock;
49
50
    protected function setUp() {
51
        parent::setUp();
52
        $this->_connectionMock = new ConnectionMock(array(), new DriverMock());
53
        $this->_emMock = EntityManagerMock::create($this->_connectionMock);
54
        // SUT
55
        $this->_unitOfWork = new UnitOfWorkMock($this->_emMock);
56
        $this->_emMock->setUnitOfWork($this->_unitOfWork);
57
    }
58
59
    protected function tearDown() {
60
    }
61
62
    public function testRegisterRemovedOnNewEntityIsIgnored()
63
    {
64
        $user = new ForumUser();
65
        $user->username = 'romanb';
66
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
67
        $this->_unitOfWork->scheduleForDelete($user);
68
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
69
    }
70
71
72
    /* Operational tests */
73
74
    public function testSavingSingleEntityWithIdentityColumnForcesInsert()
75
    {
76
        // Setup fake persister and id generator for identity generation
77
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
78
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
79
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
80
81
        // Test
82
        $user = new ForumUser();
83
        $user->username = 'romanb';
84
        $this->_unitOfWork->persist($user);
85
86
        // Check
87
        $this->assertEquals(0, count($userPersister->getInserts()));
88
        $this->assertEquals(0, count($userPersister->getUpdates()));
89
        $this->assertEquals(0, count($userPersister->getDeletes()));
90
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($user));
91
        // should no longer be scheduled for insert
92
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($user));
93
94
        // Now lets check whether a subsequent commit() does anything
95
        $userPersister->reset();
96
97
        // Test
98
        $this->_unitOfWork->commit();
99
100
        // Check.
101
        $this->assertEquals(1, count($userPersister->getInserts()));
102
        $this->assertEquals(0, count($userPersister->getUpdates()));
103
        $this->assertEquals(0, count($userPersister->getDeletes()));
104
105
        // should have an id
106
        $this->assertTrue(is_numeric($user->id));
107
    }
108
109
    /**
110
     * Tests a scenario where a save() operation is cascaded from a ForumUser
111
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
112
     */
113
    public function testCascadedIdentityColumnInsert()
114
    {
115
        // Setup fake persister and id generator for identity generation
116
        //ForumUser
117
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
118
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
119
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
120
        // ForumAvatar
121
        $avatarPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumAvatar'));
122
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumAvatar', $avatarPersister);
123
        $avatarPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
124
125
        // Test
126
        $user = new ForumUser();
127
        $user->username = 'romanb';
128
        $avatar = new ForumAvatar();
129
        $user->avatar = $avatar;
130
        $this->_unitOfWork->persist($user); // save cascaded to avatar
131
132
        $this->_unitOfWork->commit();
133
134
        $this->assertTrue(is_numeric($user->id));
135
        $this->assertTrue(is_numeric($avatar->id));
136
137
        $this->assertEquals(1, count($userPersister->getInserts()));
138
        $this->assertEquals(0, count($userPersister->getUpdates()));
139
        $this->assertEquals(0, count($userPersister->getDeletes()));
140
141
        $this->assertEquals(1, count($avatarPersister->getInserts()));
142
        $this->assertEquals(0, count($avatarPersister->getUpdates()));
143
        $this->assertEquals(0, count($avatarPersister->getDeletes()));
144
    }
145
146
    public function testChangeTrackingNotify()
147
    {
148
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedEntity'));
149
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedEntity', $persister);
150
        $itemPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedRelatedItem'));
151
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedRelatedItem', $itemPersister);
152
153
        $entity = new NotifyChangedEntity;
154
        $entity->setData('thedata');
155
        $this->_unitOfWork->persist($entity);
156
157
        $this->_unitOfWork->commit();
158
        $this->assertEquals(1, count($persister->getInserts()));
159
        $persister->reset();
160
161
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
162
163
        $entity->setData('newdata');
164
        $entity->setTransient('newtransientvalue');
165
166
        $this->assertTrue($this->_unitOfWork->isScheduledForDirtyCheck($entity));
167
168
        $this->assertEquals(array('data' => array('thedata', 'newdata')), $this->_unitOfWork->getEntityChangeSet($entity));
169
170
        $item = new NotifyChangedRelatedItem();
171
        $entity->getItems()->add($item);
172
        $item->setOwner($entity);
173
        $this->_unitOfWork->persist($item);
174
175
        $this->_unitOfWork->commit();
176
        $this->assertEquals(1, count($itemPersister->getInserts()));
177
        $persister->reset();
178
        $itemPersister->reset();
179
180
181
        $entity->getItems()->removeElement($item);
182
        $item->setOwner(null);
183
        $this->assertTrue($entity->getItems()->isDirty());
0 ignored issues
show
Bug introduced by
The method isDirty() does not seem to exist on object<Doctrine\Common\C...ctions\ArrayCollection>.

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...
184
        $this->_unitOfWork->commit();
185
        $updates = $itemPersister->getUpdates();
186
        $this->assertEquals(1, count($updates));
187
        $this->assertTrue($updates[0] === $item);
188
    }
189
190
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier()
191
    {
192
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\VersionedAssignedIdentifierEntity'));
193
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\VersionedAssignedIdentifierEntity', $persister);
194
195
        $e = new VersionedAssignedIdentifierEntity();
196
        $e->id = 42;
197
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($e));
198
        $this->assertFalse($persister->isExistsCalled());
199
    }
200
201
    public function testGetEntityStateWithAssignedIdentity()
202
    {
203
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'));
204
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\CMS\CmsPhonenumber', $persister);
205
206
        $ph = new CmsPhonenumber();
207
        $ph->phonenumber = '12345';
208
209
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($ph));
210
        $this->assertTrue($persister->isExistsCalled());
211
212
        $persister->reset();
213
214
        // if the entity is already managed the exists() check should be skipped
215
        $this->_unitOfWork->registerManaged($ph, array('phonenumber' => '12345'), array());
216
        $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($ph));
217
        $this->assertFalse($persister->isExistsCalled());
218
        $ph2 = new CmsPhonenumber();
219
        $ph2->phonenumber = '12345';
220
        $this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_unitOfWork->getEntityState($ph2));
221
        $this->assertFalse($persister->isExistsCalled());
222
    }
223
224
    /**
225
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
226
     */
227
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges()
228
    {
229
        // Setup fake persister and id generator
230
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
231
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
232
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
233
234
        // Create a test user
235
        $user = new ForumUser();
236
        $user->name = 'Jasper';
0 ignored issues
show
Bug introduced by
The property name does not seem to exist in Doctrine\Tests\Models\Forum\ForumUser.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
237
        $this->_unitOfWork->persist($user);
238
        $this->_unitOfWork->commit();
239
240
        // Schedule user for update without changes
241
        $this->_unitOfWork->scheduleForUpdate($user);
242
243
        // This commit should not raise an E_NOTICE
244
        $this->_unitOfWork->commit();
245
    }
246
247
    /**
248
     * @group DDC-1984
249
     */
250
    public function testLockWithoutEntityThrowsException()
251
    {
252
        $this->expectException(\InvalidArgumentException::class);
253
        $this->_unitOfWork->lock(null, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
254
    }
255
256
    /**
257
     * @group DDC-3490
258
     *
259
     * @dataProvider invalidAssociationValuesDataProvider
260
     *
261
     * @param mixed $invalidValue
262
     */
263
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
264
    {
265
        $this->_unitOfWork->setEntityPersister(
266
            'Doctrine\Tests\Models\Forum\ForumUser',
267
            new EntityPersisterMock(
268
                $this->_emMock,
269
                $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser')
270
            )
271
        );
272
273
        $user           = new ForumUser();
274
        $user->username = 'John';
275
        $user->avatar   = $invalidValue;
276
277
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
278
279
        $this->_unitOfWork->persist($user);
280
    }
281
282
    /**
283
     * @group DDC-3490
284
     *
285
     * @dataProvider invalidAssociationValuesDataProvider
286
     *
287
     * @param mixed $invalidValue
288
     */
289
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
290
    {
291
        $metadata = $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser');
292
293
        $this->_unitOfWork->setEntityPersister(
294
            'Doctrine\Tests\Models\Forum\ForumUser',
295
            new EntityPersisterMock($this->_emMock, $metadata)
296
        );
297
298
        $user = new ForumUser();
299
300
        $this->_unitOfWork->persist($user);
301
302
        $user->username = 'John';
303
        $user->avatar   = $invalidValue;
304
305
        $this->expectException(\Doctrine\ORM\ORMInvalidArgumentException::class);
306
307
        $this->_unitOfWork->computeChangeSet($metadata, $user);
308
    }
309
310
    /**
311
     * @group DDC-3619
312
     * @group 1338
313
     */
314
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected()
315
    {
316
        $entity     = new ForumUser();
317
        $entity->id = 123;
318
319
        $this->_unitOfWork->registerManaged($entity, array('id' => 123), array());
320
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
321
322
        $this->_unitOfWork->remove($entity);
323
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
324
325
        $this->_unitOfWork->persist($entity);
326
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
327
    }
328
329
    /**
330
     * @group 5849
331
     * @group 5850
332
     */
333
    public function testPersistedEntityAndClearManager()
334
    {
335
        $entity1 = new City(123, 'London');
336
        $entity2 = new Country(456, 'United Kingdom');
337
338
        $this->_unitOfWork->persist($entity1);
339
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
340
341
        $this->_unitOfWork->persist($entity2);
342
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
343
344
        $this->_unitOfWork->clear(Country::class);
345
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
346
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity2));
347
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($entity1));
348
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity2));
349
    }
350
351
    /**
352
     * Data Provider
353
     *
354
     * @return mixed[][]
355
     */
356
    public function invalidAssociationValuesDataProvider()
357
    {
358
        return [
359
            ['foo'],
360
            [['foo']],
361
            [''],
362
            [[]],
363
            [new stdClass()],
364
            [new ArrayCollection()],
365
        ];
366
    }
367
368
    /**
369
     * @dataProvider entitiesWithValidIdentifiersProvider
370
     *
371
     * @param object $entity
372
     * @param string $idHash
373
     *
374
     * @return void
375
     */
376
    public function testAddToIdentityMapValidIdentifiers($entity, $idHash)
377
    {
378
        $this->_unitOfWork->persist($entity);
379
        $this->_unitOfWork->addToIdentityMap($entity);
380
381
        self::assertSame($entity, $this->_unitOfWork->getByIdHash($idHash, get_class($entity)));
382
    }
383
384
    public function entitiesWithValidIdentifiersProvider()
385
    {
386
        $emptyString = new EntityWithStringIdentifier();
387
388
        $emptyString->id = '';
389
390
        $nonEmptyString = new EntityWithStringIdentifier();
391
392
        $nonEmptyString->id = uniqid('id', true);
393
394
        $emptyStrings = new EntityWithCompositeStringIdentifier();
395
396
        $emptyStrings->id1 = '';
397
        $emptyStrings->id2 = '';
398
399
        $nonEmptyStrings = new EntityWithCompositeStringIdentifier();
400
401
        $nonEmptyStrings->id1 = uniqid('id1', true);
402
        $nonEmptyStrings->id2 = uniqid('id2', true);
403
404
        $booleanTrue = new EntityWithBooleanIdentifier();
405
406
        $booleanTrue->id = true;
407
408
        $booleanFalse = new EntityWithBooleanIdentifier();
409
410
        $booleanFalse->id = false;
411
412
        return [
413
            'empty string, single field'     => [$emptyString, ''],
414
            'non-empty string, single field' => [$nonEmptyString, $nonEmptyString->id],
415
            'empty strings, two fields'      => [$emptyStrings, ' '],
416
            'non-empty strings, two fields'  => [$nonEmptyStrings, $nonEmptyStrings->id1 . ' ' . $nonEmptyStrings->id2],
417
            'boolean true'                   => [$booleanTrue, '1'],
418
            'boolean false'                  => [$booleanFalse, ''],
419
        ];
420
    }
421
422
    public function testRegisteringAManagedInstanceRequiresANonEmptyIdentifier()
423
    {
424
        $this->expectException(ORMInvalidArgumentException::class);
425
426
        $this->_unitOfWork->registerManaged(new EntityWithBooleanIdentifier(), [], []);
427
    }
428
429
    /**
430
     * @dataProvider entitiesWithInvalidIdentifiersProvider
431
     *
432
     * @param object $entity
433
     * @param array  $identifier
434
     *
435
     * @return void
436
     */
437
    public function testAddToIdentityMapInvalidIdentifiers($entity, array $identifier)
438
    {
439
        $this->expectException(ORMInvalidArgumentException::class);
440
441
        $this->_unitOfWork->registerManaged($entity, $identifier, []);
442
    }
443
444
445
    public function entitiesWithInvalidIdentifiersProvider()
446
    {
447
        $firstNullString  = new EntityWithCompositeStringIdentifier();
448
449
        $firstNullString->id2 = uniqid('id2', true);
450
451
        $secondNullString = new EntityWithCompositeStringIdentifier();
452
453
        $secondNullString->id1 = uniqid('id1', true);
454
455
        return [
456
            'null string, single field'      => [new EntityWithStringIdentifier(), ['id' => null]],
457
            'null strings, two fields'       => [new EntityWithCompositeStringIdentifier(), ['id1' => null, 'id2' => null]],
458
            'first null string, two fields'  => [$firstNullString, ['id1' => null, 'id2' => $firstNullString->id2]],
459
            'second null string, two fields' => [$secondNullString, ['id1' => $secondNullString->id1, 'id2' => null]],
460
        ];
461
    }
462
}
463
464
/**
465
 * @Entity
466
 */
467
class NotifyChangedEntity implements NotifyPropertyChanged
468
{
469
    private $_listeners = array();
470
    /**
471
     * @Id
472
     * @Column(type="integer")
473
     * @GeneratedValue
474
     */
475
    private $id;
476
    /**
477
     * @Column(type="string")
478
     */
479
    private $data;
480
481
    private $transient; // not persisted
482
483
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
484
    private $items;
485
486
    public function  __construct() {
487
        $this->items = new ArrayCollection;
488
    }
489
490
    public function getId() {
491
        return $this->id;
492
    }
493
494
    public function getItems() {
495
        return $this->items;
496
    }
497
498
    public function setTransient($value) {
499
        if ($value != $this->transient) {
500
            $this->_onPropertyChanged('transient', $this->transient, $value);
501
            $this->transient = $value;
502
        }
503
    }
504
505
    public function getData() {
506
        return $this->data;
507
    }
508
509
    public function setData($data) {
510
        if ($data != $this->data) {
511
            $this->_onPropertyChanged('data', $this->data, $data);
512
            $this->data = $data;
513
        }
514
    }
515
516
    public function addPropertyChangedListener(PropertyChangedListener $listener)
517
    {
518
        $this->_listeners[] = $listener;
519
    }
520
521
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
522
        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...
523
            foreach ($this->_listeners as $listener) {
524
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
525
            }
526
        }
527
    }
528
}
529
530
/** @Entity */
531
class NotifyChangedRelatedItem
532
{
533
    /**
534
     * @Id
535
     * @Column(type="integer")
536
     * @GeneratedValue
537
     */
538
    private $id;
539
540
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
541
    private $owner;
542
543
    public function getId() {
544
        return $this->id;
545
    }
546
547
    public function getOwner() {
548
        return $this->owner;
549
    }
550
551
    public function setOwner($owner) {
552
        $this->owner = $owner;
553
    }
554
}
555
556
/** @Entity */
557
class VersionedAssignedIdentifierEntity
558
{
559
    /**
560
     * @Id @Column(type="integer")
561
     */
562
    public $id;
563
    /**
564
     * @Version @Column(type="integer")
565
     */
566
    public $version;
567
}
568
569
/** @Entity */
570
class EntityWithStringIdentifier
571
{
572
    /**
573
     * @Id @Column(type="string")
574
     *
575
     * @var string|null
576
     */
577
    public $id;
578
}
579
580
/** @Entity */
581
class EntityWithBooleanIdentifier
582
{
583
    /**
584
     * @Id @Column(type="boolean")
585
     *
586
     * @var bool|null
587
     */
588
    public $id;
589
}
590
591
/** @Entity */
592
class EntityWithCompositeStringIdentifier
593
{
594
    /**
595
     * @Id @Column(type="string")
596
     *
597
     * @var string|null
598
     */
599
    public $id1;
600
601
    /**
602
     * @Id @Column(type="string")
603
     *
604
     * @var string|null
605
     */
606
    public $id2;
607
}
608