API Move many methods from DataObject to DataObjectSchema

This commit is contained in:
Damian Mooyman 2016-10-06 17:31:38 +13:00
parent f0dd9af699
commit 11bbed4f76
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
24 changed files with 533 additions and 586 deletions

View File

@ -212,10 +212,11 @@ class AssetControlExtension extends DataExtension
{
// Search for dbfile instances
$files = array();
foreach ($record->db() as $field => $db) {
$fields = DataObject::getSchema()->fieldSpecs($record);
foreach ($fields as $field => $db) {
$fieldObj = $record->$field;
if(!is_object($fieldObj) || !($record->$field instanceof DBFile)) {
continue;
if (!($fieldObj instanceof DBFile)) {
continue;
}
// Omit variant and merge with set

View File

@ -112,7 +112,7 @@ class ClassInfo {
);
foreach ($classes as $class) {
if (DataObject::has_own_table($class)) {
if (DataObject::getSchema()->classHasTable($class)) {
$result[$class] = $class;
}
}
@ -201,7 +201,7 @@ class ClassInfo {
if(!isset(self::$_cache_ancestry[$cacheKey])) {
$ancestry = array();
do {
if (!$tablesOnly || DataObject::has_own_table($parent)) {
if (!$tablesOnly || DataObject::getSchema()->classHasTable($parent)) {
$ancestry[$parent] = $parent;
}
} while ($parent = get_parent_class($parent));

View File

@ -222,7 +222,9 @@ class CsvBulkLoader extends BulkLoader {
// find existing object, or create new one
$existingObj = $this->findExistingObject($record, $columnMap);
/** @var DataObject $obj */
$obj = ($existingObj) ? $existingObj : new $class();
$schema = DataObject::getSchema();
// first run: find/create any relations and store them on the object
// we can't combine runs, as other columns might rely on the relation being present
@ -243,7 +245,7 @@ class CsvBulkLoader extends BulkLoader {
$relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
}
if(!$relationObj || !$relationObj->exists()) {
$relationClass = $obj->hasOneComponent($relationName);
$relationClass = $schema->hasOneComponent(get_class($obj), $relationName);
$relationObj = new $relationClass();
//write if we aren't previewing
if (!$preview) $relationObj->write();
@ -327,7 +329,7 @@ class CsvBulkLoader extends BulkLoader {
*
* @param array $record CSV data column
* @param array $columnMap
* @return mixed
* @return DataObject
*/
public function findExistingObject($record, $columnMap = []) {
$SNG_objectClass = singleton($this->objectClass);

View File

@ -87,6 +87,7 @@ class FixtureBlueprint {
try {
$class = $this->class;
$schema = DataObject::getSchema();
$obj = DataModel::inst()->$class->newObject();
// If an ID is explicitly passed, then we'll sort out the initial write straight away
@ -120,11 +121,10 @@ class FixtureBlueprint {
// Populate overrides
if($data) foreach($data as $fieldName => $fieldVal) {
// Defer relationship processing
if(
$obj->manyManyComponent($fieldName)
|| $obj->hasManyComponent($fieldName)
|| $obj->hasOneComponent($fieldName)
$schema->manyManyComponent($class, $fieldName)
|| $schema->hasManyComponent($class, $fieldName)
|| $schema->hasOneComponent($class, $fieldName)
) {
continue;
}
@ -142,8 +142,8 @@ class FixtureBlueprint {
// Populate all relations
if($data) foreach($data as $fieldName => $fieldVal) {
$isManyMany = $obj->manyManyComponent($fieldName);
$isHasMany = $obj->hasManyComponent($fieldName);
$isManyMany = $schema->manyManyComponent($class, $fieldName);
$isHasMany = $schema->hasManyComponent($class, $fieldName);
if ($isManyMany && $isHasMany) {
throw new InvalidArgumentException("$fieldName is both many_many and has_many");
}
@ -207,7 +207,7 @@ class FixtureBlueprint {
}
} else {
$hasOneField = preg_replace('/ID$/', '', $fieldName);
if($className = $obj->hasOneComponent($hasOneField)) {
if($className = $schema->hasOneComponent($class, $hasOneField)) {
$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
// Inject class for polymorphic relation
if($className === 'SilverStripe\\ORM\\DataObject') {

View File

@ -123,8 +123,8 @@ class FileField extends FormField {
/** @var File $file */
if($this->relationAutoSetting) {
// assume that the file is connected via a has-one
$objectClass = $record->hasOneComponent($this->name);
if($objectClass === 'SilverStripe\\Assets\\File' || empty($objectClass)) {
$objectClass = DataObject::getSchema()->hasOneComponent(get_class($record), $this->name);
if($objectClass === File::class || empty($objectClass)) {
// Create object of the appropriate file class
$file = Object::create($fileClass);
} else {

View File

@ -114,6 +114,7 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
$columns = $gridField->getColumns();
$currentColumn = 0;
$schema = DataObject::getSchema();
foreach($columns as $columnField) {
$currentColumn++;
$metadata = $gridField->getColumnMetadata($columnField);
@ -139,7 +140,7 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
} elseif(method_exists($tmpItem, 'hasMethod') && $tmpItem->hasMethod($methodName)) {
// The part is a relation name, so get the object/list from it
$tmpItem = $tmpItem->$methodName();
} elseif($tmpItem instanceof DataObject && $tmpItem->hasDatabaseField($methodName)) {
} elseif ($tmpItem instanceof DataObject && $schema->fieldSpec($tmpItem, $methodName, ['dbOnly'])) {
// Else, if we've found a database field at the end of the chain, we can sort on it.
// If a method is applied further to this field (E.g. 'Cost.Currency') then don't try to sort.
$allowSort = $idx === sizeof($parts) - 1;

View File

@ -543,7 +543,7 @@ class UploadField extends FileField {
if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
// has_many or many_many
$relation->setByIDList($idList);
} elseif($record->hasOneComponent($fieldname)) {
} elseif(DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) {
// has_one
$record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0;
}
@ -631,7 +631,7 @@ class UploadField extends FileField {
if(empty($allowedMaxFileNumber)) {
$record = $this->getRecord();
$name = $this->getName();
if($record && $record->hasOneComponent($name)) {
if($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) {
return 1; // Default for has_one
} else {
return null; // Default for has_many and many_many

View File

@ -219,10 +219,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Static caches used by relevant functions.
*
* @var array
*/
protected static $_cache_has_own_table = array();
protected static $_cache_get_one;
protected static $_cache_get_class_ancestry;
/**
* Cache of field labels
*
* @var array
*/
protected static $_cache_field_labels = array();
/**
@ -277,88 +283,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return DataObjectSchema
*/
public static function getSchema() {
return Injector::inst()->get('SilverStripe\ORM\DataObjectSchema');
}
/**
* Return the complete map of fields to specification on this object, including fixed_fields.
* "ID" will be included on every table.
*
* 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 database_fields($class = null) {
if(empty($class)) {
$class = get_called_class();
}
return static::getSchema()->databaseFields($class);
}
/**
* Get all database columns explicitly defined on a class in {@link DataObject::$db}
* and {@link DataObject::$has_one}. Resolves instances of {@link DBComposite}
* into the actual database fields, rather than the name of the field which
* might not equate a database column.
*
* 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 DBComposite->compositeDatabaseFields()
*
* @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 = null) {
if(empty($class)) {
$class = get_called_class();
}
// Remove fixed fields. This assumes that NO fixed_fields are composite
$fields = static::getSchema()->databaseFields($class);
$fields = array_diff_key($fields, self::config()->fixed_fields);
return $fields;
}
/**
* Returns the field class if the given db field on the class is a composite field.
* Will check all applicable ancestor classes and aggregate results.
*
* @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|false Class spec name of composite field if it exists, or false if not
*/
public static function is_composite_field($class, $name, $aggregated = true) {
$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 = null, $aggregated = true) {
// Check $class
if(empty($class)) {
$class = get_called_class();
}
return static::getSchema()->compositeFields($class, $aggregated);
return Injector::inst()->get(DataObjectSchema::class);
}
/**
@ -381,8 +306,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(!$record) {
$record = array(
'ID' => 0,
'ClassName' => get_class($this),
'RecordClassName' => get_class($this)
'ClassName' => static::class,
'RecordClassName' => static::class
);
}
@ -417,13 +342,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Identify fields that should be lazy loaded, but only on existing records
if(!empty($record['ID'])) {
$currentObj = get_class($this);
while($currentObj != 'SilverStripe\ORM\DataObject') {
$fields = self::custom_database_fields($currentObj);
foreach($fields as $field => $type) {
if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
// Get all field specs scoped to class for later lazy loading
$fields = static::getSchema()->fieldSpecs(static::class, ['includeClass', 'dbOnly']);
foreach($fields as $field => $fieldSpec) {
$fieldClass = strtok($fieldSpec, ".");
if(!array_key_exists($field, $record)) {
$this->record[$field.'_Lazy'] = $fieldClass;
}
$currentObj = get_parent_class($currentObj);
}
}
@ -478,7 +403,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function duplicate($doWrite = true) {
/** @var static $clone */
$clone = Injector::inst()->create(get_class($this), $this->toMap(), false, $this->model );
$clone = Injector::inst()->create(static::class, $this->toMap(), false, $this->model );
$clone->ID = 0;
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
@ -563,7 +488,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public function getClassName() {
$className = $this->getField("ClassName");
if (!ClassInfo::exists($className)) {
return get_class($this);
return static::class;
}
return $className;
}
@ -580,7 +505,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function setClassName($className) {
$className = trim($className);
if(!$className || !is_subclass_of($className, __CLASS__)) {
if(!$className || !is_subclass_of($className, self::class)) {
return $this;
}
@ -607,7 +532,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return DataObject The new instance of the new class, The exact type will be of the class name provided.
*/
public function newClassInstance($newClassName) {
if (!is_subclass_of($newClassName, __CLASS__)) {
if (!is_subclass_of($newClassName, self::class)) {
throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
}
@ -645,7 +570,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
if($this->class == 'SilverStripe\ORM\DataObject') return;
if(static::class === self::class) {
return;
}
// Set up accessors for joined items
if($manyMany = $this->manyMany()) {
@ -813,8 +740,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return string
*/
public function getTitle() {
if($this->hasDatabaseField('Title')) return $this->getField('Title');
if($this->hasDatabaseField('Name')) return $this->getField('Name');
$schema = static::getSchema();
if($schema->fieldSpec($this, 'Title')) {
return $this->getField('Title');
}
if($schema->fieldSpec($this, 'Name')) {
return $this->getField('Name');
}
return "#{$this->ID}";
}
@ -966,8 +898,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
// makes sure we don't merge data like ID or ClassName
$rightData = $rightObj->db();
$rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
foreach($rightData as $key=>$rightSpec) {
// Don't merge ID
if($key === 'ID') {
@ -1032,11 +963,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public function forceChange() {
// Ensure lazy fields loaded
$this->loadLazyFields();
$fields = static::getSchema()->fieldSpecs(static::class);
// $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->db())
array_keys($fields)
));
foreach($fieldNames as $fieldName) {
@ -1046,7 +978,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
// @todo Find better way to allow versioned to write a new version after forceChange
if($this->isChanged('Version')) unset($this->changed['Version']);
if($this->isChanged('Version')) {
unset($this->changed['Version']);
}
return $this;
}
@ -1154,13 +1088,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->$fieldName = $fieldValue;
}
// Set many-many defaults with an array of ids
if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
if(is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
/** @var ManyManyList $manyManyJoin */
$manyManyJoin = $this->$fieldName();
$manyManyJoin->setByIDList($fieldValue);
}
}
if($class == 'SilverStripe\ORM\DataObject') {
if($class == self::class) {
break;
}
}
@ -1183,7 +1117,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
);
}
if(Config::inst()->get('SilverStripe\ORM\DataObject', 'validation_enabled')) {
if($this->config()->get('validation_enabled')) {
$result = $this->validate();
if (!$result->valid()) {
return new ValidationException(
@ -1246,22 +1180,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param string $class Class of table to manipulate
*/
protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
$table = $this->getSchema()->tableName($class);
$schema = $this->getSchema();
$table = $schema->tableName($class);
$manipulation[$table] = array();
// Extract records for this table
foreach($this->record as $fieldName => $fieldValue) {
// Check if this record pertains to this table, and
// we're not attempting to reset the BaseTable->ID
if( empty($this->changed[$fieldName])
|| ($table === $baseTable && $fieldName === 'ID')
|| (!self::has_own_table_database_field($class, $fieldName)
&& !self::is_composite_field($class, $fieldName, false))
) {
// Ignore unchanged fields or attempts to reset the BaseTable->ID
if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
continue;
}
// Ensure this field pertains to this table
$specification = $schema->fieldSpec($class, $fieldName, ['dbOnly', 'uninherited']);
if (!$specification) {
continue;
}
// if database column doesn't correlate to a DBField instance...
$fieldObj = $this->dbObject($fieldName);
@ -1323,10 +1259,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
protected function writeManipulation($baseTable, $now, $isNewRecord) {
// Generate database manipulations for each class
$manipulation = array();
foreach($this->getClassAncestry() as $class) {
if(self::has_own_table($class)) {
$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
}
foreach(ClassInfo::ancestry(static::class, true) as $class) {
$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
}
// Allow extensions to extend this manipulation
@ -1501,7 +1435,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return array Class ancestry
*/
public function getClassAncestry() {
return ClassInfo::ancestry(get_class($this));
return ClassInfo::ancestry(static::class);
}
/**
@ -1518,12 +1452,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $this->components[$componentName];
}
if($class = $this->hasOneComponent($componentName)) {
$schema = static::getSchema();
if($class = $schema->hasOneComponent(static::class, $componentName)) {
$joinField = $componentName . 'ID';
$joinID = $this->getField($joinField);
// Extract class name for polymorphic relations
if($class === __CLASS__) {
if($class === self::class) {
$class = $this->getField($componentName . 'Class');
if(empty($class)) return null;
}
@ -1539,8 +1474,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(empty($component)) {
$component = $this->model->$class->newObject();
}
} elseif($class = $this->belongsToComponent($componentName)) {
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
} elseif($class = $schema->belongsToComponent(static::class, $componentName)) {
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
$joinID = $this->ID;
if($joinID) {
@ -1591,7 +1526,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public function getComponents($componentName) {
$result = null;
$componentClass = $this->hasManyComponent($componentName);
$schema = $this->getSchema();
$componentClass = $schema->hasManyComponent(static::class, $componentName);
if(!$componentClass) {
throw new InvalidArgumentException(sprintf(
"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
@ -1610,7 +1546,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
// Determine type and nature of foreign relation
$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
/** @var HasManyList $result */
if($polymorphic) {
$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
@ -1635,7 +1571,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function getRelationClass($relationName) {
// Parse many_many
$manyManyComponent = $this->manyManyComponent($relationName);
$manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
if ($manyManyComponent) {
list(
$relationClass, $parentClass, $componentClass,
@ -1702,6 +1638,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public function inferReciprocalComponent($remoteClass, $remoteRelation) {
$remote = DataObject::singleton($remoteClass);
$class = $remote->getRelationClass($remoteRelation);
$schema = static::getSchema();
// Validate arguments
if(!$this->isInDB()) {
@ -1715,7 +1652,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$remoteRelation
));
}
if($class === 'SilverStripe\ORM\DataObject') {
if($class === self::class) {
throw new InvalidArgumentException(sprintf(
"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
"This method does not support polymorphic relationships",
@ -1727,7 +1664,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(!is_a($this, $class, true)) {
throw new InvalidArgumentException(sprintf(
"Relation %s on %s does not refer to objects of type %s",
$remoteRelation, $remoteClass, get_class($this)
$remoteRelation, $remoteClass, static::class
));
}
@ -1737,7 +1674,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
case 'has_one': {
// Mock has_many
$joinField = "{$remoteRelation}ID";
$componentClass = static::getSchema()->classForField($remoteClass, $joinField);
$componentClass = $schema->classForField($remoteClass, $joinField);
$result = HasManyList::create($componentClass, $joinField);
if ($this->model) {
$result->setDataModel($this->model);
@ -1749,7 +1686,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
case 'belongs_to':
case 'has_many': {
// These relations must have a has_one on the other end, so find it
$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
$joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
if ($polymorphic) {
throw new InvalidArgumentException(sprintf(
"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
@ -1773,8 +1710,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
case 'belongs_many_many': {
// Get components and extra fields from parent
list($relationClass, $componentClass, $parentClass, $componentField, $parentField, $table)
= $remote->manyManyComponent($remoteRelation);
$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
= $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
$extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
// Reverse parent and component fields and create an inverse ManyManyList
/** @var RelationList $result */
@ -1798,33 +1735,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
/**
* Tries to find the database key on another object that is used to store a
* relationship to this class. If no join field can be found it defaults to 'ParentID'.
*
* If the remote field is polymorphic then $polymorphic is set to true, and the return value
* is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
*
* @param string $component Name of the relation on the current object pointing to the
* remote object.
* @param string $type the join type - either 'has_many' or 'belongs_to'
* @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
* @return string
* @throws Exception
*/
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
return $this
->getSchema()
->getRemoteJoinField(get_class($this), $component, $type, $polymorphic);
}
/**
* Returns a many-to-many component, as a ManyManyList.
* @param string $componentName Name of the many-many component
* @return RelationList|UnsavedRelationList The set of components
*/
public function getManyManyComponents($componentName) {
$manyManyComponent = $this->manyManyComponent($componentName);
$schema = static::getSchema();
$manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
if(!$manyManyComponent) {
throw new InvalidArgumentException(sprintf(
"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
@ -1845,7 +1763,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $this->unsavedRelations[$componentName];
}
$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
$extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
/** @var RelationList $result */
$result = Injector::inst()->create(
$relationClass,
@ -1887,15 +1805,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
}
/**
* Return data for a specific has_one component.
* @param string $component
* @return string|null
*/
public function hasOneComponent($component) {
return $this->getSchema()->hasOneComponent(get_class($this), $component);
}
/**
* Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
* their class name will be returned.
@ -1913,35 +1822,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
/**
* Return data for a specific belongs_to component.
* @param string $component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|null
*/
public function belongsToComponent($component, $classOnly = true) {
return $this->getSchema()->belongsToComponent(get_class($this), $component, $classOnly);
}
/**
* Return all of the database fields in this object
*
* @param string $fieldName Limit the output to a specific field name
* @param bool $includeClass If returning a single column, prefix the column with the class 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 FieldClass(args)
* format, or RecordClass.FieldClass(args) format if $includeClass is true
*/
public function db($fieldName = null, $includeClass = false) {
if ($fieldName) {
return static::getSchema()->fieldSpecification(static::class, $fieldName, $includeClass);
} else {
return static::getSchema()->fieldSpecifications(static::class);
}
}
/**
* Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
* relationships and their classes will be returned.
@ -1959,17 +1839,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
/**
* Return data for a specific has_many component.
* @param string $component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|null
*/
public function hasManyComponent($component, $classOnly = true) {
return $this->getSchema()->hasManyComponent(get_class($this), $component, $classOnly);
}
/**
* Return the many-to-many extra fields specification.
*
@ -1982,58 +1851,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
}
/**
* Return the many-to-many extra fields specification for a specific component.
* @param string $component
* @return array|null
*/
public function manyManyExtraFieldsForComponent($component) {
// Get all many_many_extraFields defined in this class or parent classes
$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
// Extra fields are immediately available
if(isset($extraFields[$component])) {
return $extraFields[$component];
}
// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$relationName = null;
// Extract class and relation name from dot-notation
if(strpos($candidate, '.') !== false) {
list($candidate, $relationName) = explode('.', $candidate, 2);
}
// If we've not already found the relation name from dot notation, we need to find a relation that points
// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
// so it's safe to assume that it's the correct one
if(!$relationName) {
$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
foreach($candidateManyManys as $relation => $relatedClass) {
if (is_a($this, $relatedClass)) {
$relationName = $relation;
}
}
}
// If we've found a matching relation on the target class, see if we can find extra fields for it
$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
}
return isset($items) ? $items : null;
}
/**
* Return information about a many-to-many component.
* The return value is an array of (parentclass, childclass). If $component is null, then all many-many
* components are returned.
*
* @see DataObject::manyManyComponent()
* @see DataObjectSchema::manyManyComponent()
* @return array|null An array of (parentclass, childclass), or an array of all many-many components
*/
public function manyMany() {
@ -2043,30 +1866,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $items;
}
/**
* Return information about a specific many_many component. Returns a numeric array.
* The first item in the array will be the class name of the relation: either
* ManyManyList or ManyManyThroughList.
*
* Standard many_many return type is:
*
* array(
* <manyManyClass>, Name of class for relation
* <classname>, The class that relation is defined in e.g. "Product"
* <candidateName>, The target class of the relation e.g. "Category"
* <parentField>, The field name pointing to <classname>'s table e.g. "ProductID"
* <childField>, The field name pointing to <candidatename>'s table e.g. "CategoryID"
* <joinTableOrRelation> The join table between the two classes e.g. "Product_Categories".
* If the class name is 'ManyManyThroughList' then this is the name of the
* has_many relation.
* )
* @param string $component The component name
* @return array|null
*/
public function manyManyComponent($component) {
return $this->getSchema()->manyManyComponent(get_class($this), $component);
}
/**
* This returns an array (if it exists) describing the database extensions that are required, or false if none
*
@ -2284,7 +2083,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Called by {@link __get()} and any getFieldName() methods you might create.
*
* @param string $field The name of the field
*
* @return mixed The field value
*/
public function getField($field) {
@ -2300,7 +2098,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
// In case of complex fields, return the DBField object
if(self::is_composite_field($this->class, $field)) {
if (static::getSchema()->compositeField(static::class, $field)) {
$this->record[$field] = $this->dbObject($field);
}
@ -2343,7 +2141,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Limit query to the current record, unless it has the Versioned extension,
// in which case it requires special handling through augmentLoadLazyFields()
$baseIDColumn = static::getSchema()->sqlColumnForField($this, 'ID');
$schema = static::getSchema();
$baseIDColumn = $schema->sqlColumnForField($this, 'ID');
$dataQuery->where([
$baseIDColumn => $this->record['ID']
])->limit(1);
@ -2352,8 +2151,8 @@ 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($class);
if($databaseFields) foreach($databaseFields as $k => $v) {
$databaseFields = $schema->databaseFields($class, false);
foreach($databaseFields as $k => $v) {
if(!isset($this->record[$k]) || $this->record[$k] === null) {
$columns[] = $k;
}
@ -2426,7 +2225,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
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());
$fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
$fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
} else {
$fields = $this->changed;
}
@ -2505,8 +2305,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// 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
if(is_object($val) && $this->db($fieldName)) {
user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
if(is_object($val) && static::getSchema()->fieldSpec(static::class, $fieldName)) {
throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
}
// if a field is not existing or has strictly changed
@ -2516,9 +2316,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// At the very least, the type has changed
$this->changed[$fieldName] = self::CHANGE_STRICT;
if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
&& $this->record[$fieldName] != $val)) {
if ((!isset($this->record[$fieldName]) && $val)
|| (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
) {
// Value has changed as well, not just the type
$this->changed[$fieldName] = self::CHANGE_VALUE;
}
@ -2558,7 +2358,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* {@inheritdoc}
*/
public function castingHelper($field) {
if ($fieldSpec = $this->db($field)) {
$fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
if ($fieldSpec) {
return $fieldSpec;
}
@ -2585,10 +2386,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return boolean True if the given field exists
*/
public function hasField($field) {
$schema = static::getSchema();
return (
array_key_exists($field, $this->record)
|| $this->db($field)
|| (substr($field,-2) == 'ID') && $this->hasOneComponent(substr($field,0, -2))
|| $schema->fieldSpec(static::class, $field)
|| (substr($field,-2) == 'ID') && $schema->hasOneComponent(static::class, substr($field,0, -2))
|| $this->hasMethod("get{$field}")
);
}
@ -2601,57 +2403,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return boolean
*/
public function hasDatabaseField($field) {
return $this->db($field)
&& ! self::is_composite_field(get_class($this), $field);
}
/**
* Returns the field type of the given field, if it belongs to this class, and not a parent.
* Note that the field type will not include constructor arguments in round brackets, only the classname.
*
* @param string $field Name of the field
* @return string The field type of the given field
*/
public function hasOwnTableDatabaseField($field) {
return self::has_own_table_database_field($this->class, $field);
}
/**
* Returns the field type of the given field, if it belongs to this class, and not a parent.
* Note that the field type will not include constructor arguments in round brackets, only the classname.
*
* @param string $class Class name to check
* @param string $field Name of the field
* @return string The field type of the given field
*/
public static function has_own_table_database_field($class, $field) {
$fieldMap = self::database_fields($class);
// Remove string-based "constructor-arguments" from the DBField definition
if(isset($fieldMap[$field])) {
$spec = $fieldMap[$field];
if(is_string($spec)) {
return strtok($spec,'(');
} else {
return $spec['type'];
}
}
return null;
}
/**
* Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
* actually looking in the database.
*
* @param string $dataClass
* @return bool
*/
public static function has_own_table($dataClass) {
if(!is_subclass_of($dataClass, 'SilverStripe\ORM\DataObject')) {
return false;
}
$fields = static::database_fields($dataClass);
return !empty($fields);
$spec = static::getSchema()->fieldSpec(static::class, $field, ['dbOnly']);
return !empty($spec);
}
/**
@ -2671,7 +2424,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
if(Permission::checkMember($member, "ADMIN")) return true;
if($this->manyManyComponent('Can' . $perm)) {
if($this->getSchema()->manyManyComponent(static::class, 'Can' . $perm)) {
if($this->ParentID && $this->SecurityType == 'Inherit') {
if(!($p = $this->Parent)) {
return false;
@ -2834,8 +2587,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function dbObject($fieldName) {
// Check for field in DB
$helper = $this->db($fieldName, true);
$helper = static::getSchema()->fieldSpec(static::class, $fieldName, ['includeClass']);
if(!$helper) {
return null;
}
@ -2983,15 +2735,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return DataList The objects matching the filter, in the class specified by $containerClass
*/
public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
$containerClass = 'SilverStripe\ORM\DataList') {
$containerClass = DataList::class) {
if($callerClass == null) {
$callerClass = get_called_class();
if($callerClass == 'SilverStripe\ORM\DataObject') {
if ($callerClass == self::class) {
throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
}
if($filter || $sort || $join || $limit || ($containerClass != 'SilverStripe\ORM\DataList')) {
if($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
. ' arguments');
}
@ -3070,7 +2822,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return DataObject $this
*/
public function flushCache($persistent = true) {
if($this->class == __CLASS__) {
if($this->class == self::class) {
self::$_cache_get_one = array();
return $this;
}
@ -3108,7 +2860,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
DBClassName::clear_classname_cache();
ClassInfo::reset_db_cache();
static::getSchema()->reset();
self::$_cache_has_own_table = array();
self::$_cache_get_one = array();
self::$_cache_field_labels = array();
}
@ -3231,7 +2982,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
if(get_parent_class($this) == 'SilverStripe\ORM\DataObject') {
if(get_parent_class($this) == self::class) {
$indexes['ClassName'] = true;
}
@ -3245,14 +2996,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function requireTable() {
// Only build the table if we've actually got fields
$fields = self::database_fields($this->class);
$table = static::getSchema()->tableName($this->class);
$extensions = self::database_extensions($this->class);
$schema = static::getSchema();
$fields = $schema->databaseFields(static::class, false);
$table = $schema->tableName(static::class);
$extensions = self::database_extensions(static::class);
$indexes = $this->databaseIndexes();
if($fields) {
$hasAutoIncPK = get_parent_class($this) === __CLASS__;
$hasAutoIncPK = get_parent_class($this) === self::class;
DB::require_table(
$table, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), $extensions
);
@ -3265,7 +3017,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$extras = $this->uninherited('many_many_extraFields');
foreach($manyMany as $component => $spec) {
// Get many_many spec
$manyManyComponent = $this->getSchema()->manyManyComponent(get_class($this), $component);
$manyManyComponent = $schema->manyManyComponent(static::class, $component);
list(
$relationClass, $parentClass, $componentClass,
$parentField, $childField, $tableOrClass
@ -3343,6 +3095,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$fields = array();
// remove the custom getters as the search should not include them
$schema = static::getSchema();
if($summaryFields) {
foreach($summaryFields as $key => $name) {
$spec = $name;
@ -3352,9 +3105,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$name = substr($name, 0, $fieldPos);
}
if($this->hasDatabaseField($name)) {
if ($schema->fieldSpec($this, $name)) {
$fields[] = $name;
} elseif($this->relObject($spec)) {
} elseif ($this->relObject($spec)) {
$fields[] = $spec;
}
}
@ -3441,7 +3194,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$ancestry = ClassInfo::ancestry($this->class);
$ancestry = array_reverse($ancestry);
if($ancestry) foreach($ancestry as $ancestorClass) {
if($ancestorClass == 'SilverStripe\\View\\ViewableData') break;
if($ancestorClass === ViewableData::class) {
break;
}
$types = array(
'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
);
@ -3500,10 +3255,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if (!$fields) {
$fields = array();
// try to scaffold a couple of usual suspects
if ($this->hasField('Name')) $fields['Name'] = 'Name';
if ($this->hasDatabaseField('Title')) $fields['Title'] = 'Title';
if ($this->hasField('Description')) $fields['Description'] = 'Description';
if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
if ($this->hasField('Name')) {
$fields['Name'] = 'Name';
}
if (static::getSchema()->fieldSpec($this, 'Title')) {
$fields['Title'] = 'Title';
}
if ($this->hasField('Description')) {
$fields['Description'] = 'Description';
}
if ($this->hasField('FirstName')) {
$fields['FirstName'] = 'First Name';
}
}
$this->extend("updateSummaryFields", $fields);
@ -3825,7 +3588,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function hasValue($field, $arguments = null, $cache = true) {
// has_one fields should not use dbObject to check if a value is given
if(!$this->hasOneComponent($field) && ($obj = $this->dbObject($field))) {
$hasOne = static::getSchema()->hasOneComponent(static::class, $field);
if(!$hasOne && ($obj = $this->dbObject($field))) {
return $obj->exists();
} else {
return parent::hasValue($field, $arguments, $cache);
@ -3851,7 +3615,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public function setJoin(DataObject $object, $alias = null) {
$this->joinRecord = $object;
if ($alias) {
if ($this->db($alias)) {
if (static::getSchema()->fieldSpec(static::class, $alias)) {
throw new InvalidArgumentException(
"Joined record $alias cannot also be a db field"
);

View File

@ -140,17 +140,37 @@ class DataObjectSchema {
/**
* Get all DB field specifications for a class, including ancestors and composite fields.
*
* @param string $class
* @param string|DataObject $classOrInstance
* @param array $options Array of options. Specify any number of the below:
* - `uninherited`: Set to true to limit to only this table
* - `dbOnly`: Exclude virtual fields (such as composite fields), and only include fields with a db column.
* - `includeClass`: If true prefix the field specification with the class name in RecordClass.Column(spec) format.
* @return array
*/
public function fieldSpecifications($class) {
$classes = ClassInfo::ancestry($class, true);
public function fieldSpecs($classOrInstance, $options = []) {
$class = ClassInfo::class_name($classOrInstance);
$uninherited = !empty($options['uninherited']) || in_array('uninherited', $options);
$dbOnly = !empty($options['dbOnly']) || in_array('dbOnly', $options);
$includeClass = !empty($options['includeClass']) || in_array('includeClass', $options);
// Walk class hierarchy
$db = [];
$classes = $uninherited ? [$class] : ClassInfo::ancestry($class);
foreach($classes as $tableClass) {
// Merge fields with new fields and composite fields
$fields = $this->databaseFields($tableClass);
$compositeFields = $this->compositeFields($tableClass, false);
$db = array_merge($db, $fields, $compositeFields);
// Find all fields on this class
$fields = $this->databaseFields($tableClass, false);
// Merge with composite fields
if (!$dbOnly) {
$compositeFields = $this->compositeFields($tableClass, false);
$fields = array_merge($fields, $compositeFields);
}
// Record specification
foreach ($fields as $name => $specification) {
$prefix = $includeClass ? "{$tableClass}." : "";
$db[$name] = $prefix . $specification;
}
}
return $db;
}
@ -159,30 +179,18 @@ class DataObjectSchema {
/**
* Get specifications for a single class field
*
* @param string $class
* @param string|DataObject $classOrInstance Name or instance of class
* @param string $fieldName
* @param bool $includeClass If returning a single column, prefix the column with the class name
* in RecordClass.Column(spec) format
* @param array $options Array of options. Specify any number of the below:
* - `uninherited`: Set to true to limit to only this table
* - `dbOnly`: Exclude virtual fields (such as composite fields), and only include fields with a db column.
* - `includeClass`: If true prefix the field specification with the class name in RecordClass.Column(spec) format.
* @return string|null Field will be a string in FieldClass(args) format, or
* RecordClass.FieldClass(args) format if $includeClass is true. Will be null if no field is found.
*/
public function fieldSpecification($class, $fieldName, $includeClass = false) {
$classes = array_reverse(ClassInfo::ancestry($class, true));
foreach($classes as $tableClass) {
// Merge fields with new fields and composite fields
$fields = $this->databaseFields($tableClass);
$compositeFields = $this->compositeFields($tableClass, false);
$db = array_merge($fields, $compositeFields);
// Check for search field
if(isset($db[$fieldName])) {
$prefix = $includeClass ? "{$tableClass}." : "";
return $prefix . $db[$fieldName];
}
}
// At end of search complete
return null;
public function fieldSpec($classOrInstance, $fieldName, $options = []) {
$specs = $this->fieldSpecs($classOrInstance, $options);
return isset($specs[$fieldName]) ? $specs[$fieldName] : null;
}
/**
@ -249,7 +257,7 @@ class DataObjectSchema {
// Generate default table name
if(!$table) {
$separator = $this->config()->table_namespace_separator;
$separator = $this->config()->get('table_namespace_separator');
$table = str_replace('\\', $separator, trim($class, '\\'));
}
@ -261,15 +269,48 @@ class DataObjectSchema {
* "ID" will be included on every table.
*
* @param string $class Class name to query from
* @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
* @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
*/
public function databaseFields($class) {
public function databaseFields($class, $aggregated = true) {
$class = ClassInfo::class_name($class);
if($class === DataObject::class) {
return [];
}
$this->cacheDatabaseFields($class);
return $this->databaseFields[$class];
$fields = $this->databaseFields[$class];
if (!$aggregated) {
return $fields;
}
// Recursively merge
$parentFields = $this->databaseFields(get_parent_class($class));
return array_merge($fields, array_diff_key($parentFields, $fields));
}
/**
* Gets a single database field.
*
* @param string $class Class name to query from
* @param string $field Field name
* @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
* @return string|null Field specification, or null if not a field
*/
public function databaseField($class, $field, $aggregated = true) {
$fields = $this->databaseFields($class, $aggregated);
return isset($fields[$field]) ? $fields[$field] : null;
}
/**
* Check if the given class has a table
*
* @param string $class
* @return bool
*/
public function classHasTable($class) {
$fields = $this->databaseFields($class, false);
return !empty($fields);
}
/**
@ -300,7 +341,20 @@ class DataObjectSchema {
// Recursively merge
$parentFields = $this->compositeFields(get_parent_class($class));
return array_merge($compositeFields, $parentFields);
return array_merge($compositeFields, array_diff_key($parentFields, $compositeFields));
}
/**
* Get a composite field for a class
*
* @param string $class Class name to query from
* @param string $field Field name
* @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
* @return string|null Field specification, or null if not a field
*/
public function compositeField($class, $field, $aggregated = true) {
$fields = $this->compositeFields($class, $aggregated);
return isset($fields[$field]) ? $fields[$field] : null;
}
/**
@ -318,7 +372,7 @@ class DataObjectSchema {
$dbFields = array();
// Ensure fixed fields appear at the start
$fixedFields = DataObject::config()->fixed_fields;
$fixedFields = DataObject::config()->get('fixed_fields');
if(get_parent_class($class) === DataObject::class) {
// Merge fixed with ClassName spec and custom db fields
$dbFields = $fixedFields;
@ -357,7 +411,7 @@ class DataObjectSchema {
}
}
// Prevent field-less tables
// Prevent field-less tables with only 'ID'
if(count($dbFields) < 2) {
$dbFields = [];
}
@ -401,14 +455,14 @@ class DataObjectSchema {
}
// Short circuit for fixed fields
$fixed = DataObject::config()->fixed_fields;
$fixed = DataObject::config()->get('fixed_fields');
if(isset($fixed[$fieldName])) {
return $this->baseDataClass($candidateClass);
}
// Find regular field
while($candidateClass) {
$fields = $this->databaseFields($candidateClass);
$fields = $this->databaseFields($candidateClass, false);
if(isset($fields[$fieldName])) {
return $candidateClass;
}
@ -425,7 +479,7 @@ class DataObjectSchema {
* Standard many_many return type is:
*
* array(
* <manyManyClass>, Name of class for relation
* <manyManyClass>, Name of class for relation. E.g. "Categories"
* <classname>, The class that relation is defined in e.g. "Product"
* <candidateName>, The target class of the relation e.g. "Category"
* <parentField>, The field name pointing to <classname>'s table e.g. "ProductID".
@ -449,35 +503,85 @@ class DataObjectSchema {
// Check if the component is defined in belongs_many_many on this class
$belongsManyMany = Config::inst()->get($parentClass, 'belongs_many_many', Config::UNINHERITED);
if(!isset($belongsManyMany[$component])) {
if (!isset($belongsManyMany[$component])) {
continue;
}
// Extract class and relation name from dot-notation
$childClass = $belongsManyMany[$component];
$relationName = null;
if(strpos($childClass, '.') !== false) {
list($childClass, $relationName) = explode('.', $childClass, 2);
}
// We need to find the inverse component name, if not explicitly given
if (!$relationName) {
$relationName = $this->getManyManyInverseRelationship($childClass, $parentClass);
}
// Check valid relation found
if (!$relationName) {
throw new LogicException("Inverse component of $childClass not found ({$class})");
}
list($childClass, $relationName)
= $this->parseBelongsManyManyComponent($parentClass, $component, $belongsManyMany[$component]);
// Build inverse relationship from other many_many, and swap parent/child
list($relationClass, $childClass, $parentClass, $childField, $parentField, $joinTable)
= $this->parseManyManyComponent($childClass, $relationName, $parentClass);
= $this->manyManyComponent($childClass, $relationName);
return [$relationClass, $parentClass, $childClass, $parentField, $childField, $joinTable];
}
return null;
}
/**
* Parse a belongs_many_many component to extract class and relationship name
*
* @param string $parentClass Name of class
* @param string $component Name of relation on class
* @param string $specification specification for this belongs_many_many
* @return array Array with child class and relation name
*/
protected function parseBelongsManyManyComponent($parentClass, $component, $specification)
{
$childClass = $specification;
$relationName = null;
if (strpos($specification, '.') !== false) {
list($childClass, $relationName) = explode('.', $specification, 2);
}
// We need to find the inverse component name, if not explicitly given
if (!$relationName) {
$relationName = $this->getManyManyInverseRelationship($childClass, $parentClass);
}
// Check valid relation found
if (!$relationName) {
throw new LogicException(
"belongs_many_many relation {$parentClass}.{$component} points to "
. "{$specification} without matching many_many"
);
}
// Return relatios
return array($childClass, $relationName);
}
/**
* Return the many-to-many extra fields specification for a specific component.
*
* @param string $class
* @param string $component
* @return array|null
*/
public function manyManyExtraFieldsForComponent($class, $component) {
// Get directly declared many_many_extraFields
$extraFields = Config::inst()->get($class, 'many_many_extraFields');
if (isset($extraFields[$component])) {
return $extraFields[$component];
}
// If not belongs_many_many then there are no components
while ($class && ($class !== DataObject::class)) {
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
if (isset($belongsManyMany[$component])) {
// Reverse relationship and find extrafields from child class
list($childClass, $relationName) = $this->parseBelongsManyManyComponent($class, $component,
$belongsManyMany[$component]);
return $this->manyManyExtraFieldsForComponent($childClass, $relationName);
}
$class = get_parent_class($class);
}
return null;
}
/**
* Return data for a specific has_many component.
*
@ -739,7 +843,7 @@ class DataObjectSchema {
// Validate the join class isn't also the name of a field or relation on either side
// of the relation
$field = $this->fieldSpecification($relationClass, $joinClass);
$field = $this->fieldSpec($relationClass, $joinClass);
if ($field) {
throw new InvalidArgumentException(
"many_many through relation {$parentClass}.{$component} {$key} class {$relationClass} "

View File

@ -240,7 +240,7 @@ class DataQuery {
$selectColumns = null;
if ($queriedColumns) {
// Restrict queried columns to that on the selected table
$tableFields = DataObject::database_fields($tableClass);
$tableFields = $schema->databaseFields($tableClass, false);
unset($tableFields['ID']);
$selectColumns = array_intersect($queriedColumns, array_keys($tableFields));
}
@ -508,14 +508,15 @@ class DataQuery {
*/
protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) {
// Add SQL for multi-value fields
$databaseFields = DataObject::database_fields($tableClass);
$compositeFields = DataObject::composite_fields($tableClass, false);
$schema = DataObject::getSchema();
$databaseFields = $schema->databaseFields($tableClass, false);
$compositeFields = $schema->compositeFields($tableClass, false);
unset($databaseFields['ID']);
foreach($databaseFields as $k => $v) {
if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
// Update $collidingFields if necessary
$expressionForField = $query->expressionForField($k);
$quotedField = DataObject::getSchema()->sqlColumnForField($tableClass, $k);
$quotedField = $schema->sqlColumnForField($tableClass, $k);
if($expressionForField) {
if(!isset($this->collidingFields[$k])) {
$this->collidingFields[$k] = array($expressionForField);
@ -528,7 +529,7 @@ class DataQuery {
}
foreach($compositeFields as $k => $v) {
if((is_null($columns) || in_array($k, $columns)) && $v) {
$tableName = DataObject::getSchema()->tableName($tableClass);
$tableName = $schema->tableName($tableClass);
$dbO = Object::create_from_string($v, $k);
$dbO->setTable($tableName);
$dbO->addToQuery($query);
@ -727,14 +728,14 @@ class DataQuery {
$modelClass = $this->dataClass;
$schema = DataObject::getSchema();
foreach($relation as $rel) {
$model = singleton($modelClass);
if ($component = $model->hasOneComponent($rel)) {
if ($component = $schema->hasOneComponent($modelClass, $rel)) {
// Join via has_one
$this->joinHasOneRelation($modelClass, $rel, $component);
$modelClass = $component;
} elseif ($component = $model->hasManyComponent($rel)) {
} elseif ($component = $schema->hasManyComponent($modelClass, $rel)) {
// Fail on non-linear relations
if($linearOnly) {
throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass");
@ -743,7 +744,7 @@ class DataQuery {
$this->joinHasManyRelation($modelClass, $rel, $component);
$modelClass = $component;
} elseif ($component = $model->manyManyComponent($rel)) {
} elseif ($component = $schema->manyManyComponent($modelClass, $rel)) {
// Fail on non-linear relations
if($linearOnly) {
throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass");
@ -833,8 +834,7 @@ class DataQuery {
// Join table with associated has_one
/** @var DataObject $model */
$model = singleton($localClass);
$foreignKey = $model->getRemoteJoinField($localField, 'has_many', $polymorphic);
$foreignKey = $schema->getRemoteJoinField($localClass, $localField, 'has_many', $polymorphic);
$localIDColumn = $schema->sqlColumnForField($localClass, 'ID');
if($polymorphic) {
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}ID");

View File

@ -336,42 +336,46 @@ class DatabaseAdmin extends Controller {
* corresponding records in their parent class tables.
*/
public function cleanup() {
$allClasses = get_declared_classes();
$baseClasses = [];
foreach($allClasses as $class) {
if(get_parent_class($class) == 'SilverStripe\ORM\DataObject') {
foreach(ClassInfo::subclassesFor(DataObject::class) as $class) {
if(get_parent_class($class) == DataObject::class) {
$baseClasses[] = $class;
}
}
$schema = DataObject::getSchema();
foreach($baseClasses as $baseClass) {
// Get data classes
$baseTable = $schema->baseDataTable($baseClass);
$subclasses = ClassInfo::subclassesFor($baseClass);
unset($subclasses[0]);
foreach($subclasses as $k => $subclass) {
if(DataObject::has_own_table($subclass)) {
if(!DataObject::getSchema()->classHasTable($subclass)) {
unset($subclasses[$k]);
}
}
if($subclasses) {
$records = DB::query("SELECT * FROM \"$baseClass\"");
$records = DB::query("SELECT * FROM \"$baseTable\"");
foreach($subclasses as $subclass) {
$subclassTable = $schema->tableName($subclass);
$recordExists[$subclass] =
DB::query("SELECT \"ID\" FROM \"$subclass\"")->keyedColumn();
DB::query("SELECT \"ID\" FROM \"$subclassTable\"")->keyedColumn();
}
foreach($records as $record) {
foreach($subclasses as $subclass) {
$subclassTable = $schema->tableName($subclass);
$id = $record['ID'];
if(($record['ClassName'] != $subclass) &&
(!is_subclass_of($record['ClassName'], $subclass)) &&
(isset($recordExists[$subclass][$id]))) {
$sql = "DELETE FROM \"$subclass\" WHERE \"ID\" = $record[ID]";
echo "<li>$sql";
DB::query($sql);
if (($record['ClassName'] != $subclass)
&& (!is_subclass_of($record['ClassName'], $subclass))
&& isset($recordExists[$subclass][$id])
) {
$sql = "DELETE FROM \"$subclassTable\" WHERE \"ID\" = ?";
echo "<li>$sql [{$id}]</li>";
DB::prepared_query($sql, [$id]);
}
}
}

View File

@ -39,7 +39,7 @@ class DBForeignKey extends DBInt {
return null;
}
$relationName = substr($this->name,0,-2);
$hasOneClass = $this->object->hasOneComponent($relationName);
$hasOneClass = DataObject::getSchema()->hasOneComponent(get_class($this->object), $relationName);
if(empty($hasOneClass)) {
return null;
}

View File

@ -736,7 +736,7 @@ class Hierarchy extends DataExtension {
if ($hide_from_cms_tree && $this->showingCMSTree()) {
$staged = $staged->exclude('ClassName', $hide_from_cms_tree);
}
if (!$showAll && $this->owner->db('ShowInMenus')) {
if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) {
$staged = $staged->filter('ShowInMenus', 1);
}
$this->owner->extend("augmentStageChildren", $staged, $showAll);
@ -753,7 +753,7 @@ class Hierarchy extends DataExtension {
* @throws Exception
*/
public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
if(!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
if(!$this->owner->hasExtension(Versioned::class)) {
throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
}
@ -773,7 +773,9 @@ class Hierarchy extends DataExtension {
if ($hide_from_cms_tree && $this->showingCMSTree()) {
$children = $children->exclude('ClassName', $hide_from_cms_tree);
}
if(!$showAll && $this->owner->db('ShowInMenus')) $children = $children->filter('ShowInMenus', 1);
if(!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) {
$children = $children->filter('ShowInMenus', 1);
}
return $children;
}

View File

@ -153,7 +153,7 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
* @return string
*/
public function getJoinAlias() {
return $this->getJoinClass();
return DataObject::getSchema()->tableName($this->getJoinClass());
}
/**

View File

@ -575,8 +575,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
public function augmentDatabase() {
$owner = $this->owner;
$class = get_class($owner);
$schema = $owner->getSchema();
$baseTable = $this->baseTable();
$classTable = $owner->getSchema()->tableName($owner);
$classTable = $schema->tableName($owner);
$isRootClass = $class === $owner->baseClass();
@ -606,10 +607,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$suffixTable = $classTable;
}
$fields = DataObject::database_fields($owner->class);
$fields = $schema->databaseFields($class, false);
unset($fields['ID']);
if($fields) {
$options = Config::inst()->get($owner->class, 'create_table_options', Config::FIRST_SET);
$options = Config::inst()->get($class, 'create_table_options', Config::FIRST_SET);
$indexes = $owner->databaseIndexes();
$extensionClass = $allSuffixes[$suffix];
if ($suffix && ($extension = $owner->getExtensionInstance($extensionClass))) {
@ -760,8 +761,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @param int $recordID ID of record to version
*/
protected function augmentWriteVersioned(&$manipulation, $class, $table, $recordID) {
$baseDataClass = DataObject::getSchema()->baseDataClass($class);
$baseDataTable = DataObject::getSchema()->tableName($baseDataClass);
$schema = DataObject::getSchema();
$baseDataClass = $schema->baseDataClass($class);
$baseDataTable = $schema->tableName($baseDataClass);
// Set up a new entry in (table)_versions
$newManipulation = array(
@ -774,8 +776,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$data = DB::prepared_query("SELECT * FROM \"{$table}\" WHERE \"ID\" = ?", array($recordID))->record();
if ($data) {
$fields = DataObject::database_fields($class);
$fields = $schema->databaseFields($class, false);
if (is_array($fields)) {
$data = array_intersect_key($data, $fields);
@ -1383,8 +1384,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
*/
public function canBeVersioned($class) {
return ClassInfo::exists($class)
&& is_subclass_of($class, 'SilverStripe\ORM\DataObject')
&& DataObject::has_own_table($class);
&& is_subclass_of($class, DataObject::class)
&& DataObject::getSchema()->classHasTable($class);
}
/**
@ -1514,11 +1515,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
return;
}
$schema = DataObject::getSchema();
$ownedHasMany = array_intersect($owns, array_keys($hasMany));
foreach($ownedHasMany as $relationship) {
// Find metadata on relationship
$joinClass = $owner->hasManyComponent($relationship);
$joinField = $owner->getRemoteJoinField($relationship, 'has_many', $polymorphic);
$joinClass = $schema->hasManyComponent(get_class($owner), $relationship);
$joinField = $schema->getRemoteJoinField(get_class($owner), $relationship, 'has_many', $polymorphic);
$idField = $polymorphic ? "{$joinField}ID" : $joinField;
$joinTable = DataObject::getSchema()->tableForField($joinClass, $idField);

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Security;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\FormField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObjectInterface;
@ -281,7 +282,11 @@ class PermissionCheckboxSetField extends FormField {
$permission->delete();
}
if($fieldname && $record && ($record->hasManyComponent($fieldname) || $record->manyManyComponent($fieldname))) {
$schema = DataObject::getSchema();
if($fieldname && $record && (
$schema->hasManyComponent(get_class($record), $fieldname)
|| $schema->manyManyComponent(get_class($record), $fieldname)
)) {
if(!$record->ID) $record->write(); // We need a record ID to write permissions

View File

@ -12,6 +12,7 @@ use SilverStripe\Core\Convert;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
@ -1009,18 +1010,18 @@ class Security extends Controller implements TemplateGlobalProvider {
return self::$database_is_ready;
}
$requiredClasses = ClassInfo::dataClassesFor('SilverStripe\\Security\\Member');
$requiredClasses[] = 'SilverStripe\\Security\\Group';
$requiredClasses[] = 'SilverStripe\\Security\\Permission';
$requiredClasses = ClassInfo::dataClassesFor(Member::class);
$requiredClasses[] = Group::class;
$requiredClasses[] = Permission::class;
$schema = DataObject::getSchema();
foreach($requiredClasses as $class) {
// Skip test classes, as not all test classes are scaffolded at once
if(is_subclass_of($class, 'SilverStripe\\Dev\\TestOnly')) {
if(is_a($class, TestOnly::class, true)) {
continue;
}
// if any of the tables aren't created in the database
$table = DataObject::getSchema()->tableName($class);
$table = $schema->tableName($class);
if(!ClassInfo::hasTable($table)) {
return false;
}
@ -1035,7 +1036,7 @@ class Security extends Controller implements TemplateGlobalProvider {
return false;
}
$objFields = DataObject::database_fields($class);
$objFields = $schema->databaseFields($class, false);
$missingFields = array_diff_key($objFields, $dbFields);
if($missingFields) {

View File

@ -859,7 +859,6 @@ A very small number of methods were chosen for deprecation, and will be removed
appropriately on queries.
* `DataList::createDataObject` is now public.
* `DataObject` constructor now has an additional parameter, which must be included in subclasses.
* `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.
* `DataObject::can` has new method signature with `$context` parameter.
@ -910,7 +909,24 @@ A very small number of methods were chosen for deprecation, and will be removed
#### <a name="overview-orm-removed"></a>ORM Removed API
* `DataObject::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs`
* `DataObject::manyManyComponent` moved to `DataObjectSchema`
* `DataObject::belongsToComponent` moved to `DataObjectSchema`
* `DataObject::hasOneComponent` moved to `DataObjectSchema`
* `DataObject::hasManyComponent` moved to `DataObjectSchema`
* `DataObject::getRemoteJoinField` moved to `DataObjectSchema`
* `DataObject::database_fields` renamed and moved to `DataObjectSchema::databaseFields`
* `DataObject::has_own_table` renamed and moved to `DataObjectSchema::classHasTable`
* `DataObject::composite_fields` renamed and moved to `DataObjectSchema::compositeFields``
* `DataObject::manyManyExtraFieldsForComponent` moved to `DataObjectSchema`
* Removed `DataObject::validateModelDefinitions`. Relations are now validated within `DataObjectSchema`
* Removed `DataObject` methods `hasOwnTableDatabaseField`, `has_own_table_database_field` and
`hasDatabaseFields` are superceded by `DataObjectSchema::fieldSpec`.
Use `$schema->fieldSpec($class, $field, ['dbOnly', 'uninherited'])`.
Exclude `uninherited` option to search all tables in the class hierarchy.
* Removed `DataObject::is_composite_field`. Use `DataObjectSchema::compositeField` instead.
* Removed `DataObject::custom_database_fields`. Use `DataObjectSchema::databaseFields`
or `DataObjectSchema::fieldSpecs` instead.
* Removed `DataList::getRelation`, as it was mutable. Use `DataList::applyRelation` instead, which is immutable.
* Removed `DataList::applyFilterContext` private method
* `Member` Field 'RememberLoginToken' removed, replaced with 'RememberLoginHashes' has_many relationship

View File

@ -26,12 +26,15 @@ use SilverStripe\Forms\GridField\GridField_ActionProvider;
use SilverStripe\Forms\GridField\GridField_DataManipulator;
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
class GridFieldTest extends SapphireTest {
protected $extraDataObjects = [
GridFieldTest_Permissions::class,
GridFieldTest_Cheerleader::class,
GridFieldTest_Player::class,
GridFieldTest_Team::class,
];
/**
* @covers SilverStripe\Forms\GridField\GridField::__construct
*/

View File

@ -48,28 +48,29 @@ class DBCompositeTest extends SapphireTest {
* Test DataObject::composite_fields() and DataObject::is_composite_field()
*/
public function testCompositeFieldMetaDataFunctions() {
$this->assertEquals('Money', DataObject::is_composite_field('DBCompositeTest_DataObject', 'MyMoney'));
$this->assertFalse(DataObject::is_composite_field('DBCompositeTest_DataObject', 'Title'));
$schema = DataObject::getSchema();
$this->assertEquals('Money', $schema->compositeField(DBCompositeTest_DataObject::class, 'MyMoney'));
$this->assertNull($schema->compositeField(DBCompositeTest_DataObject::class, 'Title'));
$this->assertEquals(
array(
'MyMoney' => 'Money',
'OverriddenMoney' => 'Money'
),
DataObject::composite_fields('DBCompositeTest_DataObject')
$schema->compositeFields(DBCompositeTest_DataObject::class)
);
$this->assertEquals('Money', DataObject::is_composite_field('SubclassedDBFieldObject', 'MyMoney'));
$this->assertEquals('Money', DataObject::is_composite_field('SubclassedDBFieldObject', 'OtherMoney'));
$this->assertFalse(DataObject::is_composite_field('SubclassedDBFieldObject', 'Title'));
$this->assertFalse(DataObject::is_composite_field('SubclassedDBFieldObject', 'OtherField'));
$this->assertEquals('Money', $schema->compositeField(SubclassedDBFieldObject::class, 'MyMoney'));
$this->assertEquals('Money', $schema->compositeField(SubclassedDBFieldObject::class, 'OtherMoney'));
$this->assertNull($schema->compositeField(SubclassedDBFieldObject::class, 'Title'));
$this->assertNull($schema->compositeField(SubclassedDBFieldObject::class, 'OtherField'));
$this->assertEquals(
array(
'MyMoney' => 'Money',
'OtherMoney' => 'Money',
'OverriddenMoney' => 'Money',
),
DataObject::composite_fields('SubclassedDBFieldObject')
$schema->compositeFields(SubclassedDBFieldObject::class)
);
}

View File

@ -139,11 +139,12 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
* by the order of classnames of existing records
*/
public function testClassNameSpecGeneration() {
$schema = DataObject::getSchema();
// Test with blank entries
DBClassName::clear_classname_cache();
$do1 = new DataObjectSchemaGenerationTest_DO();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
$fields = $schema->databaseFields(DataObjectSchemaGenerationTest_DO::class, false);
/** @skipUpgrade */
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
@ -159,9 +160,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$item1 = new DataObjectSchemaGenerationTest_IndexDO();
$item1->write();
DBClassName::clear_classname_cache();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
/** @skipUpgrade */
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
array(
'DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO',
@ -175,9 +173,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$item2 = new DataObjectSchemaGenerationTest_DO();
$item2->write();
DBClassName::clear_classname_cache();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
/** @skipUpgrade */
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
array(
'DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO',
@ -193,9 +188,6 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
$item2 = new DataObjectSchemaGenerationTest_DO();
$item2->write();
DBClassName::clear_classname_cache();
$fields = DataObject::database_fields('DataObjectSchemaGenerationTest_DO');
/** @skipUpgrade */
$this->assertEquals("DBClassName", $fields['ClassName']);
$this->assertEquals(
array(
'DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO',

View File

@ -12,6 +12,7 @@ use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\View\ViewableData;
/**
* @package framework
@ -60,8 +61,8 @@ class DataObjectTest extends SapphireTest {
}
public function testDb() {
$obj = new DataObjectTest_TeamComment();
$dbFields = $obj->db();
$schema = DataObject::getSchema();
$dbFields = $schema->fieldSpecs(DataObjectTest_TeamComment::class);
// Assert fields are included
$this->assertArrayHasKey('Name', $dbFields);
@ -73,15 +74,20 @@ class DataObjectTest extends SapphireTest {
$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'));
$this->assertEquals('Varchar', $schema->fieldSpec(DataObjectTest_TeamComment::class, 'Name'));
$this->assertEquals('Text', $schema->fieldSpec(DataObjectTest_TeamComment::class, 'Comment'));
// Test with table required
$this->assertEquals('DataObjectTest_TeamComment.Varchar', $obj->db('Name', true));
$this->assertEquals('DataObjectTest_TeamComment.Text', $obj->db('Comment', true));
$this->assertEquals(
'DataObjectTest_TeamComment.Varchar',
$schema->fieldSpec(DataObjectTest_TeamComment::class, 'Name', ['includeClass'])
);
$this->assertEquals(
'DataObjectTest_TeamComment.Text',
$schema->fieldSpec(DataObjectTest_TeamComment::class, 'Comment', ['includeClass'])
);
$obj = new DataObjectTest_ExtendedTeamComment();
$dbFields = $obj->db();
$dbFields = $schema->fieldSpecs(DataObjectTest_ExtendedTeamComment::class);
// fixed fields are still included in extended classes
$this->assertArrayHasKey('Created', $dbFields);
@ -90,7 +96,7 @@ class DataObjectTest extends SapphireTest {
$this->assertArrayHasKey('ID', $dbFields);
// Assert overloaded fields have correct data type
$this->assertEquals('HTMLText', $obj->db('Comment'));
$this->assertEquals('HTMLText', $schema->fieldSpec(DataObjectTest_ExtendedTeamComment::class, 'Comment'));
$this->assertEquals('HTMLText', $dbFields['Comment'],
'Calls to DataObject::db without a field specified return correct data types');
@ -786,7 +792,7 @@ class DataObjectTest extends SapphireTest {
$teamSingleton = singleton('DataObjectTest_Team');
$subteamInstance = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
$subteamSingleton = singleton('DataObjectTest_SubTeam');
$schema = DataObject::getSchema();
/* hasField() singleton checks */
$this->assertTrue($teamSingleton->hasField('ID'),
@ -837,35 +843,35 @@ class DataObjectTest extends SapphireTest {
/* hasDatabaseField() singleton checks */
//$this->assertTrue($teamSingleton->hasDatabaseField('ID'),
//'hasDatabaseField() finds built-in fields in singletons');
$this->assertTrue($teamSingleton->hasDatabaseField('Title'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'Title'),
'hasDatabaseField() finds custom fields in singletons');
/* hasDatabaseField() instance checks */
$this->assertFalse($teamInstance->hasDatabaseField('NonExistingField'),
$this->assertNull($schema->fieldSpec(DataObjectTest_Team::class, 'NonExistingField'),
'hasDatabaseField() doesnt find non-existing fields in instances');
//$this->assertTrue($teamInstance->hasDatabaseField('ID'),
//$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'ID'),
//'hasDatabaseField() finds built-in fields in instances');
$this->assertTrue($teamInstance->hasDatabaseField('Created'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'Created'),
'hasDatabaseField() finds built-in fields in instances');
$this->assertTrue($teamInstance->hasDatabaseField('DatabaseField'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'DatabaseField'),
'hasDatabaseField() finds custom fields in instances');
$this->assertFalse($teamInstance->hasDatabaseField('SubclassDatabaseField'),
$this->assertNull($schema->fieldSpec(DataObjectTest_Team::class, 'SubclassDatabaseField'),
'hasDatabaseField() doesnt find subclass fields in parentclass instances');
//$this->assertFalse($teamInstance->hasDatabaseField('DynamicField'),
//$this->assertNull($schema->fieldSpec(DataObjectTest_Team::class, 'DynamicField'),
//'hasDatabaseField() doesnt dynamic getters in instances');
$this->assertTrue($teamInstance->hasDatabaseField('HasOneRelationshipID'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'HasOneRelationshipID'),
'hasDatabaseField() finds foreign keys in instances');
$this->assertTrue($teamInstance->hasDatabaseField('ExtendedDatabaseField'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'ExtendedDatabaseField'),
'hasDatabaseField() finds extended fields in instances');
$this->assertTrue($teamInstance->hasDatabaseField('ExtendedHasOneRelationshipID'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_Team::class, 'ExtendedHasOneRelationshipID'),
'hasDatabaseField() finds extended foreign keys in instances');
$this->assertFalse($teamInstance->hasDatabaseField('ExtendedDynamicField'),
$this->assertNull($schema->fieldSpec(DataObjectTest_Team::class, 'ExtendedDynamicField'),
'hasDatabaseField() doesnt include extended dynamic getters in instances');
/* hasDatabaseField() subclass checks */
$this->assertTrue($subteamInstance->hasDatabaseField('DatabaseField'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_SubTeam::class, 'DatabaseField'),
'hasField() finds custom fields in subclass instances');
$this->assertTrue($subteamInstance->hasDatabaseField('SubclassDatabaseField'),
$this->assertNotEmpty($schema->fieldSpec(DataObjectTest_SubTeam::class, 'SubclassDatabaseField'),
'hasField() finds custom fields in subclass instances');
}
@ -874,9 +880,10 @@ class DataObjectTest extends SapphireTest {
* @todo Re-enable all test cases for field inheritance aggregation after behaviour has been fixed
*/
public function testFieldInheritance() {
$teamInstance = $this->objFromFixture('DataObjectTest_Team', 'team1');
$subteamInstance = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
$schema = DataObject::getSchema();
// Test logical fields (including composite)
$teamSpecifications = $schema->fieldSpecs(DataObjectTest_Team::class);
$this->assertEquals(
array(
'ID',
@ -891,10 +898,11 @@ class DataObjectTest extends SapphireTest {
'HasOneRelationshipID',
'ExtendedHasOneRelationshipID'
),
array_keys($teamInstance->db()),
'inheritedDatabaseFields() contains all fields defined on instance: base, extended and foreign keys'
array_keys($teamSpecifications),
'fieldSpecifications() contains all fields defined on instance: base, extended and foreign keys'
);
$teamFields = $schema->databaseFields(DataObjectTest_Team::class, false);
$this->assertEquals(
array(
'ID',
@ -909,10 +917,11 @@ class DataObjectTest extends SapphireTest {
'HasOneRelationshipID',
'ExtendedHasOneRelationshipID'
),
array_keys(DataObject::database_fields('DataObjectTest_Team', false)),
array_keys($teamFields),
'databaseFields() contains only fields defined on instance, including base, extended and foreign keys'
);
$subteamSpecifications = $schema->fieldSpecs(DataObjectTest_SubTeam::class);
$this->assertEquals(
array(
'ID',
@ -929,17 +938,18 @@ class DataObjectTest extends SapphireTest {
'SubclassDatabaseField',
'ParentTeamID',
),
array_keys($subteamInstance->db()),
'inheritedDatabaseFields() on subclass contains all fields, including base, extended and foreign keys'
array_keys($subteamSpecifications),
'fieldSpecifications() on subclass contains all fields, including base, extended and foreign keys'
);
$subteamFields = $schema->databaseFields(DataObjectTest_SubTeam::class, false);
$this->assertEquals(
array(
'ID',
'SubclassDatabaseField',
'ParentTeamID',
),
array_keys(DataObject::database_fields('DataObjectTest_SubTeam')),
array_keys($subteamFields),
'databaseFields() on subclass contains only fields defined on instance'
);
}
@ -1103,22 +1113,26 @@ class DataObjectTest extends SapphireTest {
DB::query("SELECT \"ID\" FROM \"DataObjectTest_Team\" WHERE \"Title\" = 'asdfasdf'")->value());
}
public function TestHasOwnTable() {
public function testHasOwnTable() {
$schema = DataObject::getSchema();
/* Test DataObject::has_own_table() returns true if the object has $has_one or $db values */
$this->assertTrue(DataObject::has_own_table("DataObjectTest_Player"));
$this->assertTrue(DataObject::has_own_table("DataObjectTest_Team"));
$this->assertTrue(DataObject::has_own_table("DataObjectTest_Fixture"));
$this->assertTrue($schema->classHasTable(DataObjectTest_Player::class));
$this->assertTrue($schema->classHasTable(DataObjectTest_Team::class));
$this->assertTrue($schema->classHasTable(DataObjectTest_Fixture::class));
/* Root DataObject that always have a table, even if they lack both $db and $has_one */
$this->assertTrue(DataObject::has_own_table("DataObjectTest_FieldlessTable"));
$this->assertTrue($schema->classHasTable(DataObjectTest_FieldlessTable::class));
/* Subclasses without $db or $has_one don't have a table */
$this->assertFalse(DataObject::has_own_table("DataObjectTest_FieldlessSubTable"));
$this->assertFalse($schema->classHasTable(DataObjectTest_FieldlessSubTable::class));
/* Return false if you don't pass it a subclass of DataObject */
$this->assertFalse(DataObject::has_own_table("SilverStripe\\ORM\\DataObject"));
$this->assertFalse(DataObject::has_own_table("SilverStripe\\View\\ViewableData"));
$this->assertFalse(DataObject::has_own_table("ThisIsntADataObject"));
$this->assertFalse($schema->classHasTable(DataObject::class));
$this->assertFalse($schema->classHasTable(ViewableData::class));
// Invalid class
$this->setExpectedException(ReflectionException::class, 'Class ThisIsntADataObject does not exist');
$this->assertFalse($schema->classHasTable("ThisIsntADataObject"));
}
public function testMerge() {
@ -1190,22 +1204,19 @@ class DataObjectTest extends SapphireTest {
public function testValidateModelDefinitionsFailsWithArray() {
Config::inst()->update('DataObjectTest_Team', 'has_one', array('NotValid' => array('NoArraysAllowed')));
$this->setExpectedException(InvalidArgumentException::class);
$object = new DataObjectTest_Team();
$object->hasOneComponent('NotValid');
DataObject::getSchema()->hasOneComponent(DataObjectTest_Team::class, 'NotValid');
}
public function testValidateModelDefinitionsFailsWithIntKey() {
Config::inst()->update('DataObjectTest_Team', 'has_many', array(12 => 'DataObjectTest_Player'));
$this->setExpectedException(InvalidArgumentException::class);
$object = new DataObjectTest_Team();
$object->hasManyComponent(12);
DataObject::getSchema()->hasManyComponent(DataObjectTest_Team::class, 12);
}
public function testValidateModelDefinitionsFailsWithIntValue() {
Config::inst()->update('DataObjectTest_Team', 'many_many', array('Players' => 12));
$this->setExpectedException(InvalidArgumentException::class);
$object = new DataObjectTest_Team();
$object->manyManyComponent('Players');
DataObject::getSchema()->manyManyComponent(DataObjectTest_Team::class, 'Players');
}
public function testNewClassInstance() {
@ -1241,7 +1252,8 @@ class DataObjectTest extends SapphireTest {
$equipmentSuppliers = $team->EquipmentSuppliers();
// Check that DataObject::many_many() works as expected
list($relationClass, $class, $targetClass, $parentField, $childField, $joinTable) = $team->manyManyComponent('Sponsors');
list($relationClass, $class, $targetClass, $parentField, $childField, $joinTable)
= DataObject::getSchema()->manyManyComponent(DataObjectTest_Team::class, 'Sponsors');
$this->assertEquals(ManyManyList::class, $relationClass);
$this->assertEquals('DataObjectTest_Team', $class,
'DataObject::many_many() didn\'t find the correct base class');
@ -1312,8 +1324,8 @@ class DataObjectTest extends SapphireTest {
}
public function testManyManyExtraFields() {
$player = $this->objFromFixture('DataObjectTest_Player', 'player1');
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
$schema = DataObject::getSchema();
// Get all extra fields
$teamExtraFields = $team->manyManyExtraFields();
@ -1330,13 +1342,13 @@ class DataObjectTest extends SapphireTest {
), $teamExtraFields);
// Extra fields are immediately available on the Team class (defined in $many_many_extraFields)
$teamExtraFields = $team->manyManyExtraFieldsForComponent('Players');
$teamExtraFields = $schema->manyManyExtraFieldsForComponent(DataObjectTest_Team::class, 'Players');
$this->assertEquals($teamExtraFields, array(
'Position' => 'Varchar(100)'
));
// We'll have to go through the relation to get the extra fields on Player
$playerExtraFields = $player->manyManyExtraFieldsForComponent('Teams');
$playerExtraFields = $schema->manyManyExtraFieldsForComponent(DataObjectTest_Player::class, 'Teams');
$this->assertEquals($playerExtraFields, array(
'Position' => 'Varchar(100)'
));
@ -1522,7 +1534,7 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals (
'DataObjectTest_Staff',
$company->hasManyComponent('CurrentStaff'),
DataObject::getSchema()->hasManyComponent(DataObjectTest_Company::class, 'CurrentStaff'),
'has_many strips field name data by default on single relationships.'
);
@ -1537,33 +1549,44 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals (
'DataObjectTest_Staff.CurrentCompany',
$company->hasManyComponent('CurrentStaff', false),
DataObject::getSchema()->hasManyComponent(DataObjectTest_Company::class, 'CurrentStaff', false),
'has_many returns field name data on single records when $classOnly is false.'
);
}
public function testGetRemoteJoinField() {
$company = new DataObjectTest_Company();
$schema = DataObject::getSchema();
$staffJoinField = $company->getRemoteJoinField('CurrentStaff', 'has_many', $polymorphic);
// Company schema
$staffJoinField = $schema->getRemoteJoinField(
DataObjectTest_Company::class, 'CurrentStaff', 'has_many', $polymorphic
);
$this->assertEquals('CurrentCompanyID', $staffJoinField);
$this->assertFalse($polymorphic, 'DataObjectTest_Company->CurrentStaff is not polymorphic');
$previousStaffJoinField = $company->getRemoteJoinField('PreviousStaff', 'has_many', $polymorphic);
$previousStaffJoinField = $schema->getRemoteJoinField(
DataObjectTest_Company::class, 'PreviousStaff', 'has_many', $polymorphic
);
$this->assertEquals('PreviousCompanyID', $previousStaffJoinField);
$this->assertFalse($polymorphic, 'DataObjectTest_Company->PreviousStaff is not polymorphic');
$ceo = new DataObjectTest_CEO();
$this->assertEquals('CEOID', $ceo->getRemoteJoinField('Company', 'belongs_to', $polymorphic));
// CEO Schema
$this->assertEquals('CEOID', $schema->getRemoteJoinField(
DataObjectTest_CEO::class, 'Company', 'belongs_to', $polymorphic
));
$this->assertFalse($polymorphic, 'DataObjectTest_CEO->Company is not polymorphic');
$this->assertEquals('PreviousCEOID', $ceo->getRemoteJoinField('PreviousCompany', 'belongs_to', $polymorphic));
$this->assertEquals('PreviousCEOID', $schema->getRemoteJoinField(
DataObjectTest_CEO::class, 'PreviousCompany', 'belongs_to', $polymorphic
));
$this->assertFalse($polymorphic, 'DataObjectTest_CEO->PreviousCompany is not polymorphic');
$team = new DataObjectTest_Team();
$this->assertEquals('Favourite', $team->getRemoteJoinField('Fans', 'has_many', $polymorphic));
// Team schema
$this->assertEquals('Favourite', $schema->getRemoteJoinField(
DataObjectTest_Team::class, 'Fans', 'has_many', $polymorphic
));
$this->assertTrue($polymorphic, 'DataObjectTest_Team->Fans is polymorphic');
$this->assertEquals('TeamID', $team->getRemoteJoinField('Comments', 'has_many', $polymorphic));
$this->assertEquals('TeamID', $schema->getRemoteJoinField(
DataObjectTest_Team::class, 'Comments', 'has_many', $polymorphic
));
$this->assertFalse($polymorphic, 'DataObjectTest_Team->Comments is not polymorphic');
}

View File

@ -206,8 +206,37 @@ class ManyManyThroughListTest extends SapphireTest
'ManyManyThroughListTest_JoinObject' => 'Text'
]);
$this->setExpectedException(InvalidArgumentException::class);
$object = new ManyManyThroughListTest_Object();
$object->manyManyComponent('Items');
DataObject::getSchema()->manyManyComponent(ManyManyThroughListTest_Object::class, 'Items');
}
public function testRelationParsing() {
$schema = DataObject::getSchema();
// Parent components
$this->assertEquals(
[
ManyManyThroughList::class,
ManyManyThroughListTest_Object::class,
ManyManyThroughListTest_Item::class,
'ParentID',
'ChildID',
ManyManyThroughListTest_JoinObject::class
],
$schema->manyManyComponent(ManyManyThroughListTest_Object::class, 'Items')
);
// Belongs_many_many is the same, but with parent/child substituted
$this->assertEquals(
[
ManyManyThroughList::class,
ManyManyThroughListTest_Item::class,
ManyManyThroughListTest_Object::class,
'ChildID',
'ParentID',
ManyManyThroughListTest_JoinObject::class
],
$schema->manyManyComponent(ManyManyThroughListTest_Item::class, 'Objects')
);
}
}

View File

@ -1,5 +1,6 @@
<?php
use SilverStripe\Dev\Debug;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList;
@ -405,32 +406,28 @@ class VersionedTest extends SapphireTest {
* Tests DataObject::hasOwnTableDatabaseField
*/
public function testHasOwnTableDatabaseFieldWithVersioned() {
$noversion = new DataObject();
$versioned = new VersionedTest_DataObject();
$versionedSub = new VersionedTest_Subclass();
$versionedAno = new VersionedTest_AnotherSubclass();
$versionField = new VersionedTest_UnversionedWithField();
$schema = DataObject::getSchema();
$this->assertFalse(
(bool) $noversion->hasOwnTableDatabaseField('Version'),
$this->assertNull(
$schema->fieldSpec(DataObject::class, 'Version', ['uninherited']),
'Plain models have no version field.'
);
$this->assertEquals(
'Int', $versioned->hasOwnTableDatabaseField('Version'),
'Int',
$schema->fieldSpec(VersionedTest_DataObject::class, 'Version', ['uninherited']),
'The versioned ext adds an Int version field.'
);
$this->assertEquals(
null,
$versionedSub->hasOwnTableDatabaseField('Version'),
$this->assertNull(
$schema->fieldSpec(VersionedTest_Subclass::class, 'Version', ['uninherited']),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertNull(
$schema->fieldSpec(VersionedTest_AnotherSubclass::class, 'Version', ['uninherited']),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertEquals(
null,
$versionedAno->hasOwnTableDatabaseField('Version'),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertEquals(
'Varchar', $versionField->hasOwnTableDatabaseField('Version'),
'Varchar(255)',
$schema->fieldSpec(VersionedTest_UnversionedWithField::class, 'Version', ['uninherited']),
'Models w/o Versioned can have their own Version field.'
);
}