diff --git a/core/model/DataObject.php b/core/model/DataObject.php index 84daaf2c7..a651d71bb 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -701,7 +701,7 @@ class DataObject extends ViewableData implements DataObjectInterface { foreach($ancestry as $idx => $class) { $classSingleton = singleton($class); foreach($this->record as $fieldName => $fieldValue) { - if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->fieldExists($fieldName)) { + if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->hasOwnTableDatabaseField($fieldName)) { $fieldObj = $this->obj($fieldName); if(!isset($manipulation[$class])) $manipulation[$class] = array(); @@ -1699,7 +1699,7 @@ class DataObject extends ViewableData implements DataObjectInterface { * @return boolean True if the given field exists */ public function hasField($field) { - return array_key_exists($field, $this->record) || $this->fieldExists($field); + return array_key_exists($field, $this->record) || $this->hasOwnTableDatabaseField($field); } /** @@ -1712,6 +1712,46 @@ class DataObject extends ViewableData implements DataObjectInterface { public function hasDatabaseField($field) { return array_key_exists($field, $this->databaseFields()); } + + /** + * Returns the field type of the given field, if it belongs to this class, and not a parent. + * Note that the field type will not include constructor arguments in round brackets, only the classname. + * + * @param string $field Name of the field + * @return string The field type of the given field + */ + public function hasOwnTableDatabaseField($field) { + // Add base fields which are not defined in static $db + if($field == "ID") return "Int"; + if($field == "ClassName" && get_parent_class($this) == "DataObject") return "Enum"; + if($field == "LastEdited" && get_parent_class($this) == "DataObject") return "Datetime"; + if($field == "Created" && get_parent_class($this) == "DataObject") return "Datetime"; + + // Add fields from Versioned decorator + if($field == "Version") return $this->hasExtension('Versioned') ? "Int" : false; + + // get cached fieldmap + $fieldMap = $this->uninherited('_cache_hasOwnTableDatabaseField'); + + // if no fieldmap is cached, get all fields + if(!$fieldMap) { + // all $db fields on this specific class (no parents) + $fieldMap = $this->uninherited('db', true); + + // all has_one relations on this specific class, + // add foreign key + $hasOne = $this->uninherited('has_one', true); + if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) { + $fieldMap[$fieldName . 'ID'] = "Int"; + } + + // set cached fieldmap + $this->set_uninherited('_cache_hasOwnTableDatabaseField', $fieldMap); + } + + // Remove string-based "constructor-arguments" from the DBField definition + return isset($fieldMap[$field]) ? strtok($fieldMap[$field],'(') : null; + } /** * Returns true if the member is allowed to do the given action. @@ -1833,32 +1873,13 @@ class DataObject extends ViewableData implements DataObjectInterface { } /** - * Returns the field type of the given field, if it belongs to this class, and not a parent. - * Can be used to detect whether the given field exists. - * Note that the field type will not include constructor arguments; only the classname. + * @deprecated 2.3 (For external use) Please use hasField(), hasDatabaseField(), hasOwnTableDatabaseField() instead * * @param string $field Name of the field - * * @return string The field type of the given field */ public function fieldExists($field) { - if($field == "ID") return "Int"; - if($field == "ClassName" && get_parent_class($this) == "DataObject") return "Enum"; - if($field == "LastEdited" && get_parent_class($this) == "DataObject") return "Datetime"; - if($field == "Created" && get_parent_class($this) == "DataObject") return "Datetime"; - - if($field == "Version") return $this->hasExtension('Versioned') ? "Int" : false; - $fieldMap = $this->uninherited('fieldExists'); - if(!$fieldMap) { - $fieldMap = $this->uninherited('db', true); - $has = $this->uninherited('has_one', true); - if($has) foreach($has as $fieldName => $fieldSchema) { - $fieldMap[$fieldName . 'ID'] = "Int"; - } - $this->set_uninherited('fieldExists', $fieldMap); - } - - return isset($fieldMap[$field]) ? strtok($fieldMap[$field],'(') : null; + return $this->hasOwnTableDatabaseField($field); } /** @@ -2057,6 +2078,7 @@ class DataObject extends ViewableData implements DataObjectInterface { * - name: value * - name: value * + * @deprecated 2.3 Use custom code * @return string The fields as an HTML unordered list */ function listOfFields() { @@ -2390,7 +2412,10 @@ class DataObject extends ViewableData implements DataObjectInterface { } /** - * Get the custom database fields for this object, from self::$db and self::$has_one + * Get the custom database fields for this object, from self::$db and self::$has_one, + * but not built-in fields like ID, ClassName, Created, LastEdited. + * + * @return array */ public function customDatabaseFields() { $db = $this->uninherited('db',true); @@ -2437,15 +2462,22 @@ class DataObject extends ViewableData implements DataObjectInterface { public function searchableFields() { // can have mixed format, need to make consistent in most verbose form $fields = $this->stat('searchable_fields'); + $labels = $this->fieldLabels(); // fallback to summary fields if(!$fields) $fields = array_keys($this->summaryFields()); + // we need to make sure the format is unified before + // augumenting fields, so decorators can apply consistent checks + // but also after augumenting fields, because the decorator + // might use the shorthand notation as well + // rewrite array, if it is using shorthand syntax $rewrite = array(); foreach($fields as $name => $specOrName) { $identifer = (is_int($name)) ? $specOrName : $name; + if(is_int($name)) { // Format: array('MyFieldName') $rewrite[$identifer] = array(); @@ -2454,7 +2486,7 @@ class DataObject extends ViewableData implements DataObjectInterface { // 'filter => 'ExactMatchFilter', // 'field' => 'NumericField', // optional // 'title' => 'My Title', // optiona. - // ) + // )) $rewrite[$identifer] = array_merge( array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')), (array)$specOrName @@ -2472,8 +2504,10 @@ class DataObject extends ViewableData implements DataObjectInterface { $rewrite[$identifer]['filter'] = 'PartialMatchFilter'; } } + $fields = $rewrite; + // apply DataObjectDecorators if present $this->extend('updateSearchableFields', $fields); return $fields; diff --git a/core/model/DataObjectDecorator.php b/core/model/DataObjectDecorator.php index 19e8a3013..a9e712890 100755 --- a/core/model/DataObjectDecorator.php +++ b/core/model/DataObjectDecorator.php @@ -45,7 +45,9 @@ abstract class DataObjectDecorator extends Extension { eval("$className::\$$relationType = array_merge((array){$className}::\$$relationType, (array)\$fields);"); $this->owner->set_stat($relationType, eval("return $className::\$$relationType;")); } - $this->owner->set_uninherited('fieldExists', null); + + // clear previously set caches from DataObject->hasOwnTableDatabaseField() + $this->owner->set_uninherited('_cache_hasOwnTableDatabaseField', null); } } } diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php index 9e2e5c97e..807ca35c3 100644 --- a/tests/DataObjectTest.php +++ b/tests/DataObjectTest.php @@ -288,6 +288,126 @@ class DataObjectTest extends SapphireTest { $existingTeam->write(); $this->assertEquals(0, DB::query("SELECT CaptainID FROM DataObjectTest_Team WHERE ID = $existingTeam->ID")->value()); } + + function testFieldExistence() { + $teamInstance = $this->objFromFixture('DataObjectTest_Team', 'team1'); + $teamSingleton = singleton('DataObjectTest_Team'); + + $subteamInstance = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1'); + $subteamSingleton = singleton('DataObjectTest_SubTeam'); + + /* hasField() singleton checks */ + $this->assertTrue($teamSingleton->hasField('ID'), 'hasField() finds built-in fields in singletons'); + $this->assertTrue($teamSingleton->hasField('Title'), 'hasField() finds custom fields in singletons'); + + /* hasField() instance checks */ + $this->assertFalse($teamInstance->hasField('NonExistingField'), 'hasField() doesnt find non-existing fields in instances'); + $this->assertTrue($teamInstance->hasField('ID'), 'hasField() finds built-in fields in instances'); + $this->assertTrue($teamInstance->hasField('Created'), 'hasField() finds built-in fields in instances'); + $this->assertTrue($teamInstance->hasField('DatabaseField'), 'hasField() finds custom fields in instances'); + //$this->assertFalse($teamInstance->hasField('SubclassDatabaseField'), 'hasField() doesnt find subclass fields in parentclass instances'); + //$this->assertTrue($teamInstance->hasField('DynamicField'), 'hasField() finds dynamic getters in instances'); + $this->assertTrue($teamInstance->hasField('HasOneRelationshipID'), 'hasField() finds foreign keys in instances'); + $this->assertTrue($teamInstance->hasField('DecoratedDatabaseField'), 'hasField() finds decorated fields in instances'); + $this->assertTrue($teamInstance->hasField('DecoratedHasOneRelationshipID'), 'hasField() finds decorated foreign keys in instances'); + //$this->assertTrue($teamInstance->hasField('DecoratedDynamicField'), 'hasField() includes decorated dynamic getters in instances'); + + /* hasField() subclass checks */ + $this->assertTrue($subteamInstance->hasField('ID'), 'hasField() finds built-in fields in subclass instances'); + $this->assertTrue($subteamInstance->hasField('Created'), 'hasField() finds built-in fields in subclass instances'); + $this->assertTrue($subteamInstance->hasField('DatabaseField'), 'hasField() finds custom fields in subclass instances'); + $this->assertTrue($subteamInstance->hasField('SubclassDatabaseField'), 'hasField() finds custom fields in subclass instances'); + //$this->assertTrue($subteamInstance->hasField('DynamicField'), 'hasField() finds dynamic getters in subclass instances'); + $this->assertTrue($subteamInstance->hasField('HasOneRelationshipID'), 'hasField() finds foreign keys in subclass instances'); + $this->assertTrue($subteamInstance->hasField('DecoratedDatabaseField'), 'hasField() finds decorated fields in subclass instances'); + $this->assertTrue($subteamInstance->hasField('DecoratedHasOneRelationshipID'), 'hasField() finds decorated foreign keys in subclass instances'); + + /* hasDatabaseField() singleton checks */ + //$this->assertTrue($teamSingleton->hasDatabaseField('ID'), 'hasDatabaseField() finds built-in fields in singletons'); + $this->assertTrue($teamSingleton->hasDatabaseField('Title'), 'hasDatabaseField() finds custom fields in singletons'); + + /* hasDatabaseField() instance checks */ + $this->assertFalse($teamInstance->hasDatabaseField('NonExistingField'), 'hasDatabaseField() doesnt find non-existing fields in instances'); + //$this->assertTrue($teamInstance->hasDatabaseField('ID'), 'hasDatabaseField() finds built-in fields in instances'); + $this->assertTrue($teamInstance->hasDatabaseField('Created'), 'hasDatabaseField() finds built-in fields in instances'); + $this->assertTrue($teamInstance->hasDatabaseField('DatabaseField'), 'hasDatabaseField() finds custom fields in instances'); + $this->assertFalse($teamInstance->hasDatabaseField('SubclassDatabaseField'), 'hasDatabaseField() doesnt find subclass fields in parentclass instances'); + //$this->assertFalse($teamInstance->hasDatabaseField('DynamicField'), 'hasDatabaseField() doesnt dynamic getters in instances'); + $this->assertTrue($teamInstance->hasDatabaseField('HasOneRelationshipID'), 'hasDatabaseField() finds foreign keys in instances'); + $this->assertTrue($teamInstance->hasDatabaseField('DecoratedDatabaseField'), 'hasDatabaseField() finds decorated fields in instances'); + $this->assertTrue($teamInstance->hasDatabaseField('DecoratedHasOneRelationshipID'), 'hasDatabaseField() finds decorated foreign keys in instances'); + $this->assertFalse($teamInstance->hasDatabaseField('DecoratedDynamicField'), 'hasDatabaseField() doesnt include decorated dynamic getters in instances'); + + /* hasDatabaseField() subclass checks */ + $this->assertTrue($subteamInstance->hasField('DatabaseField'), 'hasField() finds custom fields in subclass instances'); + $this->assertTrue($subteamInstance->hasField('SubclassDatabaseField'), 'hasField() finds custom fields in subclass instances'); + + } + + function testFieldInheritance() { + $teamInstance = $this->objFromFixture('DataObjectTest_Team', 'team1'); + $subteamInstance = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1'); + + $this->assertEquals( + array_keys($teamInstance->inheritedDatabaseFields()), + array( + //'ID', + //'ClassName', + //'Created', + //'LastEdited', + 'Title', + 'DatabaseField', + 'DecoratedDatabaseField', + 'CaptainID', + 'HasOneRelationshipID', + 'DecoratedHasOneRelationshipID' + ), + 'inheritedDatabaseFields() contains all fields defined on instance, including base fields, decorated fields and foreign keys' + ); + + $this->assertEquals( + array_keys($teamInstance->databaseFields()), + array( + //'ID', + 'ClassName', + 'Created', + 'LastEdited', + 'Title', + 'DatabaseField', + 'DecoratedDatabaseField', + 'CaptainID', + 'HasOneRelationshipID', + 'DecoratedHasOneRelationshipID' + ), + 'databaseFields() contains only fields defined on instance, including base fields, decorated fields and foreign keys' + ); + + $this->assertEquals( + array_keys($subteamInstance->inheritedDatabaseFields()), + array( + //'ID', + //'ClassName', + //'Created', + //'LastEdited', + 'SubclassDatabaseField', + 'Title', + 'DatabaseField', + 'DecoratedDatabaseField', + 'CaptainID', + 'HasOneRelationshipID', + 'DecoratedHasOneRelationshipID', + ), + 'inheritedDatabaseFields() on subclass contains all fields defined on instance, including base fields, decorated fields and foreign keys' + ); + + $this->assertEquals( + array_keys($subteamInstance->databaseFields()), + array( + 'SubclassDatabaseField', + ), + 'databaseFields() on subclass contains only fields defined on instance' + ); + } } class DataObjectTest_Player extends Member implements TestOnly { @@ -302,16 +422,49 @@ class DataObjectTest_Team extends DataObject implements TestOnly { static $db = array( 'Title' => 'Text', + 'DatabaseField' => 'Text' ); static $has_one = array( "Captain" => 'DataObjectTest_Player', + 'HasOneRelationship' => 'DataObjectTest_Player', ); static $many_many = array( 'Players' => 'DataObjectTest_Player' ); + + function getDynamicField() { + return 'dynamicfield'; + } } +class DataObjectTest_SubTeam extends DataObjectTest_Team implements TestOnly { + static $db = array( + 'SubclassDatabaseField' => 'Text' + ); +} + +class DataObjectTest_Team_Decorator extends DataObjectDecorator implements TestOnly { + + function extraDBFields() { + return array( + 'db' => array( + 'DecoratedDatabaseField' => 'Text' + ), + 'has_one' => array( + 'DecoratedHasOneRelationship' => 'DataObjectTest_Player' + ) + ); + } + + function getDecoratedDynamicField() { + return "decorated dynamic field"; + } + +} + +DataObject::add_extension('DataObjectTest_Team', 'DataObjectTest_Team_Decorator'); + ?> diff --git a/tests/DataObjectTest.yml b/tests/DataObjectTest.yml index 0d686aa85..85d487d8f 100644 --- a/tests/DataObjectTest.yml +++ b/tests/DataObjectTest.yml @@ -46,4 +46,7 @@ DataObjectTest_Player: FirstName: Player 1 player2: FirstName: Player 2 - Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2 \ No newline at end of file + Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2 +DataObjectTest_SubTeam: + subteam1: + Title: Subteam 1 \ No newline at end of file