From 11bbed4f76b2dab35ed0c1db50114eef4a29c4b7 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 6 Oct 2016 17:31:38 +1300 Subject: [PATCH] API Move many methods from DataObject to DataObjectSchema --- Assets/AssetControlExtension.php | 7 +- Core/ClassInfo.php | 4 +- Dev/CsvBulkLoader.php | 6 +- Dev/FixtureBlueprint.php | 14 +- Forms/FileField.php | 4 +- Forms/GridField/GridFieldSortableHeader.php | 3 +- Forms/UploadField.php | 4 +- ORM/DataObject.php | 486 +++++------------- ORM/DataObjectSchema.php | 212 ++++++-- ORM/DataQuery.php | 22 +- ORM/DatabaseAdmin.php | 28 +- ORM/FieldType/DBForeignKey.php | 2 +- ORM/Hierarchy/Hierarchy.php | 8 +- ORM/ManyManyThroughQueryManipulator.php | 2 +- ORM/Versioning/Versioned.php | 24 +- Security/PermissionCheckboxSetField.php | 7 +- Security/Security.php | 15 +- docs/en/04_Changelogs/4.0.0.md | 18 +- tests/forms/GridFieldTest.php | 11 +- tests/model/DBCompositeTest.php | 17 +- .../model/DataObjectSchemaGenerationTest.php | 12 +- tests/model/DataObjectTest.php | 149 +++--- tests/model/ManyManyThroughListTest.php | 33 +- tests/model/VersionedTest.php | 31 +- 24 files changed, 533 insertions(+), 586 deletions(-) diff --git a/Assets/AssetControlExtension.php b/Assets/AssetControlExtension.php index d3e9679c6..026aac0df 100644 --- a/Assets/AssetControlExtension.php +++ b/Assets/AssetControlExtension.php @@ -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 diff --git a/Core/ClassInfo.php b/Core/ClassInfo.php index 0fdfffd40..1a8d68821 100644 --- a/Core/ClassInfo.php +++ b/Core/ClassInfo.php @@ -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)); diff --git a/Dev/CsvBulkLoader.php b/Dev/CsvBulkLoader.php index eaedfb642..2ee3e1eef 100644 --- a/Dev/CsvBulkLoader.php +++ b/Dev/CsvBulkLoader.php @@ -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); diff --git a/Dev/FixtureBlueprint.php b/Dev/FixtureBlueprint.php index 2630af7a8..5392ed789 100644 --- a/Dev/FixtureBlueprint.php +++ b/Dev/FixtureBlueprint.php @@ -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') { diff --git a/Forms/FileField.php b/Forms/FileField.php index 139a6559a..0f2e2e9c8 100644 --- a/Forms/FileField.php +++ b/Forms/FileField.php @@ -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 { diff --git a/Forms/GridField/GridFieldSortableHeader.php b/Forms/GridField/GridFieldSortableHeader.php index 1c273709a..2f2c99a98 100644 --- a/Forms/GridField/GridFieldSortableHeader.php +++ b/Forms/GridField/GridFieldSortableHeader.php @@ -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; diff --git a/Forms/UploadField.php b/Forms/UploadField.php index a3cee06f0..0db64cb21 100644 --- a/Forms/UploadField.php +++ b/Forms/UploadField.php @@ -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 diff --git a/ORM/DataObject.php b/ORM/DataObject.php index 7f533d0d9..793e10d69 100644 --- a/ORM/DataObject.php +++ b/ORM/DataObject.php @@ -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( - * , Name of class for relation - * , The class that relation is defined in e.g. "Product" - * , The target class of the relation e.g. "Category" - * , The field name pointing to 's table e.g. "ProductID" - * , The field name pointing to 's table e.g. "CategoryID" - * 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 ::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 ::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" ); diff --git a/ORM/DataObjectSchema.php b/ORM/DataObjectSchema.php index 5c758e3c4..f5396f66f 100644 --- a/ORM/DataObjectSchema.php +++ b/ORM/DataObjectSchema.php @@ -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( - * , Name of class for relation + * , Name of class for relation. E.g. "Categories" * , The class that relation is defined in e.g. "Product" * , The target class of the relation e.g. "Category" * , The field name pointing to '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} " diff --git a/ORM/DataQuery.php b/ORM/DataQuery.php index 5aa0aed4d..0c5949180 100644 --- a/ORM/DataQuery.php +++ b/ORM/DataQuery.php @@ -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"); diff --git a/ORM/DatabaseAdmin.php b/ORM/DatabaseAdmin.php index db27a9a06..fd5e3574f 100644 --- a/ORM/DatabaseAdmin.php +++ b/ORM/DatabaseAdmin.php @@ -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 "
  • $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 "
  • $sql [{$id}]
  • "; + DB::prepared_query($sql, [$id]); } } } diff --git a/ORM/FieldType/DBForeignKey.php b/ORM/FieldType/DBForeignKey.php index 62ce353ad..d88439ae9 100644 --- a/ORM/FieldType/DBForeignKey.php +++ b/ORM/FieldType/DBForeignKey.php @@ -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; } diff --git a/ORM/Hierarchy/Hierarchy.php b/ORM/Hierarchy/Hierarchy.php index 623df11eb..076e534f4 100644 --- a/ORM/Hierarchy/Hierarchy.php +++ b/ORM/Hierarchy/Hierarchy.php @@ -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; } diff --git a/ORM/ManyManyThroughQueryManipulator.php b/ORM/ManyManyThroughQueryManipulator.php index 073000350..b22bc19f3 100644 --- a/ORM/ManyManyThroughQueryManipulator.php +++ b/ORM/ManyManyThroughQueryManipulator.php @@ -153,7 +153,7 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator * @return string */ public function getJoinAlias() { - return $this->getJoinClass(); + return DataObject::getSchema()->tableName($this->getJoinClass()); } /** diff --git a/ORM/Versioning/Versioned.php b/ORM/Versioning/Versioned.php index 36989305e..cf00180fa 100644 --- a/ORM/Versioning/Versioned.php +++ b/ORM/Versioning/Versioned.php @@ -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); diff --git a/Security/PermissionCheckboxSetField.php b/Security/PermissionCheckboxSetField.php index d92aae2b8..018757ab2 100644 --- a/Security/PermissionCheckboxSetField.php +++ b/Security/PermissionCheckboxSetField.php @@ -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 diff --git a/Security/Security.php b/Security/Security.php index 526c78e33..d22ccbc34 100644 --- a/Security/Security.php +++ b/Security/Security.php @@ -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) { diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index ed91dc987..1cf671c46 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -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 #### 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 diff --git a/tests/forms/GridFieldTest.php b/tests/forms/GridFieldTest.php index 43fdef5c8..feaf49594 100644 --- a/tests/forms/GridFieldTest.php +++ b/tests/forms/GridFieldTest.php @@ -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 */ diff --git a/tests/model/DBCompositeTest.php b/tests/model/DBCompositeTest.php index c2c1eea83..736173b2f 100644 --- a/tests/model/DBCompositeTest.php +++ b/tests/model/DBCompositeTest.php @@ -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) ); } diff --git a/tests/model/DataObjectSchemaGenerationTest.php b/tests/model/DataObjectSchemaGenerationTest.php index 4b1c4a246..8c0a3439a 100644 --- a/tests/model/DataObjectSchemaGenerationTest.php +++ b/tests/model/DataObjectSchemaGenerationTest.php @@ -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', diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 89d865d34..96dcf4398 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -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'); } diff --git a/tests/model/ManyManyThroughListTest.php b/tests/model/ManyManyThroughListTest.php index fb97d3362..eab6323ca 100644 --- a/tests/model/ManyManyThroughListTest.php +++ b/tests/model/ManyManyThroughListTest.php @@ -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') + ); } } diff --git a/tests/model/VersionedTest.php b/tests/model/VersionedTest.php index fa5c25065..331fe2a01 100644 --- a/tests/model/VersionedTest.php +++ b/tests/model/VersionedTest.php @@ -1,5 +1,6 @@ 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.' ); }