API Refactor CompositeDBField into an abstract class

API Refactor ClassName into DBClassName
API Update PolymorphicForeignKey to use new CompositeDBField and DBClassName

CompositeDBField is now an interface to nested fields on an underlying dataobject, allowing field manipulation to be performed at the field and dataobject level without having to synchronise them manually.
This commit is contained in:
Damian Mooyman 2015-09-04 15:49:22 +12:00
parent 43430e27cf
commit 9872fbef4d
23 changed files with 936 additions and 554 deletions

View File

@ -106,15 +106,18 @@ class MoneyField extends FormField {
* (see @link MoneyFieldTest_CustomSetter_Object for more information)
*/
public function saveInto(DataObjectInterface $dataObject) {
$fieldName = $this->name;
$fieldName = $this->getName();
if($dataObject->hasMethod("set$fieldName")) {
$dataObject->$fieldName = DBField::create_field('Money', array(
"Currency" => $this->fieldCurrency->dataValue(),
"Amount" => $this->fieldAmount->dataValue()
));
} else {
$dataObject->$fieldName->setCurrency($this->fieldCurrency->dataValue());
$dataObject->$fieldName->setAmount($this->fieldAmount->dataValue());
$currencyField = "{$fieldName}Currency";
$amountField = "{$fieldName}Amount";
$dataObject->$currencyField = $this->fieldCurrency->dataValue();
$dataObject->$amountField = $this->fieldAmount->dataValue();
}
}

View File

@ -186,8 +186,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// base fields which are not defined in static $db
private static $fixed_fields = array(
'ID' => 'Int',
'ClassName' => 'Enum',
'ID' => 'PrimaryKey',
'ClassName' => 'DBClassName',
'LastEdited' => 'SS_Datetime',
'Created' => 'SS_Datetime',
);
@ -229,45 +229,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
Config::inst()->update('DataObject', 'validation_enabled', (bool)$enable);
}
/**
* @var [string] - class => ClassName field definition cache for self::database_fields
*/
private static $classname_spec_cache = array();
/**
* Clear all cached classname specs. It's necessary to clear all cached subclassed names
* for any classes if a new class manifest is generated.
*/
public static function clear_classname_spec_cache() {
self::$classname_spec_cache = array();
PolymorphicForeignKey::clear_classname_spec_cache();
}
/**
* Determines the specification for the ClassName field for the given class
*
* @param string $class
* @param boolean $queryDB Determine if the DB may be queried for additional information
* @return string Resulting ClassName spec. If $queryDB is true this will include all
* legacy types that no longer have concrete classes in PHP
*/
public static function get_classname_spec($class, $queryDB = true) {
// Check cache
if(!empty(self::$classname_spec_cache[$class])) return self::$classname_spec_cache[$class];
// Build known class names
$classNames = ClassInfo::subclassesFor($class);
// Enhance with existing classes in order to prevent legacy details being lost
if($queryDB && DB::get_schema()->hasField($class, 'ClassName')) {
$existing = DB::query("SELECT DISTINCT \"ClassName\" FROM \"{$class}\"")->column();
$classNames = array_unique(array_merge($classNames, $existing));
}
$spec = "Enum('" . implode(', ', $classNames) . "')";
// Only cache full information if queried
if($queryDB) self::$classname_spec_cache[$class] = $spec;
return $spec;
DBClassName::clear_classname_cache();
}
/**
@ -285,7 +252,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
unset($fixed['ID']);
return array_merge(
$fixed,
array('ClassName' => self::get_classname_spec($class, $queryDB)),
self::custom_database_fields($class)
);
}
@ -318,28 +284,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Remove the original fieldname, it's not an actual database column
unset($fields[$fieldName]);
// Add all composite columns
$compositeFields = singleton($fieldClass)->compositeDatabaseFields();
if($compositeFields) foreach($compositeFields as $compositeName => $spec) {
// Add all composite columns, including polymorphic relationships
$fieldObj = Object::create_from_string($fieldClass, $fieldName);
$fieldObj->setTable($class);
$compositeFields = $fieldObj->compositeDatabaseFields();
foreach($compositeFields as $compositeName => $spec) {
$fields["{$fieldName}{$compositeName}"] = $spec;
}
}
// Add has_one relationships
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
if($hasOne) foreach(array_keys($hasOne) as $field) {
// Check if this is a polymorphic relation, in which case the relation
// is a composite field
if($hasOne[$field] === 'DataObject') {
$relationField = DBField::create_field('PolymorphicForeignKey', null, $field);
$relationField->setTable($class);
if($compositeFields = $relationField->compositeDatabaseFields()) {
foreach($compositeFields as $compositeName => $spec) {
$fields["{$field}{$compositeName}"] = $spec;
}
}
} else {
if($hasOne) foreach($hasOne as $field => $hasOneClass) {
// exclude polymorphic relationships
if($hasOneClass !== 'DataObject') {
$fields[$field . 'ID'] = 'ForeignKey';
}
}
@ -385,6 +343,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* 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.
*
* Includes composite has_one (Polymorphic) fields
*
* @param string $class Name of class to check
* @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
* @return array
*/
public static function composite_fields($class, $aggregated = true) {
if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
@ -405,6 +369,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
private static function cache_composite_fields($class) {
$compositeFields = array();
// Check db
$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
if($fields) foreach($fields as $fieldName => $fieldClass) {
if(!is_string($fieldClass)) continue;
@ -413,12 +378,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$bPos = strpos($fieldClass, '(');
if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos);
// Test to see if it implements CompositeDBField
if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
// Test to see if it extends CompositeDBField
if(is_subclass_of($fieldClass, 'CompositeDBField')) {
$compositeFields[$fieldName] = $fieldClass;
}
}
// check has_one PolymorphicForeignKey
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
if($hasOne) foreach($hasOne as $fieldName => $hasOneClass) {
if($hasOneClass === 'DataObject') {
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
}
}
DataObject::$_cache_composite_fields[$class] = $compositeFields;
}
@ -1274,8 +1247,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
}
// Ensure DBField is repopulated and written to the manipulation
$fieldObj->setValue($fieldValue, $this->record);
// Write to manipulation
$fieldObj->writeToManipulation($manipulation[$class]);
}
@ -1829,13 +1801,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Return data for a specific has_one component.
* @param string $component
* @param string $table Out parameter of the table this has_one field belongs to
* @return string|null
*/
public function hasOneComponent($component) {
$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
public function hasOneComponent($component, &$table = null) {
$classes = ClassInfo::ancestry($this, true);
if(isset($hasOnes[$component])) {
return $hasOnes[$component];
foreach(array_reverse($classes) as $class) {
$hasOnes = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
if(isset($hasOnes[$component])) {
$table = $class;
return $hasOnes[$component];
}
}
}
@ -1906,9 +1883,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Doesn't include any fields specified by self::$has_one. Use $this->hasOne() to get these fields
*
* @param string $fieldName Limit the output to a specific field name
* @param string $table Out parameter of the table this db field is set to
* @return array The database fields
*/
public function db($fieldName = null) {
public function db($fieldName = null, &$table = null) {
$classes = ClassInfo::ancestry($this, true);
// If we're looking for a specific field, we want to hit subclasses first as they may override field types
@ -1927,6 +1905,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($fieldName) {
if(isset($dbItems[$fieldName])) {
$table = $class;
return $dbItems[$fieldName];
}
} else {
@ -2405,7 +2384,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function getField($field) {
// If we already have an object in $this->record, then we should just return that
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];
}
// Do we have a field that needs to be lazy loaded?
if(isset($this->record[$field.'_Lazy'])) {
@ -2413,27 +2394,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->loadLazyFields($tableClass);
}
// Otherwise, we need to determine if this is a complex field
// In case of complex fields, return the DBField object
if(self::is_composite_field($this->class, $field)) {
$helper = $this->castingHelper($field);
$fieldObj = Object::create_from_string($helper, $field);
$compositeFields = $fieldObj->compositeDatabaseFields();
foreach ($compositeFields as $compositeName => $compositeType) {
if(isset($this->record[$field.$compositeName.'_Lazy'])) {
$tableClass = $this->record[$field.$compositeName.'_Lazy'];
$this->loadLazyFields($tableClass);
}
}
// write value only if either the field value exists,
// or a valid record has been loaded from the database
$value = (isset($this->record[$field])) ? $this->record[$field] : null;
if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
$this->record[$field] = $fieldObj;
return $this->record[$field];
$helper = $this->db($field);
$obj = Object::create_from_string($helper, $field);
$obj->setValue(null, $this, false);
$this->record[$field] = $obj;
}
return isset($this->record[$field]) ? $this->record[$field] : null;
@ -2530,7 +2496,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* )
* </code>
*
* @param boolean $databaseFieldsOnly Get only database fields that have changed
* @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
* to return all database fields, or an array for an explicit filter. false returns all fields.
* @param int $changeLevel The strictness of what is defined as change. Defaults to strict
* @return array
*/
@ -2539,12 +2506,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Update the changed array with references to changed obj-fields
foreach($this->record as $k => $v) {
// Prevents CompositeDBFields infinite looping on isChanged
if(is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
continue;
}
if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
$this->changed[$k] = self::CHANGE_VALUE;
}
}
if($databaseFieldsOnly) {
if(is_array($databaseFieldsOnly)) {
$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
} elseif($databaseFieldsOnly) {
$databaseFields = $this->inheritedDatabaseFields();
$databaseFields['ID'] = true;
$databaseFields['LastEdited'] = true;
@ -2584,7 +2557,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return boolean
*/
public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
$changed = $this->getChangedFields(false, $changeLevel);
$fields = $fieldName ? array($fieldName) : false;
$changed = $this->getChangedFields($fields, $changeLevel);
if(!isset($fieldName)) {
return !empty($changed);
}
@ -2606,16 +2580,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if (substr($fieldName, -2) == 'ID') {
unset($this->components[substr($fieldName, 0, -2)]);
}
// If we've just lazy-loaded the column, then we need to populate the $original array
if(isset($this->record[$fieldName.'_Lazy'])) {
$tableClass = $this->record[$fieldName.'_Lazy'];
$this->loadLazyFields($tableClass);
}
// Situation 1: Passing an DBField
if($val instanceof DBField) {
$val->Name = $fieldName;
$val->setName($fieldName);
$val->saveInto($this);
// If we've just lazy-loaded the column, then we need to populate the $original array by
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
// on a call to getChanged()?
$this->getField($fieldName);
$this->record[$fieldName] = $val;
// Situation 1a: Composite fields should remain bound in case they are
// later referenced to update the parent dataobject
if($val instanceof CompositeDBField) {
$val->bindTo($this);
$this->record[$fieldName] = $val;
}
// Situation 2: Passing a literal or non-DBField object
} else {
// If this is a proper database field, we shouldn't be getting non-DBField objects
@ -2637,11 +2619,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->changed[$fieldName] = self::CHANGE_VALUE;
}
// If we've just lazy-loaded the column, then we need to populate the $original array by
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
// on a call to getChanged()?
$this->getField($fieldName);
// Value is always saved back when strict check succeeds.
$this->record[$fieldName] = $val;
}
@ -2657,23 +2634,30 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @param string $fieldName Name of the field
* @param mixed $value New field value
* @return DataObject $this
* @return $this
*/
public function setCastedField($fieldName, $val) {
public function setCastedField($fieldName, $value) {
if(!$fieldName) {
user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
}
$castingHelper = $this->castingHelper($fieldName);
if($castingHelper) {
$fieldObj = Object::create_from_string($castingHelper, $fieldName);
$fieldObj->setValue($val);
$fieldObj = $this->dbObject($fieldName);
if($fieldObj) {
$fieldObj->setValue($value);
$fieldObj->saveInto($this);
} else {
$this->$fieldName = $val;
$this->$fieldName = $value;
}
return $this;
}
public function castingHelper($field) {
// Allows db to act as implicit casting override
if($fieldSpec = $this->dbHelper($field)) {
return $fieldSpec;
}
return parent::castingHelper($field);
}
/**
* Returns true if the given field exists in a database column on any of
* the objects tables and optionally look up a dynamic getter with
@ -2938,39 +2922,55 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return DBField The field as a DBField object
*/
public function dbObject($fieldName) {
// If we have a CompositeDBField object in $this->record, then return that
// If we have a DBField object in $this->record, then return that
if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
return $this->record[$fieldName];
}
// Special case for ID field
} else if($fieldName == 'ID') {
return new PrimaryKey($fieldName, $this);
// Build and populate new field otherwise
$helper = $this->dbHelper($fieldName, $table);
if($helper) {
$obj = Object::create_from_string($helper, $fieldName);
$obj->setTable($table);
$obj->setValue($this->$fieldName, $this, false);
return $obj;
}
}
// Special case for ClassName
} else if($fieldName == 'ClassName') {
$val = get_class($this);
return DBField::create_field('Varchar', $val, $fieldName);
} else if(array_key_exists($fieldName, self::$fixed_fields)) {
return DBField::create_field(self::$fixed_fields[$fieldName], $this->$fieldName, $fieldName);
/**
* Get helper class spec for the given db field.
*
* Note that child fields of composite db fields will not be detectable via this method.
* These should be resolved indirectly by referencing 'CompositeField.Child' instead of 'CompositeFieldChild'
* in your templates
*
* @param string $fieldName
* @param string $table Out parameter of the table this has_one field belongs to
* @return DBField
*/
public function dbHelper($fieldName, &$table = null) {
// Fixed fields
if(array_key_exists($fieldName, self::$fixed_fields)) {
$table = $this->baseTable();
return self::$fixed_fields[$fieldName];
}
// General casting information for items in $db
} else if($helper = $this->db($fieldName)) {
$obj = Object::create_from_string($helper, $fieldName);
$obj->setValue($this->$fieldName, $this->record, false);
return $obj;
if($helper = $this->db($fieldName, $table)) {
return $helper;
}
// Special case for has_one relationships
} else if(preg_match('/ID$/', $fieldName) && $this->hasOneComponent(substr($fieldName,0,-2))) {
$val = $this->$fieldName;
return DBField::create_field('ForeignKey', $val, $fieldName, $this);
if(preg_match('/.+ID$/', $fieldName) && $this->hasOneComponent(substr($fieldName,0,-2), $table)) {
return 'ForeignKey';
}
// has_one for polymorphic relations do not end in ID
} else if(($type = $this->hasOneComponent($fieldName)) && ($type === 'DataObject')) {
$val = $this->$fieldName();
return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
if(($type = $this->hasOneComponent($fieldName, $table)) && ($type === 'DataObject')) {
return 'PolymorphicForeignKey';
}
return null;
}
/**
@ -3736,11 +3736,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Use a casting object for a field. This is a map from
* field name to class name of the casting object.
*
* @var array
*/
private static $casting = array(
"ID" => 'Int',
"ClassName" => 'Varchar',
"ID" => "PrimaryKey",
"ClassName" => "DBClassName",
"LastEdited" => "SS_Datetime",
"Created" => "SS_Datetime",
"Title" => 'Text',

View File

@ -462,6 +462,7 @@ class DataQuery {
if($compositeFields) foreach($compositeFields as $k => $v) {
if((is_null($columns) || in_array($k, $columns)) && $v) {
$dbO = Object::create_from_string($v, $k);
$dbO->setTable($tableClass);
$dbO->addToQuery($query);
}
}

View File

@ -122,18 +122,17 @@ class ManyManyList extends RelationList {
// convert joined extra fields into their composite field types.
$value = array();
foreach($composed as $subField => $subSpec) {
if(isset($row[$fieldName . $subSpec])) {
$value[$subSpec] = $row[$fieldName . $subSpec];
foreach($composed as $subField) {
if(isset($row[$fieldName . $subField])) {
$value[$subField] = $row[$fieldName . $subField];
// don't duplicate data in the record
unset($row[$fieldName . $subSpec]);
unset($row[$fieldName . $subField]);
}
}
$obj = Object::create_from_string($this->extraFields[$fieldName], $fieldName);
$obj->setValue($value, null, false);
$add[$fieldName] = $obj;
}
}

View File

@ -7,96 +7,18 @@
*
* Example with a combined street name and number:
* <code>
* class Street extends DBField implements CompositeDBField {
* protected $streetNumber;
* protected $streetName;
* protected $isChanged = false;
* static $composite_db = return array(
* class Street extends CompositeDBField {
* private static $composite_db = return array(
* "Number" => "Int",
* "Name" => "Text"
* );
*
* function requireField() {
* DB::requireField($this->tableName, "{$this->name}Number", 'Int');
* DB::requireField($this->tableName, "{$this->name}Name", 'Text');
* }
*
* function writeToManipulation(&$manipulation) {
* if($this->getStreetName()) {
* $manipulation['fields']["{$this->name}Name"] = $this->prepValueForDB($this->getStreetName());
* } else {
* $manipulation['fields']["{$this->name}Name"] = DBField::create_field('Varchar', $this->getStreetName())
* ->nullValue();
* }
*
* if($this->getStreetNumber()) {
* $manipulation['fields']["{$this->name}Number"] = $this->prepValueForDB($this->getStreetNumber());
* } else {
* $manipulation['fields']["{$this->name}Number"] = DBField::create_field('Int', $this->getStreetNumber())
* ->nullValue();
* }
* }
*
* function addToQuery(&$query) {
* parent::addToQuery($query);
* $query->setSelect("{$this->name}Number");
* $query->setSelect("{$this->name}Name");
* }
*
* function setValue($value, $record = null, $markChanged=true) {
* if ($value instanceof Street && $value->exists()) {
* $this->setStreetName($value->getStreetName(), $markChanged);
* $this->setStreetNumber($value->getStreetNumber(), $markChanged);
* if($markChanged) $this->isChanged = true;
* } else if($record && isset($record[$this->name . 'Name']) && isset($record[$this->name . 'Number'])) {
* if($record[$this->name . 'Name'] && $record[$this->name . 'Number']) {
* $this->setStreetName($record[$this->name . 'Name'], $markChanged);
* $this->setStreetNumber($record[$this->name . 'Number'], $markChanged);
* }
* if($markChanged) $this->isChanged = true;
* } else if (is_array($value)) {
* if (array_key_exists('Name', $value)) {
* $this->setStreetName($value['Name'], $markChanged);
* }
* if (array_key_exists('Number', $value)) {
* $this->setStreetNumber($value['Number'], $markChanged);
* }
* if($markChanged) $this->isChanged = true;
* }
* }
*
* function setStreetNumber($val, $markChanged=true) {
* $this->streetNumber = $val;
* if($markChanged) $this->isChanged = true;
* }
*
* function setStreetName($val, $markChanged=true) {
* $this->streetName = $val;
* if($markChanged) $this->isChanged = true;
* }
*
* function getStreetNumber() {
* return $this->streetNumber;
* }
*
* function getStreetName() {
* return $this->streetName;
* }
*
* function isChanged() {
* return $this->isChanged;
* }
*
* function exists() {
* return ($this->getStreetName() || $this->getStreetNumber());
* }
* }
* </code>
*
* @package framework
* @subpackage model
*/
interface CompositeDBField {
abstract class CompositeDBField extends DBField {
/**
* Similiar to {@link DataObject::$db},
@ -104,48 +26,30 @@ interface CompositeDBField {
* Don't include the fields "main name",
* it will be prefixed in {@link requireField()}.
*
* @var array $composite_db
* @config
* @var array
*/
//static $composite_db;
private static $composite_db = array();
/**
* Set the value of this field in various formats.
* Used by {@link DataObject->getField()}, {@link DataObject->setCastedField()}
* {@link DataObject->dbObject()} and {@link DataObject->write()}.
*
* As this method is used both for initializing the field after construction,
* and actually changing its values, it needs a {@link $markChanged}
* parameter.
*
* @param DBField|array $value
* @param DataObject|array $record An array or object that this field is part of
* @param boolean $markChanged Indicate wether this field should be marked changed.
* Set to FALSE if you are initializing this field after construction, rather
* than setting a new value.
* Either the parent dataobject link, or a record of saved values for each field
*
* @var array|DataObject
*/
public function setValue($value, $record = null, $markChanged = true);
protected $record = array();
/**
* Used in constructing the database schema.
* Add any custom properties defined in {@link $composite_db}.
* Should make one or more calls to {@link DB::requireField()}.
*/
//abstract public function requireField();
/**
* Add the custom internal values to an INSERT or UPDATE
* request passed through the ORM with {@link DataObject->write()}.
* Fields are added in $manipulation['fields']. Please ensure
* these fields are escaped for database insertion, as no
* further processing happens before running the query.
* Use {@link DBField->prepValueForDB()}.
* Ensure to write NULL or empty values as well to allow
* unsetting a previously set field. Use {@link DBField->nullValue()}
* for the appropriate type.
* Write all nested fields into a manipulation
*
* @param array $manipulation
*/
public function writeToManipulation(&$manipulation);
public function writeToManipulation(&$manipulation) {
foreach($this->compositeDatabaseFields() as $field => $spec) {
// Write sub-manipulation
$fieldObject = $this->dbObject($field);
$fieldObject->writeToManipulation($manipulation);
}
}
/**
* Add all columns which are defined through {@link requireField()}
@ -155,30 +59,202 @@ interface CompositeDBField {
*
* @param SQLSelect $query
*/
public function addToQuery(&$query);
public function addToQuery(&$query) {
parent::addToQuery($query);
foreach($this->compositeDatabaseFields() as $field => $spec) {
$table = $this->getTable();
$key = $this->getName() . $field;
if($table) {
$query->selectField("\"{$table}\".\"{$key}\"");
} else {
$query->selectField("\"{$key}\"");
}
}
}
/**
* Return array in the format of {@link $composite_db}.
* Used by {@link DataObject->hasOwnDatabaseField()}.
*
* @return array
*/
public function compositeDatabaseFields();
public function compositeDatabaseFields() {
return $this->config()->composite_db;
}
public function isChanged() {
// When unbound, use the local changed flag
if(! ($this->record instanceof DataObject) ) {
return $this->isChanged;
}
// Defer to parent record
foreach($this->compositeDatabaseFields() as $field => $spec) {
$key = $this->getName() . $field;
if($this->record->isChanged($key)) {
return true;
}
}
return false;
}
/**
* Determines if the field has been changed since its initialization.
* Most likely relies on an internal flag thats changed when calling
* {@link setValue()} or any other custom setters on the object.
* Composite field defaults to exists only if all fields have values
*
* @return boolean
*/
public function isChanged();
public function exists() {
// By default all fields
foreach($this->compositeDatabaseFields() as $field => $spec) {
$fieldObject = $this->dbObject($field);
if(!$fieldObject->exists()) {
return false;
}
}
return true;
}
public function requireField() {
foreach($this->compositeDatabaseFields() as $field => $spec){
$key = $this->getName() . $field;
DB::requireField($this->tableName, $key, $spec);
}
}
/**
* Determines if any of the properties in this field have a value,
* meaning at least one of them is not NULL.
* Assign the given value.
* If $record is assigned to a dataobject, this field becomes a loose wrapper over
* the records on that object instead.
*
* @return boolean
* @param type $value
* @param DataObject $record
* @param type $markChanged
* @return type
*/
public function exists();
public function setValue($value, $record = null, $markChanged = true) {
$this->isChanged = $markChanged;
// When given a dataobject, bind this field to that
if($record instanceof DataObject) {
$this->bindTo($record);
$record = null;
}
foreach($this->compositeDatabaseFields() as $field => $spec) {
// Check value
if($value instanceof CompositeDBField) {
// Check if saving from another composite field
$this->setField($field, $value->getField($field));
} elseif(isset($value[$field])) {
// Check if saving from an array
$this->setField($field, $value[$field]);
}
// Load from $record
$key = $this->getName() . $field;
if(isset($record[$key])) {
$this->setField($field, $record[$key]);
}
}
}
/**
* Bind this field to the dataobject, and set the underlying table to that of the owner
*
* @param DataObject $dataObject
*/
public function bindTo($dataObject) {
$this->record = $dataObject;
}
public function saveInto($dataObject) {
foreach($this->compositeDatabaseFields() as $field => $spec) {
// Save into record
$key = $this->getName() . $field;
$dataObject->setField($key, $this->getField($field));
}
}
/**
* get value of a single composite field
*
* @param string $field
* @return mixed
*/
public function getField($field) {
// Skip invalid fields
$fields = $this->compositeDatabaseFields();
if(!isset($fields[$field])) {
return null;
}
// Check bound object
if($this->record instanceof DataObject) {
$key = $this->getName().$field;
return $this->record->getField($key);
}
// Check local record
if(isset($this->record[$field])) {
return $this->record[$field];
}
return null;
}
public function hasField($field) {
$fields = $this->compositeDatabaseFields();
return isset($fields[$field]);
}
/**
* Set value of a single composite field
*
* @param string $field
* @param mixed $value
* @param bool $markChanged
*/
public function setField($field, $value, $markChanged = true) {
// Skip non-db fields
if(!$this->hasField($field)) {
return;
}
// Set changed
if($markChanged) {
$this->isChanged = true;
}
// Set bound object
if($this->record instanceof DataObject) {
$key = $this->getName() . $field;
return $this->record->setField($key, $value);
}
// Set local record
$this->record[$field] = $value;
}
/**
* Get a db object for the named field
*
* @param string $field Field name
* @return DBField|null
*/
public function dbObject($field) {
$fields = $this->compositeDatabaseFields();
if(!isset($fields[$field])) {
return null;
}
// Build nested field
$key = $this->getName() . $field;
$spec = $fields[$field];
$fieldObject = Object::create_from_string($spec, $key);
$fieldObject->setValue($this->getField($field), null, false);
return $fieldObject;
}
}

View File

@ -46,7 +46,7 @@ class Currency extends Decimal {
else return $val;
}
public function setValue($value, $record = null) {
public function setValue($value, $record = null, $markChanged = true) {
$matches = null;
if(is_numeric($value)) {
$this->value = $value;

View File

@ -0,0 +1,202 @@
<?php
/**
* Represents a classname selector, which respects obsolete clasess.
*
* @package framework
* @subpackage model
*/
class DBClassName extends Enum {
/**
* Base classname of class to enumerate.
* If 'DataObject' then all classes are included.
* If empty, then the baseClass of the parent object will be used
*
* @var string|null
*/
protected $baseClass = null;
/**
* Parent object
*
* @var DataObject|null
*/
protected $record = null;
/**
* Classname spec cache for obsolete classes. The top level keys are the table, each of which contains
* nested arrays with keys mapped to field names. The values of the lowest level array are the classnames
*
* @var array
*/
protected static $classname_cache = array();
/**
* Clear all cached classname specs. It's necessary to clear all cached subclassed names
* for any classes if a new class manifest is generated.
*/
public static function clear_classname_cache() {
self::$classname_cache = array();
}
/**
* Create a new DBClassName field
*
* @param string $name Name of field
* @param string|null $baseClass Optional base class to limit selections
*/
public function __construct($name = null, $baseClass = null) {
$this->setBaseClass($baseClass);
parent::__construct($name);
}
/**
* @return void
*/
public function requireField() {
$parts = array(
'datatype' => 'enum',
'enums' => $this->getEnumObsolete(),
'character set' => 'utf8',
'collate' => 'utf8_general_ci',
'default' => $this->getDefault(),
'table' => $this->getTable(),
'arrayValue' => $this->arrayValue
);
$values = array(
'type' => 'enum',
'parts' => $parts
);
DB::require_field($this->getTable(), $this->getName(), $values);
}
/**
* Get the base dataclass for the list of subclasses
*
* @return string
*/
public function getBaseClass() {
// Use explicit base class
if($this->baseClass) {
return $this->baseClass;
}
// Default to the basename of the record
if($this->record) {
return ClassInfo::baseDataClass($this->record);
}
// During dev/build only the table is assigned
$tableClass = $this->getClassNameFromTable($this->getTable());
if($tableClass) {
return $tableClass;
}
// Fallback to global default
return 'DataObject';
}
/**
* Assign the base class
*
* @param string $baseClass
* @return $this
*/
public function setBaseClass($baseClass) {
$this->baseClass = $baseClass;
return $this;
}
/**
* Given a table name, find the base data class
*
* @param string $table
* @return string|null
*/
protected function getClassNameFromTable($table) {
if(empty($table)) {
return null;
}
$class = ClassInfo::baseDataClass($table);
if($class) {
return $class;
}
// If there is no class for this table, strip table modifiers (_Live / _versions) off the end
if(preg_match('/^(?<class>.+)(_[^_]+)$/i', $this->getTable(), $matches)) {
return $this->getClassNameFromTable($matches['class']);
}
return null;
}
/**
* Get list of classnames that should be selectable
*
* @return array
*/
public function getEnum() {
$classNames = ClassInfo::subclassesFor($this->getBaseClass());
unset($classNames['DataObject']);
return $classNames;
}
/**
* Get the list of classnames, including obsolete classes.
*
* If table or name are not set, or if it is not a valid field on the given table,
* then only known classnames are returned.
*
* Values cached in this method can be cleared via `DBClassName::clear_classname_cache();`
*
* @return array
*/
public function getEnumObsolete() {
// Without a table or field specified, we can only retrieve known classes
$table = $this->getTable();
$name = $this->getName();
if(empty($table) || empty($name)) {
return $this->getEnum();
}
// Ensure the table level cache exists
if(empty(self::$classname_cache[$table])) {
self::$classname_cache[$table] = array();
}
// Check existing cache
if(!empty(self::$classname_cache[$table][$name])) {
return self::$classname_cache[$table][$name];
}
// Get all class names
$classNames = $this->getEnum();
if(DB::get_schema()->hasField($table, $name)) {
$existing = DB::query("SELECT DISTINCT \"{$name}\" FROM \"{$table}\"")->column();
$classNames = array_unique(array_merge($classNames, $existing));
}
// Cache and return
self::$classname_cache[$table][$name] = $classNames;
return $classNames;
}
public function setValue($value, $record = null, $markChanged = true) {
parent::setValue($value, $record, $markChanged);
if($record instanceof DataObject) {
$this->record = $record;
}
}
public function getDefault() {
// Check for assigned default
$default = parent::getDefault();
if($default) {
return $default;
}
// Fallback to first option
$enum = $this->getEnum();
return reset($enum);
}
}

View File

@ -131,14 +131,21 @@ abstract class DBField extends ViewableData {
}
/**
* Set the value on the field.
* Set the value of this field in various formats.
* Used by {@link DataObject->getField()}, {@link DataObject->setCastedField()}
* {@link DataObject->dbObject()} and {@link DataObject->write()}.
*
* Optionally takes the whole record as an argument, to pick other values.
* As this method is used both for initializing the field after construction,
* and actually changing its values, it needs a {@link $markChanged}
* parameter.
*
* @param mixed $value
* @param array $record
* @param DataObject|array $record An array or object that this field is part of
* @param boolean $markChanged Indicate wether this field should be marked changed.
* Set to FALSE if you are initializing this field after construction, rather
* than setting a new value.
*/
public function setValue($value, $record = null) {
public function setValue($value, $record = null, $markChanged = true) {
$this->value = $value;
}
@ -207,8 +214,24 @@ abstract class DBField extends ViewableData {
}
/**
* Assign this DBField to a table
*
* @param string $tableName
* @return $this
*/
public function setTable($tableName) {
$this->tableName = $tableName;
return $this;
}
/**
* Get the table this field belongs to, if assigned
*
* @return string|null
*/
public function getTable() {
return $this->tableName;
}
/**

View File

@ -20,7 +20,7 @@
*/
class Date extends DBField {
public function setValue($value, $record = null) {
public function setValue($value, $record = null, $markChanged = true) {
if($value === false || $value === null || (is_string($value) && !strlen($value))) {
// don't try to evaluate empty values with strtotime() below, as it returns "1970-01-01" when it should be
// saved as NULL in database

View File

@ -25,7 +25,7 @@
*/
class SS_Datetime extends Date implements TemplateGlobalProvider {
public function setValue($value, $record = null) {
public function setValue($value, $record = null, $markChanged = true) {
if($value === false || $value === null || (is_string($value) && !strlen($value))) {
// don't try to evaluate empty values with strtotime() below, as it returns "1970-01-01" when it should be
// saved as NULL in database

View File

@ -9,7 +9,19 @@
*/
class Enum extends StringField {
protected $enum, $default;
/**
* List of enum values
*
* @var array
*/
protected $enum = array();
/**
* Default value
*
* @var string|null
*/
protected $default = null;
private static $default_search_filter_class = 'ExactMatchFilter';
@ -37,16 +49,12 @@ class Enum extends StringField {
*/
public function __construct($name = null, $enum = NULL, $default = NULL) {
if($enum) {
if(!is_array($enum)) {
$enum = preg_split("/ *, */", trim($enum));
}
$this->enum = $enum;
$this->setEnum($enum);
// If there's a default, then
if($default) {
if(in_array($default, $enum)) {
$this->default = $default;
if(in_array($default, $this->getEnum())) {
$this->setDefault($default);
} else {
user_error("Enum::__construct() The default value '$default' does not match any item in the"
. " enumeration", E_USER_ERROR);
@ -54,7 +62,8 @@ class Enum extends StringField {
// By default, set the default value to the first item
} else {
$this->default = reset($enum);
$enum = $this->getEnum();
$this->setDefault(reset($enum));
}
}
@ -67,11 +76,11 @@ class Enum extends StringField {
public function requireField() {
$parts = array(
'datatype' => 'enum',
'enums' => $this->enum,
'enums' => $this->getEnum(),
'character set' => 'utf8',
'collate' => 'utf8_general_ci',
'default' => $this->default,
'table' => $this->tableName,
'default' => $this->getDefault(),
'table' => $this->getTable(),
'arrayValue' => $this->arrayValue
);
@ -80,7 +89,7 @@ class Enum extends StringField {
'parts' => $parts
);
DB::require_field($this->tableName, $this->name, $values);
DB::require_field($this->getTable(), $this->getName(), $values);
}
/**
@ -91,8 +100,12 @@ class Enum extends StringField {
public function formField($title = null, $name = null, $hasEmpty = false, $value = "", $form = null,
$emptyString = null) {
if(!$title) $title = $this->name;
if(!$name) $name = $this->name;
if(!$title) {
$title = $this->getName();
}
if(!$name) {
$name = $this->getName();
}
$field = new DropdownField($name, $title, $this->enumValues(false), $value, $form);
if($hasEmpty) {
@ -129,7 +142,50 @@ class Enum extends StringField {
*/
public function enumValues($hasEmpty = false) {
return ($hasEmpty)
? array_merge(array('' => ''), ArrayLib::valuekey($this->enum))
: ArrayLib::valuekey($this->enum);
? array_merge(array('' => ''), ArrayLib::valuekey($this->getEnum()))
: ArrayLib::valuekey($this->getEnum());
}
/**
* Get list of enum values
*
* @return array
*/
public function getEnum() {
return $this->enum;
}
/**
* Set enum options
*
* @param string|array $enum
* @return $this
*/
public function setEnum($enum) {
if(!is_array($enum)) {
$enum = preg_split("/ *, */", trim($enum));
}
$this->enum = $enum;
return $this;
}
/**
* Get default vwalue
*
* @return string|null
*/
public function getDefault() {
return $this->default;
}
/**
* Set default value
*
* @param string $default
* @return $this
*/
public function setDefault($default) {
$this->default = $default;
return $this;
}
}

View File

@ -27,6 +27,9 @@ class ForeignKey extends Int {
}
public function scaffoldFormField($title = null, $params = null) {
if(empty($this->object)) {
return null;
}
$relationName = substr($this->name,0,-2);
$hasOneClass = $this->object->hasOneComponent($relationName);
@ -51,6 +54,13 @@ class ForeignKey extends Int {
return $field;
}
public function setValue($value, $record = null, $markChanged = true) {
if($record instanceof DataObject) {
$this->object = $record;
}
parent::setValue($value, $record, $markChanged);
}
}

View File

@ -22,22 +22,7 @@ require_once 'Zend/Currency.php';
* @package framework
* @subpackage model
*/
class Money extends DBField implements CompositeDBField {
/**
* @var string $getCurrency()
*/
protected $currency;
/**
* @var float $currencyAmount
*/
protected $amount;
/**
* @var boolean $isChanged
*/
protected $isChanged = false;
class Money extends CompositeDBField {
/**
* @var string $locale
@ -69,77 +54,6 @@ class Money extends DBField implements CompositeDBField {
parent::__construct($name);
}
public function compositeDatabaseFields() {
return self::$composite_db;
}
public function requireField() {
$fields = $this->compositeDatabaseFields();
if($fields) foreach($fields as $name => $type){
DB::require_field($this->tableName, $this->name.$name, $type);
}
}
public function writeToManipulation(&$manipulation) {
if($this->getCurrency()) {
$manipulation['fields'][$this->name.'Currency'] = $this->prepValueForDB($this->getCurrency());
} else {
$manipulation['fields'][$this->name.'Currency']
= DBField::create_field('Varchar', $this->getCurrency())->nullValue();
}
if($this->getAmount()) {
$manipulation['fields'][$this->name.'Amount'] = $this->getAmount();
} else {
$manipulation['fields'][$this->name.'Amount']
= DBField::create_field('Decimal', $this->getAmount())->nullValue();
}
}
public function addToQuery(&$query) {
parent::addToQuery($query);
$query->selectField(sprintf('"%sAmount"', $this->name));
$query->selectField(sprintf('"%sCurrency"', $this->name));
}
public function setValue($value, $record = null, $markChanged = true) {
// Convert an object to an array
if($record && $record instanceof DataObject) {
$record = $record->getQueriedDatabaseFields();
}
// @todo Allow resetting value to NULL through Money $value field
if ($value instanceof Money && $value->exists()) {
$this->setCurrency($value->getCurrency(), $markChanged);
$this->setAmount($value->getAmount(), $markChanged);
if($markChanged) $this->isChanged = true;
} else if($record && isset($record[$this->name . 'Amount'])) {
if($record[$this->name . 'Amount']) {
if(!empty($record[$this->name . 'Currency'])) {
$this->setCurrency($record[$this->name . 'Currency'], $markChanged);
} else if($currency = (string)$this->config()->default_currency) {
$this->setCurrency($currency, $markChanged);
}
$this->setAmount($record[$this->name . 'Amount'], $markChanged);
} else {
$this->value = $this->nullValue();
}
if($markChanged) $this->isChanged = true;
} else if (is_array($value)) {
if (array_key_exists('Currency', $value)) {
$this->setCurrency($value['Currency'], $markChanged);
}
if (array_key_exists('Amount', $value)) {
$this->setAmount($value['Amount'], $markChanged);
}
if($markChanged) $this->isChanged = true;
} else {
// @todo Allow to reset a money value by passing in NULL
//user_error('Invalid value in Money->setValue()', E_USER_ERROR);
}
}
/**
* @return string
*/
@ -173,32 +87,28 @@ class Money extends DBField implements CompositeDBField {
* @return string
*/
public function getCurrency() {
return $this->currency;
return $this->getField('Currency');
}
/**
* @param string
*/
public function setCurrency($currency, $markChanged = true) {
$this->currency = $currency;
if($markChanged) $this->isChanged = true;
$this->setField('Currency', $currency, $markChanged);
}
/**
* @todo Return casted Float DBField?
*
* @return float
*/
public function getAmount() {
return $this->amount;
return $this->getField('Amount');
}
/**
* @param float $amount
*/
public function setAmount($amount, $markChanged = true) {
$this->amount = (float)$amount;
if($markChanged) $this->isChanged = true;
$this->setField('Amount', (float)$amount, $markChanged);
}
/**
@ -216,10 +126,6 @@ class Money extends DBField implements CompositeDBField {
return (!empty($a) && is_numeric($a));
}
public function isChanged() {
return $this->isChanged;
}
/**
* @param string $locale
*/
@ -259,7 +165,7 @@ class Money extends DBField implements CompositeDBField {
/**
* @return string
*/
public function getName($currency = null, $locale = null) {
public function getCurrencyName($currency = null, $locale = null) {
if($locale === null) $locale = $this->getLocale();
if($currency === null) $currency = $this->getCurrency();
@ -290,7 +196,7 @@ class Money extends DBField implements CompositeDBField {
* @return FormField
*/
public function scaffoldFormField($title = null) {
$field = new MoneyField($this->name);
$field = new MoneyField($this->getName());
$field->setAllowedCurrencies($this->getAllowedCurrencies());
$field->setLocale($this->getLocale());

View File

@ -6,34 +6,12 @@
* @package framework
* @subpackage model
*/
class PolymorphicForeignKey extends ForeignKey implements CompositeDBField {
class PolymorphicForeignKey extends CompositeDBField {
/**
* @var boolean $isChanged
*/
protected $isChanged = false;
/**
* Value of relation class
*
* @var string
*/
protected $classValue = null;
/**
* Field definition cache for compositeDatabaseFields
*
* @var string
*/
protected static $classname_spec_cache = array();
/**
* Clear all cached classname specs. It's necessary to clear all cached subclassed names
* for any classes if a new class manifest is generated.
*/
public static function clear_classname_spec_cache() {
self::$classname_spec_cache = array();
}
private static $composite_db = array(
'ID' => 'Int',
'Class' => 'DBClassName("DataObject")'
);
public function scaffoldFormField($title = null, $params = null) {
// Opt-out of form field generation - Scaffolding should be performed on
@ -42,55 +20,23 @@ class PolymorphicForeignKey extends ForeignKey implements CompositeDBField {
return null;
}
public function requireField() {
$fields = $this->compositeDatabaseFields();
if($fields) foreach($fields as $name => $type){
DB::requireField($this->tableName, $this->name.$name, $type);
}
}
public function writeToManipulation(&$manipulation) {
// Write ID, checking that the value is valid
$manipulation['fields'][$this->name . 'ID'] = $this->exists()
? $this->prepValueForDB($this->getIDValue())
: $this->nullValue();
// Write class
$classObject = DBField::create_field('Enum', $this->getClassValue(), $this->name . 'Class');
$classObject->writeToManipulation($manipulation);
}
public function addToQuery(&$query) {
parent::addToQuery($query);
$query->selectField(
"\"{$this->tableName}\".\"{$this->name}ID\"",
"{$this->name}ID"
);
$query->selectField(
"\"{$this->tableName}\".\"{$this->name}Class\"",
"{$this->name}Class"
);
}
/**
* Get the value of the "Class" this key points to
*
* @return string Name of a subclass of DataObject
*/
public function getClassValue() {
return $this->classValue;
return $this->getField('Class');
}
/**
* Set the value of the "Class" this key points to
*
* @param string $class Name of a subclass of DataObject
* @param string $value Name of a subclass of DataObject
* @param boolean $markChanged Mark this field as changed?
*/
public function setClassValue($class, $markChanged = true) {
$this->classValue = $class;
if($markChanged) $this->isChanged = true;
public function setClassValue($value, $markChanged = true) {
$this->setField('Class', $value, $markChanged);
}
/**
@ -99,95 +45,36 @@ class PolymorphicForeignKey extends ForeignKey implements CompositeDBField {
* @return integer
*/
public function getIDValue() {
return parent::getValue();
return $this->getField('ID');
}
/**
* Sets the value of the "ID" this key points to
*
* @param integer $id
* @param integer $value
* @param boolean $markChanged Mark this field as changed?
*/
public function setIDValue($id, $markChanged = true) {
parent::setValue($id);
if($markChanged) $this->isChanged = true;
public function setIDValue($value, $markChanged = true) {
$this->setField('ID', $value, $markChanged);
}
public function setValue($value, $record = null, $markChanged = true) {
$idField = "{$this->name}ID";
$classField = "{$this->name}Class";
// Check if an object is assigned directly
// Map dataobject value to array
if($value instanceof DataObject) {
$record = array(
$idField => $value->ID,
$classField => $value->class
$value = array(
'ID' => $value->ID,
'Class' => $value->class
);
}
// Convert an object to an array
if($record instanceof DataObject) {
$record = $record->getQueriedDatabaseFields();
}
// Use $value array if record is missing
if(empty($record) && is_array($value)) {
$record = $value;
}
// Inspect presented values
if(isset($record[$idField]) && isset($record[$classField])) {
if(empty($record[$idField]) || empty($record[$classField])) {
$this->setIDValue($this->nullValue(), $markChanged);
$this->setClassValue('', $markChanged);
} else {
$this->setClassValue($record[$classField], $markChanged);
$this->setIDValue($record[$idField], $markChanged);
}
}
parent::setValue($value, $record, $markChanged);
}
public function getValue() {
if($this->exists()) {
return DataObject::get_by_id($this->getClassValue(), $this->getIDValue());
$id = $this->getIDValue();
$class = $this->getClassValue();
if($id && $class && is_subclass_of($class, 'DataObject')) {
return DataObject::get_by_id($class, $id);
}
}
public function compositeDatabaseFields() {
// Ensure the table level cache exists
if(empty(self::$classname_spec_cache[$this->tableName])) {
self::$classname_spec_cache[$this->tableName] = array();
}
// Ensure the field level cache exists
if(empty(self::$classname_spec_cache[$this->tableName][$this->name])) {
// Get all class names
$classNames = ClassInfo::subclassesFor('DataObject');
unset($classNames['DataObject']);
$schema = DB::get_schema();
if($schema->hasField($this->tableName, "{$this->name}Class")) {
$existing = DB::query("SELECT DISTINCT \"{$this->name}Class\" FROM \"{$this->tableName}\"")->column();
$classNames = array_unique(array_merge($classNames, $existing));
}
self::$classname_spec_cache[$this->tableName][$this->name]
= "Enum(array('" . implode("', '", array_filter($classNames)) . "'))";
}
return array(
'ID' => 'Int',
'Class' => self::$classname_spec_cache[$this->tableName][$this->name]
);
}
public function isChanged() {
return $this->isChanged;
}
public function exists() {
return $this->getClassValue() && $this->getIDValue();
}
}

View File

@ -19,7 +19,7 @@ class PrimaryKey extends Int {
* @param string $name
* @param DataOject $object The object that this is primary key for (should have a relation with $name)
*/
public function __construct($name = null, $object) {
public function __construct($name, $object = null) {
$this->object = $object;
parent::__construct($name);
}
@ -31,5 +31,12 @@ class PrimaryKey extends Int {
$field->setEmptyString(' ');
return $field;
}
}
public function setValue($value, $record = null, $markChanged = true) {
parent::setValue($value, $record, $markChanged);
if($record instanceof DataObject) {
$this->object = $record;
}
}
}

View File

@ -16,7 +16,7 @@
*/
class Time extends DBField {
public function setValue($value, $record = null) {
public function setValue($value, $record = null, $markChanged = true) {
if($value) {
if(preg_match( '/(\d{1,2})[:.](\d{2})([a|A|p|P|][m|M])/', $value, $match )) $this->TwelveHour( $match );
else $this->value = date('H:i:s', strtotime($value));

View File

@ -16,6 +16,15 @@ class CompositeDBFieldTest extends SapphireTest {
$this->assertTrue($obj->hasDatabaseField('MyMoneyAmount'));
$this->assertTrue($obj->hasDatabaseField('MyMoneyCurrency'));
$this->assertFalse($obj->hasDatabaseField('MyMoney'));
// Check that nested fields are exposed properly
$this->assertTrue($obj->dbObject('MyMoney')->hasField('Amount'));
$this->assertTrue($obj->dbObject('MyMoney')->hasField('Currency'));
// Not strictly correct
$this->assertFalse($obj->dbObject('MyMoney')->hasField('MyMoneyAmount'));
$this->assertFalse($obj->dbObject('MyMoney')->hasField('MyMoneyCurrency'));
$this->assertFalse($obj->dbObject('MyMoney')->hasField('MyMoney'));
}
/**
@ -24,16 +33,64 @@ class CompositeDBFieldTest extends SapphireTest {
public 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(
array(
'MyMoney' => 'Money',
'OverriddenMoney' => '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'));
$this->assertEquals(
array(
'MyMoney' => 'Money',
'OtherMoney' => 'Money',
'OverriddenMoney' => 'Money',
),
DataObject::composite_fields('SubclassedDBFieldObject')
);
}
/**
* Tests that changes to the fields affect the underlying dataobject, and vice versa
*/
public function testFieldBinding() {
$object = new CompositeDBFieldTest_DataObject();
$object->MyMoney->Currency = 'NZD';
$object->MyMoney->Amount = 100.0;
$this->assertEquals('NZD', $object->MyMoneyCurrency);
$this->assertEquals(100.0, $object->MyMoneyAmount);
$object->write();
$object2 = CompositeDBFieldTest_DataObject::get()->byID($object->ID);
$this->assertEquals('NZD', $object2->MyMoney->Currency);
$this->assertEquals(100.0, $object2->MyMoney->Amount);
$object2->MyMoneyCurrency = 'USD';
$this->assertEquals('USD', $object2->MyMoney->Currency);
$object2->MyMoney->setValue(array('Currency' => 'EUR', 'Amount' => 200.0));
$this->assertEquals('EUR', $object2->MyMoneyCurrency);
$this->assertEquals(200.0, $object2->MyMoneyAmount);
}
/**
* Ensures that composite fields are assigned to the correct tables
*/
public function testInheritedTables() {
$object1 = new CompositeDBFieldTest_DataObject();
$object2 = new SubclassedDBFieldObject();
$this->assertEquals('CompositeDBFieldTest_DataObject', $object1->dbObject('MyMoney')->getTable());
$this->assertEquals('CompositeDBFieldTest_DataObject', $object1->dbObject('OverriddenMoney')->getTable());
$this->assertEquals('CompositeDBFieldTest_DataObject', $object2->dbObject('MyMoney')->getTable());
$this->assertEquals('SubclassedDBFieldObject', $object2->dbObject('OtherMoney')->getTable());
$this->assertEquals('SubclassedDBFieldObject', $object2->dbObject('OverriddenMoney')->getTable());
}
}
@ -41,6 +98,7 @@ class CompositeDBFieldTest_DataObject extends DataObject implements TestOnly {
private static $db = array(
'Title' => 'Text',
'MyMoney' => 'Money',
'OverriddenMoney' => 'Money'
);
}
@ -48,5 +106,6 @@ class SubclassedDBFieldObject extends CompositeDBFieldTest_DataObject {
private static $db = array(
'OtherField' => 'Text',
'OtherMoney' => 'Money',
'OverriddenMoney' => 'Money'
);
}

View File

@ -0,0 +1,121 @@
<?php
class DBClassNameTest extends SapphireTest {
protected $extraDataObjects = array(
'DBClassNameTest_Object',
'DBClassNameTest_ObjectSubClass',
'DBClassNameTest_ObjectSubSubClass',
'DBClassNameTest_OtherClass'
);
/**
* Test that custom subclasses generate the right hierarchy
*/
public function testEnumList() {
// Object 1 fields
$object = new DBClassNameTest_Object();
$defaultClass = $object->dbObject('DefaultClass');
$anyClass = $object->dbObject('AnyClass');
$childClass = $object->dbObject('ChildClass');
$leafClass = $object->dbObject('LeafClass');
// Object 2 fields
$object2 = new DBClassNameTest_ObjectSubClass();
$midDefault = $object2->dbObject('MidClassDefault');
$midClass = $object2->dbObject('MidClass');
// Default fields always default to children of base class (even if put in a subclass)
$mainSubclasses = array (
'DBClassNameTest_Object' => 'DBClassNameTest_Object',
'DBClassNameTest_ObjectSubClass' => 'DBClassNameTest_ObjectSubClass',
'DBClassNameTest_ObjectSubSubClass' => 'DBClassNameTest_ObjectSubSubClass',
);
$this->assertEquals($mainSubclasses, $defaultClass->getEnumObsolete());
$this->assertEquals($mainSubclasses, $midDefault->getEnumObsolete());
// Unbound classes detect any
$anyClasses = $anyClass->getEnumObsolete();
$this->assertContains('DBClassNameTest_OtherClass', $anyClasses);
$this->assertContains('DBClassNameTest_Object', $anyClasses);
$this->assertContains('DBClassNameTest_ObjectSubClass', $anyClasses);
$this->assertContains('DBClassNameTest_ObjectSubSubClass', $anyClasses);
// Classes bound to the middle of a tree
$midSubClasses = $mainSubclasses = array (
'DBClassNameTest_ObjectSubClass' => 'DBClassNameTest_ObjectSubClass',
'DBClassNameTest_ObjectSubSubClass' => 'DBClassNameTest_ObjectSubSubClass',
);
$this->assertEquals($midSubClasses, $childClass->getEnumObsolete());
$this->assertEquals($midSubClasses, $midClass->getEnumObsolete());
// Leaf clasess contain only exactly one node
$this->assertEquals(
array('DBClassNameTest_ObjectSubSubClass' => 'DBClassNameTest_ObjectSubSubClass',),
$leafClass->getEnumObsolete()
);
}
/**
* Test that the base class can be detected under various circumstances
*/
public function testBaseClassDetection() {
// Explicit DataObject
$field1 = new DBClassName('MyClass', 'DataObject');
$this->assertEquals('DataObject', $field1->getBaseClass());
// Explicit base class
$field2 = new DBClassName('MyClass', 'DBClassNameTest_Object');
$this->assertEquals('DBClassNameTest_Object', $field2->getBaseClass());
// Explicit subclass
$field3 = new DBClassName('MyClass');
$field3->setValue(null, new DBClassNameTest_ObjectSubClass());
$this->assertEquals('DBClassNameTest_Object', $field3->getBaseClass());
// Implicit table
$field4 = new DBClassName('MyClass');
$field4->setTable('DBClassNameTest_ObjectSubClass_versions');
$this->assertEquals('DBClassNameTest_Object', $field4->getBaseClass());
// Missing
$field5 = new DBClassName('MyClass');
$this->assertEquals('DataObject', $field5->getBaseClass());
// Invalid class
$field6 = new DBClassName('MyClass');
$field6->setTable('InvalidTable');
$this->assertEquals('DataObject', $field6->getBaseClass());
}
}
class DBClassNameTest_Object extends DataObject implements TestOnly {
private static $extensions = array(
'Versioned'
);
private static $db = array(
'DefaultClass' => 'DBClassName',
'AnyClass' => 'DBClassName("DataObject")',
'ChildClass' => 'DBClassName("DBClassNameTest_ObjectSubClass")',
'LeafClass' => 'DBClassName("DBClassNameTest_ObjectSubSubClass")'
);
}
class DBClassNameTest_ObjectSubClass extends DBClassNameTest_Object {
private static $db = array(
'MidClassDefault' => 'DBClassName',
'MidClass' => 'DBClassName("DBClassNameTest_ObjectSubclass")'
);
}
class DBClassNameTest_ObjectSubSubClass extends DBClassNameTest_ObjectSubclass {
}
class DBClassNameTest_OtherClass extends DataObject implements TestOnly {
private static $db = array(
'Title' => 'Varchar'
);
}

View File

@ -130,20 +130,30 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
// Test with blank entries
DataObject::clear_classname_spec_cache();
$do1 = new DataObjectSchemaGenerationTest_DO();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
"Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')",
$fields['ClassName']
array(
'DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO',
'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'
),
$do1->dbObject('ClassName')->getEnum()
);
// Test with instance of subclass
$item1 = new DataObjectSchemaGenerationTest_IndexDO();
$item1->write();
DataObject::clear_classname_spec_cache();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
"Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')",
$fields['ClassName']
array(
'DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO',
'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'
),
$item1->dbObject('ClassName')->getEnum()
);
$item1->delete();
@ -152,9 +162,13 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$item2->write();
DataObject::clear_classname_spec_cache();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
"Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')",
$fields['ClassName']
array(
'DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO',
'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'
),
$item2->dbObject('ClassName')->getEnum()
);
$item2->delete();
@ -165,9 +179,13 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$item2->write();
DataObject::clear_classname_spec_cache();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
"Enum('DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO')",
$fields['ClassName']
array(
'DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO',
'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'
),
$item1->dbObject('ClassName')->getEnum()
);
$item1->delete();
$item2->delete();

View File

@ -10,8 +10,23 @@ class ManyManyListTest extends SapphireTest {
protected $extraDataObjects = array(
'DataObjectTest_Team',
'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
'OtherSubclassWithSameField',
'DataObjectTest_FieldlessTable',
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'ManyManyListTest_ExtraFields'
);

View File

@ -145,11 +145,11 @@ class MoneyTest extends SapphireTest {
));
$m->setLocale('ar_EG');
$this->assertSame('Estnische Krone', $m->getName('EEK','de_AT'));
$this->assertSame('يورو', $m->getName());
$this->assertSame('Estnische Krone', $m->getCurrencyName('EEK','de_AT'));
$this->assertSame('يورو', $m->getCurrencyName());
try {
$m->getName('EGP', 'xy_XY');
$m->getCurrencyName('EGP', 'xy_XY');
$this->setExpectedException("Exception");
} catch(Exception $e) {
}

View File

@ -1,8 +1,8 @@
MoneyTest_DataObject:
test1:
MyMoneyCurrency: EUR
MyMoneyAmount: 1.23
test1:
MyMoneyCurrency: EUR
MyMoneyAmount: 1.23
MoneyTest_SubClass:
test2:
MyOtherMoneyCurrency: GBP
MyOtherMoneyAmount: 2.46
test2:
MyOtherMoneyCurrency: GBP
MyOtherMoneyAmount: 2.46

View File

@ -263,17 +263,15 @@ class ViewableData extends Object implements IteratorAggregate {
* on this object.
*
* @param string $field
* @return string
* @return string Casting helper
*/
public function castingHelper($field) {
if($this->hasMethod('db') && $fieldSpec = $this->db($field)) {
return $fieldSpec;
$specs = $this->config()->casting;
if(isset($specs[$field])) {
return $specs[$field];
} elseif($this->failover) {
return $this->failover->castingHelper($field);
}
$specs = Config::inst()->get(get_class($this), 'casting');
if(isset($specs[$field])) return $specs[$field];
if($this->failover) return $this->failover->castingHelper($field);
}
/**