Passed
Pull Request — 4 (#10028)
by Steve
09:01
created

DataObjectTest::testMultipleManyManyWithSameClass()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 101
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 64
nc 1
nop 0
dl 0
loc 101
rs 8.7853
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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