Passed
Push — pulls/manymanylist-add-callbac... ( e094c2...9c86cd )
by Ingo
08:27
created

testRelationshipEmptyOnNewRecords()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Tests;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\ORM\FieldType\DBMoney;
7
use SilverStripe\ORM\ManyManyList;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Dev\SapphireTest;
10
use SilverStripe\ORM\Tests\DataObjectTest\Player;
11
use SilverStripe\ORM\Tests\DataObjectTest\Team;
12
use SilverStripe\ORM\Tests\ManyManyListTest\ExtraFieldsObject;
13
use SilverStripe\ORM\Tests\ManyManyListTest\Product;
14
use InvalidArgumentException;
15
16
class ManyManyListTest extends SapphireTest
17
{
18
19
    protected static $fixture_file = 'DataObjectTest.yml';
20
21
    public static $extra_data_objects = [
22
        ManyManyListTest\Category::class,
23
        ManyManyListTest\ExtraFieldsObject::class,
24
        ManyManyListTest\Product::class,
25
        DataObjectTest\MockDynamicAssignmentDataObject::class
26
    ];
27
28
    public static function getExtraDataObjects()
29
    {
30
        return array_merge(
31
            DataObjectTest::$extra_data_objects,
32
            ManyManyListTest::$extra_data_objects
33
        );
34
    }
35
36
    public function testAddCompositedExtraFields()
37
    {
38
        $obj = new ManyManyListTest\ExtraFieldsObject();
39
        $obj->write();
40
41
        $money = new DBMoney();
42
        $money->setAmount(100);
43
        $money->setCurrency('USD');
44
45
        // the actual test is that this does not generate an error in the sql.
46
        $obj->Clients()->add(
0 ignored issues
show
Bug introduced by
The method Clients() does not exist on SilverStripe\ORM\Tests\M...tTest\ExtraFieldsObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

46
        $obj->/** @scrutinizer ignore-call */ 
47
              Clients()->add(
Loading history...
47
            $obj,
48
            [
49
            'Worth' => $money,
50
            'Reference' => 'Foo'
51
            ]
52
        );
53
54
        $check = $obj->Clients()->First();
55
56
        $this->assertEquals('Foo', $check->Reference, 'Basic scalar fields should exist');
57
        $this->assertInstanceOf(DBMoney::class, $check->Worth, 'Composite fields should exist on the record');
58
        $this->assertEquals(100, $check->Worth->getAmount());
59
    }
60
61
    /**
62
     * This test targets a bug where appending many_many_extraFields to a query would
63
     * result in erroneous queries for sort orders that rely on _SortColumn0
64
     */
65
    public function testAddCompositedExtraFieldsWithSortColumn0()
66
    {
67
        $obj = new ExtraFieldsObject();
68
        $obj->write();
69
70
        $product = new Product();
71
        $product->Title = 'Test Product';
72
        $product->write();
73
74
        // the actual test is that this does not generate an error in the sql.
75
        $obj->Products()->add($product, [
0 ignored issues
show
Bug introduced by
The method Products() does not exist on SilverStripe\ORM\Tests\M...tTest\ExtraFieldsObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

75
        $obj->/** @scrutinizer ignore-call */ 
76
              Products()->add($product, [
Loading history...
76
            'Reference' => 'Foo'
77
        ]);
78
79
        $result = $obj->Products()->First();
80
        $this->assertEquals('Foo', $result->Reference, 'Basic scalar fields should exist');
81
        $this->assertEquals('Test Product', $result->Title);
82
    }
83
84
    public function testCreateList()
85
    {
86
        $list = ManyManyList::create(
87
            Team::class,
88
            'DataObjectTest_Team_Players',
89
            'DataObjectTest_TeamID',
90
            'DataObjectTest_PlayerID'
91
        );
92
        $this->assertEquals(2, $list->count());
93
    }
94
95
96
    public function testRelationshipEmptyOnNewRecords()
97
    {
98
        // Relies on the fact that (unrelated) teams exist in the fixture file already
99
        $newPlayer = new Player(); // many_many Teams
100
        $this->assertEquals([], $newPlayer->Teams()->column('ID'));
0 ignored issues
show
Bug introduced by
The method Teams() does not exist on SilverStripe\ORM\Tests\DataObjectTest\Player. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

100
        $this->assertEquals([], $newPlayer->/** @scrutinizer ignore-call */ Teams()->column('ID'));
Loading history...
101
    }
102
103
    public function testAddingSingleDataObjectByReference()
104
    {
105
        $player1 = $this->objFromFixture(Player::class, 'player1');
106
        $team1 = $this->objFromFixture(Team::class, 'team1');
107
        $player1->Teams()->add($team1);
0 ignored issues
show
Bug introduced by
The method Teams() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

107
        $player1->/** @scrutinizer ignore-call */ 
108
                  Teams()->add($team1);
Loading history...
108
        $player1->flushCache();
109
110
        $compareTeams = new ManyManyList(
111
            Team::class,
112
            'DataObjectTest_Team_Players',
113
            'DataObjectTest_TeamID',
114
            'DataObjectTest_PlayerID'
115
        );
116
        $compareTeams = $compareTeams->forForeignID($player1->ID);
117
        $this->assertEquals(
118
            $player1->Teams()->column('ID'),
119
            $compareTeams->column('ID'),
120
            "Adding single record as DataObject to many_many"
121
        );
122
    }
123
124
    public function testRemovingSingleDataObjectByReference()
125
    {
126
        $player1 = $this->objFromFixture(Player::class, 'player1');
127
        $team1 = $this->objFromFixture(Team::class, 'team1');
128
        $player1->Teams()->remove($team1);
129
        $player1->flushCache();
130
        $compareTeams = new ManyManyList(
131
            Team::class,
132
            'DataObjectTest_Team_Players',
133
            'DataObjectTest_TeamID',
134
            'DataObjectTest_PlayerID'
135
        );
136
        $compareTeams = $compareTeams->forForeignID($player1->ID);
137
        $this->assertEquals(
138
            $player1->Teams()->column('ID'),
139
            $compareTeams->column('ID'),
140
            "Removing single record as DataObject from many_many"
141
        );
142
    }
143
144
    public function testAddingSingleDataObjectByID()
145
    {
146
        $player1 = $this->objFromFixture(Player::class, 'player1');
147
        $team1 = $this->objFromFixture(Team::class, 'team1');
148
        $player1->Teams()->add($team1->ID);
149
        $player1->flushCache();
150
        $compareTeams = new ManyManyList(
151
            Team::class,
152
            'DataObjectTest_Team_Players',
153
            'DataObjectTest_TeamID',
154
            'DataObjectTest_PlayerID'
155
        );
156
        $compareTeams = $compareTeams->forForeignID($player1->ID);
157
        $this->assertEquals(
158
            $player1->Teams()->column('ID'),
159
            $compareTeams->column('ID'),
160
            "Adding single record as ID to many_many"
161
        );
162
    }
163
164
    public function testRemoveByID()
165
    {
166
        $player1 = $this->objFromFixture(Player::class, 'player1');
167
        $team1 = $this->objFromFixture(Team::class, 'team1');
168
        $player1->Teams()->removeByID($team1->ID);
169
        $player1->flushCache();
170
        $compareTeams = new ManyManyList(
171
            Team::class,
172
            'DataObjectTest_Team_Players',
173
            'DataObjectTest_TeamID',
174
            'DataObjectTest_PlayerID'
175
        );
176
        $compareTeams = $compareTeams->forForeignID($player1->ID);
177
        $this->assertEquals(
178
            $player1->Teams()->column('ID'),
179
            $compareTeams->column('ID'),
180
            "Removing single record as ID from many_many"
181
        );
182
    }
183
184
    public function testSetByIdList()
185
    {
186
        $player1 = $this->objFromFixture(Player::class, 'player1');
187
        $team1 = $this->objFromFixture(Team::class, 'team1');
188
        $team2 = $this->objFromFixture(Team::class, 'team2');
189
        $player1->Teams()->setByIdList([$team1->ID, $team2->ID]);
190
        $this->assertEquals([$team1->ID, $team2->ID], $player1->Teams()->sort('Title')->column());
191
        $player1->Teams()->setByIdList([$team1->ID]);
192
        $this->assertEquals([$team1->ID], $player1->Teams()->sort('Title')->column());
193
        $player1->Teams()->setByIdList([$team2->ID]);
194
        $this->assertEquals([$team2->ID], $player1->Teams()->sort('Title')->column());
195
    }
196
197
    public function testAddingWithMultipleForeignKeys()
198
    {
199
        $newPlayer = new Player();
200
        $newPlayer->write();
201
        $team1 = $this->objFromFixture(Team::class, 'team1');
202
        $team2 = $this->objFromFixture(Team::class, 'team2');
203
204
        $playersTeam1Team2 = Team::get()->relation('Players')
205
            ->forForeignID([$team1->ID, $team2->ID]);
206
        $playersTeam1Team2->add($newPlayer);
207
        $this->assertEquals(
208
            [$team1->ID, $team2->ID],
209
            $newPlayer->Teams()->sort('Title')->column('ID')
210
        );
211
    }
212
213
    public function testAddingExistingDoesntRemoveExtraFields()
214
    {
215
        $player = new Player();
216
        $player->write();
217
        $team1 = $this->objFromFixture(Team::class, 'team1');
218
219
        $team1->Players()->add($player, ['Position' => 'Captain']);
0 ignored issues
show
Bug introduced by
The method Players() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

219
        $team1->/** @scrutinizer ignore-call */ 
220
                Players()->add($player, ['Position' => 'Captain']);
Loading history...
220
        $this->assertEquals(
221
            ['Position' => 'Captain'],
222
            $team1->Players()->getExtraData('Teams', $player->ID),
223
            'Writes extrafields'
224
        );
225
226
        $team1->Players()->add($player);
227
        $this->assertEquals(
228
            ['Position' => 'Captain'],
229
            $team1->Players()->getExtraData('Teams', $player->ID),
230
            'Retains extrafields on subsequent adds with NULL fields'
231
        );
232
233
        $team1->Players()->add($player, ['Position' => 'Defense']);
234
        $this->assertEquals(
235
            ['Position' => 'Defense'],
236
            $team1->Players()->getExtraData('Teams', $player->ID),
237
            'Updates extrafields on subsequent adds with fields'
238
        );
239
240
        $team1->Players()->add($player, ['Position' => null]);
241
        $this->assertEquals(
242
            ['Position' => null],
243
            $team1->Players()->getExtraData('Teams', $player->ID),
244
            'Allows clearing of extrafields on subsequent adds'
245
        );
246
    }
247
248
    public function testSubtractOnAManyManyList()
249
    {
250
        $allList = ManyManyList::create(
251
            Player::class,
252
            'DataObjectTest_Team_Players',
253
            'DataObjectTest_PlayerID',
254
            'DataObjectTest_TeamID'
255
        );
256
        $this->assertEquals(
257
            3,
258
            $allList->count(),
259
            'Precondition; we have all 3 players connected to a team in the list'
260
        );
261
262
        $teamOneID = $this->idFromFixture(Team::class, 'team1');
263
        $teamTwoID = $this->idFromFixture(Team::class, 'team2');
264
265
        // Captain 1 belongs to one team; team1
266
        $captain1 = $this->objFromFixture(Player::class, 'captain1');
267
        $this->assertEquals(
268
            [$teamOneID],
269
            $captain1->Teams()->column("ID"),
270
            'Precondition; player2 belongs to team1'
271
        );
272
273
        // Player 2 belongs to both teams: team1, team2
274
        $player2 = $this->objFromFixture(Player::class, 'player2');
275
        $this->assertEquals(
276
            [$teamOneID,$teamTwoID],
277
            $player2->Teams()->sort('Title')->column('ID'),
278
            'Precondition; player2 belongs to team1 and team2'
279
        );
280
281
        // We want to find the teams for player2 where the captain does not belong to
282
        $teamsWithoutTheCaptain = $player2->Teams()->subtract($captain1->Teams());
283
284
        // Assertions
285
        $this->assertEquals(
286
            1,
287
            $teamsWithoutTheCaptain->count(),
288
            'The ManyManyList should onlu contain one team'
289
        );
290
        $this->assertEquals(
291
            $teamTwoID,
292
            $teamsWithoutTheCaptain->first()->ID,
293
            'The ManyManyList contains the wrong team'
294
        );
295
    }
296
297
    public function testRemoveAll()
298
    {
299
        $first = new Team();
300
        $first->write();
301
302
        $second = new Team();
303
        $second->write();
304
305
        $firstPlayers = $first->Players();
306
        $secondPlayers = $second->Players();
307
308
        $a = new Player();
309
        $a->ShirtNumber = 'a';
0 ignored issues
show
Bug Best Practice introduced by
The property ShirtNumber does not exist on SilverStripe\ORM\Tests\DataObjectTest\Player. Since you implemented __set, consider adding a @property annotation.
Loading history...
310
        $a->write();
311
312
        $b = new Player();
313
        $b->ShirtNumber = 'b';
314
        $b->write();
315
316
        $firstPlayers->add($a);
317
        $firstPlayers->add($b);
318
319
        $secondPlayers->add($a);
320
        $secondPlayers->add($b);
321
322
        $this->assertEquals(['a', 'b'], $firstPlayers->sort('ShirtNumber')->column('ShirtNumber'));
323
        $this->assertEquals(['a', 'b'], $secondPlayers->sort('ShirtNumber')->column('ShirtNumber'));
324
325
        $firstPlayers->removeAll();
326
327
        $this->assertEquals(0, count($firstPlayers));
328
        $this->assertEquals(2, count($secondPlayers));
329
330
        $firstPlayers->removeAll();
331
332
        $firstPlayers->add($a);
333
        $firstPlayers->add($b);
334
335
        $this->assertEquals(['a', 'b'], $firstPlayers->sort('ShirtNumber')->column('ShirtNumber'));
336
337
        $firstPlayers->filter('ShirtNumber', 'b')->removeAll();
338
339
        $this->assertEquals(['a'], $firstPlayers->column('ShirtNumber'));
340
        $this->assertEquals(['a', 'b'], $secondPlayers->sort('ShirtNumber')->column('ShirtNumber'));
341
342
        $this->assertNotNull(Player::get()->byID($a->ID));
343
        $this->assertNotNull(Player::get()->byID($b->ID));
344
    }
345
346
    public function testAppendExtraFieldsToQuery()
347
    {
348
        $list = new ManyManyList(
349
            ManyManyListTest\ExtraFieldsObject::class,
350
            'ManyManyListTest_ExtraFields_Clients',
351
            'ManyManyListTest_ExtraFieldsID',
352
            'ChildID',
353
            [
354
                'Worth' => 'Money',
355
                'Reference' => 'Varchar'
356
            ]
357
        );
358
359
        // ensure that ManyManyListTest_ExtraFields_Clients.ValueCurrency is
360
        // selected.
361
        $expected = 'SELECT DISTINCT "ManyManyListTest_ExtraFields_Clients"."WorthCurrency",'
362
            . ' "ManyManyListTest_ExtraFields_Clients"."WorthAmount", "ManyManyListTest_ExtraFields_Clients"."Reference",'
363
            . ' "ManyManyListTest_ExtraFields"."ClassName", "ManyManyListTest_ExtraFields"."LastEdited",'
364
            . ' "ManyManyListTest_ExtraFields"."Created", "ManyManyListTest_ExtraFields"."ID",'
365
            . ' CASE WHEN "ManyManyListTest_ExtraFields"."ClassName" IS NOT NULL THEN'
366
            . ' "ManyManyListTest_ExtraFields"."ClassName" ELSE ' . Convert::raw2sql(ManyManyListTest\ExtraFieldsObject::class, true)
367
            . ' END AS "RecordClassName" FROM "ManyManyListTest_ExtraFields" INNER JOIN'
368
            . ' "ManyManyListTest_ExtraFields_Clients" ON'
369
            . ' "ManyManyListTest_ExtraFields_Clients"."ManyManyListTest_ExtraFieldsID" ='
370
            . ' "ManyManyListTest_ExtraFields"."ID"';
371
372
        $this->assertSQLEquals($expected, $list->sql($parameters));
373
    }
374
375
    /**
376
     * This tests that we can set a default sort on a join table, even though the class doesn't exist.
377
     *
378
     * @return void
379
     */
380
    public function testSortByExtraFieldsDefaultSort()
381
    {
382
        $obj = new ManyManyListTest\ExtraFieldsObject();
383
        $obj->write();
384
385
        $obj2 = new ManyManyListTest\ExtraFieldsObject();
386
        $obj2->write();
387
388
        $money = new DBMoney();
389
        $money->setAmount(100);
390
        $money->setCurrency('USD');
391
392
        // Add two objects as relations (first is linking back to itself)
393
        $obj->Clients()->add($obj, ['Worth' => $money, 'Reference' => 'A']);
394
        $obj->Clients()->add($obj2, ['Worth' => $money, 'Reference' => 'B']);
395
396
        // Set the default sort for this relation
397
        Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference ASC');
0 ignored issues
show
Bug introduced by
The method update() does not exist on SilverStripe\Config\Coll...nfigCollectionInterface. It seems like you code against a sub-type of SilverStripe\Config\Coll...nfigCollectionInterface such as SilverStripe\Config\Coll...\MemoryConfigCollection. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

397
        Config::inst()->/** @scrutinizer ignore-call */ update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference ASC');
Loading history...
398
        $clients = $obj->Clients();
399
        $this->assertCount(2, $clients);
400
401
        list($first, $second) = $obj->Clients();
402
        $this->assertEquals('A', $first->Reference);
403
        $this->assertEquals('B', $second->Reference);
404
405
        // Now we ensure the default sort is being respected by reversing its order
406
        Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference DESC');
407
        $reverseClients = $obj->Clients();
408
        $this->assertCount(2, $reverseClients);
409
410
        list($reverseFirst, $reverseSecond) = $obj->Clients();
411
        $this->assertEquals('B', $reverseFirst->Reference);
412
        $this->assertEquals('A', $reverseSecond->Reference);
413
    }
414
415
    public function testFilteringOnPreviouslyJoinedTable()
416
    {
417
        /** @var ManyManyListTest\Category $category */
418
        $category = $this->objFromFixture(ManyManyListTest\Category::class, 'categorya');
419
420
        /** @var ManyManyList $productsRelatedToProductB */
421
        $productsRelatedToProductB = $category->Products()->filter('RelatedProducts.Title', 'Product A');
422
        $this->assertEquals(1, $productsRelatedToProductB->count());
423
    }
424
425
    public function testWriteManipulationWithNonScalarValuesAllowed()
426
    {
427
        $left = DataObjectTest\MockDynamicAssignmentDataObject::create();
428
        $left->write();
429
        $right = DataObjectTest\MockDynamicAssignmentDataObject::create();
430
        $right->write();
431
432
        $left->MockManyMany()->add($right, [
433
            'ManyManyStaticScalarOnlyField' => true,
434
            'ManyManyDynamicScalarOnlyField' => false,
435
            'ManyManyDynamicField' => true,
436
        ]);
437
438
        $pivot = $left->MockManyMany()->first();
439
440
        $this->assertNotFalse($pivot->ManyManyStaticScalarOnlyField);
441
        $this->assertNotTrue($pivot->ManyManyDynamicScalarOnlyField);
442
        $this->assertNotFalse($pivot->ManyManyDynamicField);
443
    }
444
445
    public function testWriteManipulationWithNonScalarValuesDisallowed()
446
    {
447
        $this->expectException(InvalidArgumentException::class);
448
449
        $left = DataObjectTest\MockDynamicAssignmentDataObject::create();
450
        $left->write();
451
        $right = DataObjectTest\MockDynamicAssignmentDataObject::create();
452
        $right->write();
453
454
        $left->MockManyMany()->add($right, [
455
            'ManyManyStaticScalarOnlyField' => false,
456
            'ManyManyDynamicScalarOnlyField' => true,
457
            'ManyManyDynamicField' => false,
458
        ]);
459
    }
460
461
    public function testRemoveCallbackOnRemove()
462
    {
463
        $removedIds = [];
464
465
        $base = $this->objFromFixture(Team::class, 'team1');
466
        $relation = $base->Players();
467
        $remove = $relation->First();
468
469
        $relation->setRemoveCallback(function ($list, $ids) use (&$removedIds) {
470
            $removedIds = $ids;
471
        });
472
473
        $relation->remove($remove);
474
        $this->assertEquals([$remove->ID], $removedIds);
475
    }
476
477
    public function testRemoveCallbackOnRemoveById()
478
    {
479
        $removedIds = [];
480
481
        $base = $this->objFromFixture(Team::class, 'team1');
482
        $relation = $base->Players();
483
        $remove = $relation->First();
484
485
        $relation->setRemoveCallback(function ($list, $ids) use (&$removedIds) {
486
            $removedIds = $ids;
487
        });
488
489
        $relation->removeByID($remove->ID);
490
        $this->assertEquals([$remove->ID], $removedIds);
491
    }
492
493
    public function testRemoveCallbackOnRemoveAll()
494
    {
495
        $removedIds = [];
496
497
        $base = $this->objFromFixture(Team::class, 'team1');
498
        $relation = $base->Players();
499
        $remove = $relation->column('ID');
500
501
        $relation->setRemoveCallback(function ($list, $ids) use (&$removedIds) {
502
            $removedIds = $ids;
503
        });
504
505
        $relation->removeAll();
506
        $this->assertEquals(sort($remove), sort($removedIds));
507
    }
508
}
509