diff --git a/core/model/DataObject.php b/core/model/DataObject.php index e518a87f2..5394b2b61 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -392,18 +392,41 @@ class DataObject extends ViewableData implements DataObjectInterface { } /** - * Pass a number of field changes in a map. - * Doesn't write to the database. To write the data, - * use the write() method. + * Update a number of fields on this object, given a map of the desired changes. + * + * The field names can be simple names, or you can use a dot syntax to access relations. + * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim". + * + * update() doesn't write the main object, but if you use the dot syntax, it will write() + * the related objects that it alters. * * @param array $data A map of field name to data values to update. */ public function update($data) { foreach($data as $k => $v) { - $this->$k = $v; + // Implement dot syntax for updates + if(strpos($k,'.') !== false) { + $relations = explode('.', $k); + $fieldName = array_pop($relations); + $relObj = $this; + foreach($relations as $i=>$relation) { + $relObj = $relObj->$relation(); + // If the intermediate relationship objects have been created, then write them + if($iID) $relObj->write(); + } + if($relObj) { + $relObj->$fieldName = $v; + $relObj->write(); + $relObj->flushCache(); + } else { + user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING); + } + } else { + $this->$k = $v; + } } } - + /** * Pass changes as a map, and try to * get automatic casting for these fields. @@ -1644,7 +1667,6 @@ class DataObject extends ViewableData implements DataObjectInterface { if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) { // TODO Add check for php-level defaults which are not set in the db // TODO Add check for hidden input-fields (readonly) which are not set in the db - if( // Main non type-based check (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val) diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php index c4842452c..6f82feae0 100644 --- a/tests/DataObjectTest.php +++ b/tests/DataObjectTest.php @@ -289,6 +289,17 @@ class DataObjectTest extends SapphireTest { $this->assertEquals(0, DB::query("SELECT CaptainID FROM DataObjectTest_Team WHERE ID = $existingTeam->ID")->value()); } + function testCanAccessHasOneObjectsAsMethods() { + /* If you have a has_one relation 'Captain' on $obj, and you set the $obj->CaptainID = (ID), then the object itself should + * be accessible as $obj->Captain() */ + $team = $this->objFromFixture('DataObjectTest_Team', 'team1'); + $captainID = $this->idFromFixture('DataObjectTest_Player', 'captain1'); + + $team->CaptainID = $captainID; + $this->assertNotNull($team->Captain()); + $this->assertEquals($captainID, $team->Captain()->ID); + } + function testFieldNamesThatMatchMethodNamesWork() { /* Check that a field name that corresponds to a method on DataObject will still work */ $obj = new DataObjectTest_FunnyFieldNames(); @@ -428,13 +439,42 @@ class DataObjectTest extends SapphireTest { 'databaseFields() on subclass contains only fields defined on instance' ); } + + function testDataObjectUpdate() { + /* update() calls can use the dot syntax to reference has_one relations and other methods that return objects */ + $team1 = $this->objFromFixture('DataObjectTest_Team', 'team1'); + $team1->CaptainID = $this->idFromFixture('DataObjectTest_Player', 'captain1'); + + $team1->update(array( + 'DatabaseField' => 'Something', + 'Captain.FirstName' => 'Jim', + 'Captain.Email' => 'jim@example.com', + 'Captain.FavouriteTeam.Title' => 'New and improved team 1', + )); + + /* Test the simple case of updating fields on the object itself */ + $this->assertEquals('Something', $team1->DatabaseField); + + /* Setting Captain.Email and Captain.FirstName will have updated DataObjectTest_Captain.captain1 in the database. Although update() + * doesn't usually write, it does write related records automatically. */ + $captain1 = $this->objFromFixture('DataObjectTest_Player', 'captain1'); + $this->assertEquals('Jim', $captain1->FirstName); + $this->assertEquals('jim@example.com', $captain1->Email); + + /* Jim's favourite team is team 1; we need to reload the object to the the change that setting Captain.FavouriteTeam.Title made */ + $reloadedTeam1 = $this->objFromFixture('DataObjectTest_Team', 'team1'); + $this->assertEquals('New and improved team 1', $reloadedTeam1->Title); + } } class DataObjectTest_Player extends Member implements TestOnly { - - static $belongs_many_many = array( - 'Teams' => 'DataObjectTest_Team' - ); + static $has_one = array( + 'FavouriteTeam' => 'DataObjectTest_Team', + ); + + static $belongs_many_many = array( + 'Teams' => 'DataObjectTest_Team' + ); } diff --git a/tests/DataObjectTest.yml b/tests/DataObjectTest.yml index 85d487d8f..c7aa6e058 100644 --- a/tests/DataObjectTest.yml +++ b/tests/DataObjectTest.yml @@ -42,6 +42,13 @@ DataObjectTest_Team: Title: Team 2 DataObjectTest_Player: + captain1: + FirstName: Captain 1 + FavouriteTeam: =>DataObjectTest_Team.team1 + Teams: =>DataObjectTest_Team.team1 + captain2: + FirstName: Captain 2 + Teams: =>DataObjectTest_Team.team2 player1: FirstName: Player 1 player2: