Merge pull request #3799 from kinglozzer/1377-multiple-many-many

NEW: Support multiple many_manys between the same classes (closes #1377)
This commit is contained in:
Daniel Hensby 2015-03-13 20:51:13 +00:00
commit b9283fbc06
23 changed files with 715 additions and 356 deletions

View File

@ -62,7 +62,7 @@ class JSONDataFormatter extends DataFormatter {
}
if($this->relationDepth > 0) {
foreach($obj->has_one() as $relName => $relClass) {
foreach($obj->hasOne() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -82,7 +82,7 @@ class JSONDataFormatter extends DataFormatter {
));
}
foreach($obj->has_many() as $relName => $relClass) {
foreach($obj->hasMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -103,7 +103,7 @@ class JSONDataFormatter extends DataFormatter {
$serobj->$relName = $innerParts;
}
foreach($obj->many_many() as $relName => $relClass) {
foreach($obj->manyMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering

View File

@ -68,7 +68,7 @@ class XMLDataFormatter extends DataFormatter {
}
if($this->relationDepth > 0) {
foreach($obj->has_one() as $relName => $relClass) {
foreach($obj->hasOne() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -85,7 +85,7 @@ class XMLDataFormatter extends DataFormatter {
. "\"></$relName>\n";
}
foreach($obj->has_many() as $relName => $relClass) {
foreach($obj->hasMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -103,7 +103,7 @@ class XMLDataFormatter extends DataFormatter {
$xml .= "</$relName>\n";
}
foreach($obj->many_many() as $relName => $relClass) {
foreach($obj->manyMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering

View File

@ -220,9 +220,9 @@ abstract class BulkLoader extends ViewableData {
// using $$includerelations flag as false, so that it only contain $db fields
$spec['fields'] = (array)singleton($this->objectClass)->fieldLabels(false);
$has_ones = singleton($this->objectClass)->has_one();
$has_manys = singleton($this->objectClass)->has_many();
$many_manys = singleton($this->objectClass)->many_many();
$has_ones = singleton($this->objectClass)->hasOne();
$has_manys = singleton($this->objectClass)->hasMany();
$many_manys = singleton($this->objectClass)->manyMany();
$spec['relations'] = (array)$has_ones + (array)$has_manys + (array)$many_manys;

View File

@ -120,7 +120,7 @@ class CsvBulkLoader extends BulkLoader {
$relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
}
if(!$relationObj || !$relationObj->exists()) {
$relationClass = $obj->has_one($relationName);
$relationClass = $obj->hasOneComponent($relationName);
$relationObj = new $relationClass();
//write if we aren't previewing
if (!$preview) $relationObj->write();

View File

@ -110,7 +110,11 @@ class FixtureBlueprint {
// Populate overrides
if($data) foreach($data as $fieldName => $fieldVal) {
// Defer relationship processing
if($obj->many_many($fieldName) || $obj->has_many($fieldName) || $obj->has_one($fieldName)) {
if(
$obj->manyManyComponent($fieldName)
|| $obj->hasManyComponent($fieldName)
|| $obj->hasOneComponent($fieldName)
) {
continue;
}
@ -127,7 +131,7 @@ class FixtureBlueprint {
// Populate all relations
if($data) foreach($data as $fieldName => $fieldVal) {
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
if($obj->manyManyComponent($fieldName) || $obj->hasManyComponent($fieldName)) {
$obj->write();
$parsedItems = array();
@ -165,15 +169,15 @@ class FixtureBlueprint {
$parsedItems[] = $this->parseValue($item, $fixtures);
}
if($obj->has_many($fieldName)) {
if($obj->hasManyComponent($fieldName)) {
$obj->getComponents($fieldName)->setByIDList($parsedItems);
} elseif($obj->many_many($fieldName)) {
} elseif($obj->manyManyComponent($fieldName)) {
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
}
}
} else {
$hasOneField = preg_replace('/ID$/', '', $fieldName);
if($className = $obj->has_one($hasOneField)) {
if($className = $obj->hasOneComponent($hasOneField)) {
$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
// Inject class for polymorphic relation
if($className === 'DataObject') {

View File

@ -204,6 +204,27 @@ The relationship can also be navigated in [templates](../templates).
<% end_if %>
<% end_with %>
To specify multiple $many_manys between the same classes, use the dot notation to distinguish them like below:
:::php
<?php
class Category extends DataObject {
private static $many_many = array(
'Products' => 'Product',
'FeaturedProducts' => 'Product'
);
}
class Product extends DataObject {
private static $belongs_many_many = array(
'Categories' => 'Category.Products',
'FeaturedInCategories' => 'Category.FeaturedProducts'
);
}
## many_many or belongs_many_many?
If you're unsure about whether an object should take on `many_many` or `belongs_many_many`, the best way to think about it is that the object where the relationship will be edited (i.e. via checkboxes) should contain the `many_many`. For instance, in a `many_many` of Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa.

View File

@ -109,7 +109,7 @@ class FileField extends FormField {
if($this->relationAutoSetting) {
// assume that the file is connected via a has-one
$hasOnes = $record->has_one($this->name);
$hasOnes = $record->hasOne($this->name);
// try to create a file matching the relation
$file = (is_string($hasOnes)) ? Object::create($hasOnes) : new $fileClass();
} else if($record instanceof File) {

View File

@ -93,8 +93,8 @@ class FormScaffolder extends Object {
}
// add has_one relation fields
if($this->obj->has_one()) {
foreach($this->obj->has_one() as $relationship => $component) {
if($this->obj->hasOne()) {
foreach($this->obj->hasOne() as $relationship => $component) {
if($this->restrictFields && !in_array($relationship, $this->restrictFields)) continue;
$fieldName = $component === 'DataObject'
? $relationship // Polymorphic has_one field is composite, so don't refer to ID subfield
@ -118,10 +118,10 @@ class FormScaffolder extends Object {
// only add relational fields if an ID is present
if($this->obj->ID) {
// add has_many relation fields
if($this->obj->has_many()
if($this->obj->hasMany()
&& ($this->includeRelations === true || isset($this->includeRelations['has_many']))) {
foreach($this->obj->has_many() as $relationship => $component) {
foreach($this->obj->hasMany() as $relationship => $component) {
if($this->tabbed) {
$relationTab = $fields->findOrMakeTab(
"Root.$relationship",
@ -145,10 +145,10 @@ class FormScaffolder extends Object {
}
}
if($this->obj->many_many()
if($this->obj->manyMany()
&& ($this->includeRelations === true || isset($this->includeRelations['many_many']))) {
foreach($this->obj->many_many() as $relationship => $component) {
foreach($this->obj->manyMany() as $relationship => $component) {
if($this->tabbed) {
$relationTab = $fields->findOrMakeTab(
"Root.$relationship",

View File

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

View File

@ -78,7 +78,7 @@ class DataDifferencer extends ViewableData {
$fields = array_keys($this->toRecord->toMap());
}
$hasOnes = array_merge($this->fromRecord->has_one(), $this->toRecord->has_one());
$hasOnes = array_merge($this->fromRecord->hasOne(), $this->toRecord->hasOne());
// Loop through properties
foreach($fields as $field) {

View File

@ -547,10 +547,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
// relation on the other side of this relation to point at the copy and no longer the original (being a
// has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
if ($sourceObject->has_one()) foreach($sourceObject->has_one() as $name => $type) {
if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
$this->duplicateRelations($sourceObject, $destinationObject, $name);
}
if ($sourceObject->many_many()) foreach($sourceObject->many_many() as $name => $type) {
if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
//many_many include belongs_many_many
$this->duplicateRelations($sourceObject, $destinationObject, $name);
}
@ -666,24 +666,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($this->class == 'DataObject') return;
// Set up accessors for joined items
if($manyMany = $this->many_many()) {
if($manyMany = $this->manyMany()) {
foreach($manyMany as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getManyManyComponents');
}
}
if($hasMany = $this->has_many()) {
if($hasMany = $this->hasMany()) {
foreach($hasMany as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getComponents');
}
}
if($hasOne = $this->has_one()) {
if($hasOne = $this->hasOne()) {
foreach($hasOne as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getComponent');
}
}
if($belongsTo = $this->belongs_to()) foreach(array_keys($belongsTo) as $relationship) {
if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
$this->addWrapperMethod($relationship, 'getComponent');
}
}
@ -972,7 +972,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// merge relations
if($includeRelations) {
if($manyMany = $this->many_many()) {
if($manyMany = $this->manyMany()) {
foreach($manyMany as $relationship => $class) {
$leftComponents = $leftObj->getManyManyComponents($relationship);
$rightComponents = $rightObj->getManyManyComponents($relationship);
@ -983,7 +983,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
if($hasMany = $this->has_many()) {
if($hasMany = $this->hasMany()) {
foreach($hasMany as $relationship => $class) {
$leftComponents = $leftObj->getComponents($relationship);
$rightComponents = $rightObj->getComponents($relationship);
@ -995,7 +995,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
if($hasOne = $this->has_one()) {
if($hasOne = $this->hasOne()) {
foreach($hasOne as $relationship => $class) {
$leftComponent = $leftObj->getComponent($relationship);
$rightComponent = $rightObj->getComponent($relationship);
@ -1130,7 +1130,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->$fieldName = $fieldValue;
}
// Set many-many defaults with an array of ids
if(is_array($fieldValue) && $this->many_many($fieldName)) {
if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
$manyManyJoin = $this->$fieldName();
$manyManyJoin->setByIdList($fieldValue);
}
@ -1497,7 +1497,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $this->components[$componentName];
}
if($class = $this->has_one($componentName)) {
if($class = $this->hasOneComponent($componentName)) {
$joinField = $componentName . 'ID';
$joinID = $this->getField($joinField);
@ -1514,7 +1514,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(empty($component)) {
$component = $this->model->$class->newObject();
}
} elseif($class = $this->belongs_to($componentName)) {
} elseif($class = $this->belongsToComponent($componentName)) {
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
$joinID = $this->ID;
@ -1553,18 +1553,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Returns a one-to-many relation as a HasManyList
*
* @param string $componentName Name of the component
* @param string $filter A filter to be inserted into the WHERE clause
* @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, the static
* field $default_sort on the component class will be used.
* @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
* @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
* the static field $default_sort on the component class will be used.
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead
* @param string|array $limit A limit expression to be inserted into the LIMIT clause
* @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
*
* @return HasManyList The components of the one-to-many relationship.
*/
public function getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = null) {
public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
$result = null;
if(!$componentClass = $this->has_many($componentName)) {
if(!$componentClass = $this->hasManyComponent($componentName)) {
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
. " on class '$this->class'", E_USER_ERROR);
}
@ -1575,6 +1575,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
);
}
if($filter !== null || $sort !== null || $limit !== null) {
Deprecation::notice('3.2', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
if(!$this->ID) {
if(!isset($this->unsavedRelations[$componentName])) {
@ -1647,7 +1652,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
// Extract relation from current object
$remoteClass = $this->$type($component, false);
if($type === 'has_many') {
$remoteClass = $this->hasManyComponent($component, false);
} else {
$remoteClass = $this->belongsToComponent($component, false);
}
if(empty($remoteClass)) {
throw new Exception("Unknown $type component '$component' on class '$this->class'");
}
@ -1716,8 +1726,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @todo Implement query-params
*/
public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
list($parentClass, $componentClass, $parentField, $componentField, $table)
= $this->manyManyComponent($componentName);
if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
Deprecation::notice('3.2', 'The $filter, $sort, $join and $limit parameters for
DataObject::getManyManyComponents() have been deprecated.
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
if(!$this->ID) {
@ -1730,7 +1747,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$result = ManyManyList::create(
$componentClass, $table, $componentField, $parentField,
$this->many_many_extraFields($componentName)
$this->manyManyExtraFieldsForComponent($componentName)
);
if($this->model) $result->setDataModel($this->model);
@ -1743,64 +1760,91 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
->limit($limit);
}
/**
* @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
* @param string $component
* @return array|null
*/
public function has_one($component = null) {
if($component) {
Deprecation::notice('3.2', 'Please use hasOneComponent() instead');
return $this->hasOneComponent($component);
}
Deprecation::notice('3.2', 'Please use hasOne() instead');
return $this->hasOne();
}
/**
* 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
*
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and their
* classes.
* @param string $component Deprecated - Name of component
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and
* their classes.
*/
public function has_one($component = null) {
$classes = ClassInfo::ancestry($this);
foreach($classes as $class) {
// Wait until after we reach DataObject
if(in_array($class, array('Object', 'ViewableData', 'DataObject'))) continue;
if($component) {
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
if(isset($hasOne[$component])) {
return $hasOne[$component];
}
} else {
$newItems = (array)Config::inst()->get($class, 'has_one', Config::UNINHERITED);
// Validate the data
foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$has_one has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge($newItems, (array)$items) : $newItems;
}
public function hasOne($component = null) {
if($component) {
Deprecation::notice(
'3.2',
'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
Deprecation::SCOPE_GLOBAL
);
return $this->hasOneComponent($component);
}
return isset($items) ? $items : null;
return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
}
/**
* Return data for a specific has_one component.
* @param string $component
* @return string|null
*/
public function hasOneComponent($component) {
$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
if(isset($hasOnes[$component])) {
return $hasOnes[$component];
}
}
/**
* @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
* @param string $component
* @param bool $classOnly
* @return array|null
*/
public function belongs_to($component = null, $classOnly = true) {
if($component) {
Deprecation::notice('3.2', 'Please use belongsToComponent() instead');
return $this->belongsToComponent($component, $classOnly);
}
Deprecation::notice('3.2', 'Please use belongsTo() instead');
return $this->belongsTo(null, $classOnly);
}
/**
* Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
* their class name will be returned.
*
* @param string $component
* @param string $component - Name of component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|array
*/
public function belongs_to($component = null, $classOnly = true) {
$belongsTo = $this->config()->belongs_to;
public function belongsTo($component = null, $classOnly = true) {
if($component) {
if($belongsTo && array_key_exists($component, $belongsTo)) {
$belongsTo = $belongsTo[$component];
} else {
return false;
}
Deprecation::notice(
'3.2',
'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
Deprecation::SCOPE_GLOBAL
);
return $this->belongsToComponent($component, $classOnly);
}
$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
if($belongsTo && $classOnly) {
return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
} else {
@ -1808,9 +1852,28 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
/**
* Return data for a specific belongs_to component.
* @param string $component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|false
*/
public function belongsToComponent($component, $classOnly = true) {
$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
if($belongsTo && array_key_exists($component, $belongsTo)) {
$belongsTo = $belongsTo[$component];
} else {
return false;
}
return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
}
/**
* Return all of the database fields defined in self::$db and all the parent classes.
* Doesn't include any fields specified by self::$has_one. Use $this->has_one() to get these fields
* Doesn't include any fields specified by self::$has_one. Use $this->hasOne() to get these fields
*
* @param string $fieldName Limit the output to a specific field name
* @return array The database fields
@ -1837,15 +1900,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $dbItems[$fieldName];
}
} else {
// Validate the data
foreach($dbItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$db has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " property name, and the map value should be the property type.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
}
}
@ -1853,26 +1907,42 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $items;
}
/**
* @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
* @param string $component
* @param bool $classOnly
* @return array|null
*/
public function has_many($component = null, $classOnly = true) {
if($component) {
Deprecation::notice('3.2', 'Please use hasManyComponent() instead');
return $this->hasManyComponent($component, $classOnly);
}
Deprecation::notice('3.2', 'Please use hasMany() instead');
return $this->hasMany(null, $classOnly);
}
/**
* Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
* relationships and their classes will be returned.
*
* @param string $component Name of component
* @param string $component Deprecated - Name of component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|array
* @return string|array|false
*/
public function has_many($component = null, $classOnly = true) {
$hasMany = $this->config()->has_many;
public function hasMany($component = null, $classOnly = true) {
if($component) {
if($hasMany && array_key_exists($component, $hasMany)) {
$hasMany = $hasMany[$component];
} else {
return false;
}
Deprecation::notice(
'3.2',
'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
Deprecation::SCOPE_GLOBAL
);
return $this->hasManyComponent($component, $classOnly);
}
$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
if($hasMany && $classOnly) {
return preg_replace('/(.+)?\..+/', '$1', $hasMany);
} else {
@ -1880,170 +1950,217 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
/**
* Return data for a specific has_many component.
* @param string $component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|false
*/
public function hasManyComponent($component, $classOnly = true) {
$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
if($hasMany && array_key_exists($component, $hasMany)) {
$hasMany = $hasMany[$component];
} else {
return false;
}
return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
}
/**
* @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
* manyManyExtraFieldsForComponent()
* @param string $component
* @return array
*/
public function many_many_extraFields($component = null) {
if($component) {
Deprecation::notice('3.2', 'Please use manyManyExtraFieldsForComponent() instead');
return $this->manyManyExtraFieldsForComponent($component);
}
Deprecation::notice('3.2', 'Please use manyManyExtraFields() instead');
return $this->manyManyExtraFields();
}
/**
* Return the many-to-many extra fields specification.
*
* If you don't specify a component name, it returns all
* extra fields for all components available.
*
* @param string $component Name of component
* @return array
* @param string $component Deprecated - Name of component
* @return array|null
*/
public function many_many_extraFields($component = null) {
$classes = ClassInfo::ancestry($this);
public function manyManyExtraFields($component = null) {
if($component) {
Deprecation::notice(
'3.2',
'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
to manyManyExtraFields()',
Deprecation::SCOPE_GLOBAL
);
return $this->manyManyExtraFieldsForComponent($component);
}
foreach($classes as $class) {
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
}
/**
* Return the many-to-many extra fields specification for a specific component.
* @param string $component
* @return array|null
*/
public function manyManyExtraFieldsForComponent($component) {
// Get all many_many_extraFields defined in this class or parent classes
$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
// Extra fields are immediately available
if(isset($extraFields[$component])) {
return $extraFields[$component];
}
// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$relationName = null;
// Extract class and relation name from dot-notation
if(strpos($candidate, '.') !== false) {
list($candidate, $relationName) = explode('.', $candidate, 2);
}
// Find extra fields for one component
if($component) {
$SNG_class = singleton($class);
$extraFields = $SNG_class->stat('many_many_extraFields');
// If we've not already found the relation name from dot notation, we need to find a relation that points
// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
// so it's safe to assume that it's the correct one
if(!$relationName) {
$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
// Extra fields are immediately available on this class
if(isset($extraFields[$component])) {
return $extraFields[$component];
}
$manyMany = $SNG_class->stat('many_many');
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$SNG_candidate = singleton($candidate);
$candidateManyMany = $SNG_candidate->stat('belongs_many_many');
// Find the relation given the class
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
if($relatedClass == $class) {
$relationName = $relation;
break;
}
}
if($relationName) {
$extraFields = $SNG_candidate->stat('many_many_extraFields');
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
foreach($candidateManyManys as $relation => $relatedClass) {
if($relatedClass === $this->class) {
$relationName = $relation;
}
}
}
$manyMany = $SNG_class->stat('belongs_many_many');
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$SNG_candidate = singleton($candidate);
$candidateManyMany = $SNG_candidate->stat('many_many');
// Find the relation given the class
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
if($relatedClass == $class) {
$relationName = $relation;
}
}
$extraFields = $SNG_candidate->stat('many_many_extraFields');
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
}
} else {
// Find all the extra fields for all components
$newItems = (array)Config::inst()->get($class, 'many_many_extraFields', Config::UNINHERITED);
foreach($newItems as $k => $v) {
if(!is_array($v)) {
user_error(
"$class::\$many_many_extraFields has a bad entry: "
. var_export($k, true) . " => " . var_export($v, true)
. ". Each many_many_extraFields entry should map to a field specification array.",
E_USER_ERROR
);
}
}
$items = isset($items) ? array_merge($newItems, $items) : $newItems;
// If we've found a matching relation on the target class, see if we can find extra fields for it
$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
}
return isset($items) ? $items : null;
}
/**
* @deprecated 4.0 Method has been renamed to manyMany()
* @param string $component
* @return array|null
*/
public function many_many($component = null) {
if($component) {
Deprecation::notice('3.2', 'Please use manyManyComponent() instead');
return $this->manyManyComponent($component);
}
Deprecation::notice('3.2', 'Please use manyMany() instead');
return $this->manyMany();
}
/**
* Return information about a many-to-many component.
* The return value is an array of (parentclass, childclass). If $component is null, then all many-many
* components are returned.
*
* @param string $component Name of component
*
* @return array An array of (parentclass, childclass), or an array of all many-many components
* @see DataObject::manyManyComponent()
* @param string $component Deprecated - Name of component
* @return array|null An array of (parentclass, childclass), or an array of all many-many components
*/
public function many_many($component = null) {
$classes = ClassInfo::ancestry($this);
foreach($classes as $class) {
// Wait until after we reach DataObject
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
if($component) {
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
// Try many_many
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$parentField = $class . "ID";
$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
return array($class, $candidate, $parentField, $childField, "{$class}_$component");
}
// Try belongs_many_many
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
if($candidate) {
$childField = $candidate . "ID";
// We need to find the inverse component name
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
if(!$otherManyMany) {
user_error("Inverse component of $candidate not found ({$this->class})", E_USER_ERROR);
}
foreach($otherManyMany as $inverseComponentName => $candidateClass) {
if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
return array($class, $candidate, $parentField, $childField,
"{$candidate}_$inverseComponentName");
}
}
user_error("Orphaned \$belongs_many_many value for $this->class.$component", E_USER_ERROR);
}
} else {
$newItems = (array)Config::inst()->get($class, 'many_many', Config::UNINHERITED);
// Validate the data
foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$many_many has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge($newItems, $items) : $newItems;
$newItems = (array)Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
// Validate the data
foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$belongs_many_many has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge($newItems, $items) : $newItems;
}
public function manyMany($component = null) {
if($component) {
Deprecation::notice(
'3.2',
'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
Deprecation::SCOPE_GLOBAL
);
return $this->manyManyComponent($component);
}
return isset($items) ? $items : null;
$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
$items = array_merge($manyManys, $belongsManyManys);
return $items;
}
/**
* Return information about a specific many_many component. Returns a numeric array of:
* array(
* <classname>, The class that relation is defined in e.g. "Product"
* <candidateName>, The target class of the relation e.g. "Category"
* <parentField>, The field name pointing to <classname>'s table e.g. "ProductID"
* <childField>, The field name pointing to <candidatename>'s table e.g. "CategoryID"
* <joinTable> The join table between the two classes e.g. "Product_Categories"
* )
* @param string $component The component name
* @return array|null
*/
public function manyManyComponent($component) {
$classes = $this->getClassAncestry();
foreach($classes as $class) {
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
// Check if the component is defined in many_many on this class
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$parentField = $class . "ID";
$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
return array($class, $candidate, $parentField, $childField, "{$class}_$component");
}
// Check if the component is defined in belongs_many_many on this class
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
if($candidate) {
// Extract class and relation name from dot-notation
if(strpos($candidate, '.') !== false) {
list($candidate, $relationName) = explode('.', $candidate, 2);
}
$childField = $candidate . "ID";
// We need to find the inverse component name
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
if(!$otherManyMany) {
throw new LogicException("Inverse component of $candidate not found ({$this->class})");
}
// If we've got a relation name (extracted from dot-notation), we can already work out
// the join table and candidate class name...
if(isset($relationName) && isset($otherManyMany[$relationName])) {
$candidateClass = $otherManyMany[$relationName];
$joinTable = "{$candidate}_{$relationName}";
} else {
// ... otherwise, we need to loop over the many_manys and find a relation that
// matches up to this class
foreach($otherManyMany as $inverseComponentName => $candidateClass) {
if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
$joinTable = "{$candidate}_{$inverseComponentName}";
break;
}
}
}
// If we could work out the join table, we've got all the info we need
if(isset($joinTable)) {
$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
return array($class, $candidate, $parentField, $childField, $joinTable);
}
throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
}
}
}
/**
@ -2535,7 +2652,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return (
array_key_exists($field, $this->record)
|| $this->db($field)
|| (substr($field,-2) == 'ID') && $this->has_one(substr($field,0, -2))
|| (substr($field,-2) == 'ID') && $this->hasOneComponent(substr($field,0, -2))
|| $this->hasMethod("get{$field}")
);
}
@ -2624,7 +2741,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
if(Permission::checkMember($member, "ADMIN")) return true;
if($this->many_many('Can' . $perm)) {
if($this->manyManyComponent('Can' . $perm)) {
if($this->ParentID && $this->SecurityType == 'Inherit') {
if(!($p = $this->Parent)) {
return false;
@ -2808,12 +2925,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $obj;
// Special case for has_one relationships
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
} else if(preg_match('/ID$/', $fieldName) && $this->hasOneComponent(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(($type = $this->has_one($fieldName)) && ($type === 'DataObject')) {
} else if(($type = $this->hasOneComponent($fieldName)) && ($type === 'DataObject')) {
$val = $this->$fieldName();
return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
@ -2911,16 +3028,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return String
*/
public function getReverseAssociation($className) {
if (is_array($this->many_many())) {
$many_many = array_flip($this->many_many());
if (is_array($this->manyMany())) {
$many_many = array_flip($this->manyMany());
if (array_key_exists($className, $many_many)) return $many_many[$className];
}
if (is_array($this->has_many())) {
$has_many = array_flip($this->has_many());
if (is_array($this->hasMany())) {
$has_many = array_flip($this->hasMany());
if (array_key_exists($className, $has_many)) return $has_many[$className];
}
if (is_array($this->has_one())) {
$has_one = array_flip($this->has_one());
if (is_array($this->hasOne())) {
$has_one = array_flip($this->hasOne());
if (array_key_exists($className, $has_one)) return $has_one[$className];
}
@ -3192,6 +3309,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$indexes = $this->databaseIndexes();
// Validate relationship configuration
$this->validateModelDefinitions();
if($fields) {
$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
@ -3228,6 +3348,42 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->extend('augmentDatabase', $dummy);
}
/**
* Validate that the configured relations for this class use the correct syntaxes
* @throws LogicException
*/
protected function validateModelDefinitions() {
$modelDefinitions = array(
'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
);
foreach($modelDefinitions as $defType => $relations) {
if( ! $relations) continue;
foreach($relations as $k => $v) {
if($defType === 'many_many_extraFields') {
if(!is_array($v)) {
throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
. var_export($k, true) . " => " . var_export($v, true)
. ". Each many_many_extraFields entry should map to a field specification array.");
}
} else {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
throw new LogicException("$this->class::$defType has a bad entry: "
. var_export($k, true). " => " . var_export($v, true) . ". Each map key should be a
relationship name, and the map value should be the data class to join to.");
}
}
}
}
}
/**
* Add default records to database. This function is called whenever the
* database is built, after the database tables have all been created. Overload
@ -3789,7 +3945,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function hasValue($field, $arguments = null, $cache = true) {
// has_one fields should not use dbObject to check if a value is given
if(!$this->has_one($field) && ($obj = $this->dbObject($field))) {
if(!$this->hasOneComponent($field) && ($obj = $this->dbObject($field))) {
return $obj->exists();
} else {
return parent::hasValue($field, $arguments, $cache);

View File

@ -645,7 +645,7 @@ class DataQuery {
foreach($relation as $rel) {
$model = singleton($modelClass);
if ($component = $model->has_one($rel)) {
if ($component = $model->hasOneComponent($rel)) {
if(!$this->query->isJoinedTo($component)) {
$foreignKey = $rel;
$realModelClass = ClassInfo::table_for_object_field($modelClass, "{$foreignKey}ID");
@ -668,7 +668,7 @@ class DataQuery {
}
$modelClass = $component;
} elseif ($component = $model->has_many($rel)) {
} elseif ($component = $model->hasManyComponent($rel)) {
if(!$this->query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getRemoteJoinField($rel);
@ -690,7 +690,7 @@ class DataQuery {
}
$modelClass = $component;
} elseif ($component = $model->many_many($rel)) {
} elseif ($component = $model->manyManyComponent($rel)) {
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
$componentBaseClass = ClassInfo::baseDataClass($componentClass);

View File

@ -28,7 +28,7 @@ class ForeignKey extends Int {
public function scaffoldFormField($title = null, $params = null) {
$relationName = substr($this->name,0,-2);
$hasOneClass = $this->object->has_one($relationName);
$hasOneClass = $this->object->hasOneComponent($relationName);
if($hasOneClass && singleton($hasOneClass) instanceof Image) {
$field = new UploadField($relationName, $title);

View File

@ -269,7 +269,7 @@ class PermissionCheckboxSetField extends FormField {
$permission->delete();
}
if($fieldname && $record && ($record->has_many($fieldname) || $record->many_many($fieldname))) {
if($fieldname && $record && ($record->hasManyComponent($fieldname) || $record->manyManyComponent($fieldname))) {
if(!$record->ID) $record->write(); // We need a record ID to write permissions

View File

@ -28,7 +28,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' =>
'ManyManyRelation' =>
array(
array(
"=>FixtureFactoryTest_DataObjectRelation.relation1" => array(),
@ -48,18 +48,18 @@ class FixtureBlueprintTest extends SapphireTest {
)
);
$this->assertEquals(2, $obj->ManyMany()->Count());
$this->assertNotNull($obj->ManyMany()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyMany()->find('ID', $relation2->ID));
$this->assertEquals(2, $obj->ManyManyRelation()->Count());
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation2->ID));
$this->assertEquals(
array('Label' => 'This is a label for relation 1'),
$obj->ManyMany()->getExtraData('ManyMany', $relation1->ID)
$obj->ManyManyRelation()->getExtraData('ManyManyRelation', $relation1->ID)
);
$this->assertEquals(
array('Label' => 'This is a label for relation 2'),
$obj->ManyMany()->getExtraData('ManyMany', $relation2->ID)
$obj->ManyManyRelation()->getExtraData('ManyManyRelation', $relation2->ID)
);
}
@ -92,7 +92,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' =>
'ManyManyRelation' =>
'=>FixtureFactoryTest_DataObjectRelation.relation1,' .
'=>FixtureFactoryTest_DataObjectRelation.relation2'
),
@ -104,9 +104,9 @@ class FixtureBlueprintTest extends SapphireTest {
)
);
$this->assertEquals(2, $obj->ManyMany()->Count());
$this->assertNotNull($obj->ManyMany()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyMany()->find('ID', $relation2->ID));
$this->assertEquals(2, $obj->ManyManyRelation()->Count());
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation2->ID));
}
/**
@ -119,7 +119,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' => '=>UnknownClass.relation1'
'ManyManyRelation' => '=>UnknownClass.relation1'
),
array(
'FixtureFactoryTest_DataObjectRelation' => array(
@ -139,7 +139,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' => '=>FixtureFactoryTest_DataObjectRelation.unknown_identifier'
'ManyManyRelation' => '=>FixtureFactoryTest_DataObjectRelation.unknown_identifier'
),
array(
'FixtureFactoryTest_DataObjectRelation' => array(
@ -163,7 +163,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' => 'FixtureFactoryTest_DataObjectRelation.relation1'
'ManyManyRelation' => 'FixtureFactoryTest_DataObjectRelation.relation1'
),
array(
'FixtureFactoryTest_DataObjectRelation' => array(

View File

@ -163,11 +163,11 @@ class FixtureFactoryTest_DataObject extends DataObject implements TestOnly {
);
private static $many_many = array(
"ManyMany" => "FixtureFactoryTest_DataObjectRelation"
"ManyManyRelation" => "FixtureFactoryTest_DataObjectRelation"
);
private static $many_many_extraFields = array(
"ManyMany" => array(
"ManyManyRelation" => array(
"Label" => "Varchar"
)
);

View File

@ -20,6 +20,8 @@ class DataListTest extends SapphireTest {
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest_Company',
'DataObjectTest_Fan',

View File

@ -21,6 +21,9 @@ class DataObjectLazyLoadingTest extends SapphireTest {
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'VersionedTest_DataObject',
'VersionedTest_Subclass',
'VersionedLazy_DataObject',

View File

@ -17,6 +17,8 @@ class DataObjectTest extends SapphireTest {
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
@ -1060,6 +1062,86 @@ class DataObjectTest extends SapphireTest {
);
}
protected function makeAccessible($object, $method) {
$reflectionMethod = new ReflectionMethod($object, $method);
$reflectionMethod->setAccessible(true);
return $reflectionMethod;
}
public function testValidateModelDefinitionsFailsWithArray() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'has_one', array('NotValid' => array('NoArraysAllowed')));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
}
public function testValidateModelDefinitionsFailsWithIntKey() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'has_many', array(12 => 'DataObjectTest_Player'));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
}
public function testValidateModelDefinitionsFailsWithIntValue() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'many_many', array('Players' => 12));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
}
/**
* many_many_extraFields is allowed to have an array value, so shouldn't throw an exception
*/
public function testValidateModelDefinitionsPassesWithExtraFields() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'many_many_extraFields',
array('Relations' => array('Price' => 'Int')));
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest();
$this->fail('Exception should not be thrown');
throw $e;
}
Config::unnest();
}
public function testNewClassInstance() {
$dataObject = $this->objFromFixture('DataObjectTest_Team', 'team1');
$changedDO = $dataObject->newClassInstance('DataObjectTest_SubTeam');
@ -1078,32 +1160,85 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals($changedDO->ClassName, 'DataObjectTest_SubTeam');
}
public function testMultipleManyManyWithSameClass() {
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
$sponsors = $team->Sponsors();
$equipmentSuppliers = $team->EquipmentSuppliers();
// Check that DataObject::many_many() works as expected
list($class, $targetClass, $parentField, $childField, $joinTable) = $team->manyManyComponent('Sponsors');
$this->assertEquals('DataObjectTest_Team', $class,
'DataObject::many_many() didn\'t find the correct base class');
$this->assertEquals('DataObjectTest_EquipmentCompany', $targetClass,
'DataObject::many_many() didn\'t find the correct target class for the relation');
$this->assertEquals('DataObjectTest_EquipmentCompany_SponsoredTeams', $joinTable,
'DataObject::many_many() didn\'t find the correct relation table');
// Check that ManyManyList still works
$this->assertEquals(2, $sponsors->count(), 'Rows are missing from relation');
$this->assertEquals(1, $equipmentSuppliers->count(), 'Rows are missing from relation');
// Check everything works when no relation is present
$teamWithoutSponsor = $this->objFromFixture('DataObjectTest_Team', 'team3');
$this->assertInstanceOf('ManyManyList', $teamWithoutSponsor->Sponsors());
$this->assertEquals(0, $teamWithoutSponsor->Sponsors()->count());
// Check many_many_extraFields still works
$equipmentCompany = $this->objFromFixture('DataObjectTest_EquipmentCompany', 'equipmentcompany1');
$equipmentCompany->SponsoredTeams()->add($teamWithoutSponsor, array('SponsorFee' => 1000));
$sponsoredTeams = $equipmentCompany->SponsoredTeams();
$this->assertEquals(1000, $sponsoredTeams->byID($teamWithoutSponsor->ID)->SponsorFee,
'Data from many_many_extraFields was not stored/extracted correctly');
// Check subclasses correctly inherit multiple many_manys
$subTeam = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
$this->assertEquals(2, $subTeam->Sponsors()->count(),
'Child class did not inherit multiple many_manys');
$this->assertEquals(1, $subTeam->EquipmentSuppliers()->count(),
'Child class did not inherit multiple many_manys');
// Team 2 has one EquipmentCompany sponsor and one SubEquipmentCompany
$team2 = $this->objFromFixture('DataObjectTest_Team', 'team2');
$this->assertEquals(2, $team2->Sponsors()->count(),
'Child class did not inherit multiple belongs_many_manys');
// Check many_many_extraFields also works from the belongs_many_many side
$sponsors = $team2->Sponsors();
$sponsors->add($equipmentCompany, array('SponsorFee' => 750));
$this->assertEquals(750, $sponsors->byID($equipmentCompany->ID)->SponsorFee,
'Data from many_many_extraFields was not stored/extracted correctly');
$subEquipmentCompany = $this->objFromFixture('DataObjectTest_SubEquipmentCompany', 'subequipmentcompany1');
$subTeam->Sponsors()->add($subEquipmentCompany, array('SponsorFee' => 1200));
$this->assertEquals(1200, $subTeam->Sponsors()->byID($subEquipmentCompany->ID)->SponsorFee,
'Data from inherited many_many_extraFields was not stored/extracted correctly');
}
public function testManyManyExtraFields() {
$player = $this->objFromFixture('DataObjectTest_Player', 'player1');
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
// Get all extra fields
$teamExtraFields = $team->many_many_extraFields();
$teamExtraFields = $team->manyManyExtraFields();
$this->assertEquals(array(
'Players' => array('Position' => 'Varchar(100)')
), $teamExtraFields);
// Ensure fields from parent classes are included
$subTeam = singleton('DataObjectTest_SubTeam');
$teamExtraFields = $subTeam->many_many_extraFields();
$teamExtraFields = $subTeam->manyManyExtraFields();
$this->assertEquals(array(
'Players' => array('Position' => 'Varchar(100)'),
'FormerPlayers' => array('Position' => 'Varchar(100)')
), $teamExtraFields);
// Extra fields are immediately available on the Team class (defined in $many_many_extraFields)
$teamExtraFields = $team->many_many_extraFields('Players');
$teamExtraFields = $team->manyManyExtraFieldsForComponent('Players');
$this->assertEquals($teamExtraFields, array(
'Position' => 'Varchar(100)'
));
// We'll have to go through the relation to get the extra fields on Player
$playerExtraFields = $player->many_many_extraFields('Teams');
$playerExtraFields = $player->manyManyExtraFieldsForComponent('Teams');
$this->assertEquals($playerExtraFields, array(
'Position' => 'Varchar(100)'
));
@ -1133,7 +1268,7 @@ class DataObjectTest extends SapphireTest {
// Check that ordering a many-many relation by an aggregate column doesn't fail
$player = $this->objFromFixture('DataObjectTest_Player', 'player2');
$player->Teams("", "count(DISTINCT \"DataObjectTest_Team_Players\".\"DataObjectTest_PlayerID\") DESC");
$player->Teams()->sort("count(DISTINCT \"DataObjectTest_Team_Players\".\"DataObjectTest_PlayerID\") DESC");
}
/**
@ -1286,13 +1421,13 @@ class DataObjectTest extends SapphireTest {
'CurrentStaff' => 'DataObjectTest_Staff',
'PreviousStaff' => 'DataObjectTest_Staff'
),
$company->has_many(),
$company->hasMany(),
'has_many strips field name data by default.'
);
$this->assertEquals (
'DataObjectTest_Staff',
$company->has_many('CurrentStaff'),
$company->hasManyComponent('CurrentStaff'),
'has_many strips field name data by default on single relationships.'
);
@ -1301,13 +1436,13 @@ class DataObjectTest extends SapphireTest {
'CurrentStaff' => 'DataObjectTest_Staff.CurrentCompany',
'PreviousStaff' => 'DataObjectTest_Staff.PreviousCompany'
),
$company->has_many(null, false),
$company->hasMany(null, false),
'has_many returns field name data when $classOnly is false.'
);
$this->assertEquals (
'DataObjectTest_Staff.CurrentCompany',
$company->has_many('CurrentStaff', false),
$company->hasManyComponent('CurrentStaff', false),
'has_many returns field name data on single records when $classOnly is false.'
);
}
@ -1563,6 +1698,11 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
)
);
private static $belongs_many_many = array(
'Sponsors' => 'DataObjectTest_EquipmentCompany.SponsoredTeams',
'EquipmentSuppliers' => 'DataObjectTest_EquipmentCompany.EquipmentCustomers'
);
private static $summary_fields = array(
'Title' => 'Custom Title',
'Title.UpperCase' => 'Title',
@ -1698,6 +1838,25 @@ class DataObjectTest_Company extends DataObject implements TestOnly {
);
}
class DataObjectTest_EquipmentCompany extends DataObjectTest_Company implements TestOnly {
private static $many_many = array(
'SponsoredTeams' => 'DataObjectTest_Team',
'EquipmentCustomers' => 'DataObjectTest_Team'
);
private static $many_many_extraFields = array(
'SponsoredTeams' => array(
'SponsorFee' => 'Int'
)
);
}
class DataObjectTest_SubEquipmentCompany extends DataObjectTest_EquipmentCompany implements TestOnly {
private static $db = array(
'SubclassDatabaseField' => 'Varchar'
);
}
class DataObjectTest_Staff extends DataObject implements TestOnly {
private static $has_one = array (
'CurrentCompany' => 'DataObjectTest_Company',

View File

@ -1,68 +1,82 @@
DataObjectTest_EquipmentCompany:
equipmentcompany1:
Name: Company corp
equipmentcompany2:
Name: 'Team co.'
DataObjectTest_SubEquipmentCompany:
subequipmentcompany1:
Name: John Smith and co
DataObjectTest_Team:
team1:
Title: Team 1
team2:
Title: Team 2
team3:
Title: Team 3
team1:
Title: Team 1
Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany2
team2:
Title: Team 2
Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany2,=>DataObjectTest_SubEquipmentCompany.subequipmentcompany1
EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
team3:
Title: Team 3
DataObjectTest_Player:
captain1:
FirstName: Captain
ShirtNumber: 007
FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1
IsRetired: 1
captain2:
FirstName: Captain 2
Teams: =>DataObjectTest_Team.team2
player1:
FirstName: Player 1
player2:
FirstName: Player 2
Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2
captain1:
FirstName: Captain
ShirtNumber: 007
FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1
IsRetired: 1
captain2:
FirstName: Captain 2
Teams: =>DataObjectTest_Team.team2
player1:
FirstName: Player 1
player2:
FirstName: Player 2
Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2
DataObjectTest_SubTeam:
subteam1:
Title: Subteam 1
SubclassDatabaseField: Subclassed 1
ExtendedDatabaseField: Extended 1
ParentTeam: =>DataObjectTest_Team.team1
subteam2_with_player_relation:
Title: Subteam 2
SubclassDatabaseField: Subclassed 2
ExtendedHasOneRelationship: =>DataObjectTest_Player.player1
subteam3_with_empty_fields:
Title: Subteam 3
subteam1:
Title: Subteam 1
SubclassDatabaseField: Subclassed 1
ExtendedDatabaseField: Extended 1
ParentTeam: =>DataObjectTest_Team.team1
Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany1
subteam2_with_player_relation:
Title: Subteam 2
SubclassDatabaseField: Subclassed 2
ExtendedHasOneRelationship: =>DataObjectTest_Player.player1
subteam3_with_empty_fields:
Title: Subteam 3
DataObjectTest_TeamComment:
comment1:
Name: Joe
Comment: This is a team comment by Joe
Team: =>DataObjectTest_Team.team1
comment2:
Name: Bob
Comment: This is a team comment by Bob
Team: =>DataObjectTest_Team.team1
comment3:
Name: Phil
Comment: Phil is a unique guy, and comments on team2
Team: =>DataObjectTest_Team.team2
comment1:
Name: Joe
Comment: This is a team comment by Joe
Team: =>DataObjectTest_Team.team1
comment2:
Name: Bob
Comment: This is a team comment by Bob
Team: =>DataObjectTest_Team.team1
comment3:
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
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
company1:
Name: Company corp
Owner: =>DataObjectTest_Player.player1
company1:
Name: 'Team co.'
Owner: =>DataObjectTest_Player.player2

View File

@ -46,7 +46,7 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$this->assertEquals($group->Permissions()->Count(), 0, 'The tested group has no permissions');
$this->assertEquals($untouchable->Permissions()->Count(), 1, 'The other group has one permission');
$this->assertEquals($untouchable->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($untouchable->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount, 'There are no orphaned permissions');
@ -62,14 +62,14 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$untouchable->flushCache();
$this->assertEquals($group->Permissions()->Count(), 2,
'The tested group has two permissions permission');
$this->assertEquals($group->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($group->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The tested group has ADMIN permission');
$this->assertEquals($group->Permissions("\"Code\"='NON-ADMIN'")->Count(), 1,
$this->assertEquals($group->Permissions()->where("\"Code\"='NON-ADMIN'")->Count(), 1,
'The tested group has CMS_ACCESS_AssetAdmin permission');
$this->assertEquals($untouchable->Permissions()->Count(), 1,
'The other group has one permission');
$this->assertEquals($untouchable->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($untouchable->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+2,
@ -85,12 +85,12 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$untouchable->flushCache();
$this->assertEquals($group->Permissions()->Count(), 1,
'The tested group has 1 permission');
$this->assertEquals($group->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($group->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The tested group has ADMIN permission');
$this->assertEquals($untouchable->Permissions()->Count(), 1,
'The other group has one permission');
$this->assertEquals($untouchable->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($untouchable->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+1,

View File

@ -49,7 +49,7 @@ class YamlFixtureTest extends SapphireTest {
$factory->getId("YamlFixtureTest_DataObject", "testobject1")
);
$this->assertTrue(
$object1->ManyMany()->Count() == 2,
$object1->ManyManyRelation()->Count() == 2,
"Should be two items in this relationship"
);
$this->assertGreaterThan(0, $factory->getId("YamlFixtureTest_DataObject", "testobject2"));
@ -58,7 +58,7 @@ class YamlFixtureTest extends SapphireTest {
$factory->getId("YamlFixtureTest_DataObject", "testobject2")
);
$this->assertTrue(
$object2->ManyMany()->Count() == 1,
$object2->ManyManyRelation()->Count() == 1,
"Should be one item in this relationship"
);
}
@ -79,7 +79,7 @@ class YamlFixtureTest_DataObject extends DataObject implements TestOnly {
"Name" => "Varchar"
);
private static $many_many = array(
"ManyMany" => "YamlFixtureTest_DataObjectRelation"
"ManyManyRelation" => "YamlFixtureTest_DataObjectRelation"
);
}

View File

@ -6,7 +6,7 @@ YamlFixtureTest_DataObjectRelation:
YamlFixtureTest_DataObject:
testobject1:
Name: TestObject1
ManyMany: =>YamlFixtureTest_DataObjectRelation.relation1,=>YamlFixtureTest_DataObjectRelation.relation2
ManyManyRelation: =>YamlFixtureTest_DataObjectRelation.relation1,=>YamlFixtureTest_DataObjectRelation.relation2
testobject2:
Name: TestObject2
ManyMany: =>YamlFixtureTest_DataObjectRelation.relation1
ManyManyRelation: =>YamlFixtureTest_DataObjectRelation.relation1