Merge pull request #4590 from tractorcow/pulls/4.0/make-compositedbfield-useful

API Refactor CompositeDBField into an abstract class
This commit is contained in:
Ingo Schommer 2015-09-22 11:11:30 +12:00
commit 7156718219
33 changed files with 1336 additions and 871 deletions

View File

@ -60,6 +60,7 @@ class CsvBulkLoader extends BulkLoader {
* @return null|BulkLoader_Result
*/
protected function processAll($filepath, $preview = false) {
$filepath = Director::getAbsFile($filepath);
$files = $this->splitFile($filepath);
$result = null;

View File

@ -0,0 +1,97 @@
# 4.0.0 (unreleased)
## Overview
### Framework
* Deprecate `SQLQuery` in favour `SQLSelect`
* `DataList::filter` by null now internally generates "IS NULL" or "IS NOT NULL" conditions appropriately on queries
* `DataObject::database_fields` now returns all fields on that table.
* `DataObject::db` now returns composite fields.
* `DataObject::ClassName` field has been refactored into a `DBClassName` type field.
## Upgrading
### Upgrading code that uses composite db fields.
`CompositeDBField` is now an abstract class, not an interface. In many cases, custom code that handled
saving of content into composite fields can be removed, as it is now handled by the base class.
The below describes the minimum amount of effort required to implement a composite DB field.
:::php
<?
class MyAddressField extends CompositeDBField {
private static $composite_db = array(
'Street' => 'Varchar(200)',
'Suburb' => 'Varchar(100)',
'City' => 'Varchar(100)',
'Country' => 'Varchar(100)'
);
public function scaffoldFormField($title = null) {
new AddressFormField($this->getName(), $title);
}
}
### Upgrading code that references `DataObject::database_fields` or `DataObject::db`
These methods have been updated to include base fields (such as ID, ClassName, Created, and LastEdited), as
well as composite DB fields.
`DataObject::database_fields` does not have a second parameter anymore, and can be called directly on an object
or class. E.g. `Member::database_fields()`
If user code requires the list of fields excluding base fields, then use custom_database_fields instead, or
make sure to call `unset($fields['ID']);` if this field should be excluded.
`DataObject:db()` will return all logical fields, including foreign key ids and composite DB Fields, alongside
any child fields of these composites. This method can now take a second parameter $includesTable, which
when set to true (with a field name as the first parameter), will also include the table prefix in
`Table.ClassName(args)` format.
### Update code that uses SQLQuery
SQLQuery is still implemented, but now extends the new SQLSelect class and has some methods
deprecated. Previously this class was used for both selecting and deleting, but these
have been superceded by the specialised SQLSelect and SQLDelete classes.
Take care for any code or functions which expect an object of type `SQLQuery`, as
these references should be replaced with `SQLSelect`. Legacy code which generates
`SQLQuery` can still communicate with new code that expects `SQLSelect` as it is a
subclass of `SQLSelect`, but the inverse is not true.
### Update implementations of augmentSQL
Since this method now takes a `SQLSelect` as a first parameter, existing code referencing the deprecated `SQLQuery`
type will raise a PHP error.
E.g.
Before:
:::php
function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) {
$locale = Translatable::get_current_locale();
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhere()))) {
$qry = sprintf('"Locale" = \'%s\'', Convert::raw2sql($locale));
$query->addWhere($qry);
}
}
After:
:::php
function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
$locale = Translatable::get_current_locale();
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhereParameterised($parameters)))) {
$query->addWhere(array(
'"Locale"' => $locale
));
}
}

View File

@ -1,53 +0,0 @@
# 4.0.0 (unreleased)
## Overview
### Framework
* Deprecate `SQLQuery` in favour `SQLSelect`
* `DataList::filter` by null now internally generates "IS NULL" or "IS NOT NULL" conditions appropriately on queries
## Upgrading
### Update code that uses SQLQuery
SQLQuery is still implemented, but now extends the new SQLSelect class and has some methods
deprecated. Previously this class was used for both selecting and deleting, but these
have been superceded by the specialised SQLSelect and SQLDelete classes.
Take care for any code or functions which expect an object of type `SQLQuery`, as
these references should be replaced with `SQLSelect`. Legacy code which generates
`SQLQuery` can still communicate with new code that expects `SQLSelect` as it is a
subclass of `SQLSelect`, but the inverse is not true.
### Update implementations of augmentSQL
Since this method now takes a `SQLSelect` as a first parameter, existing code referencing the deprecated `SQLQuery`
type will raise a PHP error.
E.g.
Before:
:::php
function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) {
$locale = Translatable::get_current_locale();
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhere()))) {
$qry = sprintf('"Locale" = \'%s\'', Convert::raw2sql($locale));
$query->addWhere($qry);
}
}
After:
:::php
function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
$locale = Translatable::get_current_locale();
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhereParameterised($parameters)))) {
$query->addWhere(array(
'"Locale"' => $locale
));
}
}

View File

@ -73,8 +73,9 @@ class FormScaffolder extends Object {
$mainTab->setTitle(_t('SiteTree.TABMAIN', "Main"));
}
// add database fields
foreach($this->obj->db() as $fieldName => $fieldType) {
// Add logical fields directly specified in db config
foreach($this->obj->config()->db as $fieldName => $fieldType) {
// Skip restricted fields
if($this->restrictFields && !in_array($fieldName, $this->restrictFields)) continue;
// @todo Pass localized title
@ -82,7 +83,14 @@ class FormScaffolder extends Object {
$fieldClass = $this->fieldClasses[$fieldName];
$fieldObject = new $fieldClass($fieldName);
} else {
$fieldObject = $this->obj->dbObject($fieldName)->scaffoldFormField(null, $this->getParamsArray());
$fieldObject = $this
->obj
->dbObject($fieldName)
->scaffoldFormField(null, $this->getParamsArray());
}
// Allow fields to opt-out of scaffolding
if(!$fieldObject) {
continue;
}
$fieldObject->setTitle($this->obj->fieldLabel($fieldName));
if($this->tabbed) {

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

@ -2,7 +2,6 @@
namespace SilverStripe\Framework\Logging;
use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;
/**
@ -53,7 +52,7 @@ class HTTPOutputHandler extends AbstractProcessingHandler
if(\Controller::has_curr()) {
$response = \Controller::curr()->getResponse();
} else {
$response = new SS_HTTPResponse();
$response = new \SS_HTTPResponse();
}
// If headers have been sent then these won't be used, and may throw errors that we wont' want to see.

View File

@ -175,19 +175,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Static caches used by relevant functions.
*/
public static $cache_has_own_table = array();
protected static $_cache_db = array();
protected static $_cache_has_own_table = array();
protected static $_cache_get_one;
protected static $_cache_get_class_ancestry;
protected static $_cache_composite_fields = array();
protected static $_cache_is_composite_field = array();
protected static $_cache_custom_database_fields = array();
protected static $_cache_database_fields = array();
protected static $_cache_field_labels = array();
// base fields which are not defined in static $db
/**
* Base fields which are not defined in static $db
*
* @config
* @var array
*/
private static $fixed_fields = array(
'ID' => 'Int',
'ClassName' => 'Enum',
'ID' => 'PrimaryKey',
'ClassName' => 'DBClassName',
'LastEdited' => 'SS_Datetime',
'Created' => 'SS_Datetime',
);
@ -229,68 +232,99 @@ 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();
Deprecation::notice('4.0', 'Call DBClassName::clear_classname_cache() instead');
DBClassName::clear_classname_cache();
}
/**
* Determines the specification for the ClassName field for the given class
* Return the complete map of fields to specification on this object, including fixed_fields.
* "ID" will be included on every table.
*
* @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
* Composite DB field specifications are returned by reference if necessary, but not in the return
* array.
*
* Can be called directly on an object. E.g. Member::database_fields()
*
* @param string $class Class name to query from
* @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
*/
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));
public static function database_fields($class = null) {
if(empty($class)) {
$class = get_called_class();
}
$spec = "Enum('" . implode(', ', $classNames) . "')";
// Only cache full information if queried
if($queryDB) self::$classname_spec_cache[$class] = $spec;
return $spec;
// Refresh cache
self::cache_database_fields($class);
// Return cached values
return self::$_cache_database_fields[$class];
}
/**
* Return the complete map of fields on this object, including "Created", "LastEdited" and "ClassName".
* See {@link custom_database_fields()} for a getter that excludes these "base fields".
* Cache all database and composite fields for the given class.
* Will do nothing if already cached
*
* @param string $class
* @param boolean $queryDB Determine if the DB may be queried for additional information
* @return array
* @param string $class Class name to cache
*/
public static function database_fields($class, $queryDB = true) {
if(get_parent_class($class) == 'DataObject') {
protected static function cache_database_fields($class) {
// Skip if already cached
if( isset(self::$_cache_database_fields[$class])
&& isset(self::$_cache_composite_fields[$class])
) {
return;
}
$compositeFields = array();
$dbFields = array();
// Ensure fixed fields appear at the start
$fixedFields = self::config()->fixed_fields;
if(get_parent_class($class) === 'DataObject') {
// Merge fixed with ClassName spec and custom db fields
$fixed = self::$fixed_fields;
unset($fixed['ID']);
return array_merge(
$fixed,
array('ClassName' => self::get_classname_spec($class, $queryDB)),
self::custom_database_fields($class)
);
$dbFields = $fixedFields;
} else {
$dbFields['ID'] = $fixedFields['ID'];
}
// Check each DB value as either a field or composite field
$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
foreach($db as $fieldName => $fieldSpec) {
$fieldClass = strtok($fieldSpec, '(');
if(is_subclass_of($fieldClass, 'CompositeDBField')) {
$compositeFields[$fieldName] = $fieldSpec;
} else {
$dbFields[$fieldName] = $fieldSpec;
}
}
return self::custom_database_fields($class);
// Add in all has_ones
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array();
foreach($hasOne as $fieldName => $hasOneClass) {
if($hasOneClass === 'DataObject') {
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
} else {
$dbFields["{$fieldName}ID"] = 'ForeignKey';
}
}
// Merge composite fields into DB
foreach($compositeFields as $fieldName => $fieldSpec) {
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
$fieldObj->setTable($class);
$nestedFields = $fieldObj->compositeDatabaseFields();
foreach($nestedFields as $nestedName => $nestedSpec) {
$dbFields["{$fieldName}{$nestedName}"] = $nestedSpec;
}
}
// Return cached results
self::$_cache_database_fields[$class] = $dbFields;
self::$_cache_composite_fields[$class] = $compositeFields;
}
/**
@ -302,53 +336,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited",
* see {@link database_fields()}.
*
* Can be called directly on an object. E.g. Member::custom_database_fields()
*
* @uses CompositeDBField->compositeDatabaseFields()
*
* @param string $class
* @param string $class Class name to query from
* @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
*/
public static function custom_database_fields($class) {
if(isset(self::$_cache_custom_database_fields[$class])) {
return self::$_cache_custom_database_fields[$class];
public static function custom_database_fields($class = null) {
if(empty($class)) {
$class = get_called_class();
}
// Get all fields
$fields = self::database_fields($class);
$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
// 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) {
$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 {
$fields[$field . 'ID'] = 'ForeignKey';
}
}
$output = (array) $fields;
self::$_cache_custom_database_fields[$class] = $output;
return $output;
// Remove fixed fields. This assumes that NO fixed_fields are composite
$fields = array_diff_key($fields, self::config()->fixed_fields);
return $fields;
}
/**
@ -358,68 +363,49 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param string $class Class to check
* @param string $name Field to check
* @param boolean $aggregated True if parent classes should be checked, or false to limit to this class
* @return string Class name of composite field if it exists
* @return string|false Class spec name of composite field if it exists, or false if not
*/
public static function is_composite_field($class, $name, $aggregated = true) {
$key = $class . '_' . $name . '_' . (string)$aggregated;
if(!isset(DataObject::$_cache_is_composite_field[$key])) {
$isComposite = null;
if(!isset(DataObject::$_cache_composite_fields[$class])) {
self::cache_composite_fields($class);
}
if(isset(DataObject::$_cache_composite_fields[$class][$name])) {
$isComposite = DataObject::$_cache_composite_fields[$class][$name];
} elseif($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
$isComposite = self::is_composite_field($parentClass, $name);
}
DataObject::$_cache_is_composite_field[$key] = ($isComposite) ? $isComposite : false;
}
return DataObject::$_cache_is_composite_field[$key] ?: null;
$fields = self::composite_fields($class, $aggregated);
return isset($fields[$name]) ? $fields[$name] : false;
}
/**
* 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.
*
* Can be called directly on an object. E.g. Member::composite_fields(), or Member::composite_fields(null, true)
* to aggregate.
*
* 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 List of composite fields and their class spec
*/
public static function composite_fields($class, $aggregated = true) {
if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
$compositeFields = DataObject::$_cache_composite_fields[$class];
if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
$compositeFields = array_merge($compositeFields,
self::composite_fields($parentClass));
public static function composite_fields($class = null, $aggregated = true) {
// Check $class
if(empty($class)) {
$class = get_called_class();
}
if($class === 'DataObject') {
return array();
}
return $compositeFields;
}
// Refresh cache
self::cache_database_fields($class);
/**
* Internal cacher for the composite field information
*/
private static function cache_composite_fields($class) {
$compositeFields = array();
$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
if($fields) foreach($fields as $fieldName => $fieldClass) {
if(!is_string($fieldClass)) continue;
// Strip off any parameters
$bPos = strpos($fieldClass, '(');
if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos);
// Test to see if it implements CompositeDBField
if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
$compositeFields[$fieldName] = $fieldClass;
}
// Get fields for this class
$compositeFields = self::$_cache_composite_fields[$class];
if(!$aggregated) {
return $compositeFields;
}
DataObject::$_cache_composite_fields[$class] = $compositeFields;
// Recursively merge
return array_merge(
$compositeFields,
self::composite_fields(get_parent_class($class))
);
}
/**
@ -718,25 +704,26 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Returns TRUE if all values (other than "ID") are
* considered empty (by weak boolean comparison).
* Only checks for fields listed in {@link custom_database_fields()}
*
* @todo Use DBField->hasValue()
*
* @return boolean
*/
public function isEmpty(){
$isEmpty = true;
$customFields = self::custom_database_fields(get_class($this));
if($map = $this->toMap()){
foreach($map as $k=>$v){
// only look at custom fields
if(!array_key_exists($k, $customFields)) continue;
public function isEmpty() {
$fixed = $this->config()->fixed_fields;
foreach($this->toMap() as $field => $value){
// only look at custom fields
if(isset($fixed[$field])) {
continue;
}
$dbObj = ($v instanceof DBField) ? $v : $this->dbObject($k);
$isEmpty = ($isEmpty && !$dbObj->exists());
$dbObject = $this->dbObject($field);
if(!$dbObject) {
continue;
}
if($dbObject->exists()) {
return false;
}
}
return $isEmpty;
return true;
}
/**
@ -972,15 +959,29 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
// makes sure we don't merge data like ID or ClassName
$leftData = $leftObj->inheritedDatabaseFields();
$rightData = $rightObj->inheritedDatabaseFields();
$leftData = $leftObj->db();
$rightData = $rightObj->db();
foreach($rightData as $key=>$rightSpec) {
// Don't merge ID
if($key === 'ID') {
continue;
}
// Only merge relations if allowed
if($rightSpec === 'ForeignKey' && !$includeRelations) {
continue;
}
foreach($rightData as $key=>$rightVal) {
// don't merge conflicting values if priority is 'left'
if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) continue;
if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
continue;
}
// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) continue;
if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
continue;
}
// TODO remove redundant merge of has_one fields
$leftObj->{$key} = $rightObj->{$key};
@ -1010,16 +1011,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
if($hasOne = $this->hasOne()) {
foreach($hasOne as $relationship => $class) {
$leftComponent = $leftObj->getComponent($relationship);
$rightComponent = $rightObj->getComponent($relationship);
if($leftComponent->exists() && $rightComponent->exists() && $priority == 'right') {
$leftObj->{$relationship . 'ID'} = $rightObj->{$relationship . 'ID'};
}
}
}
}
return true;
@ -1039,7 +1030,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
$fieldNames = array_unique(array_merge(
array_keys($this->record),
array_keys($this->inheritedDatabaseFields())));
array_keys($this->db())
));
foreach($fieldNames as $fieldName) {
if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
@ -1274,8 +1266,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]);
}
@ -1501,13 +1492,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return array Class ancestry
*/
public function getClassAncestry() {
if(!isset(DataObject::$_cache_get_class_ancestry[$this->class])) {
DataObject::$_cache_get_class_ancestry[$this->class] = array($this->class);
while(($class=get_parent_class(DataObject::$_cache_get_class_ancestry[$this->class][0])) != "DataObject") {
array_unshift(DataObject::$_cache_get_class_ancestry[$this->class], $class);
if(!isset(self::$_cache_get_class_ancestry[$this->class])) {
self::$_cache_get_class_ancestry[$this->class] = array($this->class);
while(($class=get_parent_class(self::$_cache_get_class_ancestry[$this->class][0])) != "DataObject") {
array_unshift(self::$_cache_get_class_ancestry[$this->class], $class);
}
}
return DataObject::$_cache_get_class_ancestry[$this->class];
return self::$_cache_get_class_ancestry[$this->class];
}
/**
@ -1832,10 +1823,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return string|null
*/
public function hasOneComponent($component) {
$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
$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])) {
return $hasOnes[$component];
}
}
}
@ -1902,13 +1896,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
/**
* Return all of the database fields defined in self::$db and all the parent classes.
* Doesn't include any fields specified by self::$has_one. Use $this->hasOne() to get these fields
* Return all of the database fields in this object
*
* @param string $fieldName Limit the output to a specific field name
* @return array The database fields
* @param string $includeTable If returning a single column, prefix the column with the table name
* in Table.Column(spec) format
* @return array|string|null The database fields, or if searching a single field, just this one field if found
* Field will be a string in ClassName(args) format, or Table.ClassName(args) format if $includeTable is true
*/
public function db($fieldName = null) {
public function db($fieldName = null, $includeTable = false) {
$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
@ -1916,25 +1912,36 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$classes = array_reverse($classes);
}
$items = array();
$db = array();
foreach($classes as $class) {
if(isset(self::$_cache_db[$class])) {
$dbItems = self::$_cache_db[$class];
} else {
$dbItems = (array) Config::inst()->get($class, 'db', Config::UNINHERITED);
self::$_cache_db[$class] = $dbItems;
}
// Merge fields with new fields and composite fields
$fields = self::database_fields($class);
$compositeFields = self::composite_fields($class, false);
$db = array_merge($db, $fields, $compositeFields);
if($fieldName) {
if(isset($dbItems[$fieldName])) {
return $dbItems[$fieldName];
// Check for search field
if($fieldName && isset($db[$fieldName])) {
// Return found field
if(!$includeTable) {
return $db[$fieldName];
}
} else {
$items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
// Set table for the given field
if(in_array($fieldName, $this->config()->fixed_fields)) {
$table = $this->baseTable();
} else {
$table = $class;
}
return $table . "." . $db[$fieldName];
}
}
return $items;
// At end of search complete
if($fieldName) {
return null;
} else {
return $db;
}
}
/**
@ -2276,6 +2283,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$field = $this->relObject($fieldName)->scaffoldSearchField();
}
// Allow fields to opt out of search
if(!$field) {
continue;
}
if (strstr($fieldName, '.')) {
$field->setName(str_replace('.', '__', $fieldName));
}
@ -2405,7 +2417,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 +2427,9 @@ 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];
$this->record[$field] = $this->dbObject($field);
}
return isset($this->record[$field]) ? $this->record[$field] : null;
@ -2479,7 +2475,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Add SQL for fields, both simple & multi-value
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
$databaseFields = self::database_fields($tableClass, false);
$databaseFields = self::database_fields($tableClass);
if($databaseFields) foreach($databaseFields as $k => $v) {
if(!isset($this->record[$k]) || $this->record[$k] === null) {
$columns[] = $k;
@ -2530,7 +2526,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,18 +2536,19 @@ 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) {
$databaseFields = $this->inheritedDatabaseFields();
$databaseFields['ID'] = true;
$databaseFields['LastEdited'] = true;
$databaseFields['Created'] = true;
$databaseFields['ClassName'] = true;
$fields = array_intersect_key((array)$this->changed, $databaseFields);
if(is_array($databaseFieldsOnly)) {
$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
} elseif($databaseFieldsOnly) {
$fields = array_intersect_key((array)$this->changed, $this->db());
} else {
$fields = $this->changed;
}
@ -2584,7 +2582,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 +2605,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 +2644,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 +2659,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->db($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
@ -2699,9 +2708,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return boolean
*/
public function hasDatabaseField($field) {
if(isset(self::$fixed_fields[$field])) return true;
return array_key_exists($field, $this->inheritedDatabaseFields());
return $this->db($field)
&& ! self::is_composite_field(get_class($this), $field);
}
/**
@ -2724,10 +2732,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return string The field type of the given field
*/
public static function has_own_table_database_field($class, $field) {
// Since database_fields omits 'ID'
if($field == "ID") return "Int";
$fieldMap = self::database_fields($class, false);
$fieldMap = self::database_fields($class);
// Remove string-based "constructor-arguments" from the DBField definition
if(isset($fieldMap[$field])) {
@ -2748,16 +2753,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(!is_subclass_of($dataClass,'DataObject')) return false;
$dataClass = ClassInfo::class_name($dataClass);
if(!isset(DataObject::$cache_has_own_table[$dataClass])) {
if(!isset(self::$_cache_has_own_table[$dataClass])) {
if(get_parent_class($dataClass) == 'DataObject') {
DataObject::$cache_has_own_table[$dataClass] = true;
self::$_cache_has_own_table[$dataClass] = true;
} else {
DataObject::$cache_has_own_table[$dataClass]
self::$_cache_has_own_table[$dataClass]
= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
}
}
return DataObject::$cache_has_own_table[$dataClass];
return self::$_cache_has_own_table[$dataClass];
}
/**
@ -2938,38 +2943,23 @@ 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(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
return $this->record[$fieldName];
$value = isset($this->record[$fieldName])
? $this->record[$fieldName]
: null;
// Special case for ID field
} else if($fieldName == 'ID') {
return new PrimaryKey($fieldName, $this);
// If we have a DBField object in $this->record, then return that
if(is_object($value)) {
return $value;
}
// 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);
// 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);
// Build and populate new field otherwise
$helper = $this->db($fieldName, true);
if($helper) {
list($table, $spec) = explode('.', $helper);
$obj = Object::create_from_string($spec, $fieldName);
$obj->setTable($table);
$obj->setValue($value, $this, false);
return $obj;
// 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);
// 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);
}
}
@ -3018,7 +3008,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Traverses to a field referenced by relationships between data objects, returning the value
* The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
*
* @param $fieldPath string
* @param $fieldName string
* @return string | null - will return null on a missing value
*/
public function relField($fieldName) {
@ -3155,24 +3145,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$cacheKey = md5(var_export($cacheComponents, true));
// Flush destroyed items out of the cache
if($cache && isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])
&& DataObject::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
&& DataObject::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
if($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
&& self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
&& self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
DataObject::$_cache_get_one[$callerClass][$cacheKey] = false;
self::$_cache_get_one[$callerClass][$cacheKey] = false;
}
if(!$cache || !isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])) {
if(!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
$item = $dl->First();
if($cache) {
DataObject::$_cache_get_one[$callerClass][$cacheKey] = $item;
if(!DataObject::$_cache_get_one[$callerClass][$cacheKey]) {
DataObject::$_cache_get_one[$callerClass][$cacheKey] = false;
self::$_cache_get_one[$callerClass][$cacheKey] = $item;
if(!self::$_cache_get_one[$callerClass][$cacheKey]) {
self::$_cache_get_one[$callerClass][$cacheKey] = false;
}
}
}
return $cache ? DataObject::$_cache_get_one[$callerClass][$cacheKey] : $item;
return $cache ? self::$_cache_get_one[$callerClass][$cacheKey] : $item;
}
/**
@ -3185,13 +3175,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function flushCache($persistent = true) {
if($this->class == 'DataObject') {
DataObject::$_cache_get_one = array();
self::$_cache_get_one = array();
return $this;
}
$classes = ClassInfo::ancestry($this->class);
foreach($classes as $class) {
if(isset(DataObject::$_cache_get_one[$class])) unset(DataObject::$_cache_get_one[$class]);
if(isset(self::$_cache_get_one[$class])) unset(self::$_cache_get_one[$class]);
}
$this->extend('flushCache');
@ -3204,27 +3194,25 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Flush the get_one global cache and destroy associated objects.
*/
public static function flush_and_destroy_cache() {
if(DataObject::$_cache_get_one) foreach(DataObject::$_cache_get_one as $class => $items) {
if(self::$_cache_get_one) foreach(self::$_cache_get_one as $class => $items) {
if(is_array($items)) foreach($items as $item) {
if($item) $item->destroy();
}
}
DataObject::$_cache_get_one = array();
self::$_cache_get_one = array();
}
/**
* Reset all global caches associated with DataObject.
*/
public static function reset() {
self::clear_classname_spec_cache();
DataObject::$cache_has_own_table = array();
DataObject::$_cache_db = array();
DataObject::$_cache_get_one = array();
DataObject::$_cache_composite_fields = array();
DataObject::$_cache_is_composite_field = array();
DataObject::$_cache_custom_database_fields = array();
DataObject::$_cache_get_class_ancestry = array();
DataObject::$_cache_field_labels = array();
DBClassName::clear_classname_cache();
self::$_cache_has_own_table = array();
self::$_cache_get_one = array();
self::$_cache_composite_fields = array();
self::$_cache_database_fields = array();
self::$_cache_get_class_ancestry = array();
self::$_cache_field_labels = array();
}
/**
@ -3450,25 +3438,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
/**
* Returns fields bu traversing the class heirachy in a bottom-up direction.
*
* Needed to avoid getCMSFields being empty when customDatabaseFields overlooks
* the inheritance chain of the $db array, where a child data object has no $db array,
* but still needs to know the properties of its parent. This should be merged into databaseFields or
* customDatabaseFields.
*
* @todo review whether this is still needed after recent API changes
* @deprecated since version 4.0
*/
public function inheritedDatabaseFields() {
$fields = array();
$currentObj = $this->class;
while($currentObj != 'DataObject') {
$fields = array_merge($fields, self::custom_database_fields($currentObj));
$currentObj = get_parent_class($currentObj);
}
return (array) $fields;
Deprecation::notice('4.0', 'Use db() instead');
return $this->db();
}
/**
@ -3736,13 +3710,10 @@ 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',
"LastEdited" => "SS_Datetime",
"Created" => "SS_Datetime",
"Title" => 'Text',
);

View File

@ -207,7 +207,8 @@ class DataQuery {
$selectColumns = null;
if ($queriedColumns) {
// Restrict queried columns to that on the selected table
$tableFields = DataObject::database_fields($tableClass, false);
$tableFields = DataObject::database_fields($tableClass);
unset($tableFields['ID']);
$selectColumns = array_intersect($queriedColumns, array_keys($tableFields));
}
@ -445,9 +446,10 @@ class DataQuery {
*/
protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) {
// Add SQL for multi-value fields
$databaseFields = DataObject::database_fields($tableClass, false);
$databaseFields = DataObject::database_fields($tableClass);
$compositeFields = DataObject::composite_fields($tableClass, false);
if($databaseFields) foreach($databaseFields as $k => $v) {
unset($databaseFields['ID']);
foreach($databaseFields as $k => $v) {
if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
// Update $collidingFields if necessary
if($expressionForField = $query->expressionForField($k)) {
@ -459,9 +461,10 @@ class DataQuery {
}
}
}
if($compositeFields) foreach($compositeFields as $k => $v) {
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);
}
}
@ -761,7 +764,7 @@ class DataQuery {
$query->setSelect(array());
$query->selectField($fieldExpression, $field);
$this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
return $query->execute()->column($field);
}

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

@ -401,7 +401,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
if ($suffix) $table = "{$classTable}_$suffix";
else $table = $classTable;
if($fields = DataObject::database_fields($this->owner->class)) {
$fields = DataObject::database_fields($this->owner->class);
unset($fields['ID']);
if($fields) {
$options = Config::inst()->get($this->owner->class, 'create_table_options', Config::FIRST_SET);
$indexes = $this->owner->databaseIndexes();
if ($suffix && ($ext = $this->owner->getExtensionInstance($allSuffixes[$suffix]))) {

View File

@ -328,7 +328,9 @@ abstract class DBSchemaManager {
}
//DB ABSTRACTION: we need to convert this to a db-specific version:
$this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
if(!isset($fieldSchema['ID'])) {
$this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
}
// Create custom fields
if ($fieldSchema) {
@ -347,6 +349,11 @@ abstract class DBSchemaManager {
$fieldObj->arrayValue = $arrayValue;
$fieldObj->setTable($table);
if($fieldObj instanceof PrimaryKey) {
$fieldObj->setAutoIncrement($hasAutoIncPK);
}
$fieldObj->requireField();
}
}

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,211 @@ 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;
}
public function castingHelper($field) {
$fields = $this->compositeDatabaseFields();
if(isset($fields[$field])) {
return $fields[$field];
}
parent::castingHelper($field);
}
}

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

@ -15,21 +15,47 @@ class PrimaryKey extends Int {
private static $default_search_filter_class = 'ExactMatchFilter';
/**
* @var bool
*/
protected $autoIncrement = true;
public function setAutoIncrement($autoIncrement) {
$this->autoIncrement = $autoIncrement;
return $this;
}
public function getAutoIncrement() {
return $this->autoIncrement;
}
public function requireField() {
$spec = DB::get_schema()->IdColumn(false, $this->getAutoIncrement());
DB::require_field($this->getTable(), $this->getName(), $spec);
}
/**
* @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);
}
public function scaffoldFormField($title = null, $params = null) {
$titleField = ($this->object->hasField('Title')) ? 'Title' : 'Name';
$map = DataList::create(get_class($this->object))->map('ID', $titleField);
$field = new DropdownField($this->name, $title, $map);
$field->setEmptyString(' ');
return $field;
return null;
}
public function scaffoldSearchField($title = null) {
parent::scaffoldFormField($title);
}
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

@ -1012,7 +1012,7 @@ class Security extends Controller implements TemplateGlobalProvider {
$dbFields = DB::field_list($table);
if(!$dbFields) return false;
$objFields = DataObject::database_fields($table, false);
$objFields = DataObject::database_fields($table);
$missingFields = array_diff_key($objFields, $dbFields);
if($missingFields) return false;

View File

@ -16,6 +16,22 @@ 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'));
// Test getField accessor
$this->assertTrue($obj->MyMoney instanceof Money);
$this->assertTrue($obj->MyMoney->hasField('Amount'));
$obj->MyMoney->Amount = 100.00;
$this->assertEquals(100.00, $obj->MyMoney->Amount);
$this->assertEquals(100.00, $obj->MyMoneyAmount);
// Not strictly correct
$this->assertFalse($obj->dbObject('MyMoney')->hasField('MyMoneyAmount'));
$this->assertFalse($obj->dbObject('MyMoney')->hasField('MyMoneyCurrency'));
$this->assertFalse($obj->dbObject('MyMoney')->hasField('MyMoney'));
}
/**
@ -23,17 +39,65 @@ 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->assertFalse(DataObject::is_composite_field('CompositeDBFieldTest_DataObject', 'Title'));
$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->assertFalse(DataObject::is_composite_field('SubclassedDBFieldObject', 'Title'));
$this->assertFalse(DataObject::is_composite_field('SubclassedDBFieldObject', 'OtherField'));
$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 +105,7 @@ class CompositeDBFieldTest_DataObject extends DataObject implements TestOnly {
private static $db = array(
'Title' => 'Text',
'MyMoney' => 'Money',
'OverriddenMoney' => 'Money'
);
}
@ -48,5 +113,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

@ -129,32 +129,46 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
public function testClassNameSpecGeneration() {
// Test with blank entries
DataObject::clear_classname_spec_cache();
DBClassName::clear_classname_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();
DBClassName::clear_classname_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();
// Test with instance of main class
$item2 = new DataObjectSchemaGenerationTest_DO();
$item2->write();
DataObject::clear_classname_spec_cache();
DBClassName::clear_classname_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();
@ -163,11 +177,15 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$item1->write();
$item2 = new DataObjectSchemaGenerationTest_DO();
$item2->write();
DataObject::clear_classname_spec_cache();
DBClassName::clear_classname_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

@ -35,19 +35,29 @@ class DataObjectTest extends SapphireTest {
// Assert fields are included
$this->assertArrayHasKey('Name', $dbFields);
// Assert the base fields are excluded
$this->assertArrayNotHasKey('Created', $dbFields);
$this->assertArrayNotHasKey('LastEdited', $dbFields);
$this->assertArrayNotHasKey('ClassName', $dbFields);
$this->assertArrayNotHasKey('ID', $dbFields);
// Assert the base fields are included
$this->assertArrayHasKey('Created', $dbFields);
$this->assertArrayHasKey('LastEdited', $dbFields);
$this->assertArrayHasKey('ClassName', $dbFields);
$this->assertArrayHasKey('ID', $dbFields);
// Assert that the correct field type is returned when passing a field
$this->assertEquals('Varchar', $obj->db('Name'));
$this->assertEquals('Text', $obj->db('Comment'));
// Test with table required
$this->assertEquals('DataObjectTest_TeamComment.Varchar', $obj->db('Name', true));
$this->assertEquals('DataObjectTest_TeamComment.Text', $obj->db('Comment', true));
$obj = new DataObjectTest_ExtendedTeamComment();
$dbFields = $obj->db();
// fixed fields are still included in extended classes
$this->assertArrayHasKey('Created', $dbFields);
$this->assertArrayHasKey('LastEdited', $dbFields);
$this->assertArrayHasKey('ClassName', $dbFields);
$this->assertArrayHasKey('ID', $dbFields);
// Assert overloaded fields have correct data type
$this->assertEquals('HTMLText', $obj->db('Comment'));
$this->assertEquals('HTMLText', $dbFields['Comment'],
@ -55,10 +65,14 @@ class DataObjectTest extends SapphireTest {
// assertEquals doesn't verify the order of array elements, so access keys manually to check order:
// expected: array('Name' => 'Varchar', 'Comment' => 'HTMLText')
reset($dbFields);
$this->assertEquals('Name', key($dbFields), 'DataObject::db returns fields in correct order');
next($dbFields);
$this->assertEquals('Comment', key($dbFields), 'DataObject::db returns fields in correct order');
$this->assertEquals(
array(
'Name',
'Comment'
),
array_slice(array_keys($dbFields), 4, 2),
'DataObject::db returns fields in correct order'
);
}
public function testConstructAcceptsValues() {
@ -807,26 +821,8 @@ class DataObjectTest extends SapphireTest {
$subteamInstance = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
$this->assertEquals(
array_keys($teamInstance->inheritedDatabaseFields()),
array(
//'ID',
//'ClassName',
//'Created',
//'LastEdited',
'Title',
'DatabaseField',
'ExtendedDatabaseField',
'CaptainID',
'HasOneRelationshipID',
'ExtendedHasOneRelationshipID'
),
'inheritedDatabaseFields() contains all fields defined on instance: base, extended and foreign keys'
);
$this->assertEquals(
array_keys(DataObject::database_fields('DataObjectTest_Team', false)),
array(
//'ID',
'ID',
'ClassName',
'LastEdited',
'Created',
@ -837,34 +833,53 @@ class DataObjectTest extends SapphireTest {
'HasOneRelationshipID',
'ExtendedHasOneRelationshipID'
),
'databaseFields() contains only fields defined on instance, including base, extended and foreign keys'
array_keys($teamInstance->db()),
'db() contains all fields defined on instance: base, extended and foreign keys'
);
$this->assertEquals(
array_keys($subteamInstance->inheritedDatabaseFields()),
array(
//'ID',
//'ClassName',
//'Created',
//'LastEdited',
'SubclassDatabaseField',
'ParentTeamID',
'ID',
'ClassName',
'LastEdited',
'Created',
'Title',
'DatabaseField',
'ExtendedDatabaseField',
'CaptainID',
'HasOneRelationshipID',
'ExtendedHasOneRelationshipID'
),
array_keys(DataObjectTest_Team::database_fields()),
'database_fields() contains only fields defined on instance, including base, extended and foreign keys'
);
$this->assertEquals(
array(
'ID',
'ClassName',
'LastEdited',
'Created',
'Title',
'DatabaseField',
'ExtendedDatabaseField',
'CaptainID',
'HasOneRelationshipID',
'ExtendedHasOneRelationshipID',
'SubclassDatabaseField',
'ParentTeamID',
),
array_keys($subteamInstance->db()),
'inheritedDatabaseFields() on subclass contains all fields, including base, extended and foreign keys'
);
$this->assertEquals(
array_keys(DataObject::database_fields('DataObjectTest_SubTeam', false)),
array(
'ID',
'SubclassDatabaseField',
'ParentTeamID',
),
array_keys(DataObject::database_fields('DataObjectTest_SubTeam')),
'databaseFields() on subclass contains only fields defined on instance'
);
}

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

@ -558,7 +558,7 @@ class SecurityTest extends FunctionalTest {
$old = Security::$force_database_is_ready;
Security::$force_database_is_ready = null;
Security::$database_is_ready = false;
DataObject::clear_classname_spec_cache();
DBClassName::clear_classname_cache();
// Assumption: The database has been built correctly by the test runner,
// and has all columns present in the ORM

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);
}
/**