Completed
Pull Request — master (#5579)
by Huberty
09:06
created

UnitOfWorkTest   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 341
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13
Metric Value
wmc 14
lcom 1
cbo 13
dl 0
loc 341
rs 10

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