Passed
Push — master ( 288a96...964b09 )
by Daniel
10:30
created

tests/php/ORM/DataObjectTest.php (1 issue)

1
<?php
2
3
namespace SilverStripe\ORM\Tests;
4
5
use InvalidArgumentException;
6
use LogicException;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Dev\SapphireTest;
9
use SilverStripe\i18n\i18n;
10
use SilverStripe\ORM\Connect\MySQLDatabase;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DataObjectSchema;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\ORM\FieldType\DBBoolean;
15
use SilverStripe\ORM\FieldType\DBDatetime;
16
use SilverStripe\ORM\FieldType\DBField;
17
use SilverStripe\ORM\FieldType\DBPolymorphicForeignKey;
18
use SilverStripe\ORM\FieldType\DBVarchar;
19
use SilverStripe\ORM\ManyManyList;
20
use SilverStripe\ORM\Tests\DataObjectTest\Player;
21
use SilverStripe\View\ViewableData;
22
use stdClass;
23
24
class DataObjectTest extends SapphireTest
25
{
26
27
    protected static $fixture_file = 'DataObjectTest.yml';
28
29
    /**
30
     * Standard set of dataobject test classes
31
     *
32
     * @var array
33
     */
34
    public static $extra_data_objects = array(
35
        DataObjectTest\Team::class,
36
        DataObjectTest\Fixture::class,
37
        DataObjectTest\SubTeam::class,
38
        DataObjectTest\OtherSubclassWithSameField::class,
39
        DataObjectTest\FieldlessTable::class,
40
        DataObjectTest\FieldlessSubTable::class,
41
        DataObjectTest\ValidatedObject::class,
42
        DataObjectTest\Player::class,
43
        DataObjectTest\TeamComment::class,
44
        DataObjectTest\EquipmentCompany::class,
45
        DataObjectTest\SubEquipmentCompany::class,
46
        DataObjectTest\ExtendedTeamComment::class,
47
        DataObjectTest\Company::class,
48
        DataObjectTest\Staff::class,
49
        DataObjectTest\CEO::class,
50
        DataObjectTest\Fan::class,
51
        DataObjectTest\Play::class,
52
        DataObjectTest\Ploy::class,
53
        DataObjectTest\Bogey::class,
54
        DataObjectTest\Sortable::class,
55
        DataObjectTest\Bracket::class,
56
        DataObjectTest\RelationParent::class,
57
        DataObjectTest\RelationChildFirst::class,
58
        DataObjectTest\RelationChildSecond::class,
59
    );
60
61
    public static function getExtraDataObjects()
62
    {
63
        return array_merge(
64
            DataObjectTest::$extra_data_objects,
65
            ManyManyListTest::$extra_data_objects
66
        );
67
    }
68
69
    public function testDb()
70
    {
71
        $schema = DataObject::getSchema();
72
        $dbFields = $schema->fieldSpecs(DataObjectTest\TeamComment::class);
73
74
        // Assert fields are included
75
        $this->assertArrayHasKey('Name', $dbFields);
76
77
        // Assert the base fields are included
78
        $this->assertArrayHasKey('Created', $dbFields);
79
        $this->assertArrayHasKey('LastEdited', $dbFields);
80
        $this->assertArrayHasKey('ClassName', $dbFields);
81
        $this->assertArrayHasKey('ID', $dbFields);
82
83
        // Assert that the correct field type is returned when passing a field
84
        $this->assertEquals('Varchar', $schema->fieldSpec(DataObjectTest\TeamComment::class, 'Name'));
85
        $this->assertEquals('Text', $schema->fieldSpec(DataObjectTest\TeamComment::class, 'Comment'));
86
87
        // Test with table required
88
        $this->assertEquals(
89
            DataObjectTest\TeamComment::class . '.Varchar',
90
            $schema->fieldSpec(DataObjectTest\TeamComment::class, 'Name', DataObjectSchema::INCLUDE_CLASS)
91
        );
92
        $this->assertEquals(
93
            DataObjectTest\TeamComment::class . '.Text',
94
            $schema->fieldSpec(DataObjectTest\TeamComment::class, 'Comment', DataObjectSchema::INCLUDE_CLASS)
95
        );
96
        $dbFields = $schema->fieldSpecs(DataObjectTest\ExtendedTeamComment::class);
97
98
        // fixed fields are still included in extended classes
99
        $this->assertArrayHasKey('Created', $dbFields);
100
        $this->assertArrayHasKey('LastEdited', $dbFields);
101
        $this->assertArrayHasKey('ClassName', $dbFields);
102
        $this->assertArrayHasKey('ID', $dbFields);
103
104
        // Assert overloaded fields have correct data type
105
        $this->assertEquals('HTMLText', $schema->fieldSpec(DataObjectTest\ExtendedTeamComment::class, 'Comment'));
106
        $this->assertEquals(
107
            'HTMLText',
108
            $dbFields['Comment'],
109
            'Calls to DataObject::db without a field specified return correct data types'
110
        );
111
112
        // assertEquals doesn't verify the order of array elements, so access keys manually to check order:
113
        // expected: array('Name' => 'Varchar', 'Comment' => 'HTMLText')
114
        $this->assertEquals(
115
            array(
116
                'Name',
117
                'Comment'
118
            ),
119
            array_slice(array_keys($dbFields), 4, 2),
120
            'DataObject::db returns fields in correct order'
121
        );
122
    }
123
124
    public function testConstructAcceptsValues()
125
    {
126
        // Values can be an array...
127
        $player = new DataObjectTest\Player(
128
            array(
129
                'FirstName' => 'James',
130
                'Surname' => 'Smith'
131
            )
132
        );
133
134
        $this->assertEquals('James', $player->FirstName);
135
        $this->assertEquals('Smith', $player->Surname);
136
137
        // ... or a stdClass inst
138
        $data = new stdClass();
139
        $data->FirstName = 'John';
140
        $data->Surname = 'Doe';
141
        $player = new DataObjectTest\Player($data);
142
143
        $this->assertEquals('John', $player->FirstName);
144
        $this->assertEquals('Doe', $player->Surname);
145
146
        // IDs should be stored as integers, not strings
147
        $player = new DataObjectTest\Player(array('ID' => '5'));
148
        $this->assertSame(5, $player->ID);
149
    }
150
151
    public function testValidObjectsForBaseFields()
152
    {
153
        $obj = new DataObjectTest\ValidatedObject();
154
155
        foreach (array('Created', 'LastEdited', 'ClassName', 'ID') as $field) {
156
            $helper = $obj->dbObject($field);
157
            $this->assertTrue(
158
                ($helper instanceof DBField),
159
                "for {$field} expected helper to be DBField, but was " . (is_object($helper) ? get_class($helper) : "null")
160
            );
161
        }
162
    }
163
164
    public function testDataIntegrityWhenTwoSubclassesHaveSameField()
165
    {
166
        // Save data into DataObjectTest_SubTeam.SubclassDatabaseField
167
        $obj = new DataObjectTest\SubTeam();
168
        $obj->SubclassDatabaseField = "obj-SubTeam";
169
        $obj->write();
170
171
        // Change the class
172
        $obj->ClassName = DataObjectTest\OtherSubclassWithSameField::class;
173
        $obj->write();
174
        $obj->flushCache();
175
176
        // Re-fetch from the database and confirm that the data is sourced from
177
        // OtherSubclassWithSameField.SubclassDatabaseField
178
        $obj = DataObject::get_by_id(DataObjectTest\Team::class, $obj->ID);
179
        $this->assertNull($obj->SubclassDatabaseField);
180
181
        // Confirm that save the object in the other direction.
182
        $obj->SubclassDatabaseField = 'obj-Other';
183
        $obj->write();
184
185
        $obj->ClassName = DataObjectTest\SubTeam::class;
186
        $obj->write();
187
        $obj->flushCache();
188
189
        // If we restore the class, the old value has been lying dormant and will be available again.
190
        // NOTE: This behaviour is volatile; we may change this in the future to clear fields that
191
        // are no longer relevant when changing ClassName
192
        $obj = DataObject::get_by_id(DataObjectTest\Team::class, $obj->ID);
193
        $this->assertEquals('obj-SubTeam', $obj->SubclassDatabaseField);
194
    }
195
196
    /**
197
     * Test deletion of DataObjects
198
     *   - Deleting using delete() on the DataObject
199
     *   - Deleting using DataObject::delete_by_id()
200
     */
201
    public function testDelete()
202
    {
203
        // Test deleting using delete() on the DataObject
204
        // Get the first page
205
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
206
        $objID = $obj->ID;
207
        // Check the page exists before deleting
208
        $this->assertTrue(is_object($obj) && $obj->exists());
209
        // Delete the page
210
        $obj->delete();
211
        // Check that page does not exist after deleting
212
        $obj = DataObject::get_by_id(DataObjectTest\Player::class, $objID);
213
        $this->assertTrue(!$obj || !$obj->exists());
214
215
216
        // Test deleting using DataObject::delete_by_id()
217
        // Get the second page
218
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain2');
219
        $objID = $obj->ID;
220
        // Check the page exists before deleting
221
        $this->assertTrue(is_object($obj) && $obj->exists());
222
        // Delete the page
223
        DataObject::delete_by_id(DataObjectTest\Player::class, $obj->ID);
224
        // Check that page does not exist after deleting
225
        $obj = DataObject::get_by_id(DataObjectTest\Player::class, $objID);
226
        $this->assertTrue(!$obj || !$obj->exists());
227
    }
228
229
    /**
230
     * Test methods that get DataObjects
231
     *   - DataObject::get()
232
     *       - All records of a DataObject
233
     *       - Filtering
234
     *       - Sorting
235
     *       - Joins
236
     *       - Limit
237
     *       - Container class
238
     *   - DataObject::get_by_id()
239
     *   - DataObject::get_one()
240
     *        - With and without caching
241
     *        - With and without ordering
242
     */
243
    public function testGet()
244
    {
245
        // Test getting all records of a DataObject
246
        $comments = DataObject::get(DataObjectTest\TeamComment::class);
247
        $this->assertEquals(3, $comments->count());
248
249
        // Test WHERE clause
250
        $comments = DataObject::get(DataObjectTest\TeamComment::class, "\"Name\"='Bob'");
251
        $this->assertEquals(1, $comments->count());
252
        foreach ($comments as $comment) {
253
            $this->assertEquals('Bob', $comment->Name);
254
        }
255
256
        // Test sorting
257
        $comments = DataObject::get(DataObjectTest\TeamComment::class, '', "\"Name\" ASC");
258
        $this->assertEquals(3, $comments->count());
259
        $this->assertEquals('Bob', $comments->first()->Name);
0 ignored issues
show
Bug Best Practice introduced by
The property Name does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
260
        $comments = DataObject::get(DataObjectTest\TeamComment::class, '', "\"Name\" DESC");
261
        $this->assertEquals(3, $comments->count());
262
        $this->assertEquals('Phil', $comments->first()->Name);
263
264
        // Test limit
265
        $comments = DataObject::get(DataObjectTest\TeamComment::class, '', "\"Name\" ASC", '', '1,2');
266
        $this->assertEquals(2, $comments->count());
267
        $this->assertEquals('Joe', $comments->first()->Name);
268
        $this->assertEquals('Phil', $comments->last()->Name);
269
270
        // Test get_by_id()
271
        $captain1ID = $this->idFromFixture(DataObjectTest\Player::class, 'captain1');
272
        $captain1 = DataObject::get_by_id(DataObjectTest\Player::class, $captain1ID);
273
        $this->assertEquals('Captain', $captain1->FirstName);
274
275
        // Test get_one() without caching
276
        $comment1 = DataObject::get_one(
277
            DataObjectTest\TeamComment::class,
278
            array(
279
                '"DataObjectTest_TeamComment"."Name"' => 'Joe'
280
            ),
281
            false
282
        );
283
        $comment1->Comment = "Something Else";
284
285
        $comment2 = DataObject::get_one(
286
            DataObjectTest\TeamComment::class,
287
            array(
288
                '"DataObjectTest_TeamComment"."Name"' => 'Joe'
289
            ),
290
            false
291
        );
292
        $this->assertNotEquals($comment1->Comment, $comment2->Comment);
293
294
        // Test get_one() with caching
295
        $comment1 = DataObject::get_one(
296
            DataObjectTest\TeamComment::class,
297
            array(
298
                '"DataObjectTest_TeamComment"."Name"' => 'Bob'
299
            ),
300
            true
301
        );
302
        $comment1->Comment = "Something Else";
303
304
        $comment2 = DataObject::get_one(
305
            DataObjectTest\TeamComment::class,
306
            array(
307
                '"DataObjectTest_TeamComment"."Name"' => 'Bob'
308
            ),
309
            true
310
        );
311
        $this->assertEquals((string)$comment1->Comment, (string)$comment2->Comment);
312
313
        // Test get_one() with order by without caching
314
        $comment = DataObject::get_one(DataObjectTest\TeamComment::class, '', false, "\"Name\" ASC");
315
        $this->assertEquals('Bob', $comment->Name);
316
317
        $comment = DataObject::get_one(DataObjectTest\TeamComment::class, '', false, "\"Name\" DESC");
318
        $this->assertEquals('Phil', $comment->Name);
319
320
        // Test get_one() with order by with caching
321
        $comment = DataObject::get_one(DataObjectTest\TeamComment::class, '', true, '"Name" ASC');
322
        $this->assertEquals('Bob', $comment->Name);
323
        $comment = DataObject::get_one(DataObjectTest\TeamComment::class, '', true, '"Name" DESC');
324
        $this->assertEquals('Phil', $comment->Name);
325
    }
326
327
    public function testGetByIDCallerClass()
328
    {
329
        $captain1ID = $this->idFromFixture(DataObjectTest\Player::class, 'captain1');
330
        $captain1 = DataObjectTest\Player::get_by_id($captain1ID);
331
        $this->assertInstanceOf(DataObjectTest\Player::class, $captain1);
332
        $this->assertEquals('Captain', $captain1->FirstName);
333
334
        $captain2ID = $this->idFromFixture(DataObjectTest\Player::class, 'captain2');
335
        // make sure we can call from any class but get the one passed as an argument
336
        $captain2 = DataObjectTest\TeamComment::get_by_id(DataObjectTest\Player::class, $captain2ID);
337
        $this->assertInstanceOf(DataObjectTest\Player::class, $captain2);
338
        $this->assertEquals('Captain 2', $captain2->FirstName);
339
    }
340
341
    public function testGetCaseInsensitive()
342
    {
343
        // Test get_one() with bad case on the classname
344
        // Note: This will succeed only if the underlying DB server supports case-insensitive
345
        // table names (e.g. such as MySQL, but not SQLite3)
346
        if (!(DB::get_conn() instanceof MySQLDatabase)) {
347
            $this->markTestSkipped('MySQL only');
348
        }
349
350
        $subteam1 = DataObject::get_one(
351
            strtolower(DataObjectTest\SubTeam::class),
352
            array(
353
                '"DataObjectTest_Team"."Title"' => 'Subteam 1'
354
            ),
355
            true
356
        );
357
        $this->assertNotEmpty($subteam1);
358
        $this->assertEquals($subteam1->Title, "Subteam 1");
359
    }
360
361
    public function testGetSubclassFields()
362
    {
363
        /* Test that fields / has_one relations from the parent table and the subclass tables are extracted */
364
        $captain1 = $this->objFromFixture(DataObjectTest\Player::class, "captain1");
365
        // Base field
366
        $this->assertEquals('Captain', $captain1->FirstName);
367
        // Subclass field
368
        $this->assertEquals('007', $captain1->ShirtNumber);
369
        // Subclass has_one relation
370
        $this->assertEquals($this->idFromFixture(DataObjectTest\Team::class, 'team1'), $captain1->FavouriteTeamID);
371
    }
372
373
    public function testGetRelationClass()
374
    {
375
        $obj = new DataObjectTest\Player();
376
        $this->assertEquals(
377
            singleton(DataObjectTest\Player::class)->getRelationClass('FavouriteTeam'),
378
            DataObjectTest\Team::class,
379
            'has_one is properly inspected'
380
        );
381
        $this->assertEquals(
382
            singleton(DataObjectTest\Company::class)->getRelationClass('CurrentStaff'),
383
            DataObjectTest\Staff::class,
384
            'has_many is properly inspected'
385
        );
386
        $this->assertEquals(
387
            singleton(DataObjectTest\Team::class)->getRelationClass('Players'),
388
            DataObjectTest\Player::class,
389
            'many_many is properly inspected'
390
        );
391
        $this->assertEquals(
392
            singleton(DataObjectTest\Player::class)->getRelationClass('Teams'),
393
            DataObjectTest\Team::class,
394
            'belongs_many_many is properly inspected'
395
        );
396
        $this->assertEquals(
397
            singleton(DataObjectTest\CEO::class)->getRelationClass('Company'),
398
            DataObjectTest\Company::class,
399
            'belongs_to is properly inspected'
400
        );
401
        $this->assertEquals(
402
            singleton(DataObjectTest\Fan::class)->getRelationClass('Favourite'),
403
            DataObject::class,
404
            'polymorphic has_one is properly inspected'
405
        );
406
    }
407
408
    /**
409
     * Test that has_one relations can be retrieved
410
     */
411
    public function testGetHasOneRelations()
412
    {
413
        $captain1 = $this->objFromFixture(DataObjectTest\Player::class, "captain1");
414
        $team1ID = $this->idFromFixture(DataObjectTest\Team::class, 'team1');
415
416
        // There will be a field called (relname)ID that contains the ID of the
417
        // object linked to via the has_one relation
418
        $this->assertEquals($team1ID, $captain1->FavouriteTeamID);
419
420
        // There will be a method called $obj->relname() that returns the object itself
421
        $this->assertEquals($team1ID, $captain1->FavouriteTeam()->ID);
422
423
        // Test that getNonReciprocalComponent can find has_one from the has_many end
424
        $this->assertEquals(
425
            $team1ID,
426
            $captain1->inferReciprocalComponent(DataObjectTest\Team::class, 'PlayerFans')->ID
427
        );
428
429
        // Check entity with polymorphic has-one
430
        $fan1 = $this->objFromFixture(DataObjectTest\Fan::class, "fan1");
431
        $this->assertTrue((bool)$fan1->hasValue('Favourite'));
432
433
        // There will be fields named (relname)ID and (relname)Class for polymorphic
434
        // entities
435
        $this->assertEquals($team1ID, $fan1->FavouriteID);
436
        $this->assertEquals(DataObjectTest\Team::class, $fan1->FavouriteClass);
437
438
        // There will be a method called $obj->relname() that returns the object itself
439
        $favourite = $fan1->Favourite();
440
        $this->assertEquals($team1ID, $favourite->ID);
441
        $this->assertInstanceOf(DataObjectTest\Team::class, $favourite);
442
443
        // check behaviour of dbObject with polymorphic relations
444
        $favouriteDBObject = $fan1->dbObject('Favourite');
445
        $favouriteValue = $favouriteDBObject->getValue();
446
        $this->assertInstanceOf(DBPolymorphicForeignKey::class, $favouriteDBObject);
447
        $this->assertEquals($favourite->ID, $favouriteValue->ID);
448
        $this->assertEquals($favourite->ClassName, $favouriteValue->ClassName);
449
    }
450
451
    public function testLimitAndCount()
452
    {
453
        $players = DataObject::get(DataObjectTest\Player::class);
454
455
        // There's 4 records in total
456
        $this->assertEquals(4, $players->count());
457
458
        // Testing "##, ##" syntax
459
        $this->assertEquals(4, $players->limit(20)->count());
460
        $this->assertEquals(4, $players->limit(20, 0)->count());
461
        $this->assertEquals(0, $players->limit(20, 20)->count());
462
        $this->assertEquals(2, $players->limit(2, 0)->count());
463
        $this->assertEquals(1, $players->limit(5, 3)->count());
464
    }
465
466
    public function testWriteNoChangesDoesntUpdateLastEdited()
467
    {
468
        // set mock now so we can be certain of LastEdited time for our test
469
        DBDatetime::set_mock_now('2017-01-01 00:00:00');
470
        $obj = new Player();
471
        $obj->FirstName = 'Test';
472
        $obj->Surname = 'Plater';
473
        $obj->Email = '[email protected]';
474
        $obj->write();
475
        $this->assertEquals('2017-01-01 00:00:00', $obj->LastEdited);
476
        $writtenObj = Player::get()->byID($obj->ID);
477
        $this->assertEquals('2017-01-01 00:00:00', $writtenObj->LastEdited);
478
479
        // set mock now so we get a new LastEdited if, for some reason, it's updated
480
        DBDatetime::set_mock_now('2017-02-01 00:00:00');
481
        $writtenObj->write();
482
        $this->assertEquals('2017-01-01 00:00:00', $writtenObj->LastEdited);
483
        $this->assertEquals($obj->ID, $writtenObj->ID);
484
485
        $reWrittenObj = Player::get()->byID($writtenObj->ID);
486
        $this->assertEquals('2017-01-01 00:00:00', $reWrittenObj->LastEdited);
487
    }
488
489
    /**
490
     * Test writing of database columns which don't correlate to a DBField,
491
     * e.g. all relation fields on has_one/has_many like "ParentID".
492
     */
493
    public function testWritePropertyWithoutDBField()
494
    {
495
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
496
        $obj->FavouriteTeamID = 99;
497
        $obj->write();
498
499
        // reload the page from the database
500
        $savedObj = DataObject::get_by_id(DataObjectTest\Player::class, $obj->ID);
501
        $this->assertTrue($savedObj->FavouriteTeamID == 99);
502
503
        // Test with porymorphic relation
504
        $obj2 = $this->objFromFixture(DataObjectTest\Fan::class, "fan1");
505
        $obj2->FavouriteID = 99;
506
        $obj2->FavouriteClass = DataObjectTest\Player::class;
507
        $obj2->write();
508
509
        $savedObj2 = DataObject::get_by_id(DataObjectTest\Fan::class, $obj2->ID);
510
        $this->assertTrue($savedObj2->FavouriteID == 99);
511
        $this->assertTrue($savedObj2->FavouriteClass == DataObjectTest\Player::class);
512
    }
513
514
    /**
515
     * Test has many relationships
516
     *   - Test getComponents() gets the ComponentSet of the other side of the relation
517
     *   - Test the IDs on the DataObjects are set correctly
518
     */
519
    public function testHasManyRelationships()
520
    {
521
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
522
523
        // Test getComponents() gets the ComponentSet of the other side of the relation
524
        $this->assertTrue($team1->Comments()->count() == 2);
525
526
        $team1Comments = [
527
            ['Comment' => 'This is a team comment by Joe'],
528
            ['Comment' => 'This is a team comment by Bob'],
529
        ];
530
531
        // Test the IDs on the DataObjects are set correctly
532
        $this->assertListEquals($team1Comments, $team1->Comments());
533
534
        // Test that has_many can be infered from the has_one via getNonReciprocalComponent
535
        $this->assertListEquals(
536
            $team1Comments,
537
            $team1->inferReciprocalComponent(DataObjectTest\TeamComment::class, 'Team')
538
        );
539
540
        // Test that we can add and remove items that already exist in the database
541
        $newComment = new DataObjectTest\TeamComment();
542
        $newComment->Name = "Automated commenter";
543
        $newComment->Comment = "This is a new comment";
544
        $newComment->write();
545
        $team1->Comments()->add($newComment);
546
        $this->assertEquals($team1->ID, $newComment->TeamID);
547
548
        $comment1 = $this->objFromFixture(DataObjectTest\TeamComment::class, 'comment1');
549
        $comment2 = $this->objFromFixture(DataObjectTest\TeamComment::class, 'comment2');
550
        $team1->Comments()->remove($comment2);
551
552
        $team1CommentIDs = $team1->Comments()->sort('ID')->column('ID');
553
        $this->assertEquals(array($comment1->ID, $newComment->ID), $team1CommentIDs);
554
555
        // Test that removing an item from a list doesn't remove it from the same
556
        // relation belonging to a different object
557
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
558
        $team2 = $this->objFromFixture(DataObjectTest\Team::class, 'team2');
559
        $team2->Comments()->remove($comment1);
560
        $team1CommentIDs = $team1->Comments()->sort('ID')->column('ID');
561
        $this->assertEquals(array($comment1->ID, $newComment->ID), $team1CommentIDs);
562
    }
563
564
565
    /**
566
     * Test has many relationships against polymorphic has_one fields
567
     *   - Test getComponents() gets the ComponentSet of the other side of the relation
568
     *   - Test the IDs on the DataObjects are set correctly
569
     */
570
    public function testHasManyPolymorphicRelationships()
571
    {
572
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
573
574
        // Test getComponents() gets the ComponentSet of the other side of the relation
575
        $this->assertTrue($team1->Fans()->count() == 2);
576
577
        // Test the IDs/Classes on the DataObjects are set correctly
578
        foreach ($team1->Fans() as $fan) {
579
            $this->assertEquals($team1->ID, $fan->FavouriteID, 'Fan has the correct FavouriteID');
580
            $this->assertEquals(DataObjectTest\Team::class, $fan->FavouriteClass, 'Fan has the correct FavouriteClass');
581
        }
582
583
        // Test that we can add and remove items that already exist in the database
584
        $newFan = new DataObjectTest\Fan();
585
        $newFan->Name = "New fan";
586
        $newFan->write();
587
        $team1->Fans()->add($newFan);
588
        $this->assertEquals($team1->ID, $newFan->FavouriteID, 'Newly created fan has the correct FavouriteID');
589
        $this->assertEquals(
590
            DataObjectTest\Team::class,
591
            $newFan->FavouriteClass,
592
            'Newly created fan has the correct FavouriteClass'
593
        );
594
595
        $fan1 = $this->objFromFixture(DataObjectTest\Fan::class, 'fan1');
596
        $fan3 = $this->objFromFixture(DataObjectTest\Fan::class, 'fan3');
597
        $team1->Fans()->remove($fan3);
598
599
        $team1FanIDs = $team1->Fans()->sort('ID')->column('ID');
600
        $this->assertEquals(array($fan1->ID, $newFan->ID), $team1FanIDs);
601
602
        // Test that removing an item from a list doesn't remove it from the same
603
        // relation belonging to a different object
604
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
605
        $player1 = $this->objFromFixture(DataObjectTest\Player::class, 'player1');
606
        $player1->Fans()->remove($fan1);
607
        $team1FanIDs = $team1->Fans()->sort('ID')->column('ID');
608
        $this->assertEquals(array($fan1->ID, $newFan->ID), $team1FanIDs);
609
    }
610
611
612
    public function testHasOneRelationship()
613
    {
614
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
615
        $player1 = $this->objFromFixture(DataObjectTest\Player::class, 'player1');
616
        $player2 = $this->objFromFixture(DataObjectTest\Player::class, 'player2');
617
        $fan1 = $this->objFromFixture(DataObjectTest\Fan::class, 'fan1');
618
619
        // Test relation probing
620
        $this->assertFalse((bool)$team1->hasValue('Captain', null, false));
621
        $this->assertFalse((bool)$team1->hasValue('CaptainID', null, false));
622
623
        // Add a captain to team 1
624
        $team1->setField('CaptainID', $player1->ID);
625
        $team1->write();
626
627
        $this->assertTrue((bool)$team1->hasValue('Captain', null, false));
628
        $this->assertTrue((bool)$team1->hasValue('CaptainID', null, false));
629
630
        $this->assertEquals(
631
            $player1->ID,
632
            $team1->Captain()->ID,
633
            'The captain exists for team 1'
634
        );
635
        $this->assertEquals(
636
            $player1->ID,
637
            $team1->getComponent('Captain')->ID,
638
            'The captain exists through the component getter'
639
        );
640
641
        $this->assertEquals(
642
            $team1->Captain()->FirstName,
643
            'Player 1',
644
            'Player 1 is the captain'
645
        );
646
        $this->assertEquals(
647
            $team1->getComponent('Captain')->FirstName,
648
            'Player 1',
649
            'Player 1 is the captain'
650
        );
651
652
        $team1->CaptainID = $player2->ID;
653
        $team1->write();
654
655
        $this->assertEquals($player2->ID, $team1->Captain()->ID);
656
        $this->assertEquals($player2->ID, $team1->getComponent('Captain')->ID);
657
        $this->assertEquals('Player 2', $team1->Captain()->FirstName);
658
        $this->assertEquals('Player 2', $team1->getComponent('Captain')->FirstName);
659
660
661
        // Set the favourite team for fan1
662
        $fan1->setField('FavouriteID', $team1->ID);
663
        $fan1->setField('FavouriteClass', get_class($team1));
664
665
        $this->assertEquals($team1->ID, $fan1->Favourite()->ID, 'The team is assigned to fan 1');
666
        $this->assertInstanceOf(get_class($team1), $fan1->Favourite(), 'The team is assigned to fan 1');
667
        $this->assertEquals(
668
            $team1->ID,
669
            $fan1->getComponent('Favourite')->ID,
670
            'The team exists through the component getter'
671
        );
672
        $this->assertInstanceOf(
673
            get_class($team1),
674
            $fan1->getComponent('Favourite'),
675
            'The team exists through the component getter'
676
        );
677
678
        $this->assertEquals(
679
            $fan1->Favourite()->Title,
680
            'Team 1',
681
            'Team 1 is the favourite'
682
        );
683
        $this->assertEquals(
684
            $fan1->getComponent('Favourite')->Title,
685
            'Team 1',
686
            'Team 1 is the favourite'
687
        );
688
    }
689
690
    /**
691
     * Test has_one used as field getter/setter
692
     */
693
    public function testHasOneAsField()
694
    {
695
        /** @var DataObjectTest\Team $team1 */
696
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
697
        $captain1 = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
698
        $captain2 = $this->objFromFixture(DataObjectTest\Player::class, 'captain2');
699
700
        // Setter: By RelationID
701
        $team1->CaptainID = $captain1->ID;
702
        $team1->write();
703
        $this->assertEquals($captain1->ID, $team1->Captain->ID);
704
705
        // Setter: New object
706
        $team1->Captain = $captain2;
707
        $team1->write();
708
        $this->assertEquals($captain2->ID, $team1->Captain->ID);
709
710
        // Setter: Custom data (required by DataDifferencer)
711
        $team1->Captain = DBField::create_field('HTMLFragment', '<p>No captain</p>');
712
        $this->assertEquals('<p>No captain</p>', $team1->Captain);
713
    }
714
715
    /**
716
     * @todo Extend type change tests (e.g. '0'==NULL)
717
     */
718
    public function testChangedFields()
719
    {
720
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
721
        $obj->FirstName = 'Captain-changed';
722
        $obj->IsRetired = true;
723
724
        $this->assertEquals(
725
            $obj->getChangedFields(true, DataObject::CHANGE_STRICT),
726
            array(
727
                'FirstName' => array(
728
                    'before' => 'Captain',
729
                    'after' => 'Captain-changed',
730
                    'level' => DataObject::CHANGE_VALUE
731
                ),
732
                'IsRetired' => array(
733
                    'before' => 1,
734
                    'after' => true,
735
                    'level' => DataObject::CHANGE_STRICT
736
                )
737
            ),
738
            'Changed fields are correctly detected with strict type changes (level=1)'
739
        );
740
741
        $this->assertEquals(
742
            $obj->getChangedFields(true, DataObject::CHANGE_VALUE),
743
            array(
744
                'FirstName' => array(
745
                    'before' => 'Captain',
746
                    'after' => 'Captain-changed',
747
                    'level' => DataObject::CHANGE_VALUE
748
                )
749
            ),
750
            'Changed fields are correctly detected while ignoring type changes (level=2)'
751
        );
752
753
        $newObj = new DataObjectTest\Player();
754
        $newObj->FirstName = "New Player";
755
        $this->assertEquals(
756
            array(
757
                'FirstName' => array(
758
                    'before' => null,
759
                    'after' => 'New Player',
760
                    'level' => DataObject::CHANGE_VALUE
761
                )
762
            ),
763
            $newObj->getChangedFields(true, DataObject::CHANGE_VALUE),
764
            'Initialised fields are correctly detected as full changes'
765
        );
766
    }
767
768
    /**
769
     * @skipUpgrade
770
     */
771
    public function testIsChanged()
772
    {
773
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
774
        $obj->NonDBField = 'bob';
775
        $obj->FirstName = 'Captain-changed';
776
        $obj->IsRetired = true; // type change only, database stores "1"
777
778
        // Now that DB fields are changed, isChanged is true
779
        $this->assertTrue($obj->isChanged('NonDBField'));
780
        $this->assertFalse($obj->isChanged('NonField'));
781
        $this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_STRICT));
782
        $this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
783
        $this->assertTrue($obj->isChanged('IsRetired', DataObject::CHANGE_STRICT));
784
        $this->assertFalse($obj->isChanged('IsRetired', DataObject::CHANGE_VALUE));
785
        $this->assertFalse($obj->isChanged('Email', 1), 'Doesnt change mark unchanged property');
786
        $this->assertFalse($obj->isChanged('Email', 2), 'Doesnt change mark unchanged property');
787
788
        $newObj = new DataObjectTest\Player();
789
        $newObj->FirstName = "New Player";
790
        $this->assertTrue($newObj->isChanged('FirstName', DataObject::CHANGE_STRICT));
791
        $this->assertTrue($newObj->isChanged('FirstName', DataObject::CHANGE_VALUE));
792
        $this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_STRICT));
793
        $this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_VALUE));
794
795
        $newObj->write();
796
        $this->assertFalse($newObj->ischanged());
797
        $this->assertFalse($newObj->isChanged('FirstName', DataObject::CHANGE_STRICT));
798
        $this->assertFalse($newObj->isChanged('FirstName', DataObject::CHANGE_VALUE));
799
        $this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_STRICT));
800
        $this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_VALUE));
801
802
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
803
        $obj->FirstName = null;
804
        $this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_STRICT));
805
        $this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
806
807
        /* Test when there's not field provided */
808
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain2');
809
        $this->assertFalse($obj->isChanged());
810
        $obj->NonDBField = 'new value';
811
        $this->assertFalse($obj->isChanged());
812
        $obj->FirstName = "New Player";
813
        $this->assertTrue($obj->isChanged());
814
815
        $obj->write();
816
        $this->assertFalse($obj->isChanged());
817
    }
818
819
    public function testRandomSort()
820
    {
821
        /* If we perform the same regularly sorted query twice, it should return the same results */
822
        $itemsA = DataObject::get(DataObjectTest\TeamComment::class, "", "ID");
823
        foreach ($itemsA as $item) {
824
            $keysA[] = $item->ID;
825
        }
826
827
        $itemsB = DataObject::get(DataObjectTest\TeamComment::class, "", "ID");
828
        foreach ($itemsB as $item) {
829
            $keysB[] = $item->ID;
830
        }
831
832
        /* Test when there's not field provided */
833
        $obj = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
834
        $obj->FirstName = "New Player";
835
        $this->assertTrue($obj->isChanged());
836
837
        $obj->write();
838
        $this->assertFalse($obj->isChanged());
839
840
        /* If we perform the same random query twice, it shouldn't return the same results */
841
        $itemsA = DataObject::get(DataObjectTest\TeamComment::class, "", DB::get_conn()->random());
842
        $itemsB = DataObject::get(DataObjectTest\TeamComment::class, "", DB::get_conn()->random());
843
        $itemsC = DataObject::get(DataObjectTest\TeamComment::class, "", DB::get_conn()->random());
844
        $itemsD = DataObject::get(DataObjectTest\TeamComment::class, "", DB::get_conn()->random());
845
        foreach ($itemsA as $item) {
846
            $keysA[] = $item->ID;
847
        }
848
        foreach ($itemsB as $item) {
849
            $keysB[] = $item->ID;
850
        }
851
        foreach ($itemsC as $item) {
852
            $keysC[] = $item->ID;
853
        }
854
        foreach ($itemsD as $item) {
855
            $keysD[] = $item->ID;
856
        }
857
858
        // These shouldn't all be the same (run it 4 times to minimise chance of an accidental collision)
859
        // There's about a 1 in a billion chance of an accidental collision
860
        $this->assertTrue($keysA != $keysB || $keysB != $keysC || $keysC != $keysD);
861
    }
862
863
    public function testWriteSavesToHasOneRelations()
864
    {
865
        /* DataObject::write() should save to a has_one relationship if you set a field called (relname)ID */
866
        $team = new DataObjectTest\Team();
867
        $captainID = $this->idFromFixture(DataObjectTest\Player::class, 'player1');
868
        $team->CaptainID = $captainID;
869
        $team->write();
870
        $this->assertEquals(
871
            $captainID,
872
            DB::query("SELECT \"CaptainID\" FROM \"DataObjectTest_Team\" WHERE \"ID\" = $team->ID")->value()
873
        );
874
875
        /* After giving it a value, you should also be able to set it back to null */
876
        $team->CaptainID = '';
877
        $team->write();
878
        $this->assertEquals(
879
            0,
880
            DB::query("SELECT \"CaptainID\" FROM \"DataObjectTest_Team\" WHERE \"ID\" = $team->ID")->value()
881
        );
882
883
        /* You should also be able to save a blank to it when it's first created */
884
        $team = new DataObjectTest\Team();
885
        $team->CaptainID = '';
886
        $team->write();
887
        $this->assertEquals(
888
            0,
889
            DB::query("SELECT \"CaptainID\" FROM \"DataObjectTest_Team\" WHERE \"ID\" = $team->ID")->value()
890
        );
891
892
        /* Ditto for existing records without a value */
893
        $existingTeam = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
894
        $existingTeam->CaptainID = '';
895
        $existingTeam->write();
896
        $this->assertEquals(
897
            0,
898
            DB::query("SELECT \"CaptainID\" FROM \"DataObjectTest_Team\" WHERE \"ID\" = $existingTeam->ID")->value()
899
        );
900
    }
901
902
    public function testCanAccessHasOneObjectsAsMethods()
903
    {
904
        /* If you have a has_one relation 'Captain' on $obj, and you set the $obj->CaptainID = (ID), then the
905
        * object itself should be accessible as $obj->Captain() */
906
        $team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
907
        $captainID = $this->idFromFixture(DataObjectTest\Player::class, 'captain1');
908
909
        $team->CaptainID = $captainID;
910
        $this->assertNotNull($team->Captain());
911
        $this->assertEquals($captainID, $team->Captain()->ID);
912
913
        // Test for polymorphic has_one relations
914
        $fan = $this->objFromFixture(DataObjectTest\Fan::class, 'fan1');
915
        $fan->FavouriteID = $team->ID;
916
        $fan->FavouriteClass = DataObjectTest\Team::class;
917
        $this->assertNotNull($fan->Favourite());
918
        $this->assertEquals($team->ID, $fan->Favourite()->ID);
919
        $this->assertInstanceOf(DataObjectTest\Team::class, $fan->Favourite());
920
    }
921
922
    public function testFieldNamesThatMatchMethodNamesWork()
923
    {
924
        /* Check that a field name that corresponds to a method on DataObject will still work */
925
        $obj = new DataObjectTest\Fixture();
926
        $obj->Data = "value1";
927
        $obj->DbObject = "value2";
928
        $obj->Duplicate = "value3";
929
        $obj->write();
930
931
        $this->assertNotNull($obj->ID);
932
        $this->assertEquals(
933
            'value1',
934
            DB::query("SELECT \"Data\" FROM \"DataObjectTest_Fixture\" WHERE \"ID\" = $obj->ID")->value()
935
        );
936
        $this->assertEquals(
937
            'value2',
938
            DB::query("SELECT \"DbObject\" FROM \"DataObjectTest_Fixture\" WHERE \"ID\" = $obj->ID")->value()
939
        );
940
        $this->assertEquals(
941
            'value3',
942
            DB::query("SELECT \"Duplicate\" FROM \"DataObjectTest_Fixture\" WHERE \"ID\" = $obj->ID")->value()
943
        );
944
    }
945
946
    /**
947
     * @todo Re-enable all test cases for field existence after behaviour has been fixed
948
     */
949
    public function testFieldExistence()
950
    {
951
        $teamInstance = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
952
        $teamSingleton = singleton(DataObjectTest\Team::class);
953
954
        $subteamInstance = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam1');
955
        $schema = DataObject::getSchema();
956
957
        /* hasField() singleton checks */
958
        $this->assertTrue(
959
            $teamSingleton->hasField('ID'),
960
            'hasField() finds built-in fields in singletons'
961
        );
962
        $this->assertTrue(
963
            $teamSingleton->hasField('Title'),
964
            'hasField() finds custom fields in singletons'
965
        );
966
967
        /* hasField() instance checks */
968
        $this->assertFalse(
969
            $teamInstance->hasField('NonExistingField'),
970
            'hasField() doesnt find non-existing fields in instances'
971
        );
972
        $this->assertTrue(
973
            $teamInstance->hasField('ID'),
974
            'hasField() finds built-in fields in instances'
975
        );
976
        $this->assertTrue(
977
            $teamInstance->hasField('Created'),
978
            'hasField() finds built-in fields in instances'
979
        );
980
        $this->assertTrue(
981
            $teamInstance->hasField('DatabaseField'),
982
            'hasField() finds custom fields in instances'
983
        );
984
        //$this->assertFalse($teamInstance->hasField('SubclassDatabaseField'),
985
        //'hasField() doesnt find subclass fields in parentclass instances');
986
        $this->assertTrue(
987
            $teamInstance->hasField('DynamicField'),
988
            'hasField() finds dynamic getters in instances'
989
        );
990
        $this->assertTrue(
991
            $teamInstance->hasField('HasOneRelationshipID'),
992
            'hasField() finds foreign keys in instances'
993
        );
994
        $this->assertTrue(
995
            $teamInstance->hasField('ExtendedDatabaseField'),
996
            'hasField() finds extended fields in instances'
997
        );
998
        $this->assertTrue(
999
            $teamInstance->hasField('ExtendedHasOneRelationshipID'),
1000
            'hasField() finds extended foreign keys in instances'
1001
        );
1002
        //$this->assertTrue($teamInstance->hasField('ExtendedDynamicField'),
1003
        //'hasField() includes extended dynamic getters in instances');
1004
1005
        /* hasField() subclass checks */
1006
        $this->assertTrue(
1007
            $subteamInstance->hasField('ID'),
1008
            'hasField() finds built-in fields in subclass instances'
1009
        );
1010
        $this->assertTrue(
1011
            $subteamInstance->hasField('Created'),
1012
            'hasField() finds built-in fields in subclass instances'
1013
        );
1014
        $this->assertTrue(
1015
            $subteamInstance->hasField('DatabaseField'),
1016
            'hasField() finds custom fields in subclass instances'
1017
        );
1018
        $this->assertTrue(
1019
            $subteamInstance->hasField('SubclassDatabaseField'),
1020
            'hasField() finds custom fields in subclass instances'
1021
        );
1022
        $this->assertTrue(
1023
            $subteamInstance->hasField('DynamicField'),
1024
            'hasField() finds dynamic getters in subclass instances'
1025
        );
1026
        $this->assertTrue(
1027
            $subteamInstance->hasField('HasOneRelationshipID'),
1028
            'hasField() finds foreign keys in subclass instances'
1029
        );
1030
        $this->assertTrue(
1031
            $subteamInstance->hasField('ExtendedDatabaseField'),
1032
            'hasField() finds extended fields in subclass instances'
1033
        );
1034
        $this->assertTrue(
1035
            $subteamInstance->hasField('ExtendedHasOneRelationshipID'),
1036
            'hasField() finds extended foreign keys in subclass instances'
1037
        );
1038
1039
        /* hasDatabaseField() singleton checks */
1040
        //$this->assertTrue($teamSingleton->hasDatabaseField('ID'),
1041
        //'hasDatabaseField() finds built-in fields in singletons');
1042
        $this->assertNotEmpty(
1043
            $schema->fieldSpec(DataObjectTest\Team::class, 'Title'),
1044
            'hasDatabaseField() finds custom fields in singletons'
1045
        );
1046
1047
        /* hasDatabaseField() instance checks */
1048
        $this->assertNull(
1049
            $schema->fieldSpec(DataObjectTest\Team::class, 'NonExistingField'),
1050
            'hasDatabaseField() doesnt find non-existing fields in instances'
1051
        );
1052
        //$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'ID'),
1053
        //'hasDatabaseField() finds built-in fields in instances');
1054
        $this->assertNotEmpty(
1055
            $schema->fieldSpec(DataObjectTest\Team::class, 'Created'),
1056
            'hasDatabaseField() finds built-in fields in instances'
1057
        );
1058
        $this->assertNotEmpty(
1059
            $schema->fieldSpec(DataObjectTest\Team::class, 'DatabaseField'),
1060
            'hasDatabaseField() finds custom fields in instances'
1061
        );
1062
        $this->assertNull(
1063
            $schema->fieldSpec(DataObjectTest\Team::class, 'SubclassDatabaseField'),
1064
            'hasDatabaseField() doesnt find subclass fields in parentclass instances'
1065
        );
1066
        //$this->assertNull($schema->fieldSpec(DataObjectTest_Team::class, 'DynamicField'),
1067
        //'hasDatabaseField() doesnt dynamic getters in instances');
1068
        $this->assertNotEmpty(
1069
            $schema->fieldSpec(DataObjectTest\Team::class, 'HasOneRelationshipID'),
1070
            'hasDatabaseField() finds foreign keys in instances'
1071
        );
1072
        $this->assertNotEmpty(
1073
            $schema->fieldSpec(DataObjectTest\Team::class, 'ExtendedDatabaseField'),
1074
            'hasDatabaseField() finds extended fields in instances'
1075
        );
1076
        $this->assertNotEmpty(
1077
            $schema->fieldSpec(DataObjectTest\Team::class, 'ExtendedHasOneRelationshipID'),
1078
            'hasDatabaseField() finds extended foreign keys in instances'
1079
        );
1080
        $this->assertNull(
1081
            $schema->fieldSpec(DataObjectTest\Team::class, 'ExtendedDynamicField'),
1082
            'hasDatabaseField() doesnt include extended dynamic getters in instances'
1083
        );
1084
1085
        /* hasDatabaseField() subclass checks */
1086
        $this->assertNotEmpty(
1087
            $schema->fieldSpec(DataObjectTest\SubTeam::class, 'DatabaseField'),
1088
            'hasField() finds custom fields in subclass instances'
1089
        );
1090
        $this->assertNotEmpty(
1091
            $schema->fieldSpec(DataObjectTest\SubTeam::class, 'SubclassDatabaseField'),
1092
            'hasField() finds custom fields in subclass instances'
1093
        );
1094
    }
1095
1096
    /**
1097
     * @todo Re-enable all test cases for field inheritance aggregation after behaviour has been fixed
1098
     */
1099
    public function testFieldInheritance()
1100
    {
1101
        $schema = DataObject::getSchema();
1102
1103
        // Test logical fields (including composite)
1104
        $teamSpecifications = $schema->fieldSpecs(DataObjectTest\Team::class);
1105
        $this->assertEquals(
1106
            array(
1107
                'ID',
1108
                'ClassName',
1109
                'LastEdited',
1110
                'Created',
1111
                'Title',
1112
                'DatabaseField',
1113
                'ExtendedDatabaseField',
1114
                'CaptainID',
1115
                'FounderID',
1116
                'HasOneRelationshipID',
1117
                'ExtendedHasOneRelationshipID'
1118
            ),
1119
            array_keys($teamSpecifications),
1120
            'fieldSpecifications() contains all fields defined on instance: base, extended and foreign keys'
1121
        );
1122
1123
        $teamFields = $schema->databaseFields(DataObjectTest\Team::class, false);
1124
        $this->assertEquals(
1125
            array(
1126
                'ID',
1127
                'ClassName',
1128
                'LastEdited',
1129
                'Created',
1130
                'Title',
1131
                'DatabaseField',
1132
                'ExtendedDatabaseField',
1133
                'CaptainID',
1134
                'FounderID',
1135
                'HasOneRelationshipID',
1136
                'ExtendedHasOneRelationshipID'
1137
            ),
1138
            array_keys($teamFields),
1139
            'databaseFields() contains only fields defined on instance, including base, extended and foreign keys'
1140
        );
1141
1142
        $subteamSpecifications = $schema->fieldSpecs(DataObjectTest\SubTeam::class);
1143
        $this->assertEquals(
1144
            array(
1145
                'ID',
1146
                'ClassName',
1147
                'LastEdited',
1148
                'Created',
1149
                'Title',
1150
                'DatabaseField',
1151
                'ExtendedDatabaseField',
1152
                'CaptainID',
1153
                'FounderID',
1154
                'HasOneRelationshipID',
1155
                'ExtendedHasOneRelationshipID',
1156
                'SubclassDatabaseField',
1157
                'ParentTeamID',
1158
            ),
1159
            array_keys($subteamSpecifications),
1160
            'fieldSpecifications() on subclass contains all fields, including base, extended  and foreign keys'
1161
        );
1162
1163
        $subteamFields = $schema->databaseFields(DataObjectTest\SubTeam::class, false);
1164
        $this->assertEquals(
1165
            array(
1166
                'ID',
1167
                'SubclassDatabaseField',
1168
                'ParentTeamID',
1169
            ),
1170
            array_keys($subteamFields),
1171
            'databaseFields() on subclass contains only fields defined on instance'
1172
        );
1173
    }
1174
1175
    public function testSearchableFields()
1176
    {
1177
        $player = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
1178
        $fields = $player->searchableFields();
1179
        $this->assertArrayHasKey(
1180
            'IsRetired',
1181
            $fields,
1182
            'Fields defined by $searchable_fields static are correctly detected'
1183
        );
1184
        $this->assertArrayHasKey(
1185
            'ShirtNumber',
1186
            $fields,
1187
            'Fields defined by $searchable_fields static are correctly detected'
1188
        );
1189
1190
        $team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1191
        $fields = $team->searchableFields();
1192
        $this->assertArrayHasKey(
1193
            'Title',
1194
            $fields,
1195
            'Fields can be inherited from the $summary_fields static, including methods called on fields'
1196
        );
1197
        $this->assertArrayHasKey(
1198
            'Captain.ShirtNumber',
1199
            $fields,
1200
            'Fields on related objects can be inherited from the $summary_fields static'
1201
        );
1202
        $this->assertArrayHasKey(
1203
            'Captain.FavouriteTeam.Title',
1204
            $fields,
1205
            'Fields on related objects can be inherited from the $summary_fields static'
1206
        );
1207
1208
        $testObj = new DataObjectTest\Fixture();
1209
        $fields = $testObj->searchableFields();
1210
        $this->assertEmpty($fields);
1211
    }
1212
1213
    public function testCastingHelper()
1214
    {
1215
        $team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1216
1217
        $this->assertEquals('Varchar', $team->castingHelper('Title'), 'db field wasn\'t casted correctly');
1218
        $this->assertEquals('HTMLVarchar', $team->castingHelper('DatabaseField'), 'db field wasn\'t casted correctly');
1219
1220
        $sponsor = $team->Sponsors()->first();
1221
        $this->assertEquals('Int', $sponsor->castingHelper('SponsorFee'), 'many_many_extraFields not casted correctly');
1222
    }
1223
1224
    public function testSummaryFieldsCustomLabels()
1225
    {
1226
        $team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1227
        $summaryFields = $team->summaryFields();
1228
1229
        $this->assertEquals(
1230
            [
1231
                'Title' => 'Custom Title',
1232
                'Title.UpperCase' => 'Title',
1233
                'Captain.ShirtNumber' => 'Captain\'s shirt number',
1234
                'Captain.FavouriteTeam.Title' => 'Captain\'s favourite team',
1235
            ],
1236
            $summaryFields
1237
        );
1238
    }
1239
1240
    public function testDataObjectUpdate()
1241
    {
1242
        /* update() calls can use the dot syntax to reference has_one relations and other methods that return
1243
        * objects */
1244
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1245
        $team1->CaptainID = $this->idFromFixture(DataObjectTest\Player::class, 'captain1');
1246
1247
        $team1->update(
1248
            array(
1249
                'DatabaseField' => 'Something',
1250
                'Captain.FirstName' => 'Jim',
1251
                'Captain.Email' => '[email protected]',
1252
                'Captain.FavouriteTeam.Title' => 'New and improved team 1',
1253
            )
1254
        );
1255
1256
        /* Test the simple case of updating fields on the object itself */
1257
        $this->assertEquals('Something', $team1->DatabaseField);
1258
1259
        /* Setting Captain.Email and Captain.FirstName will have updated DataObjectTest_Captain.captain1 in
1260
        * the database.  Although update() doesn't usually write, it does write related records automatically. */
1261
        $captain1 = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
1262
        $this->assertEquals('Jim', $captain1->FirstName);
1263
        $this->assertEquals('[email protected]', $captain1->Email);
1264
1265
        /* Jim's favourite team is team 1; we need to reload the object to the the change that setting Captain.
1266
        * FavouriteTeam.Title made */
1267
        $reloadedTeam1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1268
        $this->assertEquals('New and improved team 1', $reloadedTeam1->Title);
1269
    }
1270
1271
    public function testDataObjectUpdateNew()
1272
    {
1273
        /* update() calls can use the dot syntax to reference has_one relations and other methods that return
1274
        * objects */
1275
        $team1 = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1276
        $team1->CaptainID = 0;
1277
1278
        $team1->update(
1279
            array(
1280
                'Captain.FirstName' => 'Jim',
1281
                'Captain.FavouriteTeam.Title' => 'New and improved team 1',
1282
            )
1283
        );
1284
        /* Test that the captain ID has been updated */
1285
        $this->assertGreaterThan(0, $team1->CaptainID);
1286
1287
        /* Fetch the newly created captain */
1288
        $captain1 = DataObjectTest\Player::get()->byID($team1->CaptainID);
1289
        $this->assertEquals('Jim', $captain1->FirstName);
1290
1291
        /* Grab the favourite team and make sure it has the correct values */
1292
        $reloadedTeam1 = $captain1->FavouriteTeam();
1293
        $this->assertEquals($reloadedTeam1->ID, $captain1->FavouriteTeamID);
1294
        $this->assertEquals('New and improved team 1', $reloadedTeam1->Title);
1295
    }
1296
1297
1298
    /**
1299
     * @expectedException \SilverStripe\ORM\ValidationException
1300
     */
1301
    public function testWritingInvalidDataObjectThrowsException()
1302
    {
1303
        $validatedObject = new DataObjectTest\ValidatedObject();
1304
        $validatedObject->write();
1305
    }
1306
1307
    public function testWritingValidDataObjectDoesntThrowException()
1308
    {
1309
        $validatedObject = new DataObjectTest\ValidatedObject();
1310
        $validatedObject->Name = "Mr. Jones";
1311
1312
        $validatedObject->write();
1313
        $this->assertTrue($validatedObject->isInDB(), "Validated object was not saved to database");
1314
    }
1315
1316
    public function testSubclassCreation()
1317
    {
1318
        /* Creating a new object of a subclass should set the ClassName field correctly */
1319
        $obj = new DataObjectTest\SubTeam();
1320
        $obj->write();
1321
        $this->assertEquals(
1322
            DataObjectTest\SubTeam::class,
1323
            DB::query("SELECT \"ClassName\" FROM \"DataObjectTest_Team\" WHERE \"ID\" = $obj->ID")->value()
1324
        );
1325
    }
1326
1327
    public function testForceInsert()
1328
    {
1329
        /* If you set an ID on an object and pass forceInsert = true, then the object should be correctly created */
1330
        $conn = DB::get_conn();
1331
        if (method_exists($conn, 'allowPrimaryKeyEditing')) {
1332
            $conn->allowPrimaryKeyEditing(DataObjectTest\Team::class, true);
1333
        }
1334
        $obj = new DataObjectTest\SubTeam();
1335
        $obj->ID = 1001;
1336
        $obj->Title = 'asdfasdf';
1337
        $obj->SubclassDatabaseField = 'asdfasdf';
1338
        $obj->write(false, true);
1339
        if (method_exists($conn, 'allowPrimaryKeyEditing')) {
1340
            $conn->allowPrimaryKeyEditing(DataObjectTest\Team::class, false);
1341
        }
1342
1343
        $this->assertEquals(
1344
            DataObjectTest\SubTeam::class,
1345
            DB::query("SELECT \"ClassName\" FROM \"DataObjectTest_Team\" WHERE \"ID\" = $obj->ID")->value()
1346
        );
1347
1348
        /* Check that it actually saves to the database with the correct ID */
1349
        $this->assertEquals(
1350
            "1001",
1351
            DB::query(
1352
                "SELECT \"ID\" FROM \"DataObjectTest_SubTeam\" WHERE \"SubclassDatabaseField\" = 'asdfasdf'"
1353
            )->value()
1354
        );
1355
        $this->assertEquals(
1356
            "1001",
1357
            DB::query("SELECT \"ID\" FROM \"DataObjectTest_Team\" WHERE \"Title\" = 'asdfasdf'")->value()
1358
        );
1359
    }
1360
1361
    public function testHasOwnTable()
1362
    {
1363
        $schema = DataObject::getSchema();
1364
        /* Test DataObject::has_own_table() returns true if the object has $has_one or $db values */
1365
        $this->assertTrue($schema->classHasTable(DataObjectTest\Player::class));
1366
        $this->assertTrue($schema->classHasTable(DataObjectTest\Team::class));
1367
        $this->assertTrue($schema->classHasTable(DataObjectTest\Fixture::class));
1368
1369
        /* Root DataObject that always have a table, even if they lack both $db and $has_one */
1370
        $this->assertTrue($schema->classHasTable(DataObjectTest\FieldlessTable::class));
1371
1372
        /* Subclasses without $db or $has_one don't have a table */
1373
        $this->assertFalse($schema->classHasTable(DataObjectTest\FieldlessSubTable::class));
1374
1375
        /* Return false if you don't pass it a subclass of DataObject */
1376
        $this->assertFalse($schema->classHasTable(DataObject::class));
1377
        $this->assertFalse($schema->classHasTable(ViewableData::class));
1378
1379
        /* Invalid class name */
1380
        $this->assertFalse($schema->classHasTable("ThisIsntADataObject"));
1381
    }
1382
1383
    public function testMerge()
1384
    {
1385
        // test right merge of subclasses
1386
        $left = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam1');
1387
        $right = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam2_with_player_relation');
1388
        $leftOrigID = $left->ID;
1389
        $left->merge($right, 'right', false, false);
1390
        $this->assertEquals(
1391
            $left->Title,
1392
            'Subteam 2',
1393
            'merge() with "right" priority overwrites fields with existing values on subclasses'
1394
        );
1395
        $this->assertEquals(
1396
            $left->ID,
1397
            $leftOrigID,
1398
            'merge() with "right" priority doesnt overwrite database ID'
1399
        );
1400
1401
        // test overwriteWithEmpty flag on existing left values
1402
        $left = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam2_with_player_relation');
1403
        $right = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam3_with_empty_fields');
1404
        $left->merge($right, 'right', false, true);
1405
        $this->assertEquals(
1406
            $left->Title,
1407
            'Subteam 3',
1408
            'merge() with $overwriteWithEmpty overwrites non-empty fields on left object'
1409
        );
1410
1411
        // test overwriteWithEmpty flag on empty left values
1412
        $left = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam1');
1413
        // $SubclassDatabaseField is empty on here
1414
        $right = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam2_with_player_relation');
1415
        $left->merge($right, 'right', false, true);
1416
        $this->assertEquals(
1417
            $left->SubclassDatabaseField,
1418
            null,
1419
            'merge() with $overwriteWithEmpty overwrites empty fields on left object'
1420
        );
1421
1422
        // @todo test "left" priority flag
1423
        // @todo test includeRelations flag
1424
        // @todo test includeRelations in combination with overwriteWithEmpty
1425
        // @todo test has_one relations
1426
        // @todo test has_many and many_many relations
1427
    }
1428
1429
    public function testPopulateDefaults()
1430
    {
1431
        $obj = new DataObjectTest\Fixture();
1432
        $this->assertEquals(
1433
            $obj->MyFieldWithDefault,
1434
            'Default Value',
1435
            'Defaults are populated for in-memory object from $defaults array'
1436
        );
1437
1438
        $this->assertEquals(
1439
            $obj->MyFieldWithAltDefault,
1440
            'Default Value',
1441
            'Defaults are populated from overloaded populateDefaults() method'
1442
        );
1443
    }
1444
1445
    /**
1446
     * @expectedException \InvalidArgumentException
1447
     */
1448
    public function testValidateModelDefinitionsFailsWithArray()
1449
    {
1450
        Config::modify()->merge(DataObjectTest\Team::class, 'has_one', array('NotValid' => array('NoArraysAllowed')));
1451
        DataObject::getSchema()->hasOneComponent(DataObjectTest\Team::class, 'NotValid');
1452
    }
1453
1454
    /**
1455
     * @expectedException \InvalidArgumentException
1456
     */
1457
    public function testValidateModelDefinitionsFailsWithIntKey()
1458
    {
1459
        Config::modify()->set(DataObjectTest\Team::class, 'has_many', array(0 => DataObjectTest\Player::class));
1460
        DataObject::getSchema()->hasManyComponent(DataObjectTest\Team::class, 0);
1461
    }
1462
1463
    /**
1464
     * @expectedException \InvalidArgumentException
1465
     */
1466
    public function testValidateModelDefinitionsFailsWithIntValue()
1467
    {
1468
        Config::modify()->merge(DataObjectTest\Team::class, 'many_many', array('Players' => 12));
1469
        DataObject::getSchema()->manyManyComponent(DataObjectTest\Team::class, 'Players');
1470
    }
1471
1472
    public function testNewClassInstance()
1473
    {
1474
        $dataObject = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1475
        $changedDO = $dataObject->newClassInstance(DataObjectTest\SubTeam::class);
1476
        $changedFields = $changedDO->getChangedFields();
1477
1478
        // Don't write the record, it will reset changed fields
1479
        $this->assertInstanceOf(DataObjectTest\SubTeam::class, $changedDO);
1480
        $this->assertEquals($changedDO->ClassName, DataObjectTest\SubTeam::class);
1481
        $this->assertEquals($changedDO->RecordClassName, DataObjectTest\SubTeam::class);
1482
        $this->assertContains('ClassName', array_keys($changedFields));
1483
        $this->assertEquals($changedFields['ClassName']['before'], DataObjectTest\Team::class);
1484
        $this->assertEquals($changedFields['ClassName']['after'], DataObjectTest\SubTeam::class);
1485
        $this->assertEquals($changedFields['RecordClassName']['before'], DataObjectTest\Team::class);
1486
        $this->assertEquals($changedFields['RecordClassName']['after'], DataObjectTest\SubTeam::class);
1487
1488
        $changedDO->write();
1489
1490
        $this->assertInstanceOf(DataObjectTest\SubTeam::class, $changedDO);
1491
        $this->assertEquals($changedDO->ClassName, DataObjectTest\SubTeam::class);
1492
1493
        // Test invalid classes fail
1494
        $this->expectException(InvalidArgumentException::class);
1495
        $this->expectExceptionMessage('Controller is not a valid subclass of DataObject');
1496
        /**
1497
         * @skipUpgrade
1498
         */
1499
        $dataObject->newClassInstance('Controller');
1500
    }
1501
1502
    public function testMultipleManyManyWithSameClass()
1503
    {
1504
        $team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1505
        $company2 = $this->objFromFixture(DataObjectTest\EquipmentCompany::class, 'equipmentcompany2');
1506
        $sponsors = $team->Sponsors();
1507
        $equipmentSuppliers = $team->EquipmentSuppliers();
1508
1509
        // Check that DataObject::many_many() works as expected
1510
        $manyManyComponent = DataObject::getSchema()->manyManyComponent(DataObjectTest\Team::class, 'Sponsors');
1511
        $this->assertEquals(ManyManyList::class, $manyManyComponent['relationClass']);
1512
        $this->assertEquals(
1513
            DataObjectTest\Team::class,
1514
            $manyManyComponent['parentClass'],
1515
            'DataObject::many_many() didn\'t find the correct base class'
1516
        );
1517
        $this->assertEquals(
1518
            DataObjectTest\EquipmentCompany::class,
1519
            $manyManyComponent['childClass'],
1520
            'DataObject::many_many() didn\'t find the correct target class for the relation'
1521
        );
1522
        $this->assertEquals(
1523
            'DataObjectTest_EquipmentCompany_SponsoredTeams',
1524
            $manyManyComponent['join'],
1525
            'DataObject::many_many() didn\'t find the correct relation table'
1526
        );
1527
        $this->assertEquals('DataObjectTest_TeamID', $manyManyComponent['parentField']);
1528
        $this->assertEquals('DataObjectTest_EquipmentCompanyID', $manyManyComponent['childField']);
1529
1530
        // Check that ManyManyList still works
1531
        $this->assertEquals(2, $sponsors->count(), 'Rows are missing from relation');
1532
        $this->assertEquals(1, $equipmentSuppliers->count(), 'Rows are missing from relation');
1533
1534
        // Check everything works when no relation is present
1535
        $teamWithoutSponsor = $this->objFromFixture(DataObjectTest\Team::class, 'team3');
1536
        $this->assertInstanceOf(ManyManyList::class, $teamWithoutSponsor->Sponsors());
1537
        $this->assertEquals(0, $teamWithoutSponsor->Sponsors()->count());
1538
1539
        // Test that belongs_many_many can be infered from with getNonReciprocalComponent
1540
        $this->assertListEquals(
1541
            [
1542
                ['Name' => 'Company corp'],
1543
                ['Name' => 'Team co.'],
1544
            ],
1545
            $team->inferReciprocalComponent(DataObjectTest\EquipmentCompany::class, 'SponsoredTeams')
1546
        );
1547
1548
        // Test that many_many can be infered from getNonReciprocalComponent
1549
        $this->assertListEquals(
1550
            [
1551
                ['Title' => 'Team 1'],
1552
                ['Title' => 'Team 2'],
1553
                ['Title' => 'Subteam 1'],
1554
            ],
1555
            $company2->inferReciprocalComponent(DataObjectTest\Team::class, 'Sponsors')
1556
        );
1557
1558
        // Check many_many_extraFields still works
1559
        $equipmentCompany = $this->objFromFixture(DataObjectTest\EquipmentCompany::class, 'equipmentcompany1');
1560
        $equipmentCompany->SponsoredTeams()->add($teamWithoutSponsor, array('SponsorFee' => 1000));
1561
        $sponsoredTeams = $equipmentCompany->SponsoredTeams();
1562
        $this->assertEquals(
1563
            1000,
1564
            $sponsoredTeams->byID($teamWithoutSponsor->ID)->SponsorFee,
1565
            'Data from many_many_extraFields was not stored/extracted correctly'
1566
        );
1567
1568
        // Check subclasses correctly inherit multiple many_manys
1569
        $subTeam = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam1');
1570
        $this->assertEquals(
1571
            2,
1572
            $subTeam->Sponsors()->count(),
1573
            'Child class did not inherit multiple many_manys'
1574
        );
1575
        $this->assertEquals(
1576
            1,
1577
            $subTeam->EquipmentSuppliers()->count(),
1578
            'Child class did not inherit multiple many_manys'
1579
        );
1580
        // Team 2 has one EquipmentCompany sponsor and one SubEquipmentCompany
1581
        $team2 = $this->objFromFixture(DataObjectTest\Team::class, 'team2');
1582
        $this->assertEquals(
1583
            2,
1584
            $team2->Sponsors()->count(),
1585
            'Child class did not inherit multiple belongs_many_manys'
1586
        );
1587
1588
        // Check many_many_extraFields also works from the belongs_many_many side
1589
        $sponsors = $team2->Sponsors();
1590
        $sponsors->add($equipmentCompany, array('SponsorFee' => 750));
1591
        $this->assertEquals(
1592
            750,
1593
            $sponsors->byID($equipmentCompany->ID)->SponsorFee,
1594
            'Data from many_many_extraFields was not stored/extracted correctly'
1595
        );
1596
1597
        $subEquipmentCompany = $this->objFromFixture(DataObjectTest\SubEquipmentCompany::class, 'subequipmentcompany1');
1598
        $subTeam->Sponsors()->add($subEquipmentCompany, array('SponsorFee' => 1200));
1599
        $this->assertEquals(
1600
            1200,
1601
            $subTeam->Sponsors()->byID($subEquipmentCompany->ID)->SponsorFee,
1602
            'Data from inherited many_many_extraFields was not stored/extracted correctly'
1603
        );
1604
    }
1605
1606
    public function testManyManyExtraFields()
1607
    {
1608
        $team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
1609
        $schema = DataObject::getSchema();
1610
1611
        // Get all extra fields
1612
        $teamExtraFields = $team->manyManyExtraFields();
1613
        $this->assertEquals(
1614
            array(
1615
                'Players' => array('Position' => 'Varchar(100)')
1616
            ),
1617
            $teamExtraFields
1618
        );
1619
1620
        // Ensure fields from parent classes are included
1621
        $subTeam = singleton(DataObjectTest\SubTeam::class);
1622
        $teamExtraFields = $subTeam->manyManyExtraFields();
1623
        $this->assertEquals(
1624
            array(
1625
                'Players' => array('Position' => 'Varchar(100)'),
1626
                'FormerPlayers' => array('Position' => 'Varchar(100)')
1627
            ),
1628
            $teamExtraFields
1629
        );
1630
1631
        // Extra fields are immediately available on the Team class (defined in $many_many_extraFields)
1632
        $teamExtraFields = $schema->manyManyExtraFieldsForComponent(DataObjectTest\Team::class, 'Players');
1633
        $this->assertEquals(
1634
            $teamExtraFields,
1635
            array(
1636
                'Position' => 'Varchar(100)'
1637
            )
1638
        );
1639
1640
        // We'll have to go through the relation to get the extra fields on Player
1641
        $playerExtraFields = $schema->manyManyExtraFieldsForComponent(DataObjectTest\Player::class, 'Teams');
1642
        $this->assertEquals(
1643
            $playerExtraFields,
1644
            array(
1645
                'Position' => 'Varchar(100)'
1646
            )
1647
        );
1648
1649
        // Iterate through a many-many relationship and confirm that extra fields are included
1650
        $newTeam = new DataObjectTest\Team();
1651
        $newTeam->Title = "New team";
1652
        $newTeam->write();
1653
        $newTeamID = $newTeam->ID;
1654
1655
        $newPlayer = new DataObjectTest\Player();
1656
        $newPlayer->FirstName = "Sam";
1657
        $newPlayer->Surname = "Minnee";
1658
        $newPlayer->write();
1659
1660
        // The idea of Sam as a prop is essentially humourous.
1661
        $newTeam->Players()->add($newPlayer, array("Position" => "Prop"));
1662
1663
        // Requery and uncache everything
1664
        $newTeam->flushCache();
1665
        $newTeam = DataObject::get_by_id(DataObjectTest\Team::class, $newTeamID);
1666
1667
        // Check that the Position many_many_extraField is extracted.
1668
        $player = $newTeam->Players()->first();
1669
        $this->assertEquals('Sam', $player->FirstName);
1670
        $this->assertEquals("Prop", $player->Position);
1671
1672
        // Check that ordering a many-many relation by an aggregate column doesn't fail
1673
        $player = $this->objFromFixture(DataObjectTest\Player::class, 'player2');
1674
        $player->Teams()->sort("count(DISTINCT \"DataObjectTest_Team_Players\".\"DataObjectTest_PlayerID\") DESC");
1675
    }
1676
1677
    /**
1678
     * Check that the queries generated for many-many relation queries can have unlimitedRowCount
1679
     * called on them.
1680
     */
1681
    public function testManyManyUnlimitedRowCount()
1682
    {
1683
        $player = $this->objFromFixture(DataObjectTest\Player::class, 'player2');
1684
        // TODO: What's going on here?
1685
        $this->assertEquals(2, $player->Teams()->dataQuery()->query()->unlimitedRowCount());
1686
    }
1687
1688
    /**
1689
     * Tests that singular_name() generates sensible defaults.
1690
     */
1691
    public function testSingularName()
1692
    {
1693
        $assertions = array(
1694
            DataObjectTest\Player::class => 'Player',
1695
            DataObjectTest\Team::class => 'Team',
1696
            DataObjectTest\Fixture::class => 'Fixture',
1697
        );
1698
1699
        foreach ($assertions as $class => $expectedSingularName) {
1700
            $this->assertEquals(
1701
                $expectedSingularName,
1702
                singleton($class)->singular_name(),
1703
                "Assert that the singular_name for '$class' is correct."
1704
            );
1705
        }
1706
    }
1707
1708
    /**
1709
     * Tests that plural_name() generates sensible defaults.
1710
     */
1711
    public function testPluralName()
1712
    {
1713
        $assertions = array(
1714
            DataObjectTest\Player::class => 'Players',
1715
            DataObjectTest\Team::class => 'Teams',
1716
            DataObjectTest\Fixture::class => 'Fixtures',
1717
            DataObjectTest\Play::class => 'Plays',
1718
            DataObjectTest\Bogey::class => 'Bogeys',
1719
            DataObjectTest\Ploy::class => 'Ploys',
1720
        );
1721
        i18n::set_locale('en_NZ');
1722
        foreach ($assertions as $class => $expectedPluralName) {
1723
            $this->assertEquals(
1724
                $expectedPluralName,
1725
                DataObject::singleton($class)->plural_name(),
1726
                "Assert that the plural_name for '$class' is correct."
1727
            );
1728
            $this->assertEquals(
1729
                $expectedPluralName,
1730
                DataObject::singleton($class)->i18n_plural_name(),
1731
                "Assert that the i18n_plural_name for '$class' is correct."
1732
            );
1733
        }
1734
    }
1735
1736
    public function testHasDatabaseField()
1737
    {
1738
        $team = singleton(DataObjectTest\Team::class);
1739
        $subteam = singleton(DataObjectTest\SubTeam::class);
1740
1741
        $this->assertTrue(
1742
            $team->hasDatabaseField('Title'),
1743
            "hasOwnDatabaseField() works with \$db fields"
1744
        );
1745
        $this->assertTrue(
1746
            $team->hasDatabaseField('CaptainID'),
1747
            "hasOwnDatabaseField() works with \$has_one fields"
1748
        );
1749
        $this->assertFalse(
1750
            $team->hasDatabaseField('NonExistentField'),
1751
            "hasOwnDatabaseField() doesn't detect non-existend fields"
1752
        );
1753
        $this->assertTrue(
1754
            $team->hasDatabaseField('ExtendedDatabaseField'),
1755
            "hasOwnDatabaseField() works with extended fields"
1756
        );
1757
        $this->assertFalse(
1758
            $team->hasDatabaseField('SubclassDatabaseField'),
1759
            "hasOwnDatabaseField() doesn't pick up fields in subclasses on parent class"
1760
        );
1761
1762
        $this->assertTrue(
1763
            $subteam->hasDatabaseField('SubclassDatabaseField'),
1764
            "hasOwnDatabaseField() picks up fields in subclasses"
1765
        );
1766
    }
1767
1768
    public function testFieldTypes()
1769
    {
1770
        $obj = new DataObjectTest\Fixture();
1771
        $obj->DateField = '1988-01-02';
1772
        $obj->DatetimeField = '1988-03-04 06:30';
1773
        $obj->write();
1774
        $obj->flushCache();
1775
1776
        $obj = DataObject::get_by_id(DataObjectTest\Fixture::class, $obj->ID);
1777
        $this->assertEquals('1988-01-02', $obj->DateField);
1778
        $this->assertEquals('1988-03-04 06:30:00', $obj->DatetimeField);
1779
    }
1780
1781
    public function testTwoSubclassesWithTheSameFieldNameWork()
1782
    {
1783
        // Create two objects of different subclasses, setting the values of fields that are
1784
        // defined separately in each subclass
1785
        $obj1 = new DataObjectTest\SubTeam();
1786
        $obj1->SubclassDatabaseField = "obj1";
1787
        $obj2 = new DataObjectTest\OtherSubclassWithSameField();
1788
        $obj2->SubclassDatabaseField = "obj2";
1789
1790
        // Write them to the database
1791
        $obj1->write();
1792
        $obj2->write();
1793
1794
        // Check that the values of those fields are properly read from the database
1795
        $values = DataObject::get(
1796
            DataObjectTest\Team::class,
1797
            "\"DataObjectTest_Team\".\"ID\" IN
1798
			($obj1->ID, $obj2->ID)"
1799
        )->column("SubclassDatabaseField");
1800
        $this->assertEquals(array_intersect($values, array('obj1', 'obj2')), $values);
1801
    }
1802
1803
    public function testClassNameSetForNewObjects()
1804
    {
1805
        $d = new DataObjectTest\Player();
1806
        $this->assertEquals(DataObjectTest\Player::class, $d->ClassName);
1807
    }
1808
1809
    public function testHasValue()
1810
    {
1811
        $team = new DataObjectTest\Team();
1812
        $this->assertFalse($team->hasValue('Title', null, false));
1813
        $this->assertFalse($team->hasValue('DatabaseField', null, false));
1814
1815
        $team->Title = 'hasValue';
1816
        $this->assertTrue($team->hasValue('Title', null, false));
1817
        $this->assertFalse($team->hasValue('DatabaseField', null, false));
1818
1819
        $team->Title = '<p></p>';
1820
        $this->assertTrue(
1821
            $team->hasValue('Title', null, false),
1822
            'Test that an empty paragraph is a value for non-HTML fields.'
1823
        );
1824
1825
        $team->DatabaseField = 'hasValue';
1826
        $this->assertTrue($team->hasValue('Title', null, false));
1827
        $this->assertTrue($team->hasValue('DatabaseField', null, false));
1828
    }
1829
1830
    public function testHasMany()
1831
    {
1832
        $company = new DataObjectTest\Company();
1833
1834
        $this->assertEquals(
1835
            array(
1836
                'CurrentStaff' => DataObjectTest\Staff::class,
1837
                'PreviousStaff' => DataObjectTest\Staff::class
1838
            ),
1839
            $company->hasMany(),
1840
            'has_many strips field name data by default.'
1841
        );
1842
1843
        $this->assertEquals(
1844
            DataObjectTest\Staff::class,
1845
            DataObject::getSchema()->hasManyComponent(DataObjectTest\Company::class, 'CurrentStaff'),
1846
            'has_many strips field name data by default on single relationships.'
1847
        );
1848
1849
        $this->assertEquals(
1850
            array(
1851
                'CurrentStaff' => DataObjectTest\Staff::class . '.CurrentCompany',
1852
                'PreviousStaff' => DataObjectTest\Staff::class . '.PreviousCompany'
1853
            ),
1854
            $company->hasMany(false),
1855
            'has_many returns field name data when $classOnly is false.'
1856
        );
1857
1858
        $this->assertEquals(
1859
            DataObjectTest\Staff::class . '.CurrentCompany',
1860
            DataObject::getSchema()->hasManyComponent(DataObjectTest\Company::class, 'CurrentStaff', false),
1861
            'has_many returns field name data on single records when $classOnly is false.'
1862
        );
1863
    }
1864
1865
    public function testGetRemoteJoinField()
1866
    {
1867
        $schema = DataObject::getSchema();
1868
1869
        // Company schema
1870
        $staffJoinField = $schema->getRemoteJoinField(
1871
            DataObjectTest\Company::class,
1872
            'CurrentStaff',
1873
            'has_many',
1874
            $polymorphic
1875
        );
1876
        $this->assertEquals('CurrentCompanyID', $staffJoinField);
1877
        $this->assertFalse($polymorphic, 'DataObjectTest_Company->CurrentStaff is not polymorphic');
1878
        $previousStaffJoinField = $schema->getRemoteJoinField(
1879
            DataObjectTest\Company::class,
1880
            'PreviousStaff',
1881
            'has_many',
1882
            $polymorphic
1883
        );
1884
        $this->assertEquals('PreviousCompanyID', $previousStaffJoinField);
1885
        $this->assertFalse($polymorphic, 'DataObjectTest_Company->PreviousStaff is not polymorphic');
1886
1887
        // CEO Schema
1888
        $this->assertEquals(
1889
            'CEOID',
1890
            $schema->getRemoteJoinField(
1891
                DataObjectTest\CEO::class,
1892
                'Company',
1893
                'belongs_to',
1894
                $polymorphic
1895
            )
1896
        );
1897
        $this->assertFalse($polymorphic, 'DataObjectTest_CEO->Company is not polymorphic');
1898
        $this->assertEquals(
1899
            'PreviousCEOID',
1900
            $schema->getRemoteJoinField(
1901
                DataObjectTest\CEO::class,
1902
                'PreviousCompany',
1903
                'belongs_to',
1904
                $polymorphic
1905
            )
1906
        );
1907
        $this->assertFalse($polymorphic, 'DataObjectTest_CEO->PreviousCompany is not polymorphic');
1908
1909
        // Team schema
1910
        $this->assertEquals(
1911
            'Favourite',
1912
            $schema->getRemoteJoinField(
1913
                DataObjectTest\Team::class,
1914
                'Fans',
1915
                'has_many',
1916
                $polymorphic
1917
            )
1918
        );
1919
        $this->assertTrue($polymorphic, 'DataObjectTest_Team->Fans is polymorphic');
1920
        $this->assertEquals(
1921
            'TeamID',
1922
            $schema->getRemoteJoinField(
1923
                DataObjectTest\Team::class,
1924
                'Comments',
1925
                'has_many',
1926
                $polymorphic
1927
            )
1928
        );
1929
        $this->assertFalse($polymorphic, 'DataObjectTest_Team->Comments is not polymorphic');
1930
    }
1931
1932
    public function testBelongsTo()
1933
    {
1934
        $company = new DataObjectTest\Company();
1935
        $ceo = new DataObjectTest\CEO();
1936
1937
        $company->Name = 'New Company';
1938
        $company->write();
1939
        $ceo->write();
1940
1941
        // Test belongs_to assignment
1942
        $company->CEOID = $ceo->ID;
1943
        $company->write();
1944
1945
        $this->assertEquals($company->ID, $ceo->Company()->ID, 'belongs_to returns the right results.');
1946
1947
        // Test belongs_to can be infered via getNonReciprocalComponent
1948
        // Note: Will be returned as has_many since the belongs_to is ignored.
1949
        $this->assertListEquals(
1950
            [['Name' => 'New Company']],
1951
            $ceo->inferReciprocalComponent(DataObjectTest\Company::class, 'CEO')
1952
        );
1953
1954
        // Test has_one to a belongs_to can be infered via getNonReciprocalComponent
1955
        $this->assertEquals(
1956
            $ceo->ID,
1957
            $company->inferReciprocalComponent(DataObjectTest\CEO::class, 'Company')->ID
1958
        );
1959
1960
        // Test automatic creation of class where no assigment exists
1961
        $ceo = new DataObjectTest\CEO();
1962
        $ceo->write();
1963
1964
        $this->assertTrue(
1965
            $ceo->Company() instanceof DataObjectTest\Company,
1966
            'DataObjects across belongs_to relations are automatically created.'
1967
        );
1968
        $this->assertEquals($ceo->ID, $ceo->Company()->CEOID, 'Remote IDs are automatically set.');
1969
1970
        // Write object with components
1971
        $ceo->Name = 'Edward Scissorhands';
1972
        $ceo->write(false, false, false, true);
1973
        $this->assertTrue($ceo->Company()->isInDB(), 'write() writes belongs_to components to the database.');
1974
1975
        $newCEO = DataObject::get_by_id(DataObjectTest\CEO::class, $ceo->ID);
1976
        $this->assertEquals(
1977
            $ceo->Company()->ID,
1978
            $newCEO->Company()->ID,
1979
            'belongs_to can be retrieved from the database.'
1980
        );
1981
    }
1982
1983
    public function testBelongsToPolymorphic()
1984
    {
1985
        $company = new DataObjectTest\Company();
1986
        $ceo = new DataObjectTest\CEO();
1987
1988
        $company->write();
1989
        $ceo->write();
1990
1991
        // Test belongs_to assignment
1992
        $company->OwnerID = $ceo->ID;
1993
        $company->OwnerClass = DataObjectTest\CEO::class;
1994
        $company->write();
1995
1996
        $this->assertEquals($company->ID, $ceo->CompanyOwned()->ID, 'belongs_to returns the right results.');
1997
        $this->assertInstanceOf(
1998
            DataObjectTest\Company::class,
1999
            $ceo->CompanyOwned(),
2000
            'belongs_to returns the right results.'
2001
        );
2002
2003
        // Test automatic creation of class where no assigment exists
2004
        $ceo = new DataObjectTest\CEO();
2005
        $ceo->write();
2006
2007
        $this->assertTrue(
2008
            $ceo->CompanyOwned() instanceof DataObjectTest\Company,
2009
            'DataObjects across polymorphic belongs_to relations are automatically created.'
2010
        );
2011
        $this->assertEquals($ceo->ID, $ceo->CompanyOwned()->OwnerID, 'Remote IDs are automatically set.');
2012
        $this->assertInstanceOf($ceo->CompanyOwned()->OwnerClass, $ceo, 'Remote class is automatically  set');
2013
2014
        // Write object with components
2015
        $ceo->write(false, false, false, true);
2016
        $this->assertTrue($ceo->CompanyOwned()->isInDB(), 'write() writes belongs_to components to the database.');
2017
2018
        $newCEO = DataObject::get_by_id(DataObjectTest\CEO::class, $ceo->ID);
2019
        $this->assertEquals(
2020
            $ceo->CompanyOwned()->ID,
2021
            $newCEO->CompanyOwned()->ID,
2022
            'polymorphic belongs_to can be retrieved from the database.'
2023
        );
2024
    }
2025
2026
    /**
2027
     * @expectedException LogicException
2028
     */
2029
    public function testInvalidate()
2030
    {
2031
        $do = new DataObjectTest\Fixture();
2032
        $do->write();
2033
2034
        $do->delete();
2035
2036
        $do->delete(); // Prohibit invalid object manipulation
2037
        $do->write();
2038
        $do->duplicate();
2039
    }
2040
2041
    public function testToMap()
2042
    {
2043
        $obj = $this->objFromFixture(DataObjectTest\SubTeam::class, 'subteam1');
2044
2045
        $map = $obj->toMap();
2046
2047
        $this->assertArrayHasKey('ID', $map, 'Contains base fields');
2048
        $this->assertArrayHasKey('Title', $map, 'Contains fields from parent class');
2049
        $this->assertArrayHasKey('SubclassDatabaseField', $map, 'Contains fields from concrete class');
2050
2051
        $this->assertEquals(
2052
            $obj->ID,
2053
            $map['ID'],
2054
            'Contains values from base fields'
2055
        );
2056
        $this->assertEquals(
2057
            $obj->Title,
2058
            $map['Title'],
2059
            'Contains values from parent class fields'
2060
        );
2061
        $this->assertEquals(
2062
            $obj->SubclassDatabaseField,
2063
            $map['SubclassDatabaseField'],
2064
            'Contains values from concrete class fields'
2065
        );
2066
2067
        $newObj = new DataObjectTest\SubTeam();
2068
        $this->assertArrayHasKey('Title', $map, 'Contains null fields');
2069
    }
2070
2071
    public function testIsEmpty()
2072
    {
2073
        $objEmpty = new DataObjectTest\Team();
2074
        $this->assertTrue($objEmpty->isEmpty(), 'New instance without populated defaults is empty');
2075
2076
        $objEmpty->Title = '0'; //
2077
        $this->assertFalse($objEmpty->isEmpty(), 'Zero value in attribute considered non-empty');
2078
    }
2079
2080
    public function testRelField()
2081
    {
2082
        $captain1 = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
2083
        // Test traversal of a single has_one
2084
        $this->assertEquals("Team 1", $captain1->relField('FavouriteTeam.Title'));
2085
        // Test direct field access
2086
        $this->assertEquals("Captain", $captain1->relField('FirstName'));
2087
2088
        // Test empty link
2089
        $captain2 = $this->objFromFixture(DataObjectTest\Player::class, 'captain2');
2090
        $this->assertEmpty($captain2->relField('FavouriteTeam.Title'));
2091
        $this->assertNull($captain2->relField('FavouriteTeam.ReturnsNull'));
2092
        $this->assertNull($captain2->relField('FavouriteTeam.ReturnsNull.Title'));
2093
2094
        $player = $this->objFromFixture(DataObjectTest\Player::class, 'player2');
2095
        // Test that we can traverse more than once, and that arbitrary methods are okay
2096
        $this->assertEquals("Team 1", $player->relField('Teams.First.Title'));
2097
2098
        $newPlayer = new DataObjectTest\Player();
2099
        $this->assertNull($newPlayer->relField('Teams.First.Title'));
2100
2101
        // Test that relField works on db field manipulations
2102
        $comment = $this->objFromFixture(DataObjectTest\TeamComment::class, 'comment3');
2103
        $this->assertEquals("PHIL IS A UNIQUE GUY, AND COMMENTS ON TEAM2", $comment->relField('Comment.UpperCase'));
2104
2105
        // relField throws exception on invalid properties
2106
        $this->expectException(LogicException::class);
2107
        $this->expectExceptionMessage("Not is not a relation/field on " . DataObjectTest\TeamComment::class);
2108
        $comment->relField('Not.A.Field');
2109
    }
2110
2111
    public function testRelObject()
2112
    {
2113
        $captain1 = $this->objFromFixture(DataObjectTest\Player::class, 'captain1');
2114
2115
        // Test traversal of a single has_one
2116
        $this->assertInstanceOf(DBVarchar::class, $captain1->relObject('FavouriteTeam.Title'));
2117
        $this->assertEquals("Team 1", $captain1->relObject('FavouriteTeam.Title')->getValue());
2118
2119
        // Test empty link
2120
        $captain2 = $this->objFromFixture(DataObjectTest\Player::class, 'captain2');
2121
        $this->assertEmpty($captain2->relObject('FavouriteTeam.Title')->getValue());
2122
        $this->assertNull($captain2->relObject('FavouriteTeam.ReturnsNull.Title'));
2123
2124
        // Test direct field access
2125
        $this->assertInstanceOf(DBBoolean::class, $captain1->relObject('IsRetired'));
2126
        $this->assertEquals(1, $captain1->relObject('IsRetired')->getValue());
2127
2128
        $player = $this->objFromFixture(DataObjectTest\Player::class, 'player2');
2129
        // Test that we can traverse more than once, and that arbitrary methods are okay
2130
        $this->assertInstanceOf(DBVarchar::class, $player->relObject('Teams.First.Title'));
2131
        $this->assertEquals("Team 1", $player->relObject('Teams.First.Title')->getValue());
2132
2133
        // relObject throws exception on invalid properties
2134
        $this->expectException(LogicException::class);
2135
        $this->expectExceptionMessage("Not is not a relation/field on " . DataObjectTest\Player::class);
2136
        $player->relObject('Not.A.Field');
2137
    }
2138
2139
    public function testLateStaticBindingStyle()
2140
    {
2141
        // Confirm that DataObjectTest_Player::get() operates as excepted
2142
        $this->assertEquals(4, DataObjectTest\Player::get()->count());
2143
        $this->assertInstanceOf(DataObjectTest\Player::class, DataObjectTest\Player::get()->first());
2144
2145
        // You can't pass arguments to LSB syntax - use the DataList methods instead.
2146
        $this->expectException(InvalidArgumentException::class);
2147
2148
        DataObjectTest\Player::get(null, "\"ID\" = 1");
2149
    }
2150
2151
    /**
2152
     * @expectedException \InvalidArgumentException
2153
     */
2154
    public function testBrokenLateStaticBindingStyle()
2155
    {
2156
        // If you call DataObject::get() you have to pass a first argument
2157
        DataObject::get();
2158
    }
2159
2160
    public function testBigIntField()
2161
    {
2162
        $staff = new DataObjectTest\Staff();
2163
        $staff->Salary = PHP_INT_MAX;
2164
        $staff->write();
2165
        $this->assertEquals(PHP_INT_MAX, DataObjectTest\Staff::get()->byID($staff->ID)->Salary);
2166
    }
2167
2168
    public function testGetOneMissingValueReturnsNull()
2169
    {
2170
2171
        // Test that missing values return null
2172
        $this->assertEquals(null, DataObject::get_one(
2173
            DataObjectTest\TeamComment::class,
2174
            ['"DataObjectTest_TeamComment"."Name"' => 'does not exists']
2175
        ));
2176
    }
2177
}
2178