mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #6114 from open-sausages/pulls/4.0/many-many-through
API Implement many_many through
This commit is contained in:
commit
8a2dddb01d
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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') {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -736,9 +736,13 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
||||
* @param array $row
|
||||
* @return DataObject
|
||||
*/
|
||||
protected function createDataObject($row) {
|
||||
public function createDataObject($row) {
|
||||
$class = $this->dataClass;
|
||||
|
||||
if (empty($row['ClassName'])) {
|
||||
$row['ClassName'] = $class;
|
||||
}
|
||||
|
||||
// Failover from RecordClassName to ClassName
|
||||
if(empty($row['RecordClassName'])) {
|
||||
$row['RecordClassName'] = $row['ClassName'];
|
||||
|
@ -153,6 +153,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
protected $record;
|
||||
|
||||
/**
|
||||
* If selected through a many_many through relation, this is the instance of the through record
|
||||
*
|
||||
* @var DataObject
|
||||
*/
|
||||
protected $joinRecord;
|
||||
|
||||
/**
|
||||
* Represents a field that hasn't changed (before === after, thus before == after)
|
||||
*/
|
||||
@ -212,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();
|
||||
|
||||
/**
|
||||
@ -270,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -374,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
|
||||
);
|
||||
}
|
||||
|
||||
@ -410,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,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);
|
||||
@ -556,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;
|
||||
}
|
||||
@ -573,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;
|
||||
}
|
||||
|
||||
@ -600,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");
|
||||
}
|
||||
|
||||
@ -638,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()) {
|
||||
@ -806,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}";
|
||||
}
|
||||
@ -959,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') {
|
||||
@ -1025,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) {
|
||||
@ -1039,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;
|
||||
}
|
||||
|
||||
@ -1147,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;
|
||||
}
|
||||
}
|
||||
@ -1176,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(
|
||||
@ -1239,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);
|
||||
@ -1316,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
|
||||
@ -1422,13 +1363,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function writeComponents($recursive = false) {
|
||||
if(!$this->components) {
|
||||
return $this;
|
||||
if($this->components) {
|
||||
foreach ($this->components as $component) {
|
||||
$component->write(false, false, false, $recursive);
|
||||
}
|
||||
}
|
||||
|
||||
foreach($this->components as $component) {
|
||||
$component->write(false, false, false, $recursive);
|
||||
if ($join = $this->getJoin()) {
|
||||
$join->write(false, false, false, $recursive);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -1491,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1508,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 === 'SilverStripe\\ORM\\DataObject') {
|
||||
if($class === self::class) {
|
||||
$class = $this->getField($componentName . 'Class');
|
||||
if(empty($class)) return null;
|
||||
}
|
||||
@ -1529,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) {
|
||||
@ -1581,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'",
|
||||
@ -1600,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);
|
||||
@ -1624,12 +1570,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return string Class name, or null if not found.
|
||||
*/
|
||||
public function getRelationClass($relationName) {
|
||||
// Parse many_many
|
||||
$manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
|
||||
if ($manyManyComponent) {
|
||||
list(
|
||||
$relationClass, $parentClass, $componentClass,
|
||||
$parentField, $childField, $tableOrClass
|
||||
) = $manyManyComponent;
|
||||
return $componentClass;
|
||||
}
|
||||
|
||||
// Go through all relationship configuration fields.
|
||||
$candidates = array_merge(
|
||||
($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
|
||||
($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
|
||||
($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
|
||||
($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
|
||||
($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
|
||||
);
|
||||
|
||||
@ -1682,9 +1636,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function inferReciprocalComponent($remoteClass, $remoteRelation) {
|
||||
/** @var DataObject $remote */
|
||||
$remote = $remoteClass::singleton();
|
||||
$remote = DataObject::singleton($remoteClass);
|
||||
$class = $remote->getRelationClass($remoteRelation);
|
||||
$schema = static::getSchema();
|
||||
|
||||
// Validate arguments
|
||||
if(!$this->isInDB()) {
|
||||
@ -1698,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",
|
||||
@ -1710,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
|
||||
));
|
||||
}
|
||||
|
||||
@ -1720,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);
|
||||
@ -1732,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" .
|
||||
@ -1755,13 +1709,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
case 'many_many':
|
||||
case 'belongs_many_many': {
|
||||
// Get components and extra fields from parent
|
||||
list($componentClass, $parentClass, $componentField, $parentField, $table)
|
||||
= $remote->manyManyComponent($remoteRelation);
|
||||
$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
|
||||
list($relationClass, $componentClass, $parentClass, $componentField, $parentField, $table)
|
||||
= $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
|
||||
$extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
|
||||
|
||||
// Reverse parent and component fields and create an inverse ManyManyList
|
||||
/** @var ManyManyList $result */
|
||||
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
|
||||
/** @var RelationList $result */
|
||||
$result = Injector::inst()->create(
|
||||
$relationClass, $componentClass, $table, $componentField, $parentField, $extraFields
|
||||
);
|
||||
if($this->model) {
|
||||
$result->setDataModel($this->model);
|
||||
}
|
||||
@ -1779,96 +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) {
|
||||
// Extract relation from current object
|
||||
if($type === 'has_many') {
|
||||
$remoteClass = $this->hasManyComponent($component, false);
|
||||
} else {
|
||||
$remoteClass = $this->belongsToComponent($component, false);
|
||||
}
|
||||
|
||||
if(empty($remoteClass)) {
|
||||
throw new Exception("Unknown $type component '$component' on class '$this->class'");
|
||||
}
|
||||
if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
|
||||
throw new Exception(
|
||||
"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
|
||||
);
|
||||
}
|
||||
|
||||
// If presented with an explicit field name (using dot notation) then extract field name
|
||||
$remoteField = null;
|
||||
if(strpos($remoteClass, '.') !== false) {
|
||||
list($remoteClass, $remoteField) = explode('.', $remoteClass);
|
||||
}
|
||||
|
||||
// Reference remote has_one to check against
|
||||
$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
|
||||
|
||||
// Without an explicit field name, attempt to match the first remote field
|
||||
// with the same type as the current class
|
||||
if(empty($remoteField)) {
|
||||
// look for remote has_one joins on this class or any parent classes
|
||||
$remoteRelationsMap = array_flip($remoteRelations);
|
||||
foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
|
||||
if(array_key_exists($class, $remoteRelationsMap)) {
|
||||
$remoteField = $remoteRelationsMap[$class];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In case of an indeterminate remote field show an error
|
||||
if(empty($remoteField)) {
|
||||
$polymorphic = false;
|
||||
$message = "No has_one found on class '$remoteClass'";
|
||||
if($type == 'has_many') {
|
||||
// include a hint for has_many that is missing a has_one
|
||||
$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
|
||||
$message .= " requires a has_one on '$remoteClass'";
|
||||
}
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
// If given an explicit field name ensure the related class specifies this
|
||||
if(empty($remoteRelations[$remoteField])) {
|
||||
throw new Exception("Missing expected has_one named '$remoteField'
|
||||
on class '$remoteClass' referenced by $type named '$component'
|
||||
on class {$this->class}"
|
||||
);
|
||||
}
|
||||
|
||||
// Inspect resulting found relation
|
||||
if($remoteRelations[$remoteField] === 'SilverStripe\ORM\DataObject') {
|
||||
$polymorphic = true;
|
||||
return $remoteField; // Composite polymorphic field does not include 'ID' suffix
|
||||
} else {
|
||||
$polymorphic = false;
|
||||
return $remoteField . 'ID';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a many-to-many component, as a ManyManyList.
|
||||
* @param string $componentName Name of the many-many component
|
||||
* @return ManyManyList|UnsavedRelationList The set of components
|
||||
* @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'",
|
||||
@ -1877,7 +1751,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
));
|
||||
}
|
||||
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent;
|
||||
list($relationClass, $parentClass, $componentClass, $parentField, $componentField, $tableOrClass)
|
||||
= $manyManyComponent;
|
||||
|
||||
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
|
||||
if(!$this->ID) {
|
||||
@ -1888,9 +1763,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return $this->unsavedRelations[$componentName];
|
||||
}
|
||||
|
||||
$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
|
||||
/** @var ManyManyList $result */
|
||||
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
|
||||
$extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
|
||||
/** @var RelationList $result */
|
||||
$result = Injector::inst()->create(
|
||||
$relationClass,
|
||||
$componentClass,
|
||||
$tableOrClass,
|
||||
$componentField,
|
||||
$parentField,
|
||||
$extraFields
|
||||
);
|
||||
|
||||
|
||||
// Store component data in query meta-data
|
||||
@ -1923,42 +1805,15 @@ 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) {
|
||||
$classes = ClassInfo::ancestry($this, true);
|
||||
|
||||
foreach(array_reverse($classes) as $class) {
|
||||
$hasOnes = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
|
||||
if(isset($hasOnes[$component])) {
|
||||
return $hasOnes[$component];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param string $component - Name of 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|array
|
||||
*/
|
||||
public function belongsTo($component = null, $classOnly = true) {
|
||||
if($component) {
|
||||
Deprecation::notice(
|
||||
'4.0',
|
||||
'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
return $this->belongsToComponent($component, $classOnly);
|
||||
}
|
||||
|
||||
public function belongsTo($classOnly = true) {
|
||||
$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
|
||||
if($belongsTo && $classOnly) {
|
||||
return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
|
||||
@ -1967,87 +1822,15 @@ 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) {
|
||||
$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
|
||||
|
||||
if($belongsTo && array_key_exists($component, $belongsTo)) {
|
||||
$belongsTo = $belongsTo[$component];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$classes = ClassInfo::ancestry($this, true);
|
||||
|
||||
// If we're looking for a specific field, we want to hit subclasses first as they may override field types
|
||||
if($fieldName) {
|
||||
$classes = array_reverse($classes);
|
||||
}
|
||||
|
||||
$db = array();
|
||||
foreach($classes as $class) {
|
||||
// Merge fields with new fields and composite fields
|
||||
$fields = self::database_fields($class);
|
||||
$compositeFields = self::composite_fields($class, false);
|
||||
$db = array_merge($db, $fields, $compositeFields);
|
||||
|
||||
// Check for search field
|
||||
if($fieldName && isset($db[$fieldName])) {
|
||||
// Return found field
|
||||
if(!$includeClass) {
|
||||
return $db[$fieldName];
|
||||
}
|
||||
return $class . "." . $db[$fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
// At end of search complete
|
||||
if($fieldName) {
|
||||
return null;
|
||||
} else {
|
||||
return $db;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param string $component Deprecated - Name of 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|array|false
|
||||
*/
|
||||
public function hasMany($component = null, $classOnly = true) {
|
||||
if($component) {
|
||||
Deprecation::notice(
|
||||
'4.0',
|
||||
'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
return $this->hasManyComponent($component, $classOnly);
|
||||
}
|
||||
|
||||
public function hasMany($classOnly = true) {
|
||||
$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
|
||||
if($hasMany && $classOnly) {
|
||||
return preg_replace('/(.+)?\..+/', '$1', $hasMany);
|
||||
@ -2056,25 +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) {
|
||||
$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
|
||||
|
||||
if($hasMany && array_key_exists($component, $hasMany)) {
|
||||
$hasMany = $hasMany[$component];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the many-to-many extra fields specification.
|
||||
*
|
||||
@ -2087,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() {
|
||||
@ -2148,81 +1866,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return information about a specific many_many component. Returns a numeric array of:
|
||||
* array(
|
||||
* <classname>, The class that relation is defined in e.g. "Product"
|
||||
* <candidateName>, The target class of the relation e.g. "Category"
|
||||
* <parentField>, The field name pointing to <classname>'s table e.g. "ProductID"
|
||||
* <childField>, The field name pointing to <candidatename>'s table e.g. "CategoryID"
|
||||
* <joinTable> The join table between the two classes e.g. "Product_Categories"
|
||||
* )
|
||||
* @param string $component The component name
|
||||
* @return array|null
|
||||
*/
|
||||
public function manyManyComponent($component) {
|
||||
$classes = $this->getClassAncestry();
|
||||
foreach($classes as $class) {
|
||||
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
|
||||
// Check if the component is defined in many_many on this class
|
||||
if(isset($manyMany[$component])) {
|
||||
$candidate = $manyMany[$component];
|
||||
$classTable = static::getSchema()->tableName($class);
|
||||
$candidateTable = static::getSchema()->tableName($candidate);
|
||||
$parentField = "{$classTable}ID";
|
||||
$childField = $class === $candidate ? "ChildID" : "{$candidateTable}ID";
|
||||
$joinTable = "{$classTable}_{$component}";
|
||||
return array($class, $candidate, $parentField, $childField, $joinTable);
|
||||
}
|
||||
|
||||
// Check if the component is defined in belongs_many_many on this class
|
||||
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
|
||||
if(!isset($belongsManyMany[$component])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract class and relation name from dot-notation
|
||||
$candidate = $belongsManyMany[$component];
|
||||
$relationName = null;
|
||||
if(strpos($candidate, '.') !== false) {
|
||||
list($candidate, $relationName) = explode('.', $candidate, 2);
|
||||
}
|
||||
$candidateTable = static::getSchema()->tableName($candidate);
|
||||
$childField = $candidateTable . "ID";
|
||||
|
||||
// We need to find the inverse component name, if not explicitly given
|
||||
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
|
||||
if(!$relationName && $otherManyMany) {
|
||||
foreach($otherManyMany as $inverseComponentName => $childClass) {
|
||||
if($childClass === $class || is_subclass_of($class, $childClass)) {
|
||||
$relationName = $inverseComponentName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check valid relation found
|
||||
if(!$relationName || !$otherManyMany || !isset($otherManyMany[$relationName])) {
|
||||
throw new LogicException("Inverse component of $candidate not found ({$this->class})");
|
||||
}
|
||||
|
||||
// If we've got a relation name (extracted from dot-notation), we can already work out
|
||||
// the join table and candidate class name...
|
||||
$childClass = $otherManyMany[$relationName];
|
||||
$joinTable = "{$candidateTable}_{$relationName}";
|
||||
|
||||
// If we could work out the join table, we've got all the info we need
|
||||
if ($childClass === $candidate) {
|
||||
$parentField = "ChildID";
|
||||
} else {
|
||||
$childTable = static::getSchema()->tableName($childClass);
|
||||
$parentField = "{$childTable}ID";
|
||||
}
|
||||
return array($class, $candidate, $parentField, $childField, $joinTable);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns an array (if it exists) describing the database extensions that are required, or false if none
|
||||
*
|
||||
@ -2440,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) {
|
||||
@ -2456,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);
|
||||
}
|
||||
|
||||
@ -2499,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);
|
||||
@ -2508,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;
|
||||
}
|
||||
@ -2582,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;
|
||||
}
|
||||
@ -2661,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
|
||||
@ -2672,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;
|
||||
}
|
||||
@ -2714,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;
|
||||
}
|
||||
|
||||
@ -2741,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}")
|
||||
);
|
||||
}
|
||||
@ -2757,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2827,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;
|
||||
@ -2990,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;
|
||||
}
|
||||
@ -3139,15 +2735,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return DataList The objects matching the filter, in the class specified by $containerClass
|
||||
*/
|
||||
public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
|
||||
$containerClass = 'SilverStripe\ORM\DataList') {
|
||||
$containerClass = DataList::class) {
|
||||
|
||||
if($callerClass == null) {
|
||||
$callerClass = get_called_class();
|
||||
if($callerClass == 'SilverStripe\ORM\DataObject') {
|
||||
if ($callerClass == self::class) {
|
||||
throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
|
||||
}
|
||||
|
||||
if($filter || $sort || $join || $limit || ($containerClass != 'SilverStripe\ORM\DataList')) {
|
||||
if($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
|
||||
throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
|
||||
. ' arguments');
|
||||
}
|
||||
@ -3226,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;
|
||||
}
|
||||
@ -3264,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();
|
||||
}
|
||||
@ -3387,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;
|
||||
}
|
||||
|
||||
@ -3401,16 +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();
|
||||
|
||||
// Validate relationship configuration
|
||||
$this->validateModelDefinitions();
|
||||
if($fields) {
|
||||
$hasAutoIncPK = get_parent_class($this) === 'SilverStripe\ORM\DataObject';
|
||||
$hasAutoIncPK = get_parent_class($this) === self::class;
|
||||
DB::require_table(
|
||||
$table, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), $extensions
|
||||
);
|
||||
@ -3421,29 +3015,34 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Build any child tables for many_many items
|
||||
if($manyMany = $this->uninherited('many_many')) {
|
||||
$extras = $this->uninherited('many_many_extraFields');
|
||||
foreach($manyMany as $relationship => $childClass) {
|
||||
// Build field list
|
||||
if($this->class === $childClass) {
|
||||
$childField = "ChildID";
|
||||
} else {
|
||||
$childTable = $this->getSchema()->tableName($childClass);
|
||||
$childField = "{$childTable}ID";
|
||||
foreach($manyMany as $component => $spec) {
|
||||
// Get many_many spec
|
||||
$manyManyComponent = $schema->manyManyComponent(static::class, $component);
|
||||
list(
|
||||
$relationClass, $parentClass, $componentClass,
|
||||
$parentField, $childField, $tableOrClass
|
||||
) = $manyManyComponent;
|
||||
|
||||
// Skip if backed by actual class
|
||||
if (class_exists($tableOrClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build fields
|
||||
$manymanyFields = array(
|
||||
"{$table}ID" => "Int",
|
||||
$parentField => "Int",
|
||||
$childField => "Int",
|
||||
);
|
||||
if(isset($extras[$relationship])) {
|
||||
$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
|
||||
if(isset($extras[$component])) {
|
||||
$manymanyFields = array_merge($manymanyFields, $extras[$component]);
|
||||
}
|
||||
|
||||
// Build index list
|
||||
$manymanyIndexes = array(
|
||||
"{$table}ID" => true,
|
||||
$parentField => true,
|
||||
$childField => true,
|
||||
);
|
||||
$manyManyTable = "{$table}_$relationship";
|
||||
DB::require_table($manyManyTable, $manymanyFields, $manymanyIndexes, true, null, $extensions);
|
||||
DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3451,42 +3050,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$this->extend('augmentDatabase', $dummy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the configured relations for this class use the correct syntaxes
|
||||
* @throws LogicException
|
||||
*/
|
||||
protected function validateModelDefinitions() {
|
||||
$modelDefinitions = array(
|
||||
'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
|
||||
'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
|
||||
'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
|
||||
'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
|
||||
'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
|
||||
'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
|
||||
'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
|
||||
);
|
||||
|
||||
foreach($modelDefinitions as $defType => $relations) {
|
||||
if( ! $relations) continue;
|
||||
|
||||
foreach($relations as $k => $v) {
|
||||
if($defType === 'many_many_extraFields') {
|
||||
if(!is_array($v)) {
|
||||
throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
|
||||
. var_export($k, true) . " => " . var_export($v, true)
|
||||
. ". Each many_many_extraFields entry should map to a field specification array.");
|
||||
}
|
||||
} else {
|
||||
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
|
||||
throw new LogicException("$this->class::$defType has a bad entry: "
|
||||
. var_export($k, true). " => " . var_export($v, true) . ". Each map key should be a
|
||||
relationship name, and the map value should be the data class to join to.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default records to database. This function is called whenever the
|
||||
* database is built, after the database tables have all been created. Overload
|
||||
@ -3532,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;
|
||||
@ -3541,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;
|
||||
}
|
||||
}
|
||||
@ -3630,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)
|
||||
);
|
||||
@ -3689,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);
|
||||
|
||||
@ -4014,11 +3588,41 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If selected through a many_many through relation, this is the instance of the joined record
|
||||
*
|
||||
* @return DataObject
|
||||
*/
|
||||
public function getJoin() {
|
||||
return $this->joinRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set joining object
|
||||
*
|
||||
* @param DataObject $object
|
||||
* @param string $alias Alias
|
||||
* @return $this
|
||||
*/
|
||||
public function setJoin(DataObject $object, $alias = null) {
|
||||
$this->joinRecord = $object;
|
||||
if ($alias) {
|
||||
if (static::getSchema()->fieldSpec(static::class, $alias)) {
|
||||
throw new InvalidArgumentException(
|
||||
"Joined record $alias cannot also be a db field"
|
||||
);
|
||||
}
|
||||
$this->record[$alias] = $object;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\ORM;
|
||||
|
||||
use Exception;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\ORM\FieldType\DBComposite;
|
||||
@ -118,7 +119,7 @@ class DataObjectSchema {
|
||||
$class = ClassInfo::class_name($class);
|
||||
$current = $class;
|
||||
while ($next = get_parent_class($current)) {
|
||||
if ($next === 'SilverStripe\ORM\DataObject') {
|
||||
if ($next === DataObject::class) {
|
||||
return $current;
|
||||
}
|
||||
$current = $next;
|
||||
@ -136,6 +137,62 @@ class DataObjectSchema {
|
||||
return $this->tableName($this->baseDataClass($class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all DB field specifications for a class, including ancestors and composite fields.
|
||||
*
|
||||
* @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 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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get specifications for a single class field
|
||||
*
|
||||
* @param string|DataObject $classOrInstance Name or instance of class
|
||||
* @param string $fieldName
|
||||
* @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 fieldSpec($classOrInstance, $fieldName, $options = []) {
|
||||
$specs = $this->fieldSpecs($classOrInstance, $options);
|
||||
return isset($specs[$fieldName]) ? $specs[$fieldName] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the class for the given table
|
||||
*
|
||||
@ -169,8 +226,8 @@ class DataObjectSchema {
|
||||
return;
|
||||
}
|
||||
$this->tableNames = [];
|
||||
foreach(ClassInfo::subclassesFor('SilverStripe\ORM\DataObject') as $class) {
|
||||
if($class === 'SilverStripe\ORM\DataObject') {
|
||||
foreach(ClassInfo::subclassesFor(DataObject::class) as $class) {
|
||||
if($class === DataObject::class) {
|
||||
continue;
|
||||
}
|
||||
$table = $this->buildTableName($class);
|
||||
@ -200,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, '\\'));
|
||||
}
|
||||
|
||||
@ -212,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 === 'SilverStripe\ORM\DataObject') {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -238,7 +328,7 @@ class DataObjectSchema {
|
||||
*/
|
||||
public function compositeFields($class, $aggregated = true) {
|
||||
$class = ClassInfo::class_name($class);
|
||||
if($class === 'SilverStripe\ORM\DataObject') {
|
||||
if($class === DataObject::class) {
|
||||
return [];
|
||||
}
|
||||
$this->cacheDatabaseFields($class);
|
||||
@ -251,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -269,8 +372,8 @@ class DataObjectSchema {
|
||||
$dbFields = array();
|
||||
|
||||
// Ensure fixed fields appear at the start
|
||||
$fixedFields = DataObject::config()->fixed_fields;
|
||||
if(get_parent_class($class) === 'SilverStripe\ORM\DataObject') {
|
||||
$fixedFields = DataObject::config()->get('fixed_fields');
|
||||
if(get_parent_class($class) === DataObject::class) {
|
||||
// Merge fixed with ClassName spec and custom db fields
|
||||
$dbFields = $fixedFields;
|
||||
} else {
|
||||
@ -291,7 +394,7 @@ class DataObjectSchema {
|
||||
// Add in all has_ones
|
||||
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array();
|
||||
foreach($hasOne as $fieldName => $hasOneClass) {
|
||||
if($hasOneClass === 'SilverStripe\ORM\DataObject') {
|
||||
if($hasOneClass === DataObject::class) {
|
||||
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
|
||||
} else {
|
||||
$dbFields["{$fieldName}ID"] = 'ForeignKey';
|
||||
@ -308,7 +411,7 @@ class DataObjectSchema {
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent field-less tables
|
||||
// Prevent field-less tables with only 'ID'
|
||||
if(count($dbFields) < 2) {
|
||||
$dbFields = [];
|
||||
}
|
||||
@ -347,19 +450,19 @@ class DataObjectSchema {
|
||||
public function classForField($candidateClass, $fieldName) {
|
||||
// normalise class name
|
||||
$candidateClass = ClassInfo::class_name($candidateClass);
|
||||
if($candidateClass === 'SilverStripe\\ORM\\DataObject') {
|
||||
if($candidateClass === DataObject::class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -367,4 +470,456 @@ class DataObjectSchema {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* RELATION_MANY_MANY or RELATION_MANY_MANY_THROUGH constant value.
|
||||
*
|
||||
* Standard many_many return type is:
|
||||
*
|
||||
* array(
|
||||
* <manyManyClass>, Name of class for relation. E.g. "Categories"
|
||||
* <classname>, The class that relation is defined in e.g. "Product"
|
||||
* <candidateName>, The target class of the relation e.g. "Category"
|
||||
* <parentField>, The field name pointing to <classname>'s table e.g. "ProductID".
|
||||
* <childField>, The field name pointing to <candidatename>'s table e.g. "CategoryID".
|
||||
* <joinTableOrRelation> The join table between the two classes e.g. "Product_Categories".
|
||||
* If the class name is 'ManyManyThroughList' then this is the name of the
|
||||
* has_many relation.
|
||||
* )
|
||||
* @param string $class Name of class to get component for
|
||||
* @param string $component The component name
|
||||
* @return array|null
|
||||
*/
|
||||
public function manyManyComponent($class, $component) {
|
||||
$classes = ClassInfo::ancestry($class);
|
||||
foreach($classes as $parentClass) {
|
||||
// Check if the component is defined in many_many on this class
|
||||
$manyMany = Config::inst()->get($parentClass, 'many_many', Config::UNINHERITED);
|
||||
if(isset($manyMany[$component])) {
|
||||
return $this->parseManyManyComponent($parentClass, $component, $manyMany[$component]);
|
||||
}
|
||||
|
||||
// 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])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract class and relation name from dot-notation
|
||||
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->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.
|
||||
*
|
||||
* @param string $class Parent class
|
||||
* @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($class, $component, $classOnly = true) {
|
||||
$hasMany = (array)Config::inst()->get($class, 'has_many');
|
||||
if(!isset($hasMany[$component])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove has_one specifier if given
|
||||
$hasMany = $hasMany[$component];
|
||||
$hasManyClass = strtok($hasMany, '.');
|
||||
|
||||
// Validate
|
||||
$this->checkRelationClass($class, $component, $hasManyClass, 'has_many');
|
||||
return $classOnly ? $hasManyClass : $hasMany;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data for a specific has_one component.
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $component
|
||||
* @return string|null
|
||||
*/
|
||||
public function hasOneComponent($class, $component) {
|
||||
$hasOnes = Config::inst()->get($class, 'has_one');
|
||||
if(!isset($hasOnes[$component])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate
|
||||
$relationClass = $hasOnes[$component];
|
||||
$this->checkRelationClass($class, $component, $relationClass, 'has_one');
|
||||
return $relationClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data for a specific belongs_to component.
|
||||
*
|
||||
* @param string $class
|
||||
* @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($class, $component, $classOnly = true) {
|
||||
$belongsTo = (array)Config::inst()->get($class, 'belongs_to');
|
||||
if(!isset($belongsTo[$component])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove has_one specifier if given
|
||||
$belongsTo = $belongsTo[$component];
|
||||
$belongsToClass = strtok($belongsTo, '.');
|
||||
|
||||
// Validate
|
||||
$this->checkRelationClass($class, $component, $belongsToClass, 'belongs_to');
|
||||
return $classOnly ? $belongsToClass : $belongsTo;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $parentClass Parent class name
|
||||
* @param string $component ManyMany name
|
||||
* @param string|array $specification Declaration of many_many relation type
|
||||
* @return array
|
||||
*/
|
||||
protected function parseManyManyComponent($parentClass, $component, $specification)
|
||||
{
|
||||
// Check if this is many_many_through
|
||||
if (is_array($specification)) {
|
||||
// Validate join, parent and child classes
|
||||
$joinClass = $this->checkManyManyJoinClass($parentClass, $component, $specification);
|
||||
$parentClass = $this->checkManyManyFieldClass($parentClass, $component, $joinClass, $specification, 'from');
|
||||
$joinChildClass = $this->checkManyManyFieldClass($parentClass, $component, $joinClass, $specification, 'to');
|
||||
return [
|
||||
ManyManyThroughList::class,
|
||||
$parentClass,
|
||||
$joinChildClass,
|
||||
$specification['from'] . 'ID',
|
||||
$specification['to'] . 'ID',
|
||||
$joinClass,
|
||||
];
|
||||
}
|
||||
|
||||
// Validate $specification class is valid
|
||||
$this->checkRelationClass($parentClass, $component, $specification, 'many_many');
|
||||
|
||||
// automatic scaffolded many_many table
|
||||
$classTable = $this->tableName($parentClass);
|
||||
$parentField = "{$classTable}ID";
|
||||
if ($parentClass === $specification) {
|
||||
$childField = "ChildID";
|
||||
} else {
|
||||
$candidateTable = $this->tableName($specification);
|
||||
$childField = "{$candidateTable}ID";
|
||||
}
|
||||
$joinTable = "{$classTable}_{$component}";
|
||||
return [
|
||||
ManyManyList::class,
|
||||
$parentClass,
|
||||
$specification,
|
||||
$parentField,
|
||||
$childField,
|
||||
$joinTable,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a many_many on the child class that points back to this many_many
|
||||
*
|
||||
* @param string $childClass
|
||||
* @param string $parentClass
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getManyManyInverseRelationship($childClass, $parentClass)
|
||||
{
|
||||
$otherManyMany = Config::inst()->get($childClass, 'many_many', Config::UNINHERITED);
|
||||
if (!$otherManyMany) {
|
||||
return null;
|
||||
}
|
||||
foreach ($otherManyMany as $inverseComponentName => $nextClass) {
|
||||
if ($nextClass === $parentClass) {
|
||||
return $inverseComponentName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $class
|
||||
* @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($class, $component, $type = 'has_many', &$polymorphic = false) {
|
||||
// Extract relation from current object
|
||||
if($type === 'has_many') {
|
||||
$remoteClass = $this->hasManyComponent($class, $component, false);
|
||||
} else {
|
||||
$remoteClass = $this->belongsToComponent($class, $component, false);
|
||||
}
|
||||
|
||||
if(empty($remoteClass)) {
|
||||
throw new Exception("Unknown $type component '$component' on class '$class'");
|
||||
}
|
||||
if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
|
||||
throw new Exception(
|
||||
"Class '$remoteClass' not found, but used in $type component '$component' on class '$class'"
|
||||
);
|
||||
}
|
||||
|
||||
// If presented with an explicit field name (using dot notation) then extract field name
|
||||
$remoteField = null;
|
||||
if(strpos($remoteClass, '.') !== false) {
|
||||
list($remoteClass, $remoteField) = explode('.', $remoteClass);
|
||||
}
|
||||
|
||||
// Reference remote has_one to check against
|
||||
$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
|
||||
|
||||
// Without an explicit field name, attempt to match the first remote field
|
||||
// with the same type as the current class
|
||||
if(empty($remoteField)) {
|
||||
// look for remote has_one joins on this class or any parent classes
|
||||
$remoteRelationsMap = array_flip($remoteRelations);
|
||||
foreach(array_reverse(ClassInfo::ancestry($class)) as $class) {
|
||||
if(array_key_exists($class, $remoteRelationsMap)) {
|
||||
$remoteField = $remoteRelationsMap[$class];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In case of an indeterminate remote field show an error
|
||||
if(empty($remoteField)) {
|
||||
$polymorphic = false;
|
||||
$message = "No has_one found on class '$remoteClass'";
|
||||
if($type == 'has_many') {
|
||||
// include a hint for has_many that is missing a has_one
|
||||
$message .= ", the has_many relation from '$class' to '$remoteClass'";
|
||||
$message .= " requires a has_one on '$remoteClass'";
|
||||
}
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
// If given an explicit field name ensure the related class specifies this
|
||||
if(empty($remoteRelations[$remoteField])) {
|
||||
throw new Exception("Missing expected has_one named '$remoteField'
|
||||
on class '$remoteClass' referenced by $type named '$component'
|
||||
on class {$class}"
|
||||
);
|
||||
}
|
||||
|
||||
// Inspect resulting found relation
|
||||
if($remoteRelations[$remoteField] === DataObject::class) {
|
||||
$polymorphic = true;
|
||||
return $remoteField; // Composite polymorphic field does not include 'ID' suffix
|
||||
} else {
|
||||
$polymorphic = false;
|
||||
return $remoteField . 'ID';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the to or from field on a has_many mapping class
|
||||
*
|
||||
* @param string $parentClass Name of parent class
|
||||
* @param string $component Name of many_many component
|
||||
* @param string $joinClass Class for the joined table
|
||||
* @param array $specification Complete many_many specification
|
||||
* @param string $key Name of key to check ('from' or 'to')
|
||||
* @return string Class that matches the given relation
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function checkManyManyFieldClass($parentClass, $component, $joinClass, $specification, $key)
|
||||
{
|
||||
// Ensure value for this key exists
|
||||
if (empty($specification[$key])) {
|
||||
throw new InvalidArgumentException(
|
||||
"many_many relation {$parentClass}.{$component} has missing {$key} which "
|
||||
. "should be a has_one on class {$joinClass}"
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the field exists on the given object
|
||||
$relation = $specification[$key];
|
||||
$relationClass = $this->hasOneComponent($joinClass, $relation);
|
||||
if (empty($relationClass)) {
|
||||
throw new InvalidArgumentException(
|
||||
"many_many through relation {$parentClass}.{$component} {$key} references a field name "
|
||||
. "{$joinClass}::{$relation} which is not a has_one"
|
||||
);
|
||||
}
|
||||
|
||||
// Check for polymorphic
|
||||
if ($relationClass === DataObject::class) {
|
||||
throw new InvalidArgumentException(
|
||||
"many_many through relation {$parentClass}.{$component} {$key} references a polymorphic field "
|
||||
. "{$joinClass}::{$relation} which is not supported"
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the join class isn't also the name of a field or relation on either side
|
||||
// of the relation
|
||||
$field = $this->fieldSpec($relationClass, $joinClass);
|
||||
if ($field) {
|
||||
throw new InvalidArgumentException(
|
||||
"many_many through relation {$parentClass}.{$component} {$key} class {$relationClass} "
|
||||
. " cannot have a db field of the same name of the join class {$joinClass}"
|
||||
);
|
||||
}
|
||||
|
||||
// Validate bad types on parent relation
|
||||
if ($key === 'from' && $relationClass !== $parentClass) {
|
||||
throw new InvalidArgumentException(
|
||||
"many_many through relation {$parentClass}.{$component} {$key} references a field name "
|
||||
. "{$joinClass}::{$relation} of type {$relationClass}; {$parentClass} expected"
|
||||
);
|
||||
}
|
||||
return $relationClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $parentClass Name of parent class
|
||||
* @param string $component Name of many_many component
|
||||
* @param array $specification Complete many_many specification
|
||||
* @return string Name of join class
|
||||
*/
|
||||
protected function checkManyManyJoinClass($parentClass, $component, $specification)
|
||||
{
|
||||
if (empty($specification['through'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"many_many relation {$parentClass}.{$component} has missing through which should be "
|
||||
. "a DataObject class name to be used as a join table"
|
||||
);
|
||||
}
|
||||
$joinClass = $specification['through'];
|
||||
if (!class_exists($joinClass)) {
|
||||
throw new InvalidArgumentException(
|
||||
"many_many relation {$parentClass}.{$component} has through class \"{$joinClass}\" which does not exist"
|
||||
);
|
||||
}
|
||||
return $joinClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a given class is valid for a relation
|
||||
*
|
||||
* @param string $class Parent class
|
||||
* @param string $component Component name
|
||||
* @param string $relationClass Candidate class to check
|
||||
* @param string $type Relation type (e.g. has_one)
|
||||
*/
|
||||
protected function checkRelationClass($class, $component, $relationClass, $type)
|
||||
{
|
||||
if (!is_string($component) || is_numeric($component)) {
|
||||
throw new InvalidArgumentException(
|
||||
"{$class} has invalid {$type} relation name"
|
||||
);
|
||||
}
|
||||
if (!is_string($relationClass)) {
|
||||
throw new InvalidArgumentException(
|
||||
"{$type} relation {$class}.{$component} is not a class name"
|
||||
);
|
||||
}
|
||||
if (!class_exists($relationClass)) {
|
||||
throw new InvalidArgumentException(
|
||||
"{$type} relation {$class}.{$component} references class {$relationClass} which doesn't exist"
|
||||
);
|
||||
}
|
||||
// Support polymorphic has_one
|
||||
if ($type === 'has_one') {
|
||||
$valid = is_a($relationClass, DataObject::class, true);
|
||||
} else {
|
||||
$valid = is_subclass_of($relationClass, DataObject::class, true);
|
||||
}
|
||||
if (!$valid) {
|
||||
throw new InvalidArgumentException(
|
||||
"{$type} relation {$class}.{$component} references class {$relationClass} "
|
||||
. " which is not a subclass of " . DataObject::class
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,13 @@ class DataQuery {
|
||||
*/
|
||||
protected $collidingFields = array();
|
||||
|
||||
/**
|
||||
* Allows custom callback to be registered before getFinalisedQuery is called.
|
||||
*
|
||||
* @var DataQueryManipulator[]
|
||||
*/
|
||||
protected $dataQueryManipulators = [];
|
||||
|
||||
private $queriedColumns = null;
|
||||
|
||||
/**
|
||||
@ -187,9 +194,14 @@ class DataQuery {
|
||||
if($queriedColumns) {
|
||||
$queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName'));
|
||||
}
|
||||
$query = clone $this->query;
|
||||
|
||||
// Apply manipulators before finalising query
|
||||
foreach($this->getDataQueryManipulators() as $manipulator) {
|
||||
$manipulator->beforeGetFinalisedQuery($this, $queriedColumns, $query);
|
||||
}
|
||||
|
||||
$schema = DataObject::getSchema();
|
||||
$query = clone $this->query;
|
||||
$baseDataClass = $schema->baseDataClass($this->dataClass());
|
||||
$baseIDColumn = $schema->sqlColumnForField($baseDataClass, 'ID');
|
||||
$ancestorClasses = ClassInfo::ancestry($this->dataClass(), true);
|
||||
@ -228,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));
|
||||
}
|
||||
@ -310,6 +322,11 @@ class DataQuery {
|
||||
|
||||
$this->ensureSelectContainsOrderbyColumns($query);
|
||||
|
||||
// Apply post-finalisation manipulations
|
||||
foreach($this->getDataQueryManipulators() as $manipulator) {
|
||||
$manipulator->afterGetFinalisedQuery($this, $queriedColumns, $query);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@ -491,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);
|
||||
@ -511,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);
|
||||
@ -710,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");
|
||||
@ -726,15 +744,16 @@ 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");
|
||||
}
|
||||
// Join via many_many
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
|
||||
list($relationClass, $parentClass, $componentClass, $parentField, $componentField, $relationTable)
|
||||
= $component;
|
||||
$this->joinManyManyRelationship(
|
||||
$parentClass, $componentClass, $parentField, $componentField, $relationTable
|
||||
$relationClass, $parentClass, $componentClass, $parentField, $componentField, $relationTable
|
||||
);
|
||||
$modelClass = $componentClass;
|
||||
|
||||
@ -778,10 +797,8 @@ class DataQuery {
|
||||
$localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID");
|
||||
$this->query->addLeftJoin($foreignBaseTable, "{$foreignIDColumn} = {$localColumn}");
|
||||
|
||||
/**
|
||||
* add join clause to the component's ancestry classes so that the search filter could search on
|
||||
* its ancestor fields.
|
||||
*/
|
||||
// Add join clause to the component's ancestry classes so that the search filter could search on
|
||||
// its ancestor fields.
|
||||
$ancestry = ClassInfo::ancestry($foreignClass, true);
|
||||
if(!empty($ancestry)){
|
||||
$ancestry = array_reverse($ancestry);
|
||||
@ -817,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");
|
||||
@ -833,10 +849,8 @@ class DataQuery {
|
||||
$this->query->addLeftJoin($foreignTable, "{$foreignKeyIDColumn} = {$localIDColumn}");
|
||||
}
|
||||
|
||||
/**
|
||||
* add join clause to the component's ancestry classes so that the search filter could search on
|
||||
* its ancestor fields.
|
||||
*/
|
||||
// Add join clause to the component's ancestry classes so that the search filter could search on
|
||||
// its ancestor fields.
|
||||
$ancestry = ClassInfo::ancestry($foreignClass, true);
|
||||
$ancestry = array_reverse($ancestry);
|
||||
foreach($ancestry as $ancestor) {
|
||||
@ -850,20 +864,27 @@ class DataQuery {
|
||||
/**
|
||||
* Join table via many_many relationship
|
||||
*
|
||||
* @param string $relationClass
|
||||
* @param string $parentClass
|
||||
* @param string $componentClass
|
||||
* @param string $parentField
|
||||
* @param string $componentField
|
||||
* @param string $relationTable Name of relation table
|
||||
* @param string $relationClassOrTable Name of relation table
|
||||
*/
|
||||
protected function joinManyManyRelationship($parentClass, $componentClass, $parentField, $componentField, $relationTable) {
|
||||
protected function joinManyManyRelationship(
|
||||
$relationClass, $parentClass, $componentClass, $parentField, $componentField, $relationClassOrTable
|
||||
) {
|
||||
$schema = DataObject::getSchema();
|
||||
|
||||
if (class_exists($relationClassOrTable)) {
|
||||
$relationClassOrTable = $schema->tableName($relationClassOrTable);
|
||||
}
|
||||
|
||||
// Join on parent table
|
||||
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID');
|
||||
$this->query->addLeftJoin(
|
||||
$relationTable,
|
||||
"\"$relationTable\".\"$parentField\" = {$parentIDColumn}"
|
||||
$relationClassOrTable,
|
||||
"\"$relationClassOrTable\".\"$parentField\" = {$parentIDColumn}"
|
||||
);
|
||||
|
||||
// Join on base table of component class
|
||||
@ -873,14 +894,12 @@ class DataQuery {
|
||||
if (!$this->query->isJoinedTo($componentBaseTable)) {
|
||||
$this->query->addLeftJoin(
|
||||
$componentBaseTable,
|
||||
"\"$relationTable\".\"$componentField\" = {$componentIDColumn}"
|
||||
"\"$relationClassOrTable\".\"$componentField\" = {$componentIDColumn}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* add join clause to the component's ancestry classes so that the search filter could search on
|
||||
* its ancestor fields.
|
||||
*/
|
||||
// Add join clause to the component's ancestry classes so that the search filter could search on
|
||||
// its ancestor fields.
|
||||
$ancestry = ClassInfo::ancestry($componentClass, true);
|
||||
$ancestry = array_reverse($ancestry);
|
||||
foreach($ancestry as $ancestor) {
|
||||
@ -1017,4 +1036,26 @@ class DataQuery {
|
||||
public function getQueryParams() {
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query manipulators
|
||||
*
|
||||
* @return DataQueryManipulator[]
|
||||
*/
|
||||
public function getDataQueryManipulators()
|
||||
{
|
||||
return $this->dataQueryManipulators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign callback to be invoked in getFinalisedQuery()
|
||||
*
|
||||
* @param DataQueryManipulator $manipulator
|
||||
* @return $this
|
||||
*/
|
||||
public function pushQueryManipulator(DataQueryManipulator $manipulator)
|
||||
{
|
||||
$this->dataQueryManipulators[] = $manipulator;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
29
ORM/DataQueryManipulator.php
Normal file
29
ORM/DataQueryManipulator.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM;
|
||||
|
||||
use SilverStripe\ORM\Queries\SQLSelect;
|
||||
|
||||
/**
|
||||
* Allows middleware to modily finalised dataquery on a per-instance basis
|
||||
*/
|
||||
interface DataQueryManipulator
|
||||
{
|
||||
/**
|
||||
* Invoked prior to getFinalisedQuery()
|
||||
*
|
||||
* @param DataQuery $dataQuery
|
||||
* @param array $queriedColumns
|
||||
* @param SQLSelect $sqlSelect
|
||||
*/
|
||||
public function beforeGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlSelect);
|
||||
|
||||
/**
|
||||
* Invoked after getFinalisedQuery()
|
||||
*
|
||||
* @param DataQuery $dataQuery
|
||||
* @param array $queriedColumns
|
||||
* @param SQLSelect $sqlQuery
|
||||
*/
|
||||
public function afterGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlQuery);
|
||||
}
|
@ -336,42 +336,46 @@ class DatabaseAdmin extends Controller {
|
||||
* corresponding records in their parent class tables.
|
||||
*/
|
||||
public function cleanup() {
|
||||
$allClasses = get_declared_classes();
|
||||
$baseClasses = [];
|
||||
foreach($allClasses as $class) {
|
||||
if(get_parent_class($class) == 'SilverStripe\ORM\DataObject') {
|
||||
foreach(ClassInfo::subclassesFor(DataObject::class) as $class) {
|
||||
if(get_parent_class($class) == DataObject::class) {
|
||||
$baseClasses[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
$schema = DataObject::getSchema();
|
||||
foreach($baseClasses as $baseClass) {
|
||||
// Get data classes
|
||||
$baseTable = $schema->baseDataTable($baseClass);
|
||||
$subclasses = ClassInfo::subclassesFor($baseClass);
|
||||
unset($subclasses[0]);
|
||||
foreach($subclasses as $k => $subclass) {
|
||||
if(DataObject::has_own_table($subclass)) {
|
||||
if(!DataObject::getSchema()->classHasTable($subclass)) {
|
||||
unset($subclasses[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
if($subclasses) {
|
||||
$records = DB::query("SELECT * FROM \"$baseClass\"");
|
||||
$records = DB::query("SELECT * FROM \"$baseTable\"");
|
||||
|
||||
|
||||
foreach($subclasses as $subclass) {
|
||||
$subclassTable = $schema->tableName($subclass);
|
||||
$recordExists[$subclass] =
|
||||
DB::query("SELECT \"ID\" FROM \"$subclass\"")->keyedColumn();
|
||||
DB::query("SELECT \"ID\" FROM \"$subclassTable\"")->keyedColumn();
|
||||
}
|
||||
|
||||
foreach($records as $record) {
|
||||
foreach($subclasses as $subclass) {
|
||||
$subclassTable = $schema->tableName($subclass);
|
||||
$id = $record['ID'];
|
||||
if(($record['ClassName'] != $subclass) &&
|
||||
(!is_subclass_of($record['ClassName'], $subclass)) &&
|
||||
(isset($recordExists[$subclass][$id]))) {
|
||||
$sql = "DELETE FROM \"$subclass\" WHERE \"ID\" = $record[ID]";
|
||||
echo "<li>$sql";
|
||||
DB::query($sql);
|
||||
if (($record['ClassName'] != $subclass)
|
||||
&& (!is_subclass_of($record['ClassName'], $subclass))
|
||||
&& isset($recordExists[$subclass][$id])
|
||||
) {
|
||||
$sql = "DELETE FROM \"$subclassTable\" WHERE \"ID\" = ?";
|
||||
echo "<li>$sql [{$id}]</li>";
|
||||
DB::prepared_query($sql, [$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\ORM;
|
||||
|
||||
use BadMethodCallException;
|
||||
use SilverStripe\Core\Object;
|
||||
use SilverStripe\ORM\Queries\SQLSelect;
|
||||
use SilverStripe\ORM\Queries\SQLDelete;
|
||||
@ -122,7 +123,7 @@ class ManyManyList extends RelationList {
|
||||
* @param array $row
|
||||
* @return DataObject
|
||||
*/
|
||||
protected function createDataObject($row) {
|
||||
public function createDataObject($row) {
|
||||
// remove any composed fields
|
||||
$add = array();
|
||||
|
||||
@ -210,23 +211,30 @@ class ManyManyList extends RelationList {
|
||||
if(empty($extraFields)) $extraFields = array();
|
||||
|
||||
// Determine ID of new record
|
||||
$itemID = null;
|
||||
if(is_numeric($item)) {
|
||||
$itemID = $item;
|
||||
} elseif($item instanceof $this->dataClass) {
|
||||
$itemID = $item->ID;
|
||||
} else {
|
||||
throw new InvalidArgumentException("ManyManyList::add() expecting a $this->dataClass object, or ID value",
|
||||
E_USER_ERROR);
|
||||
throw new InvalidArgumentException(
|
||||
"ManyManyList::add() expecting a $this->dataClass object, or ID value"
|
||||
);
|
||||
}
|
||||
if (empty($itemID)) {
|
||||
throw new InvalidArgumentException("ManyManyList::add() doesn't accept unsaved records");
|
||||
}
|
||||
|
||||
// Validate foreignID
|
||||
$foreignIDs = $this->getForeignID();
|
||||
if(empty($foreignIDs)) {
|
||||
throw new Exception("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING);
|
||||
throw new BadMethodCallException("ManyManyList::add() can't be called until a foreign ID is set");
|
||||
}
|
||||
|
||||
// Apply this item to each given foreign ID record
|
||||
if(!is_array($foreignIDs)) $foreignIDs = array($foreignIDs);
|
||||
if(!is_array($foreignIDs)) {
|
||||
$foreignIDs = array($foreignIDs);
|
||||
}
|
||||
foreach($foreignIDs as $foreignID) {
|
||||
// Check for existing records for this item
|
||||
if($foreignFilter = $this->foreignIDWriteFilter($foreignID)) {
|
||||
@ -309,7 +317,7 @@ class ManyManyList extends RelationList {
|
||||
if($filter = $this->foreignIDWriteFilter($this->getForeignID())) {
|
||||
$query->setWhere($filter);
|
||||
} else {
|
||||
user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING);
|
||||
user_error("Can't call ManyManyList::remove() until a foreign ID is set");
|
||||
}
|
||||
|
||||
$query->addWhere(array(
|
||||
@ -372,7 +380,7 @@ class ManyManyList extends RelationList {
|
||||
}
|
||||
|
||||
if(!is_numeric($itemID)) {
|
||||
user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR);
|
||||
throw new InvalidArgumentException('ManyManyList::getExtraData() passed a non-numeric child ID');
|
||||
}
|
||||
|
||||
$cleanExtraFields = array();
|
||||
@ -384,7 +392,7 @@ class ManyManyList extends RelationList {
|
||||
if($filter) {
|
||||
$query->setWhere($filter);
|
||||
} else {
|
||||
user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING);
|
||||
throw new BadMethodCallException("Can't call ManyManyList::getExtraData() until a foreign ID is set");
|
||||
}
|
||||
$query->addWhere(array(
|
||||
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
|
||||
|
190
ORM/ManyManyThroughList.php
Normal file
190
ORM/ManyManyThroughList.php
Normal file
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM;
|
||||
|
||||
use BadMethodCallException;
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
|
||||
/**
|
||||
* ManyManyList backed by a dataobject join table
|
||||
*/
|
||||
class ManyManyThroughList extends RelationList
|
||||
{
|
||||
/**
|
||||
* @var ManyManyThroughQueryManipulator
|
||||
*/
|
||||
protected $manipulator;
|
||||
|
||||
/**
|
||||
* Create a new ManyManyRelationList object. This relation will utilise an intermediary dataobject
|
||||
* as a join table, unlike ManyManyList which scaffolds a table automatically.
|
||||
*
|
||||
* @param string $dataClass The class of the DataObjects that this will list.
|
||||
* @param string $joinClass Class name of the joined dataobject record
|
||||
* @param string $localKey The key in the join table that maps to the dataClass' PK.
|
||||
* @param string $foreignKey The key in the join table that maps to joined class' PK.
|
||||
*
|
||||
* @example new ManyManyThroughList('Banner', 'PageBanner', 'BannerID', 'PageID');
|
||||
*/
|
||||
public function __construct($dataClass, $joinClass, $localKey, $foreignKey) {
|
||||
parent::__construct($dataClass);
|
||||
|
||||
// Inject manipulator
|
||||
$this->manipulator = ManyManyThroughQueryManipulator::create($joinClass, $localKey, $foreignKey);
|
||||
$this->dataQuery->pushQueryManipulator($this->manipulator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't apply foreign ID filter until getFinalisedQuery()
|
||||
*
|
||||
* @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current ids as
|
||||
* per getForeignID
|
||||
* @return array Condition In array(SQL => parameters format)
|
||||
*/
|
||||
protected function foreignIDFilter($id = null) {
|
||||
// foreignIDFilter is applied to the HasManyList via ManyManyThroughQueryManipulator, not here
|
||||
return [];
|
||||
}
|
||||
|
||||
public function createDataObject($row) {
|
||||
// Add joined record
|
||||
$joinRow = [];
|
||||
$joinAlias = $this->manipulator->getJoinAlias();
|
||||
$prefix = $joinAlias . '_';
|
||||
foreach ($row as $key => $value) {
|
||||
if (strpos($key, $prefix) === 0) {
|
||||
$joinKey = substr($key, strlen($prefix));
|
||||
$joinRow[$joinKey] = $value;
|
||||
unset($row[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create parent record
|
||||
$record = parent::createDataObject($row);
|
||||
|
||||
// Create joined record
|
||||
if ($joinRow) {
|
||||
$joinClass = $this->manipulator->getJoinClass();
|
||||
$joinQueryParams = $this->manipulator->extractInheritableQueryParameters($this->dataQuery);
|
||||
$joinRecord = Injector::inst()->create($joinClass, $joinRow, false, $this->model, $joinQueryParams);
|
||||
$record->setJoin($joinRecord, $joinAlias);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given item from this list.
|
||||
*
|
||||
* Note that for a ManyManyList, the item is never actually deleted, only
|
||||
* the join table is affected.
|
||||
*
|
||||
* @param DataObject $item
|
||||
*/
|
||||
public function remove($item) {
|
||||
if(!($item instanceof $this->dataClass)) {
|
||||
throw new InvalidArgumentException(
|
||||
"ManyManyThroughList::remove() expecting a {$this->dataClass} object"
|
||||
);
|
||||
}
|
||||
|
||||
$this->removeByID($item->ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given item from this list.
|
||||
*
|
||||
* Note that for a ManyManyList, the item is never actually deleted, only
|
||||
* the join table is affected
|
||||
*
|
||||
* @param int $itemID The item ID
|
||||
*/
|
||||
public function removeByID($itemID) {
|
||||
if(!is_numeric($itemID)) {
|
||||
throw new InvalidArgumentException("ManyManyThroughList::removeById() expecting an ID");
|
||||
}
|
||||
|
||||
// Find has_many row with a local key matching the given id
|
||||
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
|
||||
$records = $hasManyList->filter($this->manipulator->getLocalKey(), $itemID);
|
||||
|
||||
// Rather than simple un-associating the record (as in has_many list)
|
||||
// Delete the actual mapping row as many_many deletions behave.
|
||||
/** @var DataObject $record */
|
||||
foreach($records as $record) {
|
||||
$record->delete();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeAll()
|
||||
{
|
||||
// Empty has_many table matching the current foreign key
|
||||
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
|
||||
$hasManyList->removeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $item
|
||||
* @param array $extraFields
|
||||
*/
|
||||
public function add($item, $extraFields = []) {
|
||||
// Ensure nulls or empty strings are correctly treated as empty arrays
|
||||
if(empty($extraFields)) {
|
||||
$extraFields = array();
|
||||
}
|
||||
|
||||
// Determine ID of new record
|
||||
$itemID = null;
|
||||
if(is_numeric($item)) {
|
||||
$itemID = $item;
|
||||
} elseif($item instanceof $this->dataClass) {
|
||||
$itemID = $item->ID;
|
||||
} else {
|
||||
throw new InvalidArgumentException(
|
||||
"ManyManyThroughList::add() expecting a $this->dataClass object, or ID value"
|
||||
);
|
||||
}
|
||||
if (empty($itemID)) {
|
||||
throw new InvalidArgumentException("ManyManyThroughList::add() doesn't accept unsaved records");
|
||||
}
|
||||
|
||||
// Validate foreignID
|
||||
$foreignIDs = $this->getForeignID();
|
||||
if (empty($foreignIDs)) {
|
||||
throw new BadMethodCallException("ManyManyList::add() can't be called until a foreign ID is set");
|
||||
}
|
||||
|
||||
// Apply this item to each given foreign ID record
|
||||
if(!is_array($foreignIDs)) {
|
||||
$foreignIDs = [$foreignIDs];
|
||||
}
|
||||
$foreignIDsToAdd = array_combine($foreignIDs, $foreignIDs);
|
||||
|
||||
// Update existing records
|
||||
$localKey = $this->manipulator->getLocalKey();
|
||||
$foreignKey = $this->manipulator->getForeignKey();
|
||||
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
|
||||
$records = $hasManyList->filter($localKey, $itemID);
|
||||
/** @var DataObject $record */
|
||||
foreach($records as $record) {
|
||||
if ($extraFields) {
|
||||
foreach ($extraFields as $field => $value) {
|
||||
$record->$field = $value;
|
||||
}
|
||||
$record->write();
|
||||
}
|
||||
//
|
||||
$foreignID = $record->$foreignKey;
|
||||
unset($foreignIDsToAdd[$foreignID]);
|
||||
}
|
||||
|
||||
// Once existing records are updated, add missing mapping records
|
||||
foreach($foreignIDsToAdd as $foreignID) {
|
||||
$record = $hasManyList->createDataObject($extraFields ?: []);
|
||||
$record->$foreignKey = $foreignID;
|
||||
$record->$localKey = $itemID;
|
||||
$record->write();
|
||||
}
|
||||
}
|
||||
}
|
223
ORM/ManyManyThroughQueryManipulator.php
Normal file
223
ORM/ManyManyThroughQueryManipulator.php
Normal file
@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace SilverStripe\ORM;
|
||||
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\ORM\Queries\SQLSelect;
|
||||
|
||||
/**
|
||||
* Injected into DataQuery to augment getFinalisedQuery() with a join table
|
||||
*/
|
||||
class ManyManyThroughQueryManipulator implements DataQueryManipulator
|
||||
{
|
||||
|
||||
use Injectable;
|
||||
|
||||
/**
|
||||
* DataObject that backs the joining table
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $joinClass;
|
||||
|
||||
/**
|
||||
* Key that joins to the data class
|
||||
*
|
||||
* @var string $localKey
|
||||
*/
|
||||
protected $localKey;
|
||||
|
||||
/**
|
||||
* Key that joins to the parent class
|
||||
*
|
||||
* @var string $foreignKey
|
||||
*/
|
||||
protected $foreignKey;
|
||||
|
||||
/**
|
||||
* Build query manipulator for a given join table. Additional parameters (foreign key, etc)
|
||||
* will be infered at evaluation from query parameters set via the ManyManyThroughList
|
||||
*
|
||||
* @param string $joinClass Class name of the joined dataobject record
|
||||
* @param string $localKey The key in the join table that maps to the dataClass' PK.
|
||||
* @param string $foreignKey The key in the join table that maps to joined class' PK.
|
||||
*/
|
||||
public function __construct($joinClass, $localKey, $foreignKey)
|
||||
{
|
||||
$this->setJoinClass($joinClass);
|
||||
$this->setLocalKey($localKey);
|
||||
$this->setForeignKey($foreignKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getJoinClass()
|
||||
{
|
||||
return $this->joinClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $joinClass
|
||||
* @return $this
|
||||
*/
|
||||
public function setJoinClass($joinClass)
|
||||
{
|
||||
$this->joinClass = $joinClass;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalKey()
|
||||
{
|
||||
return $this->localKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $localKey
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocalKey($localKey)
|
||||
{
|
||||
$this->localKey = $localKey;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignKey()
|
||||
{
|
||||
return $this->foreignKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $foreignKey
|
||||
* @return $this
|
||||
*/
|
||||
public function setForeignKey($foreignKey)
|
||||
{
|
||||
$this->foreignKey = $foreignKey;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get has_many relationship between parent and join table (for a given DataQuery)
|
||||
*
|
||||
* @param DataQuery $query
|
||||
* @return HasManyList
|
||||
*/
|
||||
public function getParentRelationship(DataQuery $query) {
|
||||
// Create has_many
|
||||
$list = HasManyList::create($this->getJoinClass(), $this->getForeignKey());
|
||||
$list = $list->setDataQueryParam($this->extractInheritableQueryParameters($query));
|
||||
|
||||
// Limit to given foreign key
|
||||
if ($foreignID = $query->getQueryParam('Foreign.ID')) {
|
||||
$list = $list->forForeignID($foreignID);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the query parameters that should be inherited from the base many_many
|
||||
* to the nested has_many list.
|
||||
*
|
||||
* @param DataQuery $query
|
||||
* @return mixed
|
||||
*/
|
||||
public function extractInheritableQueryParameters(DataQuery $query) {
|
||||
$params = $query->getQueryParams();
|
||||
|
||||
// Remove `Foreign.` query parameters for created objects,
|
||||
// as this would interfere with relations on those objects.
|
||||
foreach(array_keys($params) as $key) {
|
||||
if(stripos($key, 'Foreign.') === 0) {
|
||||
unset($params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get inheritable parameters from an instance of the base query dataclass
|
||||
$inst = Injector::inst()->create($query->dataClass());
|
||||
$inst->setSourceQueryParams($params);
|
||||
return $inst->getInheritableQueryParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of join table alias for use in queries.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getJoinAlias() {
|
||||
return DataObject::getSchema()->tableName($this->getJoinClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked prior to getFinalisedQuery()
|
||||
*
|
||||
* @param DataQuery $dataQuery
|
||||
* @param array $queriedColumns
|
||||
* @param SQLSelect $sqlSelect
|
||||
*/
|
||||
public function beforeGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlSelect)
|
||||
{
|
||||
// Get metadata and SQL from join table
|
||||
$hasManyRelation = $this->getParentRelationship($dataQuery);
|
||||
$joinTableSQLSelect = $hasManyRelation->dataQuery()->query();
|
||||
$joinTableSQL = $joinTableSQLSelect->sql($joinTableParameters);
|
||||
$joinTableColumns = array_keys($joinTableSQLSelect->getSelect()); // Get aliases (keys) only
|
||||
$joinTableAlias = $this->getJoinAlias();
|
||||
|
||||
// Get fields to join on
|
||||
$localKey = $this->getLocalKey();
|
||||
$schema = DataObject::getSchema();
|
||||
$baseTable = $schema->baseDataClass($dataQuery->dataClass());
|
||||
$childField = $schema->sqlColumnForField($baseTable, 'ID');
|
||||
|
||||
// Add select fields
|
||||
foreach($joinTableColumns as $joinTableColumn) {
|
||||
$sqlSelect->selectField(
|
||||
"\"{$joinTableAlias}\".\"{$joinTableColumn}\"",
|
||||
"{$joinTableAlias}_{$joinTableColumn}"
|
||||
);
|
||||
}
|
||||
|
||||
// Apply join and record sql for later insertion (at end of replacements)
|
||||
$sqlSelect->addInnerJoin(
|
||||
'(SELECT $$_SUBQUERY_$$)',
|
||||
"\"{$joinTableAlias}\".\"{$localKey}\" = {$childField}",
|
||||
$joinTableAlias,
|
||||
20,
|
||||
$joinTableParameters
|
||||
);
|
||||
$dataQuery->setQueryParam('Foreign.JoinTableSQL', $joinTableSQL);
|
||||
|
||||
// After this join, and prior to afterGetFinalisedQuery, $sqlSelect will be populated with the
|
||||
// necessary sql rewrites (versioned, etc) that effect the base table.
|
||||
// By using a placeholder for the subquery we can protect the subquery (already rewritten)
|
||||
// from being re-written a second time. However we DO want the join predicate (above) to be rewritten.
|
||||
// See http://php.net/manual/en/function.str-replace.php#refsect1-function.str-replace-notes
|
||||
// for the reason we only add the final substitution at the end of getFinalisedQuery()
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after getFinalisedQuery()
|
||||
*
|
||||
* @param DataQuery $dataQuery
|
||||
* @param array $queriedColumns
|
||||
* @param SQLSelect $sqlQuery
|
||||
*/
|
||||
public function afterGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlQuery)
|
||||
{
|
||||
// Inject final replacement after manipulation has been performed on the base dataquery
|
||||
$joinTableSQL = $dataQuery->getQueryParam('Foreign.JoinTableSQL');
|
||||
if ($joinTableSQL) {
|
||||
$sqlQuery->replaceText('SELECT $$_SUBQUERY_$$', $joinTableSQL);
|
||||
$dataQuery->setQueryParam('Foreign.JoinTableSQL', null);
|
||||
}
|
||||
}
|
||||
}
|
@ -146,12 +146,7 @@ class SQLSelect extends SQLConditionalExpression {
|
||||
$fields = array($fields);
|
||||
}
|
||||
foreach($fields as $idx => $field) {
|
||||
if(preg_match('/^(.*) +AS +"([^"]*)"/i', $field, $matches)) {
|
||||
Deprecation::notice("3.0", "Use selectField() to specify column aliases");
|
||||
$this->selectField($matches[1], $matches[2]);
|
||||
} else {
|
||||
$this->selectField($field, is_numeric($idx) ? null : $idx);
|
||||
}
|
||||
$this->selectField($field, is_numeric($idx) ? null : $idx);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -167,8 +162,11 @@ class SQLSelect extends SQLConditionalExpression {
|
||||
*/
|
||||
public function selectField($field, $alias = null) {
|
||||
if(!$alias) {
|
||||
if(preg_match('/"([^"]+)"$/', $field, $matches)) $alias = $matches[1];
|
||||
else $alias = $field;
|
||||
if(preg_match('/"([^"]+)"$/', $field, $matches)) {
|
||||
$alias = $matches[1];
|
||||
} else {
|
||||
$alias = $field;
|
||||
}
|
||||
}
|
||||
$this->select[$alias] = $field;
|
||||
return $this;
|
||||
|
@ -12,7 +12,9 @@ use Exception;
|
||||
abstract class RelationList extends DataList implements Relation {
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* Any number of foreign keys to apply to this list
|
||||
*
|
||||
* @return string|array|null
|
||||
*/
|
||||
public function getForeignID() {
|
||||
return $this->dataQuery->getQueryParam('Foreign.ID');
|
||||
@ -47,10 +49,8 @@ abstract class RelationList extends DataList implements Relation {
|
||||
// Calculate the new filter
|
||||
$filter = $this->foreignIDFilter($id);
|
||||
|
||||
$list = $this->alterDataQuery(function($query) use ($id, $filter){
|
||||
/** @var DataQuery $query */
|
||||
$list = $this->alterDataQuery(function(DataQuery $query) use ($id, $filter){
|
||||
// Check if there is an existing filter, remove if there is
|
||||
/** @var DataQuery $query */
|
||||
$currentFilter = $query->getQueryParam('Foreign.Filter');
|
||||
if($currentFilter) {
|
||||
try {
|
||||
|
@ -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);
|
||||
|
||||
@ -1041,7 +1042,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
if(!$ownedClass) {
|
||||
continue;
|
||||
}
|
||||
if($ownedClass === 'SilverStripe\ORM\DataObject') {
|
||||
if($ownedClass === DataObject::class) {
|
||||
throw new LogicException(sprintf(
|
||||
"Relation %s on class %s cannot be owned as it is polymorphic",
|
||||
$owned, $class
|
||||
@ -1131,17 +1132,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
|
||||
/** @var Versioned|DataObject $item */
|
||||
foreach($items as $item) {
|
||||
// Identify item
|
||||
$itemKey = $item->class . '/' . $item->ID;
|
||||
|
||||
// Skip unsaved, unversioned, or already checked objects
|
||||
if(!$item->isInDB() || !$item->has_extension('SilverStripe\ORM\Versioning\Versioned') || isset($list[$itemKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save record
|
||||
$list[$itemKey] = $item;
|
||||
$added[$itemKey] = $item;
|
||||
$this->mergeRelatedObject($list, $added, $item);
|
||||
}
|
||||
return $added;
|
||||
}
|
||||
@ -1393,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1524,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);
|
||||
|
||||
@ -2494,4 +2486,30 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
public function hasStages() {
|
||||
return $this->mode === static::STAGEDVERSIONED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge single object into a list
|
||||
*
|
||||
* @param ArrayList $list Global list. Object will not be added if already added to this list.
|
||||
* @param ArrayList $added Additional list to insert into
|
||||
* @param DataObject $item Item to add
|
||||
* @return mixed
|
||||
*/
|
||||
protected function mergeRelatedObject($list, $added, $item)
|
||||
{
|
||||
// Identify item
|
||||
$itemKey = get_class($item) . '/' . $item->ID;
|
||||
|
||||
// Write if saved, versioned, and not already added
|
||||
if ($item->isInDB() && $item->has_extension('SilverStripe\ORM\Versioning\Versioned') && !isset($list[$itemKey])) {
|
||||
$list[$itemKey] = $item;
|
||||
$added[$itemKey] = $item;
|
||||
}
|
||||
|
||||
// Add joined record (from many_many through) automatically
|
||||
$joined = $item->getJoin();
|
||||
if ($joined) {
|
||||
$this->mergeRelatedObject($list, $added, $joined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -213,32 +213,18 @@ This is not mandatory unless the relationship would be otherwise ambiguous.
|
||||
|
||||
## many_many
|
||||
|
||||
Defines many-to-many joins. A new table, (this-class)_(relationship-name), will be created with a pair of ID fields.
|
||||
Defines many-to-many joins, which uses a third table created between the two to join pairs.
|
||||
There are two ways in which this can be declared, which are described below, depending on
|
||||
how the developer wishes to manage this join table.
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
Please specify a $belongs_many_many-relationship on the related class as well, in order to have the necessary accessors
|
||||
available on both ends.
|
||||
Please specify a $belongs_many_many-relationship on the related class as well, in order
|
||||
to have the necessary accessors available on both ends.
|
||||
</div>
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
|
||||
private static $many_many = array(
|
||||
"Supporters" => "Supporter",
|
||||
);
|
||||
}
|
||||
|
||||
class Supporter extends DataObject {
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
"Supports" => "Team",
|
||||
);
|
||||
}
|
||||
|
||||
Much like the `has_one` relationship, `many_many` can be navigated through the `ORM` as well. The only difference being
|
||||
you will get an instance of [api:ManyManyList] rather than the object.
|
||||
Much like the `has_one` relationship, `many_many` can be navigated through the `ORM` as well.
|
||||
The only difference being you will get an instance of [api:SilverStripe\ORM\ManyManyList] or
|
||||
[api:SilverStripe\ORM\ManyManyThroughList] rather than the object.
|
||||
|
||||
:::php
|
||||
$team = Team::get()->byId(1);
|
||||
@ -247,16 +233,127 @@ you will get an instance of [api:ManyManyList] rather than the object.
|
||||
// returns a 'ManyManyList' instance.
|
||||
|
||||
|
||||
### Automatic many_many table
|
||||
|
||||
If you specify only a single class as the other side of the many-many relationship, then a
|
||||
table will be automatically created between the two (this-class)_(relationship-name), will
|
||||
be created with a pair of ID fields.
|
||||
|
||||
Extra fields on the mapping table can be created by declaring a `many_many_extraFields`
|
||||
config to add extra columns.
|
||||
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
private static $many_many = [
|
||||
"Supporters" => "Supporter",
|
||||
];
|
||||
private static $many_many_extraFields = [
|
||||
'Supporters' => [
|
||||
'Ranking' => 'Int'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
class Supporter extends DataObject {
|
||||
|
||||
private static $belongs_many_many = [
|
||||
"Supports" => "Team",
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
### many_many through relationship joined on a separate DataObject
|
||||
|
||||
If necessary, a third DataObject class can instead be specified as the joining table,
|
||||
rather than having the ORM generate an automatically scaffolded table. This has the following
|
||||
advantages:
|
||||
|
||||
- Allows versioning of the mapping table, including support for the
|
||||
[ownership api](/developer_guides/model/versioning).
|
||||
- Allows support of other extensions on the mapping table (e.g. subsites).
|
||||
- Extra fields can be managed separately to the joined dataobject, even via a separate
|
||||
GridField or form.
|
||||
|
||||
This is declared via array syntax, with the following keys on the many_many:
|
||||
- `through` Class name of the mapping table
|
||||
- `from` Name of the has_one relationship pointing back at the object declaring many_many
|
||||
- `to` Name of the has_one relationship pointing to the object declaring belongs_many_many.
|
||||
|
||||
Note: The `through` class must not also be the name of any field or relation on the parent
|
||||
or child record.
|
||||
|
||||
The syntax for `belongs_many_many` is unchanged.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Team extends DataObject {
|
||||
private static $many_many = [
|
||||
"Supporters" => [
|
||||
'through' => 'TeamSupporter',
|
||||
'from' => 'Team',
|
||||
'to' => 'Supporter',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
class Supporter extends DataObject {
|
||||
private static $belongs_many_many = [
|
||||
"Supports" => "Team",
|
||||
];
|
||||
}
|
||||
|
||||
class TeamSupporter extends DataObject {
|
||||
private static $db = [
|
||||
'Ranking' => 'Int',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Team' => 'Team',
|
||||
'Supporter' => 'Supporter'
|
||||
];
|
||||
}
|
||||
|
||||
In order to filter on the join table during queries, you can use the class name of the joining table
|
||||
for any sql conditions.
|
||||
|
||||
|
||||
:::php
|
||||
$team = Team::get()->byId(1);
|
||||
$supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]);
|
||||
|
||||
|
||||
Note: ->filter() currently does not support joined fields natively due to the fact that the
|
||||
query for the join table is isolated from the outer query controlled by DataList.
|
||||
|
||||
|
||||
### Using many_many in templates
|
||||
|
||||
The relationship can also be navigated in [templates](../templates).
|
||||
The joined record can be accessed via `Join` or `TeamSupporter` property (many_many through only)
|
||||
|
||||
:::ss
|
||||
<% with $Supporter %>
|
||||
<% loop $Supports %>
|
||||
Supports $Title
|
||||
Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %>
|
||||
<% end_if %>
|
||||
<% end_with %>
|
||||
|
||||
To specify multiple $many_manys between the same classes, use the dot notation to distinguish them like below:
|
||||
|
||||
You can also use `$Join` in place of the join class alias (`$TeamSupporter`), if your template
|
||||
is class-agnostic and doesn't know the type of the join table.
|
||||
|
||||
## belongs_many_many
|
||||
|
||||
The belongs_many_many represents the other side of the relationship on the target data class.
|
||||
When using either a basic many_many or a many_many through, the syntax for belongs_many_many is the same.
|
||||
|
||||
To specify multiple $many_manys between the same classes, specify use the dot notation to
|
||||
distinguish them like below:
|
||||
|
||||
|
||||
:::php
|
||||
<?php
|
||||
@ -277,10 +374,12 @@ To specify multiple $many_manys between the same classes, use the dot notation t
|
||||
);
|
||||
}
|
||||
|
||||
## many_many or belongs_many_many?
|
||||
|
||||
If you're unsure about whether an object should take on `many_many` or `belongs_many_many`, the best way to think about it is that the object where the relationship will be edited (i.e. via checkboxes) should contain the `many_many`. For instance, in a `many_many` of Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa.
|
||||
|
||||
If you're unsure about whether an object should take on `many_many` or `belongs_many_many`,
|
||||
the best way to think about it is that the object where the relationship will be edited
|
||||
(i.e. via checkboxes) should contain the `many_many`. For instance, in a `many_many` of
|
||||
Product => Categories, the `Product` should contain the `many_many`, because it is much
|
||||
more likely that the user will select Categories for a Product than vice-versa.
|
||||
|
||||
## Adding relations
|
||||
|
||||
|
@ -852,9 +852,13 @@ A very small number of methods were chosen for deprecation, and will be removed
|
||||
#### <a name="overview-orm-api"></a>ORM API Additions / Changes
|
||||
|
||||
* Deprecate `SQLQuery` in favour `SQLSelect`
|
||||
* `DataList::filter` by null now internally generates "IS NULL" or "IS NOT NULL" conditions appropriately on queries
|
||||
* `DataObject.many_many` 'through' relationships now support join dataobjects in place of
|
||||
automatically generated join tables. See the [/developer_guides/relations](datamodel relationship docs)
|
||||
for more info.
|
||||
* `DataList::filter` by null now internally generates "IS NULL" or "IS NOT NULL" conditions
|
||||
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.
|
||||
@ -905,6 +909,24 @@ A very small number of methods were chosen for deprecation, and will be removed
|
||||
|
||||
#### <a name="overview-orm-removed"></a>ORM Removed API
|
||||
|
||||
* `DataObject::db` removed and replaced with `DataObjectSchema::fieldSpec` and `DataObjectSchema::fieldSpecs`
|
||||
* `DataObject::manyManyComponent` moved to `DataObjectSchema`
|
||||
* `DataObject::belongsToComponent` moved to `DataObjectSchema`
|
||||
* `DataObject::hasOneComponent` moved to `DataObjectSchema`
|
||||
* `DataObject::hasManyComponent` moved to `DataObjectSchema`
|
||||
* `DataObject::getRemoteJoinField` moved to `DataObjectSchema`
|
||||
* `DataObject::database_fields` renamed and moved to `DataObjectSchema::databaseFields`
|
||||
* `DataObject::has_own_table` renamed and moved to `DataObjectSchema::classHasTable`
|
||||
* `DataObject::composite_fields` renamed and moved to `DataObjectSchema::compositeFields``
|
||||
* `DataObject::manyManyExtraFieldsForComponent` moved to `DataObjectSchema`
|
||||
* Removed `DataObject::validateModelDefinitions`. Relations are now validated within `DataObjectSchema`
|
||||
* Removed `DataObject` methods `hasOwnTableDatabaseField`, `has_own_table_database_field` and
|
||||
`hasDatabaseFields` are superceded by `DataObjectSchema::fieldSpec`.
|
||||
Use `$schema->fieldSpec($class, $field, ['dbOnly', 'uninherited'])`.
|
||||
Exclude `uninherited` option to search all tables in the class hierarchy.
|
||||
* Removed `DataObject::is_composite_field`. Use `DataObjectSchema::compositeField` instead.
|
||||
* Removed `DataObject::custom_database_fields`. Use `DataObjectSchema::databaseFields`
|
||||
or `DataObjectSchema::fieldSpecs` instead.
|
||||
* Removed `DataList::getRelation`, as it was mutable. Use `DataList::applyRelation` instead, which is immutable.
|
||||
* Removed `DataList::applyFilterContext` private method
|
||||
* `Member` Field 'RememberLoginToken' removed, replaced with 'RememberLoginHashes' has_many relationship
|
||||
@ -913,6 +935,10 @@ A very small number of methods were chosen for deprecation, and will be removed
|
||||
* Removed `DBString::LimitWordCountXML()` method. Use `LimitWordCount` for XML safe version.
|
||||
* Removed `SiteTree::getExistsOnLive()`. Use `isPublished()` instead.
|
||||
* Removed `SiteTree::getIsDeletedFromStage()`. Use `isOnDraft()` instead (inverse case).
|
||||
* `DataObject.many_many` no longer supports triangular resolution. Both the `many_many` and `belongs_many_many`
|
||||
must point directly to the specific class on the opposing side, not a subclass or parent.
|
||||
* `DataObject::validateModelDefinitions()` has been removed. Validation and parsing of config is now handled
|
||||
within `DataObjectSchema`.
|
||||
|
||||
### <a name="overview-filesystem"></a>Filesystem API
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -3,13 +3,16 @@
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\Connect\MySQLDatabase;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
@ -58,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);
|
||||
@ -71,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);
|
||||
@ -88,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');
|
||||
|
||||
@ -784,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'),
|
||||
@ -835,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');
|
||||
|
||||
}
|
||||
@ -872,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',
|
||||
@ -889,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',
|
||||
@ -907,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',
|
||||
@ -927,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'
|
||||
);
|
||||
}
|
||||
@ -1101,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() {
|
||||
@ -1186,77 +1202,21 @@ class DataObjectTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testValidateModelDefinitionsFailsWithArray() {
|
||||
Config::nest();
|
||||
|
||||
$object = new DataObjectTest_Team;
|
||||
$method = $this->makeAccessible($object, 'validateModelDefinitions');
|
||||
|
||||
Config::inst()->update('DataObjectTest_Team', 'has_one', array('NotValid' => array('NoArraysAllowed')));
|
||||
$this->setExpectedException('LogicException');
|
||||
|
||||
try {
|
||||
$method->invoke($object);
|
||||
} catch(Exception $e) {
|
||||
Config::unnest(); // Catch the exception so we can unnest config before failing the test
|
||||
throw $e;
|
||||
}
|
||||
$this->setExpectedException(InvalidArgumentException::class);
|
||||
DataObject::getSchema()->hasOneComponent(DataObjectTest_Team::class, 'NotValid');
|
||||
}
|
||||
|
||||
public function testValidateModelDefinitionsFailsWithIntKey() {
|
||||
Config::nest();
|
||||
|
||||
$object = new DataObjectTest_Team;
|
||||
$method = $this->makeAccessible($object, 'validateModelDefinitions');
|
||||
|
||||
Config::inst()->update('DataObjectTest_Team', 'has_many', array(12 => 'DataObjectTest_Player'));
|
||||
$this->setExpectedException('LogicException');
|
||||
|
||||
try {
|
||||
$method->invoke($object);
|
||||
} catch(Exception $e) {
|
||||
Config::unnest(); // Catch the exception so we can unnest config before failing the test
|
||||
throw $e;
|
||||
}
|
||||
$this->setExpectedException(InvalidArgumentException::class);
|
||||
DataObject::getSchema()->hasManyComponent(DataObjectTest_Team::class, 12);
|
||||
}
|
||||
|
||||
public function testValidateModelDefinitionsFailsWithIntValue() {
|
||||
Config::nest();
|
||||
|
||||
$object = new DataObjectTest_Team;
|
||||
$method = $this->makeAccessible($object, 'validateModelDefinitions');
|
||||
|
||||
Config::inst()->update('DataObjectTest_Team', 'many_many', array('Players' => 12));
|
||||
$this->setExpectedException('LogicException');
|
||||
|
||||
try {
|
||||
$method->invoke($object);
|
||||
} catch(Exception $e) {
|
||||
Config::unnest(); // Catch the exception so we can unnest config before failing the test
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* many_many_extraFields is allowed to have an array value, so shouldn't throw an exception
|
||||
*/
|
||||
public function testValidateModelDefinitionsPassesWithExtraFields() {
|
||||
Config::nest();
|
||||
|
||||
$object = new DataObjectTest_Team;
|
||||
$method = $this->makeAccessible($object, 'validateModelDefinitions');
|
||||
|
||||
Config::inst()->update('DataObjectTest_Team', 'many_many_extraFields',
|
||||
array('Relations' => array('Price' => 'Int')));
|
||||
|
||||
try {
|
||||
$method->invoke($object);
|
||||
} catch(Exception $e) {
|
||||
Config::unnest();
|
||||
$this->fail('Exception should not be thrown');
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Config::unnest();
|
||||
$this->setExpectedException(InvalidArgumentException::class);
|
||||
DataObject::getSchema()->manyManyComponent(DataObjectTest_Team::class, 'Players');
|
||||
}
|
||||
|
||||
public function testNewClassInstance() {
|
||||
@ -1292,13 +1252,17 @@ class DataObjectTest extends SapphireTest {
|
||||
$equipmentSuppliers = $team->EquipmentSuppliers();
|
||||
|
||||
// Check that DataObject::many_many() works as expected
|
||||
list($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');
|
||||
$this->assertEquals('DataObjectTest_EquipmentCompany', $targetClass,
|
||||
'DataObject::many_many() didn\'t find the correct target class for the relation');
|
||||
$this->assertEquals('DataObjectTest_EquipmentCompany_SponsoredTeams', $joinTable,
|
||||
'DataObject::many_many() didn\'t find the correct relation table');
|
||||
$this->assertEquals('DataObjectTest_TeamID', $parentField);
|
||||
$this->assertEquals('DataObjectTest_EquipmentCompanyID', $childField);
|
||||
|
||||
// Check that ManyManyList still works
|
||||
$this->assertEquals(2, $sponsors->count(), 'Rows are missing from relation');
|
||||
@ -1360,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();
|
||||
@ -1378,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)'
|
||||
));
|
||||
@ -1570,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.'
|
||||
);
|
||||
|
||||
@ -1579,39 +1543,50 @@ class DataObjectTest extends SapphireTest {
|
||||
'CurrentStaff' => 'DataObjectTest_Staff.CurrentCompany',
|
||||
'PreviousStaff' => 'DataObjectTest_Staff.PreviousCompany'
|
||||
),
|
||||
$company->hasMany(null, false),
|
||||
$company->hasMany(false),
|
||||
'has_many returns field name data when $classOnly is false.'
|
||||
);
|
||||
|
||||
$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');
|
||||
}
|
||||
|
||||
|
368
tests/model/ManyManyThroughListTest.php
Normal file
368
tests/model/ManyManyThroughListTest.php
Normal file
@ -0,0 +1,368 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\ManyManyThroughList;
|
||||
use SilverStripe\ORM\Versioning\Versioned;
|
||||
|
||||
class ManyManyThroughListTest extends SapphireTest
|
||||
{
|
||||
protected static $fixture_file = 'ManyManyThroughListTest.yml';
|
||||
|
||||
protected $extraDataObjects = [
|
||||
ManyManyThroughListTest_Item::class,
|
||||
ManyManyThroughListTest_JoinObject::class,
|
||||
ManyManyThroughListTest_Object::class,
|
||||
ManyManyThroughListTest_VersionedItem::class,
|
||||
ManyManyThroughListTest_VersionedJoinObject::class,
|
||||
ManyManyThroughListTest_VersionedObject::class,
|
||||
];
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
DataObject::reset();
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
DataObject::reset();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testSelectJoin() {
|
||||
/** @var ManyManyThroughListTest_Object $parent */
|
||||
$parent = $this->objFromFixture(ManyManyThroughListTest_Object::class, 'parent1');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'item 1'],
|
||||
['Title' => 'item 2']
|
||||
],
|
||||
$parent->Items()
|
||||
);
|
||||
// Check filters on list work
|
||||
$item1 = $parent->Items()->filter('Title', 'item 1')->first();
|
||||
$this->assertNotNull($item1);
|
||||
$this->assertNotNull($item1->getJoin());
|
||||
$this->assertEquals('join 1', $item1->getJoin()->Title);
|
||||
$this->assertInstanceOf(
|
||||
ManyManyThroughListTest_JoinObject::class,
|
||||
$item1->ManyManyThroughListTest_JoinObject
|
||||
);
|
||||
$this->assertEquals('join 1', $item1->ManyManyThroughListTest_JoinObject->Title);
|
||||
|
||||
// Check filters on list work
|
||||
$item2 = $parent->Items()->filter('Title', 'item 2')->first();
|
||||
$this->assertNotNull($item2);
|
||||
$this->assertNotNull($item2->getJoin());
|
||||
$this->assertEquals('join 2', $item2->getJoin()->Title);
|
||||
$this->assertEquals('join 2', $item2->ManyManyThroughListTest_JoinObject->Title);
|
||||
|
||||
// To filter on join table need to use some raw sql
|
||||
$item2 = $parent->Items()->where(['"ManyManyThroughListTest_JoinObject"."Title"' => 'join 2'])->first();
|
||||
$this->assertNotNull($item2);
|
||||
$this->assertEquals('item 2', $item2->Title);
|
||||
$this->assertNotNull($item2->getJoin());
|
||||
$this->assertEquals('join 2', $item2->getJoin()->Title);
|
||||
$this->assertEquals('join 2', $item2->ManyManyThroughListTest_JoinObject->Title);
|
||||
|
||||
// Test sorting on join table
|
||||
$items = $parent->Items()->sort('"ManyManyThroughListTest_JoinObject"."Sort"');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'item 2'],
|
||||
['Title' => 'item 1'],
|
||||
],
|
||||
$items
|
||||
);
|
||||
|
||||
$items = $parent->Items()->sort('"ManyManyThroughListTest_JoinObject"."Sort" ASC');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'item 1'],
|
||||
['Title' => 'item 2'],
|
||||
],
|
||||
$items
|
||||
);
|
||||
$items = $parent->Items()->sort('"ManyManyThroughListTest_JoinObject"."Title" DESC');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'item 2'],
|
||||
['Title' => 'item 1'],
|
||||
],
|
||||
$items
|
||||
);
|
||||
}
|
||||
|
||||
public function testAdd() {
|
||||
/** @var ManyManyThroughListTest_Object $parent */
|
||||
$parent = $this->objFromFixture(ManyManyThroughListTest_Object::class, 'parent1');
|
||||
$newItem = new ManyManyThroughListTest_Item();
|
||||
$newItem->Title = 'my new item';
|
||||
$newItem->write();
|
||||
$parent->Items()->add($newItem, ['Title' => 'new join record']);
|
||||
|
||||
// Check select
|
||||
$newItem = $parent->Items()->filter(['Title' => 'my new item'])->first();
|
||||
$this->assertNotNull($newItem);
|
||||
$this->assertEquals('my new item', $newItem->Title);
|
||||
$this->assertInstanceOf(
|
||||
ManyManyThroughListTest_JoinObject::class,
|
||||
$newItem->getJoin()
|
||||
);
|
||||
$this->assertInstanceOf(
|
||||
ManyManyThroughListTest_JoinObject::class,
|
||||
$newItem->ManyManyThroughListTest_JoinObject
|
||||
);
|
||||
$this->assertEquals('new join record', $newItem->ManyManyThroughListTest_JoinObject->Title);
|
||||
}
|
||||
|
||||
public function testRemove() {
|
||||
/** @var ManyManyThroughListTest_Object $parent */
|
||||
$parent = $this->objFromFixture(ManyManyThroughListTest_Object::class, 'parent1');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'item 1'],
|
||||
['Title' => 'item 2']
|
||||
],
|
||||
$parent->Items()
|
||||
);
|
||||
$item1 = $parent->Items()->filter(['Title' => 'item 1'])->first();
|
||||
$parent->Items()->remove($item1);
|
||||
$this->assertDOSEquals(
|
||||
[['Title' => 'item 2']],
|
||||
$parent->Items()
|
||||
);
|
||||
}
|
||||
|
||||
public function testPublishing() {
|
||||
/** @var ManyManyThroughListTest_VersionedObject $draftParent */
|
||||
$draftParent = $this->objFromFixture(ManyManyThroughListTest_VersionedObject::class, 'parent1');
|
||||
$draftParent->publishRecursive();
|
||||
|
||||
// Modify draft stage
|
||||
$item1 = $draftParent->Items()->filter(['Title' => 'versioned item 1'])->first();
|
||||
$item1->Title = 'new versioned item 1';
|
||||
$item1->getJoin()->Title = 'new versioned join 1';
|
||||
$item1->write(false, false, false, true); // Write joined components
|
||||
$draftParent->Title = 'new versioned title';
|
||||
$draftParent->write();
|
||||
|
||||
// Check owned objects on stage
|
||||
$draftOwnedObjects = $draftParent->findOwned(true);
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'new versioned join 1'],
|
||||
['Title' => 'versioned join 2'],
|
||||
['Title' => 'new versioned item 1'],
|
||||
['Title' => 'versioned item 2'],
|
||||
],
|
||||
$draftOwnedObjects
|
||||
);
|
||||
|
||||
// Check live record is still old values
|
||||
// This tests that both the join table and many_many tables
|
||||
// inherit the necessary query parameters from the parent object.
|
||||
/** @var ManyManyThroughListTest_VersionedObject $liveParent */
|
||||
$liveParent = Versioned::get_by_stage(
|
||||
ManyManyThroughListTest_VersionedObject::class,
|
||||
Versioned::LIVE
|
||||
)->byID($draftParent->ID);
|
||||
$liveOwnedObjects = $liveParent->findOwned(true);
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'versioned join 1'],
|
||||
['Title' => 'versioned join 2'],
|
||||
['Title' => 'versioned item 1'],
|
||||
['Title' => 'versioned item 2'],
|
||||
],
|
||||
$liveOwnedObjects
|
||||
);
|
||||
|
||||
// Publish draft changes
|
||||
$draftParent->publishRecursive();
|
||||
$liveParent = Versioned::get_by_stage(
|
||||
ManyManyThroughListTest_VersionedObject::class,
|
||||
Versioned::LIVE
|
||||
)->byID($draftParent->ID);
|
||||
$liveOwnedObjects = $liveParent->findOwned(true);
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'new versioned join 1'],
|
||||
['Title' => 'versioned join 2'],
|
||||
['Title' => 'new versioned item 1'],
|
||||
['Title' => 'versioned item 2'],
|
||||
],
|
||||
$liveOwnedObjects
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validation
|
||||
*/
|
||||
public function testValidateModelValidatesJoinType() {
|
||||
DataObject::reset();
|
||||
ManyManyThroughListTest_Item::config()->update('db', [
|
||||
'ManyManyThroughListTest_JoinObject' => 'Text'
|
||||
]);
|
||||
$this->setExpectedException(InvalidArgumentException::class);
|
||||
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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic parent object
|
||||
*
|
||||
* @property string $Title
|
||||
* @method ManyManyThroughList Items()
|
||||
*/
|
||||
class ManyManyThroughListTest_Object extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $many_many = [
|
||||
'Items' => [
|
||||
'through' => ManyManyThroughListTest_JoinObject::class,
|
||||
'from' => 'Parent',
|
||||
'to' => 'Child',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property string $Title
|
||||
* @method ManyManyThroughListTest_Object Parent()
|
||||
* @method ManyManyThroughListTest_Item Child()
|
||||
*/
|
||||
class ManyManyThroughListTest_JoinObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar',
|
||||
'Sort' => 'Int',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Parent' => ManyManyThroughListTest_Object::class,
|
||||
'Child' => ManyManyThroughListTest_Item::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property string $Title
|
||||
* @method ManyManyThroughList Objects()
|
||||
*/
|
||||
class ManyManyThroughListTest_Item extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $belongs_many_many = [
|
||||
'Objects' => 'ManyManyThroughListTest_Object.Items'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic parent object
|
||||
*
|
||||
* @property string $Title
|
||||
* @method ManyManyThroughList Items()
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class ManyManyThroughListTest_VersionedObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
|
||||
private static $owns = [
|
||||
'Items' // Should automatically own both mapping and child records
|
||||
];
|
||||
|
||||
private static $many_many = [
|
||||
'Items' => [
|
||||
'through' => ManyManyThroughListTest_VersionedJoinObject::class,
|
||||
'from' => 'Parent',
|
||||
'to' => 'Child',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property string $Title
|
||||
* @method ManyManyThroughListTest_VersionedObject Parent()
|
||||
* @method ManyManyThroughListTest_VersionedItem Child()
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class ManyManyThroughListTest_VersionedJoinObject extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Parent' => ManyManyThroughListTest_VersionedObject::class,
|
||||
'Child' => ManyManyThroughListTest_VersionedItem::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property string $Title
|
||||
* @method ManyManyThroughList Objects()
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class ManyManyThroughListTest_VersionedItem extends DataObject implements TestOnly
|
||||
{
|
||||
private static $db = [
|
||||
'Title' => 'Varchar'
|
||||
];
|
||||
|
||||
private static $extensions = [
|
||||
Versioned::class
|
||||
];
|
||||
|
||||
private static $belongs_many_many = [
|
||||
'Objects' => 'ManyManyThroughListTest_VersionedObject.Items'
|
||||
];
|
||||
}
|
||||
|
36
tests/model/ManyManyThroughListTest.yml
Normal file
36
tests/model/ManyManyThroughListTest.yml
Normal file
@ -0,0 +1,36 @@
|
||||
ManyManyThroughListTest_Object:
|
||||
parent1:
|
||||
Title: 'my object'
|
||||
ManyManyThroughListTest_Item:
|
||||
child1:
|
||||
Title: 'item 1'
|
||||
child2:
|
||||
Title: 'item 2'
|
||||
ManyManyThroughListTest_JoinObject:
|
||||
join1:
|
||||
Title: 'join 1'
|
||||
Sort: 4
|
||||
Parent: =>ManyManyThroughListTest_Object.parent1
|
||||
Child: =>ManyManyThroughListTest_Item.child1
|
||||
join2:
|
||||
Title: 'join 2'
|
||||
Sort: 2
|
||||
Parent: =>ManyManyThroughListTest_Object.parent1
|
||||
Child: =>ManyManyThroughListTest_Item.child2
|
||||
ManyManyThroughListTest_VersionedObject:
|
||||
parent1:
|
||||
Title: 'versioned object'
|
||||
ManyManyThroughListTest_VersionedItem:
|
||||
child1:
|
||||
Title: 'versioned item 1'
|
||||
child2:
|
||||
Title: 'versioned item 2'
|
||||
ManyManyThroughListTest_VersionedJoinObject:
|
||||
join1:
|
||||
Title: 'versioned join 1'
|
||||
Parent: =>ManyManyThroughListTest_VersionedObject.parent1
|
||||
Child: =>ManyManyThroughListTest_VersionedItem.child1
|
||||
join2:
|
||||
Title: 'versioned join 2'
|
||||
Parent: =>ManyManyThroughListTest_VersionedObject.parent1
|
||||
Child: =>ManyManyThroughListTest_VersionedItem.child2
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\HasManyList;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
@ -405,32 +406,28 @@ class VersionedTest extends SapphireTest {
|
||||
* Tests DataObject::hasOwnTableDatabaseField
|
||||
*/
|
||||
public function testHasOwnTableDatabaseFieldWithVersioned() {
|
||||
$noversion = new DataObject();
|
||||
$versioned = new VersionedTest_DataObject();
|
||||
$versionedSub = new VersionedTest_Subclass();
|
||||
$versionedAno = new VersionedTest_AnotherSubclass();
|
||||
$versionField = new VersionedTest_UnversionedWithField();
|
||||
$schema = DataObject::getSchema();
|
||||
|
||||
$this->assertFalse(
|
||||
(bool) $noversion->hasOwnTableDatabaseField('Version'),
|
||||
$this->assertNull(
|
||||
$schema->fieldSpec(DataObject::class, 'Version', ['uninherited']),
|
||||
'Plain models have no version field.'
|
||||
);
|
||||
$this->assertEquals(
|
||||
'Int', $versioned->hasOwnTableDatabaseField('Version'),
|
||||
'Int',
|
||||
$schema->fieldSpec(VersionedTest_DataObject::class, 'Version', ['uninherited']),
|
||||
'The versioned ext adds an Int version field.'
|
||||
);
|
||||
$this->assertEquals(
|
||||
null,
|
||||
$versionedSub->hasOwnTableDatabaseField('Version'),
|
||||
$this->assertNull(
|
||||
$schema->fieldSpec(VersionedTest_Subclass::class, 'Version', ['uninherited']),
|
||||
'Sub-classes of a versioned model don\'t have a Version field.'
|
||||
);
|
||||
$this->assertNull(
|
||||
$schema->fieldSpec(VersionedTest_AnotherSubclass::class, 'Version', ['uninherited']),
|
||||
'Sub-classes of a versioned model don\'t have a Version field.'
|
||||
);
|
||||
$this->assertEquals(
|
||||
null,
|
||||
$versionedAno->hasOwnTableDatabaseField('Version'),
|
||||
'Sub-classes of a versioned model don\'t have a Version field.'
|
||||
);
|
||||
$this->assertEquals(
|
||||
'Varchar', $versionField->hasOwnTableDatabaseField('Version'),
|
||||
'Varchar(255)',
|
||||
$schema->fieldSpec(VersionedTest_UnversionedWithField::class, 'Version', ['uninherited']),
|
||||
'Models w/o Versioned can have their own Version field.'
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user