mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #4590 from tractorcow/pulls/4.0/make-compositedbfield-useful
API Refactor CompositeDBField into an abstract class
This commit is contained in:
commit
7156718219
@ -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;
|
||||
|
97
docs/en/04_Changelogs/4.0.0.md
Normal file
97
docs/en/04_Changelogs/4.0.0.md
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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]))) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
202
model/fieldtypes/DBClassName.php
Normal file
202
model/fieldtypes/DBClassName.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
121
tests/model/DBClassNameTest.php
Normal file
121
tests/model/DBClassNameTest.php
Normal 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'
|
||||
);
|
||||
}
|
@ -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();
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
@ -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'
|
||||
);
|
||||
|
||||
|
@ -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) {
|
||||
}
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user