Completed
Pull Request — master (#5850)
by Rico
68:47 queued 03:40
created

UnitOfWorkTest   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 15
lcom 1
cbo 13
dl 0
loc 340
rs 10
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 8 1
A tearDown() 0 2 1
A testRegisterRemovedOnNewEntityIsIgnored() 0 8 1
B testSavingSingleEntityWithIdentityColumnForcesInsert() 0 34 1
B testCascadedIdentityColumnInsert() 0 32 1
B testChangeTrackingNotify() 0 43 1
A testGetEntityStateOnVersionedEntityWithAssignedIdentifier() 0 10 1
A testGetEntityStateWithAssignedIdentity() 0 22 1
A testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges() 0 19 1
A testLockWithoutEntityThrowsException() 0 5 1
A testRejectsPersistenceOfObjectsWithInvalidAssociationValue() 0 18 1
A testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue() 0 20 1
A testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected() 0 14 1
A testPersistedEntityAndClearManager() 0 20 1
A invalidAssociationValuesDataProvider() 0 11 1
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\UnitOfWork;
10
use Doctrine\Tests\Mocks\ConnectionMock;
11
use Doctrine\Tests\Mocks\DriverMock;
12
use Doctrine\Tests\Mocks\EntityManagerMock;
13
use Doctrine\Tests\Mocks\EntityPersisterMock;
14
use Doctrine\Tests\Mocks\UnitOfWorkMock;
15
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
16
use Doctrine\Tests\Models\Forum\ForumAvatar;
17
use Doctrine\Tests\Models\Forum\ForumUser;
18
use Doctrine\Tests\OrmTestCase;
19
use stdClass;
20
21
/**
22
 * UnitOfWork tests.
23
 */
24
class UnitOfWorkTest extends OrmTestCase
25
{
26
    /**
27
     * SUT
28
     *
29
     * @var UnitOfWorkMock
30
     */
31
    private $_unitOfWork;
32
33
    /**
34
     * Provides a sequence mock to the UnitOfWork
35
     *
36
     * @var ConnectionMock
37
     */
38
    private $_connectionMock;
39
40
    /**
41
     * The EntityManager mock that provides the mock persisters
42
     *
43
     * @var EntityManagerMock
44
     */
45
    private $_emMock;
46
47
    protected function setUp() {
48
        parent::setUp();
49
        $this->_connectionMock = new ConnectionMock(array(), new DriverMock());
50
        $this->_emMock = EntityManagerMock::create($this->_connectionMock);
51
        // SUT
52
        $this->_unitOfWork = new UnitOfWorkMock($this->_emMock);
53
        $this->_emMock->setUnitOfWork($this->_unitOfWork);
54
    }
55
56
    protected function tearDown() {
57
    }
58
59
    public function testRegisterRemovedOnNewEntityIsIgnored()
60
    {
61
        $user = new ForumUser();
62
        $user->username = 'romanb';
63
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
64
        $this->_unitOfWork->scheduleForDelete($user);
65
        $this->assertFalse($this->_unitOfWork->isScheduledForDelete($user));
66
    }
67
68
69
    /* Operational tests */
70
71
    public function testSavingSingleEntityWithIdentityColumnForcesInsert()
72
    {
73
        // Setup fake persister and id generator for identity generation
74
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
75
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
76
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
77
78
        // Test
79
        $user = new ForumUser();
80
        $user->username = 'romanb';
81
        $this->_unitOfWork->persist($user);
82
83
        // Check
84
        $this->assertEquals(0, count($userPersister->getInserts()));
85
        $this->assertEquals(0, count($userPersister->getUpdates()));
86
        $this->assertEquals(0, count($userPersister->getDeletes()));
87
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($user));
88
        // should no longer be scheduled for insert
89
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($user));
90
91
        // Now lets check whether a subsequent commit() does anything
92
        $userPersister->reset();
93
94
        // Test
95
        $this->_unitOfWork->commit();
96
97
        // Check.
98
        $this->assertEquals(1, count($userPersister->getInserts()));
99
        $this->assertEquals(0, count($userPersister->getUpdates()));
100
        $this->assertEquals(0, count($userPersister->getDeletes()));
101
102
        // should have an id
103
        $this->assertTrue(is_numeric($user->id));
104
    }
105
106
    /**
107
     * Tests a scenario where a save() operation is cascaded from a ForumUser
108
     * to its associated ForumAvatar, both entities using IDENTITY id generation.
109
     */
110
    public function testCascadedIdentityColumnInsert()
111
    {
112
        // Setup fake persister and id generator for identity generation
113
        //ForumUser
114
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
115
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
116
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
117
        // ForumAvatar
118
        $avatarPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumAvatar'));
119
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumAvatar', $avatarPersister);
120
        $avatarPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
121
122
        // Test
123
        $user = new ForumUser();
124
        $user->username = 'romanb';
125
        $avatar = new ForumAvatar();
126
        $user->avatar = $avatar;
127
        $this->_unitOfWork->persist($user); // save cascaded to avatar
128
129
        $this->_unitOfWork->commit();
130
131
        $this->assertTrue(is_numeric($user->id));
132
        $this->assertTrue(is_numeric($avatar->id));
133
134
        $this->assertEquals(1, count($userPersister->getInserts()));
135
        $this->assertEquals(0, count($userPersister->getUpdates()));
136
        $this->assertEquals(0, count($userPersister->getDeletes()));
137
138
        $this->assertEquals(1, count($avatarPersister->getInserts()));
139
        $this->assertEquals(0, count($avatarPersister->getUpdates()));
140
        $this->assertEquals(0, count($avatarPersister->getDeletes()));
141
    }
142
143
    public function testChangeTrackingNotify()
144
    {
145
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedEntity'));
146
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedEntity', $persister);
147
        $itemPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\NotifyChangedRelatedItem'));
148
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\NotifyChangedRelatedItem', $itemPersister);
149
150
        $entity = new NotifyChangedEntity;
151
        $entity->setData('thedata');
152
        $this->_unitOfWork->persist($entity);
153
154
        $this->_unitOfWork->commit();
155
        $this->assertEquals(1, count($persister->getInserts()));
156
        $persister->reset();
157
158
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
159
160
        $entity->setData('newdata');
161
        $entity->setTransient('newtransientvalue');
162
163
        $this->assertTrue($this->_unitOfWork->isScheduledForDirtyCheck($entity));
164
165
        $this->assertEquals(array('data' => array('thedata', 'newdata')), $this->_unitOfWork->getEntityChangeSet($entity));
166
167
        $item = new NotifyChangedRelatedItem();
168
        $entity->getItems()->add($item);
169
        $item->setOwner($entity);
170
        $this->_unitOfWork->persist($item);
171
172
        $this->_unitOfWork->commit();
173
        $this->assertEquals(1, count($itemPersister->getInserts()));
174
        $persister->reset();
175
        $itemPersister->reset();
176
177
178
        $entity->getItems()->removeElement($item);
179
        $item->setOwner(null);
180
        $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...
181
        $this->_unitOfWork->commit();
182
        $updates = $itemPersister->getUpdates();
183
        $this->assertEquals(1, count($updates));
184
        $this->assertTrue($updates[0] === $item);
185
    }
186
187
    public function testGetEntityStateOnVersionedEntityWithAssignedIdentifier()
188
    {
189
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\ORM\VersionedAssignedIdentifierEntity'));
190
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\ORM\VersionedAssignedIdentifierEntity', $persister);
191
192
        $e = new VersionedAssignedIdentifierEntity();
193
        $e->id = 42;
194
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($e));
195
        $this->assertFalse($persister->isExistsCalled());
196
    }
197
198
    public function testGetEntityStateWithAssignedIdentity()
199
    {
200
        $persister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'));
201
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\CMS\CmsPhonenumber', $persister);
202
203
        $ph = new CmsPhonenumber();
204
        $ph->phonenumber = '12345';
205
206
        $this->assertEquals(UnitOfWork::STATE_NEW, $this->_unitOfWork->getEntityState($ph));
207
        $this->assertTrue($persister->isExistsCalled());
208
209
        $persister->reset();
210
211
        // if the entity is already managed the exists() check should be skipped
212
        $this->_unitOfWork->registerManaged($ph, array('phonenumber' => '12345'), array());
213
        $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($ph));
214
        $this->assertFalse($persister->isExistsCalled());
215
        $ph2 = new CmsPhonenumber();
216
        $ph2->phonenumber = '12345';
217
        $this->assertEquals(UnitOfWork::STATE_DETACHED, $this->_unitOfWork->getEntityState($ph2));
218
        $this->assertFalse($persister->isExistsCalled());
219
    }
220
221
    /**
222
     * DDC-2086 [GH-484] Prevented 'Undefined index' notice when updating.
223
     */
224
    public function testNoUndefinedIndexNoticeOnScheduleForUpdateWithoutChanges()
225
    {
226
        // Setup fake persister and id generator
227
        $userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser'));
228
        $userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
229
        $this->_unitOfWork->setEntityPersister('Doctrine\Tests\Models\Forum\ForumUser', $userPersister);
230
231
        // Create a test user
232
        $user = new ForumUser();
233
        $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...
234
        $this->_unitOfWork->persist($user);
235
        $this->_unitOfWork->commit();
236
237
        // Schedule user for update without changes
238
        $this->_unitOfWork->scheduleForUpdate($user);
239
240
        // This commit should not raise an E_NOTICE
241
        $this->_unitOfWork->commit();
242
    }
243
244
    /**
245
     * @group DDC-1984
246
     */
247
    public function testLockWithoutEntityThrowsException()
248
    {
249
        $this->setExpectedException('InvalidArgumentException');
250
        $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...
251
    }
252
253
    /**
254
     * @group DDC-3490
255
     *
256
     * @dataProvider invalidAssociationValuesDataProvider
257
     *
258
     * @param mixed $invalidValue
259
     */
260
    public function testRejectsPersistenceOfObjectsWithInvalidAssociationValue($invalidValue)
261
    {
262
        $this->_unitOfWork->setEntityPersister(
263
            'Doctrine\Tests\Models\Forum\ForumUser',
264
            new EntityPersisterMock(
265
                $this->_emMock,
266
                $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser')
267
            )
268
        );
269
270
        $user           = new ForumUser();
271
        $user->username = 'John';
272
        $user->avatar   = $invalidValue;
273
274
        $this->setExpectedException('Doctrine\ORM\ORMInvalidArgumentException');
275
276
        $this->_unitOfWork->persist($user);
277
    }
278
279
    /**
280
     * @group DDC-3490
281
     *
282
     * @dataProvider invalidAssociationValuesDataProvider
283
     *
284
     * @param mixed $invalidValue
285
     */
286
    public function testRejectsChangeSetComputationForObjectsWithInvalidAssociationValue($invalidValue)
287
    {
288
        $metadata = $this->_emMock->getClassMetadata('Doctrine\Tests\Models\Forum\ForumUser');
289
290
        $this->_unitOfWork->setEntityPersister(
291
            'Doctrine\Tests\Models\Forum\ForumUser',
292
            new EntityPersisterMock($this->_emMock, $metadata)
293
        );
294
295
        $user = new ForumUser();
296
297
        $this->_unitOfWork->persist($user);
298
299
        $user->username = 'John';
300
        $user->avatar   = $invalidValue;
301
302
        $this->setExpectedException('Doctrine\ORM\ORMInvalidArgumentException');
303
304
        $this->_unitOfWork->computeChangeSet($metadata, $user);
305
    }
306
307
    /**
308
     * @group DDC-3619
309
     * @group 1338
310
     */
311
    public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGarbageCollected()
312
    {
313
        $entity     = new ForumUser();
314
        $entity->id = 123;
315
316
        $this->_unitOfWork->registerManaged($entity, array('id' => 123), array());
317
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
318
319
        $this->_unitOfWork->remove($entity);
320
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity));
321
322
        $this->_unitOfWork->persist($entity);
323
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity));
324
    }
325
326
    public function testPersistedEntityAndClearManager()
327
    {
328
        $entity1     = new ForumUser();
329
        $entity1->id = 123;
330
331
        $entity2     = new ForumAvatar();
332
        $entity2->id = 456;
333
334
        $this->_unitOfWork->persist($entity1);
335
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
336
337
        $this->_unitOfWork->persist($entity2);
338
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity2));
339
340
        $this->_unitOfWork->clear(ForumAvatar::class);
341
        $this->assertTrue($this->_unitOfWork->isInIdentityMap($entity1));
342
        $this->assertFalse($this->_unitOfWork->isInIdentityMap($entity2));
343
        $this->assertTrue($this->_unitOfWork->isScheduledForInsert($entity1));
344
        $this->assertFalse($this->_unitOfWork->isScheduledForInsert($entity2));
345
    }
346
347
    /**
348
     * Data Provider
349
     *
350
     * @return mixed[][]
351
     */
352
    public function invalidAssociationValuesDataProvider()
353
    {
354
        return [
355
            ['foo'],
356
            [['foo']],
357
            [''],
358
            [[]],
359
            [new stdClass()],
360
            [new ArrayCollection()],
361
        ];
362
    }
363
}
364
365
/**
366
 * @Entity
367
 */
368
class NotifyChangedEntity implements NotifyPropertyChanged
369
{
370
    private $_listeners = array();
371
    /**
372
     * @Id
373
     * @Column(type="integer")
374
     * @GeneratedValue
375
     */
376
    private $id;
377
    /**
378
     * @Column(type="string")
379
     */
380
    private $data;
381
382
    private $transient; // not persisted
383
384
    /** @OneToMany(targetEntity="NotifyChangedRelatedItem", mappedBy="owner") */
385
    private $items;
386
387
    public function  __construct() {
388
        $this->items = new ArrayCollection;
389
    }
390
391
    public function getId() {
392
        return $this->id;
393
    }
394
395
    public function getItems() {
396
        return $this->items;
397
    }
398
399
    public function setTransient($value) {
400
        if ($value != $this->transient) {
401
            $this->_onPropertyChanged('transient', $this->transient, $value);
402
            $this->transient = $value;
403
        }
404
    }
405
406
    public function getData() {
407
        return $this->data;
408
    }
409
410
    public function setData($data) {
411
        if ($data != $this->data) {
412
            $this->_onPropertyChanged('data', $this->data, $data);
413
            $this->data = $data;
414
        }
415
    }
416
417
    public function addPropertyChangedListener(PropertyChangedListener $listener)
418
    {
419
        $this->_listeners[] = $listener;
420
    }
421
422
    protected function _onPropertyChanged($propName, $oldValue, $newValue) {
423
        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...
424
            foreach ($this->_listeners as $listener) {
425
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
426
            }
427
        }
428
    }
429
}
430
431
/** @Entity */
432
class NotifyChangedRelatedItem
433
{
434
    /**
435
     * @Id
436
     * @Column(type="integer")
437
     * @GeneratedValue
438
     */
439
    private $id;
440
441
    /** @ManyToOne(targetEntity="NotifyChangedEntity", inversedBy="items") */
442
    private $owner;
443
444
    public function getId() {
445
        return $this->id;
446
    }
447
448
    public function getOwner() {
449
        return $this->owner;
450
    }
451
452
    public function setOwner($owner) {
453
        $this->owner = $owner;
454
    }
455
}
456
457
/** @Entity */
458
class VersionedAssignedIdentifierEntity
459
{
460
    /**
461
     * @Id @Column(type="integer")
462
     */
463
    public $id;
464
    /**
465
     * @Version @Column(type="integer")
466
     */
467
    public $version;
468
}
469