mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
API Polymorphic has_one behaviour
This commit is contained in:
parent
26f805fbb3
commit
7c60c73dbb
@ -171,12 +171,15 @@ class FixtureBlueprint {
|
||||
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
||||
}
|
||||
}
|
||||
} elseif($obj->has_one($fieldName)) {
|
||||
// Sets has_one with relation name
|
||||
$obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures);
|
||||
} elseif($obj->has_one(preg_replace('/ID$/', '', $fieldName))) {
|
||||
// Sets has_one with database field
|
||||
$obj->$fieldName = $this->parseValue($fieldVal, $fixtures);
|
||||
} else {
|
||||
$hasOneField = preg_replace('/ID$/', '', $fieldName);
|
||||
if($className = $obj->has_one($hasOneField)) {
|
||||
$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
|
||||
// Inject class for polymorphic relation
|
||||
if($className === 'DataObject') {
|
||||
$obj->{$hasOneField.'Class'} = $fieldClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$obj->write();
|
||||
@ -261,11 +264,13 @@ class FixtureBlueprint {
|
||||
* Parse a value from a fixture file. If it starts with =>
|
||||
* it will get an ID from the fixture dictionary
|
||||
*
|
||||
* @param String $fieldVal
|
||||
* @param Array $fixtures See {@link createObject()}
|
||||
* @return String Fixture database ID, or the original value
|
||||
* @param string $fieldVal
|
||||
* @param array $fixtures See {@link createObject()}
|
||||
* @param string $class If the value parsed is a class relation, this parameter
|
||||
* will be given the value of that class's name
|
||||
* @return string Fixture database ID, or the original value
|
||||
*/
|
||||
protected function parseValue($value, $fixtures = null) {
|
||||
protected function parseValue($value, $fixtures = null, &$class = null) {
|
||||
if(substr($value,0,2) == '=>') {
|
||||
// Parse a dictionary reference - used to set foreign keys
|
||||
list($class, $identifier) = explode('.', substr($value,2), 2);
|
||||
|
@ -96,6 +96,7 @@ class TestRunner extends Controller {
|
||||
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
|
||||
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
|
||||
DataObject::clear_classname_spec_cache();
|
||||
PolymorphicForeignKey::clear_classname_spec_cache();
|
||||
}
|
||||
|
||||
public function init() {
|
||||
|
@ -21,7 +21,8 @@ Every object of this class **or any of its subclasses** will have an entry in th
|
||||
* Every field listed in the data object's **$db** array will be included in this table.
|
||||
* For every relationship listed in the data object's **$has_one** array, there will be an integer field included in the
|
||||
table. This will contain the ID of the data-object being linked to. The database field name will be of the form
|
||||
"(relationship-name)ID", for example, ParentID.
|
||||
"(relationship-name)ID", for example, ParentID. For polymorphic has_one relationships, there is an additional
|
||||
"(relationship-name)Class" field to identify the class this ID corresponds to. See [datamodel](/topics/datamodel#has_one).
|
||||
|
||||
### ID Generation
|
||||
|
||||
|
@ -533,6 +533,46 @@ relationship to link to its parent element in the tree:
|
||||
);
|
||||
}
|
||||
|
||||
A has_one can also be polymorphic, which allows any type of object to be associated.
|
||||
This is useful where there could be many use cases for a particular data structure.
|
||||
|
||||
An additional column is created called "`<relationship-name>`Class", which along
|
||||
with the ID column identifies the object.
|
||||
|
||||
To specify that a has_one relation is polymorphic set the type to 'DataObject'.
|
||||
Ideally, the associated has_many (or belongs_to) should be specified with dot notation.
|
||||
|
||||
::php
|
||||
|
||||
class Player extends DataObject {
|
||||
private static $has_many = array(
|
||||
"Fans" => "Fan.FanOf"
|
||||
);
|
||||
}
|
||||
|
||||
class Team extends DataObject {
|
||||
private static $has_many = array(
|
||||
"Fans" => "Fan.FanOf"
|
||||
);
|
||||
}
|
||||
|
||||
// Type of object returned by $fan->FanOf() will vary
|
||||
class Fan extends DataObject {
|
||||
|
||||
// Generates columns FanOfID and FanOfClass
|
||||
private static $has_one = array(
|
||||
"FanOf" => "DataObject"
|
||||
);
|
||||
}
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
Note: The use of polymorphic relationships can affect query performance, especially
|
||||
on joins, and also increases the complexity of the database and necessary user code.
|
||||
They should be used sparingly, and only where additional complexity would otherwise
|
||||
be necessary. E.g. Additional parent classes for each respective relationship, or
|
||||
duplication of code.
|
||||
</div>
|
||||
|
||||
### has_many
|
||||
|
||||
Defines 1-to-many joins. A database-column named ""`<relationship-name>`ID""
|
||||
|
@ -96,13 +96,16 @@ class FormScaffolder extends Object {
|
||||
if($this->obj->has_one()) {
|
||||
foreach($this->obj->has_one() as $relationship => $component) {
|
||||
if($this->restrictFields && !in_array($relationship, $this->restrictFields)) continue;
|
||||
$fieldName = "{$relationship}ID";
|
||||
$fieldName = $component === 'DataObject'
|
||||
? $relationship // Polymorphic has_one field is composite, so don't refer to ID subfield
|
||||
: "{$relationship}ID";
|
||||
if($this->fieldClasses && isset($this->fieldClasses[$fieldName])) {
|
||||
$fieldClass = $this->fieldClasses[$fieldName];
|
||||
$hasOneField = new $fieldClass($fieldName);
|
||||
} else {
|
||||
$hasOneField = $this->obj->dbObject($fieldName)->scaffoldFormField(null, $this->getParamsArray());
|
||||
}
|
||||
if(empty($hasOneField)) continue; // Allow fields to opt out of scaffolding
|
||||
$hasOneField->setTitle($this->obj->fieldLabel($relationship));
|
||||
if($this->tabbed) {
|
||||
$fields->addFieldToTab("Root.Main", $hasOneField);
|
||||
|
@ -313,7 +313,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Add has_one relationships
|
||||
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
|
||||
if($hasOne) foreach(array_keys($hasOne) as $field) {
|
||||
$fields[$field . 'ID'] = 'ForeignKey';
|
||||
|
||||
// Check if this is a polymorphic relation, in which case the relation
|
||||
// is a composite field
|
||||
if($hasOne[$field] === 'DataObject') {
|
||||
$relationField = DBField::create_field('PolymorphicForeignKey', null, $field);
|
||||
$relationField->setTable($class);
|
||||
if($compositeFields = $relationField->compositeDatabaseFields()) {
|
||||
foreach($compositeFields as $compositeName => $spec) {
|
||||
$fields["{$field}{$compositeName}"] = $spec;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$fields[$field . 'ID'] = 'ForeignKey';
|
||||
}
|
||||
}
|
||||
|
||||
$output = (array) $fields;
|
||||
@ -1412,7 +1425,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Return a component object from a one to one relationship, as a DataObject.
|
||||
* If no component is available, an 'empty component' will be returned.
|
||||
* If no component is available, an 'empty component' will be returned for
|
||||
* non-polymorphic relations, or for polymorphic relations with a class set.
|
||||
*
|
||||
* @param string $componentName Name of the component
|
||||
*
|
||||
@ -1427,24 +1441,40 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$joinField = $componentName . 'ID';
|
||||
$joinID = $this->getField($joinField);
|
||||
|
||||
// Extract class name for polymorphic relations
|
||||
if($class === 'DataObject') {
|
||||
$class = $this->getField($componentName . 'Class');
|
||||
if(empty($class)) return null;
|
||||
}
|
||||
|
||||
if($joinID) {
|
||||
$component = $this->model->$class->byID($joinID);
|
||||
}
|
||||
|
||||
if(!isset($component) || !$component) {
|
||||
if(empty($component)) {
|
||||
$component = $this->model->$class->newObject();
|
||||
}
|
||||
} elseif($class = $this->belongs_to($componentName)) {
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to');
|
||||
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
|
||||
$joinID = $this->ID;
|
||||
|
||||
if($joinID) {
|
||||
$component = DataObject::get_one($class, "\"$joinField\" = $joinID");
|
||||
$filter = $polymorphic
|
||||
? "\"{$joinField}ID\" = '".Convert::raw2sql($joinID)."' AND
|
||||
\"{$joinField}Class\" = '".Convert::raw2sql($this->class)."'"
|
||||
: "\"{$joinField}\" = '".Convert::raw2sql($joinID)."'";
|
||||
$component = DataObject::get_one($class, $filter);
|
||||
}
|
||||
|
||||
if(!isset($component) || !$component) {
|
||||
if(empty($component)) {
|
||||
$component = $this->model->$class->newObject();
|
||||
$component->$joinField = $this->ID;
|
||||
if($polymorphic) {
|
||||
$component->{$joinField.'ID'} = $this->ID;
|
||||
$component->{$joinField.'Class'} = $this->class;
|
||||
} else {
|
||||
$component->$joinField = $this->ID;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
|
||||
@ -1489,15 +1519,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return $this->unsavedRelations[$componentName];
|
||||
}
|
||||
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'has_many');
|
||||
// Determine type and nature of foreign relation
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
|
||||
if($polymorphic) {
|
||||
$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
|
||||
} else {
|
||||
$result = HasManyList::create($componentClass, $joinField);
|
||||
}
|
||||
|
||||
$result = HasManyList::create($componentClass, $joinField);
|
||||
if($this->model) $result->setDataModel($this->model);
|
||||
$result = $result->forForeignID($this->ID);
|
||||
|
||||
$result = $result->where($filter)->limit($limit)->sort($sort);
|
||||
|
||||
return $result;
|
||||
return $result
|
||||
->forForeignID($this->ID)
|
||||
->where($filter)
|
||||
->limit($limit)
|
||||
->sort($sort);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1540,17 +1576,23 @@ 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'.
|
||||
* 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'.
|
||||
*
|
||||
* @param string $component
|
||||
* 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
|
||||
*/
|
||||
public function getRemoteJoinField($component, $type = 'has_many') {
|
||||
$remoteClass = $this->$type($component, false);
|
||||
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
|
||||
|
||||
if(!$remoteClass) {
|
||||
// Extract relation from current object
|
||||
$remoteClass = $this->$type($component, false);
|
||||
if(empty($remoteClass)) {
|
||||
throw new Exception("Unknown $type component '$component' on class '$this->class'");
|
||||
}
|
||||
if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
|
||||
@ -1559,28 +1601,56 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
);
|
||||
}
|
||||
|
||||
if($fieldPos = strpos($remoteClass, '.')) {
|
||||
return substr($remoteClass, $fieldPos + 1) . 'ID';
|
||||
// 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');
|
||||
if(!is_array($remoteRelations)) {
|
||||
$remoteRelations = array();
|
||||
}
|
||||
$remoteRelations = array_flip($remoteRelations);
|
||||
|
||||
// look for remote has_one joins on this class or any parent classes
|
||||
foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
|
||||
if(array_key_exists($class, $remoteRelations)) return $remoteRelations[$class] . 'ID';
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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'";
|
||||
// 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] === 'DataObject') {
|
||||
$polymorphic = true;
|
||||
return $remoteField; // Composite polymorphic field does not include 'ID' suffix
|
||||
} else {
|
||||
$polymorphic = false;
|
||||
return $remoteField . 'ID';
|
||||
}
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1602,20 +1672,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return $this->unsavedRelations[$componentName];
|
||||
}
|
||||
|
||||
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField,
|
||||
$this->many_many_extraFields($componentName));
|
||||
$result = ManyManyList::create(
|
||||
$componentClass, $table, $componentField, $parentField,
|
||||
$this->many_many_extraFields($componentName)
|
||||
);
|
||||
if($this->model) $result->setDataModel($this->model);
|
||||
|
||||
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
|
||||
// foreignID set elsewhere.
|
||||
$result = $result->forForeignID($this->ID);
|
||||
|
||||
return $result->where($filter)->sort($sort)->limit($limit);
|
||||
return $result
|
||||
->forForeignID($this->ID)
|
||||
->where($filter)
|
||||
->sort($sort)
|
||||
->limit($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
|
||||
* their classes.
|
||||
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
|
||||
* their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
|
||||
*
|
||||
* @param string $component Name of component
|
||||
*
|
||||
@ -2463,9 +2537,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
// all has_one relations on this specific class,
|
||||
// add foreign key
|
||||
|
||||
$hasOne = Config::inst()->get($this->class, 'has_one', Config::UNINHERITED);
|
||||
if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) {
|
||||
$fieldMap[$fieldName . 'ID'] = "ForeignKey";
|
||||
if($fieldSchema === 'DataObject') {
|
||||
// For polymorphic has_one relation break into individual subfields
|
||||
$fieldMap[$fieldName . 'ID'] = "Int";
|
||||
$fieldMap[$fieldName . 'Class'] = "Enum";
|
||||
$fieldMap[$fieldName] = "PolymorphicForeignKey";
|
||||
} else {
|
||||
$fieldMap[$fieldName . 'ID'] = "ForeignKey";
|
||||
}
|
||||
}
|
||||
|
||||
// set cached fieldmap
|
||||
@ -2704,6 +2786,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
|
||||
$val = $this->$fieldName;
|
||||
return DBField::create_field('ForeignKey', $val, $fieldName, $this);
|
||||
|
||||
// has_one for polymorphic relations do not end in ID
|
||||
} else if($this->has_one($fieldName)) {
|
||||
$val = $this->$fieldName;
|
||||
return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
129
model/PolymorphicHasManyList.php
Normal file
129
model/PolymorphicHasManyList.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a has_many list linked against a polymorphic relationship
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class PolymorphicHasManyList extends HasManyList {
|
||||
|
||||
/**
|
||||
* Name of foreign key field that references the class name of the relation
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $classForeignKey;
|
||||
|
||||
/**
|
||||
* Retrieve the name of the class this relation is filtered by
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignClass() {
|
||||
return $this->dataQuery->getQueryParam('Foreign.Class');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new PolymorphicHasManyList relation list.
|
||||
*
|
||||
* @param string $dataClass The class of the DataObjects that this will list.
|
||||
* @param string $foreignField The name of the composite foreign relation field. Used
|
||||
* to generate the ID and Class foreign keys.
|
||||
* @param string $foreignClass Name of the class filter this relation is filtered against
|
||||
*/
|
||||
function __construct($dataClass, $foreignField, $foreignClass) {
|
||||
|
||||
// Set both id foreign key (as in HasManyList) and the class foreign key
|
||||
parent::__construct($dataClass, "{$foreignField}ID");
|
||||
$this->classForeignKey = "{$foreignField}Class";
|
||||
|
||||
// Ensure underlying DataQuery globally references the class filter
|
||||
$this->dataQuery->setQueryParam('Foreign.Class', $foreignClass);
|
||||
|
||||
// For queries with multiple foreign IDs (such as that generated by
|
||||
// DataList::relation) the filter must be generalised to filter by subclasses
|
||||
$classNames = Convert::raw2sql(ClassInfo::subclassesFor($foreignClass));
|
||||
$this->dataQuery->where(sprintf(
|
||||
"\"{$this->classForeignKey}\" IN ('%s')",
|
||||
implode("', '", $classNames)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the item to this relation.
|
||||
*
|
||||
* It does so by setting the relationFilters.
|
||||
*
|
||||
* @param $item The DataObject to be added, or its ID
|
||||
*/
|
||||
public function add($item) {
|
||||
if(is_numeric($item)) {
|
||||
$item = DataObject::get_by_id($this->dataClass, $item);
|
||||
} else if(!($item instanceof $this->dataClass)) {
|
||||
user_error(
|
||||
"PolymorphicHasManyList::add() expecting a $this->dataClass object, or ID value",
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
$foreignID = $this->getForeignID();
|
||||
|
||||
// Validate foreignID
|
||||
if(!$foreignID) {
|
||||
user_error(
|
||||
"PolymorphicHasManyList::add() can't be called until a foreign ID is set",
|
||||
E_USER_WARNING
|
||||
);
|
||||
return;
|
||||
}
|
||||
if(is_array($foreignID)) {
|
||||
user_error(
|
||||
"PolymorphicHasManyList::add() can't be called on a list linked to mulitple foreign IDs",
|
||||
E_USER_WARNING
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignKey = $this->foreignKey;
|
||||
$classForeignKey = $this->classForeignKey;
|
||||
$item->$foreignKey = $foreignID;
|
||||
$item->$classForeignKey = $this->getForeignClass();
|
||||
|
||||
$item->write();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from this relation.
|
||||
* Doesn't actually remove the item, it just clears the foreign key value.
|
||||
*
|
||||
* @param $item The DataObject to be removed
|
||||
* @todo Maybe we should delete the object instead?
|
||||
*/
|
||||
public function remove($item) {
|
||||
if(!($item instanceof $this->dataClass)) {
|
||||
throw new InvalidArgumentException("HasManyList::remove() expecting a $this->dataClass object, or ID",
|
||||
E_USER_ERROR);
|
||||
}
|
||||
|
||||
// Don't remove item with unrelated class key
|
||||
$foreignClass = $this->getForeignClass();
|
||||
$classNames = ClassInfo::subclassesFor($foreignClass);
|
||||
$classForeignKey = $this->classForeignKey;
|
||||
if(!in_array($item->$classForeignKey, $classNames)) return;
|
||||
|
||||
// Don't remove item which doesn't belong to this list
|
||||
$foreignID = $this->getForeignID();
|
||||
$foreignKey = $this->foreignKey;
|
||||
|
||||
if( empty($foreignID)
|
||||
|| (is_array($foreignID) && in_array($item->$foreignKey, $foreignID))
|
||||
|| $foreignID == $item->$foreignKey
|
||||
) {
|
||||
$item->$foreignKey = null;
|
||||
$item->$classForeignKey = null;
|
||||
$item->write();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
193
model/fieldtypes/PolymorphicForeignKey.php
Normal file
193
model/fieldtypes/PolymorphicForeignKey.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A special ForeignKey class that handles relations with arbitrary class types
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class PolymorphicForeignKey extends ForeignKey implements CompositeDBField {
|
||||
|
||||
/**
|
||||
* @var boolean $isChanged
|
||||
*/
|
||||
protected $isChanged = false;
|
||||
|
||||
/**
|
||||
* Value of relation class
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $classValue = null;
|
||||
|
||||
/**
|
||||
* Field definition cache for compositeDatabaseFields
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $classname_spec_cache = array();
|
||||
|
||||
/**
|
||||
* Clear all cached classname specs. It's necessary to clear all cached subclassed names
|
||||
* for any classes if a new class manifest is generated.
|
||||
*/
|
||||
public static function clear_classname_spec_cache() {
|
||||
self::$classname_spec_cache = array();
|
||||
}
|
||||
|
||||
public function scaffoldFormField($title = null, $params = null) {
|
||||
// Opt-out of form field generation - Scaffolding should be performed on
|
||||
// the has_many end, or set programatically.
|
||||
// @todo - Investigate suitable FormField
|
||||
return null;
|
||||
}
|
||||
|
||||
public function requireField() {
|
||||
$fields = $this->compositeDatabaseFields();
|
||||
if($fields) foreach($fields as $name => $type){
|
||||
DB::requireField($this->tableName, $this->name.$name, $type);
|
||||
}
|
||||
}
|
||||
|
||||
public function writeToManipulation(&$manipulation) {
|
||||
|
||||
// Write ID, checking that the value is valid
|
||||
$manipulation['fields'][$this->name . 'ID'] = $this->exists()
|
||||
? $this->prepValueForDB($this->getIDValue())
|
||||
: $this->nullValue();
|
||||
|
||||
// Write class
|
||||
$classObject = DBField::create_field('Enum', $this->getClassValue(), $this->name . 'Class');
|
||||
$classObject->writeToManipulation($manipulation);
|
||||
}
|
||||
|
||||
public function addToQuery(&$query) {
|
||||
parent::addToQuery($query);
|
||||
$query->selectField(
|
||||
"\"{$this->tableName}\".\"{$this->name}ID\"",
|
||||
"{$this->name}ID"
|
||||
);
|
||||
$query->selectField(
|
||||
"\"{$this->tableName}\".\"{$this->name}Class\"",
|
||||
"{$this->name}Class"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the "Class" this key points to
|
||||
*
|
||||
* @return string Name of a subclass of DataObject
|
||||
*/
|
||||
public function getClassValue() {
|
||||
return $this->classValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the "Class" this key points to
|
||||
*
|
||||
* @param string $class Name of a subclass of DataObject
|
||||
* @param boolean $markChanged Mark this field as changed?
|
||||
*/
|
||||
public function setClassValue($class, $markChanged = true) {
|
||||
$this->classValue = $class;
|
||||
if($markChanged) $this->isChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the "ID" this key points to
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getIDValue() {
|
||||
return parent::getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the "ID" this key points to
|
||||
*
|
||||
* @param integer $id
|
||||
* @param boolean $markChanged Mark this field as changed?
|
||||
*/
|
||||
public function setIDValue($id, $markChanged = true) {
|
||||
parent::setValue($id);
|
||||
if($markChanged) $this->isChanged = true;
|
||||
}
|
||||
|
||||
public function setValue($value, $record = null, $markChanged = true) {
|
||||
$idField = "{$this->name}ID";
|
||||
$classField = "{$this->name}Class";
|
||||
|
||||
// Check if an object is assigned directly
|
||||
if($value instanceof DataObject) {
|
||||
$record = array(
|
||||
$idField => $value->ID,
|
||||
$classField => $value->class
|
||||
);
|
||||
}
|
||||
|
||||
// Convert an object to an array
|
||||
if($record instanceof DataObject) {
|
||||
$record = $record->getQueriedDatabaseFields();
|
||||
}
|
||||
|
||||
// Use $value array if record is missing
|
||||
if(empty($record) && is_array($value)) {
|
||||
$record = $value;
|
||||
}
|
||||
|
||||
// Inspect presented values
|
||||
if(isset($record[$idField]) && isset($record[$classField])) {
|
||||
if(empty($record[$idField]) || empty($record[$classField])) {
|
||||
$this->setIDValue($this->nullValue(), $markChanged);
|
||||
$this->setClassValue('', $markChanged);
|
||||
} else {
|
||||
$this->setClassValue($record[$classField], $markChanged);
|
||||
$this->setIDValue($record[$idField], $markChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getValue() {
|
||||
if($this->exists()) {
|
||||
return DataObject::get_by_id($this->getClassValue(), $this->getIDValue());
|
||||
}
|
||||
}
|
||||
|
||||
public function compositeDatabaseFields() {
|
||||
|
||||
// Ensure the table level cache exists
|
||||
if(empty(self::$classname_spec_cache[$this->tableName])) {
|
||||
self::$classname_spec_cache[$this->tableName] = array();
|
||||
}
|
||||
|
||||
// Ensure the field level cache exists
|
||||
if(empty(self::$classname_spec_cache[$this->tableName][$this->name])) {
|
||||
|
||||
// Get all class names
|
||||
$classNames = ClassInfo::subclassesFor('DataObject');
|
||||
unset($classNames['DataObject']);
|
||||
|
||||
$db = DB::getConn();
|
||||
if($db->hasField($this->tableName, "{$this->name}Class")) {
|
||||
$existing = $db->query("SELECT DISTINCT \"{$this->name}Class\" FROM \"{$this->tableName}\"")->column();
|
||||
$classNames = array_unique(array_merge($classNames, $existing));
|
||||
}
|
||||
|
||||
self::$classname_spec_cache[$this->tableName][$this->name]
|
||||
= "Enum(array('" . implode("', '", array_filter($classNames)) . "'))";
|
||||
}
|
||||
|
||||
return array(
|
||||
'ID' => 'Int',
|
||||
'Class' => self::$classname_spec_cache[$this->tableName][$this->name]
|
||||
);
|
||||
}
|
||||
|
||||
public function isChanged() {
|
||||
return $this->isChanged;
|
||||
}
|
||||
|
||||
public function exists() {
|
||||
return $this->getClassValue() && $this->getIDValue();
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ class FormScaffolderTest extends SapphireTest {
|
||||
$this->assertNotNull($fields->dataFieldByName('AuthorID'),
|
||||
'getCMSFields() includes has_one fields on singletons');
|
||||
$this->assertNull($fields->dataFieldByName('Tags'),
|
||||
'getCMSFields() doesnt include many_many fields if no ID is present');
|
||||
"getCMSFields() doesn't include many_many fields if no ID is present");
|
||||
}
|
||||
|
||||
public function testGetCMSFieldsInstance() {
|
||||
@ -47,6 +47,14 @@ class FormScaffolderTest extends SapphireTest {
|
||||
'getCMSFields() includes has_one fields on instances');
|
||||
$this->assertNotNull($fields->dataFieldByName('Tags'),
|
||||
'getCMSFields() includes many_many fields if ID is present on instances');
|
||||
$this->assertNotNull($fields->dataFieldByName('SubjectOfArticles'),
|
||||
'getCMSFields() includes polymorphic has_many fields if ID is present on instances');
|
||||
$this->assertNull($fields->dataFieldByName('Subject'),
|
||||
"getCMSFields() doesn't include polymorphic has_one field");
|
||||
$this->assertNull($fields->dataFieldByName('SubjectID'),
|
||||
"getCMSFields() doesn't include polymorphic has_one id field");
|
||||
$this->assertNull($fields->dataFieldByName('SubjectClass'),
|
||||
"getCMSFields() doesn't include polymorphic has_one class field");
|
||||
}
|
||||
|
||||
public function testUpdateCMSFields() {
|
||||
@ -111,11 +119,15 @@ class FormScaffolderTest_Article extends DataObject implements TestOnly {
|
||||
'Content' => 'HTMLText'
|
||||
);
|
||||
private static $has_one = array(
|
||||
'Author' => 'FormScaffolderTest_Author'
|
||||
'Author' => 'FormScaffolderTest_Author',
|
||||
'Subject' => 'DataObject'
|
||||
);
|
||||
private static $many_many = array(
|
||||
'Tags' => 'FormScaffolderTest_Tag',
|
||||
);
|
||||
private static $has_many = array(
|
||||
'SubjectOfArticles' => 'FormScaffolderTest_Article.Subject'
|
||||
);
|
||||
}
|
||||
|
||||
class FormScaffolderTest_Author extends Member implements TestOnly {
|
||||
@ -123,7 +135,8 @@ class FormScaffolderTest_Author extends Member implements TestOnly {
|
||||
'ProfileImage' => 'Image'
|
||||
);
|
||||
private static $has_many = array(
|
||||
'Articles' => 'FormScaffolderTest_Article'
|
||||
'Articles' => 'FormScaffolderTest_Article.Author',
|
||||
'SubjectOfArticles' => 'FormScaffolderTest_Article.Subject'
|
||||
);
|
||||
}
|
||||
class FormScaffolderTest_Tag extends DataObject implements TestOnly {
|
||||
@ -133,6 +146,9 @@ class FormScaffolderTest_Tag extends DataObject implements TestOnly {
|
||||
private static $belongs_many_many = array(
|
||||
'Articles' => 'FormScaffolderTest_Article'
|
||||
);
|
||||
private static $has_many = array(
|
||||
'SubjectOfArticles' => 'FormScaffolderTest_Article.Subject'
|
||||
);
|
||||
}
|
||||
class FormScaffolderTest_ArticleExtension extends DataExtension implements TestOnly {
|
||||
private static $db = array(
|
||||
|
@ -16,7 +16,9 @@ class DataObjectTest extends SapphireTest {
|
||||
'DataObjectTest_FieldlessSubTable',
|
||||
'DataObjectTest_ValidatedObject',
|
||||
'DataObjectTest_Player',
|
||||
'DataObjectTest_TeamComment'
|
||||
'DataObjectTest_TeamComment',
|
||||
'DataObjectTest\NamespacedClass',
|
||||
'DataObjectTest\RelationClass',
|
||||
);
|
||||
|
||||
public function testBaseFieldsExcludedFromDb() {
|
||||
@ -206,15 +208,54 @@ class DataObjectTest extends SapphireTest {
|
||||
'belongs_many_many is properly inspected');
|
||||
$this->assertEquals(singleton('DataObjectTest_CEO')->getRelationClass('Company'), 'DataObjectTest_Company',
|
||||
'belongs_to is properly inspected');
|
||||
$this->assertEquals(singleton('DataObjectTest_Fan')->getRelationClass('Favourite'), 'DataObject',
|
||||
'polymorphic has_one is properly inspected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that has_one relations can be retrieved
|
||||
*/
|
||||
public function testGetHasOneRelations() {
|
||||
$captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1");
|
||||
/* There will be a field called (relname)ID that contains the ID of the object linked to via the
|
||||
* has_one relation */
|
||||
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeamID);
|
||||
/* There will be a method called $obj->relname() that returns the object itself */
|
||||
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeam()->ID);
|
||||
$team1ID = $this->idFromFixture('DataObjectTest_Team', 'team1');
|
||||
|
||||
// There will be a field called (relname)ID that contains the ID of the
|
||||
// object linked to via the has_one relation
|
||||
$this->assertEquals($team1ID, $captain1->FavouriteTeamID);
|
||||
|
||||
// There will be a method called $obj->relname() that returns the object itself
|
||||
$this->assertEquals($team1ID, $captain1->FavouriteTeam()->ID);
|
||||
|
||||
// Check entity with polymorphic has-one
|
||||
$fan1 = $this->objFromFixture("DataObjectTest_Fan", "fan1");
|
||||
|
||||
// There will be fields named (relname)ID and (relname)Class for polymorphic
|
||||
// entities
|
||||
$this->assertEquals($team1ID, $fan1->FavouriteID);
|
||||
$this->assertEquals('DataObjectTest_Team', $fan1->FavouriteClass);
|
||||
|
||||
// There will be a method called $obj->relname() that returns the object itself
|
||||
$favourite = $fan1->Favourite();
|
||||
$this->assertEquals($team1ID, $favourite->ID);
|
||||
$this->assertInstanceOf('DataObjectTest_Team', $favourite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple test to ensure that namespaced classes and polymorphic relations work together
|
||||
*/
|
||||
public function testPolymorphicNamespacedRelations() {
|
||||
$parent = new \DataObjectTest\NamespacedClass();
|
||||
$parent->Name = 'New Parent';
|
||||
$parent->write();
|
||||
|
||||
$child = new \DataObjectTest\RelationClass();
|
||||
$child->Title = 'New Child';
|
||||
$child->write();
|
||||
$parent->Relations()->add($child);
|
||||
|
||||
$this->assertEquals(1, $parent->Relations()->count());
|
||||
$this->assertEquals(array('New Child'), $parent->Relations()->column('Title'));
|
||||
$this->assertEquals('New Parent', $child->Parent()->Name);
|
||||
}
|
||||
|
||||
public function testLimitAndCount() {
|
||||
@ -240,9 +281,20 @@ class DataObjectTest extends SapphireTest {
|
||||
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
|
||||
$obj->FavouriteTeamID = 99;
|
||||
$obj->write();
|
||||
|
||||
// reload the page from the database
|
||||
$savedObj = DataObject::get_by_id('DataObjectTest_Player', $obj->ID);
|
||||
$this->assertTrue($savedObj->FavouriteTeamID == 99);
|
||||
|
||||
// Test with porymorphic relation
|
||||
$obj2 = $this->objFromFixture("DataObjectTest_Fan", "fan1");
|
||||
$obj2->FavouriteID = 99;
|
||||
$obj2->FavouriteClass = 'DataObjectTest_Player';
|
||||
$obj2->write();
|
||||
|
||||
$savedObj2 = DataObject::get_by_id('DataObjectTest_Fan', $obj2->ID);
|
||||
$this->assertTrue($savedObj2->FavouriteID == 99);
|
||||
$this->assertTrue($savedObj2->FavouriteClass == 'DataObjectTest_Player');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -285,9 +337,57 @@ class DataObjectTest extends SapphireTest {
|
||||
$this->assertEquals(array($comment1->ID, $newComment->ID), $team1CommentIDs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test has many relationships against polymorphic has_one fields
|
||||
* - Test getComponents() gets the ComponentSet of the other side of the relation
|
||||
* - Test the IDs on the DataObjects are set correctly
|
||||
*/
|
||||
public function testHasManyPolymorphicRelationships() {
|
||||
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||
|
||||
// Test getComponents() gets the ComponentSet of the other side of the relation
|
||||
$this->assertTrue($team1->Fans()->Count() == 2);
|
||||
|
||||
// Test the IDs/Classes on the DataObjects are set correctly
|
||||
foreach($team1->Fans() as $fan) {
|
||||
$this->assertEquals($team1->ID, $fan->FavouriteID, 'Fan has the correct FavouriteID');
|
||||
$this->assertEquals('DataObjectTest_Team', $fan->FavouriteClass, 'Fan has the correct FavouriteClass');
|
||||
}
|
||||
|
||||
// Test that we can add and remove items that already exist in the database
|
||||
$newFan = new DataObjectTest_Fan();
|
||||
$newFan->Name = "New fan";
|
||||
$newFan->write();
|
||||
$team1->Fans()->add($newFan);
|
||||
$this->assertEquals($team1->ID, $newFan->FavouriteID, 'Newly created fan has the correct FavouriteID');
|
||||
$this->assertEquals(
|
||||
'DataObjectTest_Team',
|
||||
$newFan->FavouriteClass,
|
||||
'Newly created fan has the correct FavouriteClass'
|
||||
);
|
||||
|
||||
$fan1 = $this->objFromFixture('DataObjectTest_Fan', 'fan1');
|
||||
$fan3 = $this->objFromFixture('DataObjectTest_Fan', 'fan3');
|
||||
$team1->Fans()->remove($fan3);
|
||||
|
||||
$team1FanIDs = $team1->Fans()->sort('ID')->column('ID');
|
||||
$this->assertEquals(array($fan1->ID, $newFan->ID), $team1FanIDs);
|
||||
|
||||
// Test that removing an item from a list doesn't remove it from the same
|
||||
// relation belonging to a different object
|
||||
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||
$player1 = $this->objFromFixture('DataObjectTest_Player', 'player1');
|
||||
$player1->Fans()->remove($fan1);
|
||||
$team1FanIDs = $team1->Fans()->sort('ID')->column('ID');
|
||||
$this->assertEquals(array($fan1->ID, $newFan->ID), $team1FanIDs);
|
||||
}
|
||||
|
||||
|
||||
public function testHasOneRelationship() {
|
||||
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||
$player1 = $this->objFromFixture('DataObjectTest_Player', 'player1');
|
||||
$fan1 = $this->objFromFixture('DataObjectTest_Fan', 'fan1');
|
||||
|
||||
// Add a captain to team 1
|
||||
$team1->setField('CaptainID', $player1->ID);
|
||||
@ -302,6 +402,24 @@ class DataObjectTest extends SapphireTest {
|
||||
'Player 1 is the captain');
|
||||
$this->assertEquals($team1->getComponent('Captain')->FirstName, 'Player 1',
|
||||
'Player 1 is the captain');
|
||||
|
||||
// Set the favourite team for fan1
|
||||
$fan1->setField('FavouriteID', $team1->ID);
|
||||
$fan1->setField('FavouriteClass', $team1->class);
|
||||
|
||||
$this->assertEquals($team1->ID, $fan1->Favourite()->ID, 'The team is assigned to fan 1');
|
||||
$this->assertInstanceOf($team1->class, $fan1->Favourite(), 'The team is assigned to fan 1');
|
||||
$this->assertEquals($team1->ID, $fan1->getComponent('Favourite')->ID,
|
||||
'The team exists through the component getter'
|
||||
);
|
||||
$this->assertInstanceOf($team1->class, $fan1->getComponent('Favourite'),
|
||||
'The team exists through the component getter'
|
||||
);
|
||||
|
||||
$this->assertEquals($fan1->Favourite()->Title, 'Team 1',
|
||||
'Team 1 is the favourite');
|
||||
$this->assertEquals($fan1->getComponent('Favourite')->Title, 'Team 1',
|
||||
'Team 1 is the favourite');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -465,6 +583,14 @@ class DataObjectTest extends SapphireTest {
|
||||
$team->CaptainID = $captainID;
|
||||
$this->assertNotNull($team->Captain());
|
||||
$this->assertEquals($captainID, $team->Captain()->ID);
|
||||
|
||||
// Test for polymorphic has_one relations
|
||||
$fan = $this->objFromFixture('DataObjectTest_Fan', 'fan1');
|
||||
$fan->FavouriteID = $team->ID;
|
||||
$fan->FavouriteClass = $team->class;
|
||||
$this->assertNotNull($fan->Favourite());
|
||||
$this->assertEquals($team->ID, $fan->Favourite()->ID);
|
||||
$this->assertInstanceOf($team->class, $fan->Favourite());
|
||||
}
|
||||
|
||||
public function testFieldNamesThatMatchMethodNamesWork() {
|
||||
@ -569,9 +695,9 @@ class DataObjectTest extends SapphireTest {
|
||||
'hasDatabaseField() doesnt include extended dynamic getters in instances');
|
||||
|
||||
/* hasDatabaseField() subclass checks */
|
||||
$this->assertTrue($subteamInstance->hasField('DatabaseField'),
|
||||
$this->assertTrue($subteamInstance->hasDatabaseField('DatabaseField'),
|
||||
'hasField() finds custom fields in subclass instances');
|
||||
$this->assertTrue($subteamInstance->hasField('SubclassDatabaseField'),
|
||||
$this->assertTrue($subteamInstance->hasDatabaseField('SubclassDatabaseField'),
|
||||
'hasField() finds custom fields in subclass instances');
|
||||
|
||||
}
|
||||
@ -1078,13 +1204,26 @@ class DataObjectTest extends SapphireTest {
|
||||
public function testGetRemoteJoinField() {
|
||||
$company = new DataObjectTest_Company();
|
||||
|
||||
$this->assertEquals('CurrentCompanyID', $company->getRemoteJoinField('CurrentStaff'));
|
||||
$this->assertEquals('PreviousCompanyID', $company->getRemoteJoinField('PreviousStaff'));
|
||||
$staffJoinField = $company->getRemoteJoinField('CurrentStaff', 'has_many', $polymorphic);
|
||||
$this->assertEquals('CurrentCompanyID', $staffJoinField);
|
||||
$this->assertFalse($polymorphic, 'DataObjectTest_Company->CurrentStaff is not polymorphic');
|
||||
$previousStaffJoinField = $company->getRemoteJoinField('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'));
|
||||
$this->assertEquals('PreviousCEOID', $ceo->getRemoteJoinField('PreviousCompany', 'belongs_to'));
|
||||
$this->assertEquals('CEOID', $ceo->getRemoteJoinField('Company', 'belongs_to', $polymorphic));
|
||||
$this->assertFalse($polymorphic, 'DataObjectTest_CEO->Company is not polymorphic');
|
||||
$this->assertEquals('PreviousCEOID', $ceo->getRemoteJoinField('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));
|
||||
$this->assertTrue($polymorphic, 'DataObjectTest_Team->Fans is polymorphic');
|
||||
$this->assertEquals('TeamID', $team->getRemoteJoinField('Comments', 'has_many', $polymorphic));
|
||||
$this->assertFalse($polymorphic, 'DataObjectTest_Team->Comments is not polymorphic');
|
||||
}
|
||||
|
||||
public function testBelongsTo() {
|
||||
@ -1094,11 +1233,13 @@ class DataObjectTest extends SapphireTest {
|
||||
$company->write();
|
||||
$ceo->write();
|
||||
|
||||
// Test belongs_to assignment
|
||||
$company->CEOID = $ceo->ID;
|
||||
$company->write();
|
||||
|
||||
$this->assertEquals($company->ID, $ceo->Company()->ID, 'belongs_to returns the right results.');
|
||||
|
||||
// Test automatic creation of class where no assigment exists
|
||||
$ceo = new DataObjectTest_CEO();
|
||||
$ceo->write();
|
||||
|
||||
@ -1108,6 +1249,7 @@ class DataObjectTest extends SapphireTest {
|
||||
);
|
||||
$this->assertEquals($ceo->ID, $ceo->Company()->CEOID, 'Remote IDs are automatically set.');
|
||||
|
||||
// Write object with components
|
||||
$ceo->write(false, false, false, true);
|
||||
$this->assertTrue($ceo->Company()->isInDB(), 'write() writes belongs_to components to the database.');
|
||||
|
||||
@ -1117,6 +1259,44 @@ class DataObjectTest extends SapphireTest {
|
||||
);
|
||||
}
|
||||
|
||||
public function testBelongsToPolymorphic() {
|
||||
$company = new DataObjectTest_Company();
|
||||
$ceo = new DataObjectTest_CEO();
|
||||
|
||||
$company->write();
|
||||
$ceo->write();
|
||||
|
||||
// Test belongs_to assignment
|
||||
$company->OwnerID = $ceo->ID;
|
||||
$company->OwnerClass = $ceo->class;
|
||||
$company->write();
|
||||
|
||||
$this->assertEquals($company->ID, $ceo->CompanyOwned()->ID, 'belongs_to returns the right results.');
|
||||
$this->assertEquals($company->class, $ceo->CompanyOwned()->class, 'belongs_to returns the right results.');
|
||||
|
||||
// Test automatic creation of class where no assigment exists
|
||||
$ceo = new DataObjectTest_CEO();
|
||||
$ceo->write();
|
||||
|
||||
$this->assertTrue (
|
||||
$ceo->CompanyOwned() instanceof DataObjectTest_Company,
|
||||
'DataObjects across polymorphic belongs_to relations are automatically created.'
|
||||
);
|
||||
$this->assertEquals($ceo->ID, $ceo->CompanyOwned()->OwnerID, 'Remote IDs are automatically set.');
|
||||
$this->assertInstanceOf($ceo->CompanyOwned()->OwnerClass, $ceo, 'Remote class is automatically set');
|
||||
|
||||
// Write object with components
|
||||
$ceo->write(false, false, false, true);
|
||||
$this->assertTrue($ceo->CompanyOwned()->isInDB(), 'write() writes belongs_to components to the database.');
|
||||
|
||||
$newCEO = DataObject::get_by_id('DataObjectTest_CEO', $ceo->ID);
|
||||
$this->assertEquals (
|
||||
$ceo->CompanyOwned()->ID,
|
||||
$newCEO->CompanyOwned()->ID,
|
||||
'polymorphic belongs_to can be retrieved from the database.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException LogicException
|
||||
*/
|
||||
@ -1229,6 +1409,14 @@ class DataObjectTest_Player extends Member implements TestOnly {
|
||||
'Teams' => 'DataObjectTest_Team'
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'Fans' => 'DataObjectTest_Fan.Favourite' // Polymorphic - Player fans
|
||||
);
|
||||
|
||||
private static $belongs_to = array (
|
||||
'CompanyOwned' => 'DataObjectTest_Company.Owner'
|
||||
);
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'IsRetired',
|
||||
'ShirtNumber'
|
||||
@ -1249,7 +1437,8 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
|
||||
|
||||
private static $has_many = array(
|
||||
'SubTeams' => 'DataObjectTest_SubTeam',
|
||||
'Comments' => 'DataObjectTest_TeamComment'
|
||||
'Comments' => 'DataObjectTest_TeamComment',
|
||||
'Fans' => 'DataObjectTest_Fan.Favourite' // Polymorphic - Team fans
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
@ -1369,9 +1558,15 @@ class DataObjectTest_ValidatedObject extends DataObject implements TestOnly {
|
||||
}
|
||||
|
||||
class DataObjectTest_Company extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $has_one = array (
|
||||
'CEO' => 'DataObjectTest_CEO',
|
||||
'PreviousCEO' => 'DataObjectTest_CEO'
|
||||
'PreviousCEO' => 'DataObjectTest_CEO',
|
||||
'Owner' => 'DataObject' // polymorphic
|
||||
);
|
||||
|
||||
private static $has_many = array (
|
||||
@ -1390,7 +1585,8 @@ class DataObjectTest_Staff extends DataObject {
|
||||
class DataObjectTest_CEO extends DataObjectTest_Staff {
|
||||
private static $belongs_to = array (
|
||||
'Company' => 'DataObjectTest_Company.CEO',
|
||||
'PreviousCompany' => 'DataObjectTest_Company.PreviousCEO'
|
||||
'PreviousCompany' => 'DataObjectTest_Company.PreviousCEO',
|
||||
'CompanyOwned' => 'DataObjectTest_Company.Owner'
|
||||
);
|
||||
}
|
||||
|
||||
@ -1406,5 +1602,17 @@ class DataObjectTest_TeamComment extends DataObject {
|
||||
|
||||
}
|
||||
|
||||
class DataObjectTest_Fan extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar(255)'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Favourite' => 'DataObject', // Polymorphic relation
|
||||
'SecondFavourite' => 'DataObject'
|
||||
);
|
||||
}
|
||||
|
||||
DataObjectTest_Team::add_extension('DataObjectTest_Team_Extension');
|
||||
|
||||
|
@ -45,3 +45,24 @@ DataObjectTest_TeamComment:
|
||||
Name: Phil
|
||||
Comment: Phil is a unique guy, and comments on team2
|
||||
Team: =>DataObjectTest_Team.team2
|
||||
DataObjectTest_Fan:
|
||||
fan1:
|
||||
Name: Damian
|
||||
Favourite: =>DataObjectTest_Team.team1
|
||||
fan2:
|
||||
Name: Stephen
|
||||
Favourite: =>DataObjectTest_Player.player1
|
||||
SecondFavourite: =>DataObjectTest_Team.team2
|
||||
fan3:
|
||||
Name: Richard
|
||||
Favourite: =>DataObjectTest_Team.team1
|
||||
fan4:
|
||||
Name: Mitch
|
||||
Favourite: =>DataObjectTest_SubTeam.subteam1
|
||||
DataObjectTest_Company:
|
||||
company1:
|
||||
Name: Company corp
|
||||
Owner: =>DataObjectTest_Player.player1
|
||||
company1:
|
||||
Name: 'Team co.'
|
||||
Owner: =>DataObjectTest_Player.player2
|
||||
|
@ -6,8 +6,23 @@ namespace DataObjectTest;
|
||||
* Right now this is only used in DataListTest, but extending it to DataObjectTest in the future would make sense.
|
||||
* Note that it was deliberated named to include "\N" to try and trip bad code up.
|
||||
*/
|
||||
class NamespacedClass extends \DataObject {
|
||||
class NamespacedClass extends \DataObject implements \TestOnly {
|
||||
private static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'Relations' => 'DataObjectTest\RelationClass'
|
||||
);
|
||||
}
|
||||
|
||||
class RelationClass extends \DataObject implements \TestOnly {
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Parent' => 'DataObject'
|
||||
);
|
||||
}
|
||||
|
106
tests/model/PolymorphicHasManyListTest.php
Normal file
106
tests/model/PolymorphicHasManyListTest.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Tests the PolymorphicHasManyList class
|
||||
*
|
||||
* @see PolymorphicHasManyList
|
||||
*
|
||||
* @todo Complete
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage tests
|
||||
*/
|
||||
class PolymorphicHasManyListTest extends SapphireTest {
|
||||
|
||||
// Borrow the model from DataObjectTest
|
||||
protected static $fixture_file = 'DataObjectTest.yml';
|
||||
|
||||
protected $extraDataObjects = array(
|
||||
'DataObjectTest_Team',
|
||||
'DataObjectTest_SubTeam',
|
||||
'DataObjectTest_Player',
|
||||
'DataObjectTest_Fan'
|
||||
);
|
||||
|
||||
public function testRelationshipEmptyOnNewRecords() {
|
||||
// Relies on the fact that (unrelated) comments exist in the fixture file already
|
||||
$newTeam = new DataObjectTest_Team(); // has_many Comments
|
||||
$this->assertEquals(array(), $newTeam->Fans()->column('ID'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that DataList::relation works with PolymorphicHasManyList
|
||||
*/
|
||||
public function testFilterRelation() {
|
||||
|
||||
// Check that expected teams exist
|
||||
$list = DataObjectTest_Team::get();
|
||||
$this->assertEquals(
|
||||
array('Subteam 1', 'Subteam 2', 'Subteam 3', 'Team 1', 'Team 2', 'Team 3'),
|
||||
$list->sort('Title')->column('Title')
|
||||
);
|
||||
|
||||
// Check that fan list exists
|
||||
$fans = $list->relation('Fans');
|
||||
$this->assertEquals(array('Damian', 'Mitch', 'Richard'), $fans->sort('Name')->column('Name'));
|
||||
|
||||
// Modify list of fans and retest
|
||||
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||
$newFan1 = DataObjectTest_Fan::create();
|
||||
$newFan1->Name = 'Bobby';
|
||||
$newFan1->write();
|
||||
$newFan2 = DataObjectTest_Fan::create();
|
||||
$newFan2->Name = 'Mindy';
|
||||
$newFan2->write();
|
||||
$team1->Fans()->add($newFan1);
|
||||
$subteam1->Fans()->add($newFan2);
|
||||
$fans = DataObjectTest_Team::get()->relation('Fans');
|
||||
$this->assertEquals(array('Bobby', 'Damian', 'Richard'), $team1->Fans()->sort('Name')->column('Name'));
|
||||
$this->assertEquals(array('Mindy', 'Mitch'), $subteam1->Fans()->sort('Name')->column('Name'));
|
||||
$this->assertEquals(array('Bobby', 'Damian', 'Mindy', 'Mitch', 'Richard'), $fans->sort('Name')->column('Name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that related objects can be removed from a relation
|
||||
*/
|
||||
public function testRemoveRelation() {
|
||||
|
||||
// Check that expected teams exist
|
||||
$list = DataObjectTest_Team::get();
|
||||
$this->assertEquals(
|
||||
array('Subteam 1', 'Subteam 2', 'Subteam 3', 'Team 1', 'Team 2', 'Team 3'),
|
||||
$list->sort('Title')->column('Title')
|
||||
);
|
||||
|
||||
// Test that each team has the correct fans
|
||||
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||
$this->assertEquals(array('Damian', 'Richard'), $team1->Fans()->sort('Name')->column('Name'));
|
||||
$this->assertEquals(array('Mitch'), $subteam1->Fans()->sort('Name')->column('Name'));
|
||||
|
||||
// Test that removing items from unrelated team has no effect
|
||||
$team1fan = $this->objFromFixture('DataObjectTest_Fan', 'fan1');
|
||||
$subteam1fan = $this->objFromFixture('DataObjectTest_Fan', 'fan4');
|
||||
$team1->Fans()->remove($subteam1fan);
|
||||
$subteam1->Fans()->remove($team1fan);
|
||||
$this->assertEquals(array('Damian', 'Richard'), $team1->Fans()->sort('Name')->column('Name'));
|
||||
$this->assertEquals(array('Mitch'), $subteam1->Fans()->sort('Name')->column('Name'));
|
||||
$this->assertEquals($team1->ID, $team1fan->FavouriteID);
|
||||
$this->assertEquals('DataObjectTest_Team', $team1fan->FavouriteClass);
|
||||
$this->assertEquals($subteam1->ID, $subteam1fan->FavouriteID);
|
||||
$this->assertEquals('DataObjectTest_SubTeam', $subteam1fan->FavouriteClass);
|
||||
|
||||
// Test that removing items from the related team resets the has_one relations on the fan
|
||||
$team1fan = $this->objFromFixture('DataObjectTest_Fan', 'fan1');
|
||||
$subteam1fan = $this->objFromFixture('DataObjectTest_Fan', 'fan4');
|
||||
$team1->Fans()->remove($team1fan);
|
||||
$subteam1->Fans()->remove($subteam1fan);
|
||||
$this->assertEquals(array('Richard'), $team1->Fans()->sort('Name')->column('Name'));
|
||||
$this->assertEquals(array(), $subteam1->Fans()->sort('Name')->column('Name'));
|
||||
$this->assertEmpty($team1fan->FavouriteID);
|
||||
$this->assertEmpty($team1fan->FavouriteClass);
|
||||
$this->assertEmpty($subteam1fan->FavouriteID);
|
||||
$this->assertEmpty($subteam1fan->FavouriteClass);
|
||||
}
|
||||
}
|
@ -104,6 +104,33 @@ class UnsavedRelationListTest extends SapphireTest {
|
||||
), $object->Children());
|
||||
}
|
||||
|
||||
public function testHasManyPolymorphic() {
|
||||
$object = new UnsavedRelationListTest_DataObject;
|
||||
|
||||
$children = $object->RelatedObjects();
|
||||
$children->add(new UnsavedRelationListTest_DataObject(array('Name' => 'A')));
|
||||
$children->add(new UnsavedRelationListTest_DataObject(array('Name' => 'B')));
|
||||
$children->add(new UnsavedRelationListTest_DataObject(array('Name' => 'C')));
|
||||
|
||||
$children = $object->RelatedObjects();
|
||||
|
||||
$this->assertDOSEquals(array(
|
||||
array('Name' => 'A'),
|
||||
array('Name' => 'B'),
|
||||
array('Name' => 'C')
|
||||
), $children);
|
||||
|
||||
$object->write();
|
||||
|
||||
$this->assertNotEquals($children, $object->RelatedObjects());
|
||||
|
||||
$this->assertDOSEquals(array(
|
||||
array('Name' => 'A'),
|
||||
array('Name' => 'B'),
|
||||
array('Name' => 'C')
|
||||
), $object->RelatedObjects());
|
||||
}
|
||||
|
||||
public function testManyManyNew() {
|
||||
$object = new UnsavedRelationListTest_DataObject;
|
||||
|
||||
@ -192,10 +219,12 @@ class UnsavedRelationListTest_DataObject extends DataObject implements TestOnly
|
||||
|
||||
private static $has_one = array(
|
||||
'Parent' => 'UnsavedRelationListTest_DataObject',
|
||||
'RelatedObject' => 'DataObject'
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'Children' => 'UnsavedRelationListTest_DataObject',
|
||||
'Children' => 'UnsavedRelationListTest_DataObject.Parent',
|
||||
'RelatedObjects' => 'UnsavedRelationListTest_DataObject.RelatedObject'
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
|
Loading…
x
Reference in New Issue
Block a user