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);
|
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} elseif($obj->has_one($fieldName)) {
|
} else {
|
||||||
// Sets has_one with relation name
|
$hasOneField = preg_replace('/ID$/', '', $fieldName);
|
||||||
$obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures);
|
if($className = $obj->has_one($hasOneField)) {
|
||||||
} elseif($obj->has_one(preg_replace('/ID$/', '', $fieldName))) {
|
$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
|
||||||
// Sets has_one with database field
|
// Inject class for polymorphic relation
|
||||||
$obj->$fieldName = $this->parseValue($fieldVal, $fixtures);
|
if($className === 'DataObject') {
|
||||||
|
$obj->{$hasOneField.'Class'} = $fieldClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$obj->write();
|
$obj->write();
|
||||||
@ -261,11 +264,13 @@ class FixtureBlueprint {
|
|||||||
* Parse a value from a fixture file. If it starts with =>
|
* Parse a value from a fixture file. If it starts with =>
|
||||||
* it will get an ID from the fixture dictionary
|
* it will get an ID from the fixture dictionary
|
||||||
*
|
*
|
||||||
* @param String $fieldVal
|
* @param string $fieldVal
|
||||||
* @param Array $fixtures See {@link createObject()}
|
* @param array $fixtures See {@link createObject()}
|
||||||
* @return String Fixture database ID, or the original value
|
* @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) == '=>') {
|
if(substr($value,0,2) == '=>') {
|
||||||
// Parse a dictionary reference - used to set foreign keys
|
// Parse a dictionary reference - used to set foreign keys
|
||||||
list($class, $identifier) = explode('.', substr($value,2), 2);
|
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
|
// 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)
|
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
|
||||||
DataObject::clear_classname_spec_cache();
|
DataObject::clear_classname_spec_cache();
|
||||||
|
PolymorphicForeignKey::clear_classname_spec_cache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function init() {
|
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.
|
* 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
|
* 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
|
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
|
### 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
|
### has_many
|
||||||
|
|
||||||
Defines 1-to-many joins. A database-column named ""`<relationship-name>`ID""
|
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()) {
|
if($this->obj->has_one()) {
|
||||||
foreach($this->obj->has_one() as $relationship => $component) {
|
foreach($this->obj->has_one() as $relationship => $component) {
|
||||||
if($this->restrictFields && !in_array($relationship, $this->restrictFields)) continue;
|
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])) {
|
if($this->fieldClasses && isset($this->fieldClasses[$fieldName])) {
|
||||||
$fieldClass = $this->fieldClasses[$fieldName];
|
$fieldClass = $this->fieldClasses[$fieldName];
|
||||||
$hasOneField = new $fieldClass($fieldName);
|
$hasOneField = new $fieldClass($fieldName);
|
||||||
} else {
|
} else {
|
||||||
$hasOneField = $this->obj->dbObject($fieldName)->scaffoldFormField(null, $this->getParamsArray());
|
$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));
|
$hasOneField->setTitle($this->obj->fieldLabel($relationship));
|
||||||
if($this->tabbed) {
|
if($this->tabbed) {
|
||||||
$fields->addFieldToTab("Root.Main", $hasOneField);
|
$fields->addFieldToTab("Root.Main", $hasOneField);
|
||||||
|
@ -313,7 +313,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
// Add has_one relationships
|
// Add has_one relationships
|
||||||
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
|
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
|
||||||
if($hasOne) foreach(array_keys($hasOne) as $field) {
|
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;
|
$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.
|
* 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
|
* @param string $componentName Name of the component
|
||||||
*
|
*
|
||||||
@ -1427,24 +1441,40 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
$joinField = $componentName . 'ID';
|
$joinField = $componentName . 'ID';
|
||||||
$joinID = $this->getField($joinField);
|
$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) {
|
if($joinID) {
|
||||||
$component = $this->model->$class->byID($joinID);
|
$component = $this->model->$class->byID($joinID);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isset($component) || !$component) {
|
if(empty($component)) {
|
||||||
$component = $this->model->$class->newObject();
|
$component = $this->model->$class->newObject();
|
||||||
}
|
}
|
||||||
} elseif($class = $this->belongs_to($componentName)) {
|
} elseif($class = $this->belongs_to($componentName)) {
|
||||||
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to');
|
|
||||||
|
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
|
||||||
$joinID = $this->ID;
|
$joinID = $this->ID;
|
||||||
|
|
||||||
if($joinID) {
|
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 = $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 {
|
} else {
|
||||||
throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
|
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];
|
return $this->unsavedRelations[$componentName];
|
||||||
}
|
}
|
||||||
|
|
||||||
$joinField = $this->getRemoteJoinField($componentName, 'has_many');
|
// Determine type and nature of foreign relation
|
||||||
|
$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
|
||||||
$result = HasManyList::create($componentClass, $joinField);
|
if($polymorphic) {
|
||||||
|
$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
|
||||||
|
} else {
|
||||||
|
$result = HasManyList::create($componentClass, $joinField);
|
||||||
|
}
|
||||||
|
|
||||||
if($this->model) $result->setDataModel($this->model);
|
if($this->model) $result->setDataModel($this->model);
|
||||||
$result = $result->forForeignID($this->ID);
|
|
||||||
|
|
||||||
$result = $result->where($filter)->limit($limit)->sort($sort);
|
return $result
|
||||||
|
->forForeignID($this->ID)
|
||||||
return $result;
|
->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
|
* Tries to find the database key on another object that is used to store a
|
||||||
* field can be found it defaults to 'ParentID'.
|
* 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
|
* @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 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
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getRemoteJoinField($component, $type = 'has_many') {
|
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
|
||||||
$remoteClass = $this->$type($component, 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'");
|
throw new Exception("Unknown $type component '$component' on class '$this->class'");
|
||||||
}
|
}
|
||||||
if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
|
if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
|
||||||
@ -1559,28 +1601,56 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($fieldPos = strpos($remoteClass, '.')) {
|
// If presented with an explicit field name (using dot notation) then extract field name
|
||||||
return substr($remoteClass, $fieldPos + 1) . 'ID';
|
$remoteField = null;
|
||||||
|
if(strpos($remoteClass, '.') !== false) {
|
||||||
|
list($remoteClass, $remoteField) = explode('.', $remoteClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
$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
|
// Reference remote has_one to check against
|
||||||
foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
|
$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = "No has_one found on class '$remoteClass'";
|
// Inspect resulting found relation
|
||||||
if($type == 'has_many') {
|
if($remoteRelations[$remoteField] === 'DataObject') {
|
||||||
// include a hint for has_many that is missing a has_one
|
$polymorphic = true;
|
||||||
$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
|
return $remoteField; // Composite polymorphic field does not include 'ID' suffix
|
||||||
$message .= " requires a has_one on '$remoteClass'";
|
} 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];
|
return $this->unsavedRelations[$componentName];
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField,
|
$result = ManyManyList::create(
|
||||||
$this->many_many_extraFields($componentName));
|
$componentClass, $table, $componentField, $parentField,
|
||||||
|
$this->many_many_extraFields($componentName)
|
||||||
|
);
|
||||||
if($this->model) $result->setDataModel($this->model);
|
if($this->model) $result->setDataModel($this->model);
|
||||||
|
|
||||||
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
|
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
|
||||||
// foreignID set elsewhere.
|
// foreignID set elsewhere.
|
||||||
$result = $result->forForeignID($this->ID);
|
return $result
|
||||||
|
->forForeignID($this->ID)
|
||||||
return $result->where($filter)->sort($sort)->limit($limit);
|
->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
|
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
|
||||||
* their classes.
|
* 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
|
* @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,
|
// all has_one relations on this specific class,
|
||||||
// add foreign key
|
// add foreign key
|
||||||
|
|
||||||
$hasOne = Config::inst()->get($this->class, 'has_one', Config::UNINHERITED);
|
$hasOne = Config::inst()->get($this->class, 'has_one', Config::UNINHERITED);
|
||||||
if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) {
|
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
|
// 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))) {
|
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
|
||||||
$val = $this->$fieldName;
|
$val = $this->$fieldName;
|
||||||
return DBField::create_field('ForeignKey', $val, $fieldName, $this);
|
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'),
|
$this->assertNotNull($fields->dataFieldByName('AuthorID'),
|
||||||
'getCMSFields() includes has_one fields on singletons');
|
'getCMSFields() includes has_one fields on singletons');
|
||||||
$this->assertNull($fields->dataFieldByName('Tags'),
|
$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() {
|
public function testGetCMSFieldsInstance() {
|
||||||
@ -47,6 +47,14 @@ class FormScaffolderTest extends SapphireTest {
|
|||||||
'getCMSFields() includes has_one fields on instances');
|
'getCMSFields() includes has_one fields on instances');
|
||||||
$this->assertNotNull($fields->dataFieldByName('Tags'),
|
$this->assertNotNull($fields->dataFieldByName('Tags'),
|
||||||
'getCMSFields() includes many_many fields if ID is present on instances');
|
'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() {
|
public function testUpdateCMSFields() {
|
||||||
@ -111,11 +119,15 @@ class FormScaffolderTest_Article extends DataObject implements TestOnly {
|
|||||||
'Content' => 'HTMLText'
|
'Content' => 'HTMLText'
|
||||||
);
|
);
|
||||||
private static $has_one = array(
|
private static $has_one = array(
|
||||||
'Author' => 'FormScaffolderTest_Author'
|
'Author' => 'FormScaffolderTest_Author',
|
||||||
|
'Subject' => 'DataObject'
|
||||||
);
|
);
|
||||||
private static $many_many = array(
|
private static $many_many = array(
|
||||||
'Tags' => 'FormScaffolderTest_Tag',
|
'Tags' => 'FormScaffolderTest_Tag',
|
||||||
);
|
);
|
||||||
|
private static $has_many = array(
|
||||||
|
'SubjectOfArticles' => 'FormScaffolderTest_Article.Subject'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormScaffolderTest_Author extends Member implements TestOnly {
|
class FormScaffolderTest_Author extends Member implements TestOnly {
|
||||||
@ -123,7 +135,8 @@ class FormScaffolderTest_Author extends Member implements TestOnly {
|
|||||||
'ProfileImage' => 'Image'
|
'ProfileImage' => 'Image'
|
||||||
);
|
);
|
||||||
private static $has_many = array(
|
private static $has_many = array(
|
||||||
'Articles' => 'FormScaffolderTest_Article'
|
'Articles' => 'FormScaffolderTest_Article.Author',
|
||||||
|
'SubjectOfArticles' => 'FormScaffolderTest_Article.Subject'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
class FormScaffolderTest_Tag extends DataObject implements TestOnly {
|
class FormScaffolderTest_Tag extends DataObject implements TestOnly {
|
||||||
@ -133,6 +146,9 @@ class FormScaffolderTest_Tag extends DataObject implements TestOnly {
|
|||||||
private static $belongs_many_many = array(
|
private static $belongs_many_many = array(
|
||||||
'Articles' => 'FormScaffolderTest_Article'
|
'Articles' => 'FormScaffolderTest_Article'
|
||||||
);
|
);
|
||||||
|
private static $has_many = array(
|
||||||
|
'SubjectOfArticles' => 'FormScaffolderTest_Article.Subject'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
class FormScaffolderTest_ArticleExtension extends DataExtension implements TestOnly {
|
class FormScaffolderTest_ArticleExtension extends DataExtension implements TestOnly {
|
||||||
private static $db = array(
|
private static $db = array(
|
||||||
|
@ -16,7 +16,9 @@ class DataObjectTest extends SapphireTest {
|
|||||||
'DataObjectTest_FieldlessSubTable',
|
'DataObjectTest_FieldlessSubTable',
|
||||||
'DataObjectTest_ValidatedObject',
|
'DataObjectTest_ValidatedObject',
|
||||||
'DataObjectTest_Player',
|
'DataObjectTest_Player',
|
||||||
'DataObjectTest_TeamComment'
|
'DataObjectTest_TeamComment',
|
||||||
|
'DataObjectTest\NamespacedClass',
|
||||||
|
'DataObjectTest\RelationClass',
|
||||||
);
|
);
|
||||||
|
|
||||||
public function testBaseFieldsExcludedFromDb() {
|
public function testBaseFieldsExcludedFromDb() {
|
||||||
@ -206,15 +208,54 @@ class DataObjectTest extends SapphireTest {
|
|||||||
'belongs_many_many is properly inspected');
|
'belongs_many_many is properly inspected');
|
||||||
$this->assertEquals(singleton('DataObjectTest_CEO')->getRelationClass('Company'), 'DataObjectTest_Company',
|
$this->assertEquals(singleton('DataObjectTest_CEO')->getRelationClass('Company'), 'DataObjectTest_Company',
|
||||||
'belongs_to is properly inspected');
|
'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() {
|
public function testGetHasOneRelations() {
|
||||||
$captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1");
|
$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
|
$team1ID = $this->idFromFixture('DataObjectTest_Team', 'team1');
|
||||||
* has_one relation */
|
|
||||||
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeamID);
|
// There will be a field called (relname)ID that contains the ID of the
|
||||||
/* There will be a method called $obj->relname() that returns the object itself */
|
// object linked to via the has_one relation
|
||||||
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeam()->ID);
|
$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() {
|
public function testLimitAndCount() {
|
||||||
@ -240,9 +281,20 @@ class DataObjectTest extends SapphireTest {
|
|||||||
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
|
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
|
||||||
$obj->FavouriteTeamID = 99;
|
$obj->FavouriteTeamID = 99;
|
||||||
$obj->write();
|
$obj->write();
|
||||||
|
|
||||||
// reload the page from the database
|
// reload the page from the database
|
||||||
$savedObj = DataObject::get_by_id('DataObjectTest_Player', $obj->ID);
|
$savedObj = DataObject::get_by_id('DataObjectTest_Player', $obj->ID);
|
||||||
$this->assertTrue($savedObj->FavouriteTeamID == 99);
|
$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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -284,10 +336,58 @@ class DataObjectTest extends SapphireTest {
|
|||||||
$team1CommentIDs = $team1->Comments()->sort('ID')->column('ID');
|
$team1CommentIDs = $team1->Comments()->sort('ID')->column('ID');
|
||||||
$this->assertEquals(array($comment1->ID, $newComment->ID), $team1CommentIDs);
|
$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() {
|
public function testHasOneRelationship() {
|
||||||
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
$player1 = $this->objFromFixture('DataObjectTest_Player', 'player1');
|
$player1 = $this->objFromFixture('DataObjectTest_Player', 'player1');
|
||||||
|
$fan1 = $this->objFromFixture('DataObjectTest_Fan', 'fan1');
|
||||||
|
|
||||||
// Add a captain to team 1
|
// Add a captain to team 1
|
||||||
$team1->setField('CaptainID', $player1->ID);
|
$team1->setField('CaptainID', $player1->ID);
|
||||||
@ -302,6 +402,24 @@ class DataObjectTest extends SapphireTest {
|
|||||||
'Player 1 is the captain');
|
'Player 1 is the captain');
|
||||||
$this->assertEquals($team1->getComponent('Captain')->FirstName, 'Player 1',
|
$this->assertEquals($team1->getComponent('Captain')->FirstName, 'Player 1',
|
||||||
'Player 1 is the captain');
|
'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;
|
$team->CaptainID = $captainID;
|
||||||
$this->assertNotNull($team->Captain());
|
$this->assertNotNull($team->Captain());
|
||||||
$this->assertEquals($captainID, $team->Captain()->ID);
|
$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() {
|
public function testFieldNamesThatMatchMethodNamesWork() {
|
||||||
@ -569,9 +695,9 @@ class DataObjectTest extends SapphireTest {
|
|||||||
'hasDatabaseField() doesnt include extended dynamic getters in instances');
|
'hasDatabaseField() doesnt include extended dynamic getters in instances');
|
||||||
|
|
||||||
/* hasDatabaseField() subclass checks */
|
/* hasDatabaseField() subclass checks */
|
||||||
$this->assertTrue($subteamInstance->hasField('DatabaseField'),
|
$this->assertTrue($subteamInstance->hasDatabaseField('DatabaseField'),
|
||||||
'hasField() finds custom fields in subclass instances');
|
'hasField() finds custom fields in subclass instances');
|
||||||
$this->assertTrue($subteamInstance->hasField('SubclassDatabaseField'),
|
$this->assertTrue($subteamInstance->hasDatabaseField('SubclassDatabaseField'),
|
||||||
'hasField() finds custom fields in subclass instances');
|
'hasField() finds custom fields in subclass instances');
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1078,13 +1204,26 @@ class DataObjectTest extends SapphireTest {
|
|||||||
public function testGetRemoteJoinField() {
|
public function testGetRemoteJoinField() {
|
||||||
$company = new DataObjectTest_Company();
|
$company = new DataObjectTest_Company();
|
||||||
|
|
||||||
$this->assertEquals('CurrentCompanyID', $company->getRemoteJoinField('CurrentStaff'));
|
$staffJoinField = $company->getRemoteJoinField('CurrentStaff', 'has_many', $polymorphic);
|
||||||
$this->assertEquals('PreviousCompanyID', $company->getRemoteJoinField('PreviousStaff'));
|
$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();
|
$ceo = new DataObjectTest_CEO();
|
||||||
|
|
||||||
$this->assertEquals('CEOID', $ceo->getRemoteJoinField('Company', 'belongs_to'));
|
$this->assertEquals('CEOID', $ceo->getRemoteJoinField('Company', 'belongs_to', $polymorphic));
|
||||||
$this->assertEquals('PreviousCEOID', $ceo->getRemoteJoinField('PreviousCompany', 'belongs_to'));
|
$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() {
|
public function testBelongsTo() {
|
||||||
@ -1094,11 +1233,13 @@ class DataObjectTest extends SapphireTest {
|
|||||||
$company->write();
|
$company->write();
|
||||||
$ceo->write();
|
$ceo->write();
|
||||||
|
|
||||||
|
// Test belongs_to assignment
|
||||||
$company->CEOID = $ceo->ID;
|
$company->CEOID = $ceo->ID;
|
||||||
$company->write();
|
$company->write();
|
||||||
|
|
||||||
$this->assertEquals($company->ID, $ceo->Company()->ID, 'belongs_to returns the right results.');
|
$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 = new DataObjectTest_CEO();
|
||||||
$ceo->write();
|
$ceo->write();
|
||||||
|
|
||||||
@ -1108,6 +1249,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
);
|
);
|
||||||
$this->assertEquals($ceo->ID, $ceo->Company()->CEOID, 'Remote IDs are automatically set.');
|
$this->assertEquals($ceo->ID, $ceo->Company()->CEOID, 'Remote IDs are automatically set.');
|
||||||
|
|
||||||
|
// Write object with components
|
||||||
$ceo->write(false, false, false, true);
|
$ceo->write(false, false, false, true);
|
||||||
$this->assertTrue($ceo->Company()->isInDB(), 'write() writes belongs_to components to the database.');
|
$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
|
* @expectedException LogicException
|
||||||
*/
|
*/
|
||||||
@ -1228,6 +1408,14 @@ class DataObjectTest_Player extends Member implements TestOnly {
|
|||||||
private static $belongs_many_many = array(
|
private static $belongs_many_many = array(
|
||||||
'Teams' => 'DataObjectTest_Team'
|
'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(
|
private static $searchable_fields = array(
|
||||||
'IsRetired',
|
'IsRetired',
|
||||||
@ -1249,7 +1437,8 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
|
|||||||
|
|
||||||
private static $has_many = array(
|
private static $has_many = array(
|
||||||
'SubTeams' => 'DataObjectTest_SubTeam',
|
'SubTeams' => 'DataObjectTest_SubTeam',
|
||||||
'Comments' => 'DataObjectTest_TeamComment'
|
'Comments' => 'DataObjectTest_TeamComment',
|
||||||
|
'Fans' => 'DataObjectTest_Fan.Favourite' // Polymorphic - Team fans
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $many_many = array(
|
private static $many_many = array(
|
||||||
@ -1369,9 +1558,15 @@ class DataObjectTest_ValidatedObject extends DataObject implements TestOnly {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DataObjectTest_Company extends DataObject {
|
class DataObjectTest_Company extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Varchar'
|
||||||
|
);
|
||||||
|
|
||||||
private static $has_one = array (
|
private static $has_one = array (
|
||||||
'CEO' => 'DataObjectTest_CEO',
|
'CEO' => 'DataObjectTest_CEO',
|
||||||
'PreviousCEO' => 'DataObjectTest_CEO'
|
'PreviousCEO' => 'DataObjectTest_CEO',
|
||||||
|
'Owner' => 'DataObject' // polymorphic
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $has_many = array (
|
private static $has_many = array (
|
||||||
@ -1390,7 +1585,8 @@ class DataObjectTest_Staff extends DataObject {
|
|||||||
class DataObjectTest_CEO extends DataObjectTest_Staff {
|
class DataObjectTest_CEO extends DataObjectTest_Staff {
|
||||||
private static $belongs_to = array (
|
private static $belongs_to = array (
|
||||||
'Company' => 'DataObjectTest_Company.CEO',
|
'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');
|
DataObjectTest_Team::add_extension('DataObjectTest_Team_Extension');
|
||||||
|
|
||||||
|
@ -45,3 +45,24 @@ DataObjectTest_TeamComment:
|
|||||||
Name: Phil
|
Name: Phil
|
||||||
Comment: Phil is a unique guy, and comments on team2
|
Comment: Phil is a unique guy, and comments on team2
|
||||||
Team: =>DataObjectTest_Team.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.
|
* 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.
|
* 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(
|
private static $db = array(
|
||||||
'Name' => 'Varchar',
|
'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);
|
||||||
|
}
|
||||||
|
}
|
@ -103,6 +103,33 @@ class UnsavedRelationListTest extends SapphireTest {
|
|||||||
array('Name' => 'C')
|
array('Name' => 'C')
|
||||||
), $object->Children());
|
), $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() {
|
public function testManyManyNew() {
|
||||||
$object = new UnsavedRelationListTest_DataObject;
|
$object = new UnsavedRelationListTest_DataObject;
|
||||||
@ -192,10 +219,12 @@ class UnsavedRelationListTest_DataObject extends DataObject implements TestOnly
|
|||||||
|
|
||||||
private static $has_one = array(
|
private static $has_one = array(
|
||||||
'Parent' => 'UnsavedRelationListTest_DataObject',
|
'Parent' => 'UnsavedRelationListTest_DataObject',
|
||||||
|
'RelatedObject' => 'DataObject'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $has_many = array(
|
private static $has_many = array(
|
||||||
'Children' => 'UnsavedRelationListTest_DataObject',
|
'Children' => 'UnsavedRelationListTest_DataObject.Parent',
|
||||||
|
'RelatedObjects' => 'UnsavedRelationListTest_DataObject.RelatedObject'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $many_many = array(
|
private static $many_many = array(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user