API CHANGE: Added DataObject::is_composite_field() and DataObject::composite_fields() to efficiently keep track of which fields are composite.

API CHANGE: Deprecated DataObject::databaseFields() in favour of the static DataObject::database_fields()

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@84161 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sam Minnee 2009-08-11 08:49:52 +00:00
parent d8113a667e
commit 5790ddf708
5 changed files with 142 additions and 53 deletions

View File

@ -195,20 +195,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public static function custom_database_fields($class) { public static function custom_database_fields($class) {
$fields = Object::uninherited_static($class, 'db'); $fields = Object::uninherited_static($class, 'db');
// Remove CompositeDBField instances, and replace with the actually used fields foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
if($fields) foreach($fields as $fieldName => $fieldClass) {
// Strip off any parameters
if(strpos('(', $fieldClass) !== FALSE) $fieldClass = substr(0,strpos('(', $fieldClass), $fieldClass);
if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
// Remove the original fieldname, its not an actual database column // Remove the original fieldname, its not an actual database column
unset($fields[$fieldName]); unset($fields[$fieldName]);
// Add all composite columns // Add all composite columns
$compositeFields = singleton($fieldClass)->compositeDatabaseFields(); $compositeFields = singleton($fieldClass)->compositeDatabaseFields();
if($compositeFields) foreach($compositeFields as $compositeName => $spec) { if($compositeFields) foreach($compositeFields as $compositeName => $spec) {
$fields["{$fieldName}{$compositeName}"] = $spec; $fields["{$fieldName}{$compositeName}"] = $spec;
} }
} }
}
// Add has_one relationships // Add has_one relationships
$hasOne = Object::uninherited_static($class, 'has_one'); $hasOne = Object::uninherited_static($class, 'has_one');
@ -219,6 +215,64 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return (array)$fields; return (array)$fields;
} }
private static $_cache_custom_database_fields = array();
/**
* Returns the field class if the given db field on the class is a composite field.
* Will check all applicable ancestor classes and aggregate results.
*/
static function is_composite_field($class, $name, $aggregated = true) {
if(!isset(self::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
if(isset(self::$_cache_composite_fields[$class][$name])) {
return self::$_cache_composite_fields[$class][$name];
} else if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
return self::is_composite_field($parentClass, $name);
}
}
/**
* Returns a list of all the composite if the given db field on the class is a composite field.
* Will check all applicable ancestor classes and aggregate results.
*/
static function composite_fields($class, $aggregated = true) {
if(!isset(self::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
$compositeFields = self::$_cache_composite_fields[$class];
if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
$compositeFields = array_merge($compositeFields,
self::composite_fields($parentClass));
}
return $compositeFields;
}
/**
* Internal cacher for the composite field information
*/
private static function cache_composite_fields($class) {
$compositeFields = array();
$fields = Object::uninherited_static($class, 'db');
if($fields) foreach($fields as $fieldName => $fieldClass) {
// Strip off any parameters
$bPos = strpos('(', $fieldClass);
if($bPos !== FALSE) $fieldClass = substr(0,$bPos, $fieldClass);
// Test to see if it implements CompositeDBField
if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
$compositeFields[$fieldName] = $fieldClass;
}
}
self::$_cache_composite_fields[$class] = $compositeFields;
}
private static $_cache_composite_fields = array();
/** /**
* Construct a new DataObject. * Construct a new DataObject.
@ -1494,29 +1548,34 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return string|array The class of the one-to-many component, or an array of all one-to-many components and their classes. * @return string|array The class of the one-to-many component, or an array of all one-to-many components and their classes.
*/ */
public function has_many($component = null) { public function has_many($component = null) {
$classes = ClassInfo::ancestry($this); static $ignoreClasses = array('ViewableData', 'Object', 'DataObject');
$classes = ClassInfo::ancestry($this->class);
foreach($classes as $class) {
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
if($component) { if($component) {
$hasMany = Object::uninherited_static($class, 'has_many'); foreach($classes as $class) {
if(in_array($class, $ignoreClasses)) continue;
if(isset($hasMany[$component])) { $hasMany = (array)Object::uninherited_static($class, 'has_many');
return $hasMany[$component]; if(isset($hasMany[$component])) return $hasMany[$component];
} }
} else { } else {
$items = array();
foreach($classes as $class) {
if(in_array($class, $ignoreClasses)) continue;
$newItems = (array)Object::uninherited_static($class, 'has_many'); $newItems = (array)Object::uninherited_static($class, 'has_many');
// Validate the data // Validate the data
foreach($newItems as $k => $v) { foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$has_many has a bad entry: " if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$has_many has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a relationship name, and the map value should be the data class to join to.", E_USER_ERROR); . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
} }
$items = isset($items) ? array_merge($newItems, (array)$items) : $newItems; $items = array_merge($newItems, (array)$items);
}
} }
return isset($items) ? $items : null;
return $items;
}
} }
/** /**
@ -1868,8 +1927,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(isset($this->record[$field]) && is_object($this->record[$field])) return $this->record[$field]; if(isset($this->record[$field]) && is_object($this->record[$field])) return $this->record[$field];
// Otherwise, we need to determine if this is a complex field // Otherwise, we need to determine if this is a complex field
$fieldClass = $this->db($field); if(self::is_composite_field($this->class, $field)) {
if($fieldClass && ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
$helperPair = $this->castingHelperPair($field); $helperPair = $this->castingHelperPair($field);
$constructor = $helperPair['castingHelper']; $constructor = $helperPair['castingHelper'];
$fieldName = $field; $fieldName = $field;
@ -1927,7 +1985,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
if($databaseFieldsOnly) { if($databaseFieldsOnly) {
$customDatabaseFields = $this->customDatabaseFields(); $customDatabaseFields = self::custom_database_fields($this->class);
$fields = array_intersect_key($this->changed, $customDatabaseFields); $fields = array_intersect_key($this->changed, $customDatabaseFields);
} else { } else {
$fields = $this->changed; $fields = $this->changed;
@ -2088,20 +2146,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// if no fieldmap is cached, get all fields // if no fieldmap is cached, get all fields
if(!$fieldMap) { if(!$fieldMap) {
$fieldMap = Object::uninherited_static($this->class, 'db');
// all $db fields on this specific class (no parents) // all $db fields on this specific class (no parents)
$fieldMap = $this->uninherited('db', true); foreach(self::composite_fields($this->class, false) as $fieldname => $fieldtype) {
if($fieldMap) foreach($fieldMap as $fieldname => $fieldtype){
if(ClassInfo::classImplements($fieldtype, 'CompositeDBField')){
$combined_db = singleton($fieldtype)->compositeDatabaseFields(); $combined_db = singleton($fieldtype)->compositeDatabaseFields();
foreach($combined_db as $name => $type){ foreach($combined_db as $name => $type){
$fieldMap[$fieldname.$name] = $type; $fieldMap[$fieldname.$name] = $type;
} }
} }
}
// all has_one relations on this specific class, // all has_one relations on this specific class,
// add foreign key // add foreign key
$hasOne = $this->uninherited('has_one', true); $hasOne = Object::uninherited_static($this->class, 'has_one');
if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) { if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) {
$fieldMap[$fieldName . 'ID'] = "ForeignKey"; $fieldMap[$fieldName . 'ID'] = "ForeignKey";
} }
@ -2400,13 +2457,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Add SQL for multi-value fields // Add SQL for multi-value fields
$databaseFields = self::database_fields($tableClass); $databaseFields = self::database_fields($tableClass);
$compositeFields = self::composite_fields($tableClass, false);
if($databaseFields) foreach($databaseFields as $k => $v) { if($databaseFields) foreach($databaseFields as $k => $v) {
if(ClassInfo::classImplements($v, 'CompositeDBField')) { if(!isset($compositeFields[$k])) {
singleton($tableClass)->dbObject($k)->addToQuery($query);
} else {
$query->select[] = "\"$tableClass\".\"$k\""; $query->select[] = "\"$tableClass\".\"$k\"";
} }
} }
if($compositeFields) foreach($compositeFields as $k => $v) {
$this->dbObject($k)->addToQuery($query);
}
} }
} }
@ -2771,7 +2830,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
public function requireTable() { public function requireTable() {
// Only build the table if we've actually got fields // Only build the table if we've actually got fields
$fields = $this->databaseFields(); $fields = self::database_fields($this->class);
$indexes = $this->databaseIndexes(); $indexes = $this->databaseIndexes();
if($fields) { if($fields) {
@ -2842,6 +2901,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @see DataObject::database_fields() * @see DataObject::database_fields()
*/ */
public function databaseFields() { public function databaseFields() {
user_error("databaseFields() is deprecated; use self::database_fields() "
. "instead", E_USER_NOTICE);
return self::database_fields($this->class); return self::database_fields($this->class);
} }
@ -2849,6 +2910,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @see DataObject::custom_database_fields() * @see DataObject::custom_database_fields()
*/ */
public function customDatabaseFields() { public function customDatabaseFields() {
user_error("customDatabaseFields() is deprecated; use self::custom_database_fields() "
. "instead", E_USER_NOTICE);
return self::custom_database_fields($this->class); return self::custom_database_fields($this->class);
} }

View File

@ -264,7 +264,7 @@ class DatabaseAdmin extends Controller {
$subclasses = ClassInfo::subclassesFor($baseClass); $subclasses = ClassInfo::subclassesFor($baseClass);
unset($subclasses[0]); unset($subclasses[0]);
foreach($subclasses as $k => $subclass) { foreach($subclasses as $k => $subclass) {
if(!singleton($subclass)->databaseFields()) { if(DataObject::database_fields($subclass)) {
unset($subclasses[$k]); unset($subclasses[$k]);
} }
} }

View File

@ -211,7 +211,7 @@ class Versioned extends DataObjectDecorator {
// Build a list of suffixes whose tables need versioning // Build a list of suffixes whose tables need versioning
$allSuffixes = array(); $allSuffixes = array();
foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) { foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) {
if ($this->owner->hasExtension($versionableExtension) && singleton($versionableExtension)->stat('enabled')) { if ($this->owner->hasExtension($versionableExtension)) {
$allSuffixes = array_merge($allSuffixes, (array)$suffixes); $allSuffixes = array_merge($allSuffixes, (array)$suffixes);
foreach ((array)$suffixes as $suffix) { foreach ((array)$suffixes as $suffix) {
$allSuffixes[$suffix] = $versionableExtension; $allSuffixes[$suffix] = $versionableExtension;
@ -229,7 +229,7 @@ class Versioned extends DataObjectDecorator {
if ($suffix) $table = "{$classTable}_$suffix"; if ($suffix) $table = "{$classTable}_$suffix";
else $table = $classTable; else $table = $classTable;
if(($fields = $this->owner->databaseFields())) { if($fields = DataObject::database_fields($this->owner->class)) {
$indexes = $this->owner->databaseIndexes(); $indexes = $this->owner->databaseIndexes();
if ($suffix && ($ext = $this->owner->extInstance($allSuffixes[$suffix]))) { if ($suffix && ($ext = $this->owner->extInstance($allSuffixes[$suffix]))) {
if (!$ext->isVersionedTable($table)) continue; if (!$ext->isVersionedTable($table)) continue;
@ -392,8 +392,9 @@ class Versioned extends DataObjectDecorator {
* @return boolean * @return boolean
*/ */
function canBeVersioned($table) { function canBeVersioned($table) {
$dbFields = singleton($table)->databaseFields(); return ClassInfo::exists($table)
return !(!ClassInfo::exists($table) || !is_subclass_of($table, 'DataObject' ) || empty( $dbFields )); && ClassInfo::is_subclass_of($table, 'DataObject')
&& DataObject::has_own_table($table);
} }
/** /**

View File

@ -460,7 +460,7 @@ class DataObjectTest extends SapphireTest {
); );
$this->assertEquals( $this->assertEquals(
array_keys($teamInstance->databaseFields()), array_keys(DataObject::database_fields('DataObjectTest_Team')),
array( array(
//'ID', //'ID',
'ClassName', 'ClassName',
@ -495,7 +495,7 @@ class DataObjectTest extends SapphireTest {
); );
$this->assertEquals( $this->assertEquals(
array_keys($subteamInstance->databaseFields()), array_keys(DataObject::database_fields('DataObjectTest_SubTeam')),
array( array(
'SubclassDatabaseField', 'SubclassDatabaseField',
), ),

View File

@ -11,12 +11,37 @@ class CompositeDBFieldTest extends SapphireTest {
$this->assertTrue($obj->hasDatabaseField('MyMoneyCurrency')); $this->assertTrue($obj->hasDatabaseField('MyMoneyCurrency'));
$this->assertFalse($obj->hasDatabaseField('MyMoney')); $this->assertFalse($obj->hasDatabaseField('MyMoney'));
} }
/**
* Test DataObject::composite_fields() and DataObject::is_composite_field()
*/
function testCompositeFieldMetaDataFunctions() {
$this->assertEquals('Money', DataObject::is_composite_field('CompositeDBFieldTest_DataObject', 'MyMoney'));
$this->assertNull(DataObject::is_composite_field('CompositeDBFieldTest_DataObject', 'Title'));
$this->assertEquals(array('MyMoney' => 'Money'),
DataObject::composite_fields('CompositeDBFieldTest_DataObject'));
$this->assertEquals('Money', DataObject::is_composite_field('SubclassedDBFieldObject', 'MyMoney'));
$this->assertEquals('Money', DataObject::is_composite_field('SubclassedDBFieldObject', 'OtherMoney'));
$this->assertNull(DataObject::is_composite_field('SubclassedDBFieldObject', 'Title'));
$this->assertNull(DataObject::is_composite_field('SubclassedDBFieldObject', 'OtherField'));
$this->assertEquals(array('MyMoney' => 'Money', 'OtherMoney' => 'Money'),
DataObject::composite_fields('SubclassedDBFieldObject'));
}
} }
class CompositeDBFieldTest_DataObject extends DataObject implements TestOnly { class CompositeDBFieldTest_DataObject extends DataObject {
static $db = array( static $db = array(
'Title' => 'Text', 'Title' => 'Text',
'MyMoney' => 'Money', 'MyMoney' => 'Money',
); );
} }
class SubclassedDBFieldObject extends CompositeDBFieldTest_DataObject {
static $db = array(
'OtherField' => 'Text',
'OtherMoney' => 'Money',
);
}
?> ?>