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

View File

@ -68,7 +68,7 @@ class XMLDataFormatter extends DataFormatter {
} }
if($this->relationDepth > 0) { if($this->relationDepth > 0) {
foreach($obj->has_one() as $relName => $relClass) { foreach($obj->hasOne() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue; if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering // Field filtering
@ -85,7 +85,7 @@ class XMLDataFormatter extends DataFormatter {
. "\"></$relName>\n"; . "\"></$relName>\n";
} }
foreach($obj->has_many() as $relName => $relClass) { foreach($obj->hasMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue; if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering // Field filtering
@ -103,7 +103,7 @@ class XMLDataFormatter extends DataFormatter {
$xml .= "</$relName>\n"; $xml .= "</$relName>\n";
} }
foreach($obj->many_many() as $relName => $relClass) { foreach($obj->manyMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue; if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering // 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 // using $$includerelations flag as false, so that it only contain $db fields
$spec['fields'] = (array)singleton($this->objectClass)->fieldLabels(false); $spec['fields'] = (array)singleton($this->objectClass)->fieldLabels(false);
$has_ones = singleton($this->objectClass)->has_one(); $has_ones = singleton($this->objectClass)->hasOne();
$has_manys = singleton($this->objectClass)->has_many(); $has_manys = singleton($this->objectClass)->hasMany();
$many_manys = singleton($this->objectClass)->many_many(); $many_manys = singleton($this->objectClass)->manyMany();
$spec['relations'] = (array)$has_ones + (array)$has_manys + (array)$many_manys; $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); $relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
} }
if(!$relationObj || !$relationObj->exists()) { if(!$relationObj || !$relationObj->exists()) {
$relationClass = $obj->has_one($relationName); $relationClass = $obj->hasOneComponent($relationName);
$relationObj = new $relationClass(); $relationObj = new $relationClass();
//write if we aren't previewing //write if we aren't previewing
if (!$preview) $relationObj->write(); if (!$preview) $relationObj->write();

View File

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

View File

@ -204,6 +204,27 @@ The relationship can also be navigated in [templates](../templates).
<% end_if %> <% end_if %>
<% end_with %> <% 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? ## many_many or belongs_many_many?
If you're unsure about whether an object should take on `many_many` or `belongs_many_many`, the best way to think about it is that the object where the relationship will be edited (i.e. via checkboxes) should contain the `many_many`. For instance, in a `many_many` of Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa. If you're unsure about whether an object should take on `many_many` or `belongs_many_many`, the best way to think about it is that the object where the relationship will be edited (i.e. via checkboxes) should contain the `many_many`. For instance, in a `many_many` of Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa.

View File

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

View File

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

View File

@ -502,7 +502,7 @@ class UploadField extends FileField {
if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) { if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
// has_many or many_many // has_many or many_many
$relation->setByIDList($idList); $relation->setByIDList($idList);
} elseif($record->has_one($fieldname)) { } elseif($record->hasOneComponent($fieldname)) {
// has_one // has_one
$record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0; $record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0;
} }
@ -590,7 +590,7 @@ class UploadField extends FileField {
if(empty($allowedMaxFileNumber)) { if(empty($allowedMaxFileNumber)) {
$record = $this->getRecord(); $record = $this->getRecord();
$name = $this->getName(); $name = $this->getName();
if($record && $record->has_one($name)) { if($record && $record->hasOneComponent($name)) {
return 1; // Default for has_one return 1; // Default for has_one
} else { } else {
return null; // Default for has_many and many_many 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()); $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 // Loop through properties
foreach($fields as $field) { 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 // 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 // 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 // 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); $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 //many_many include belongs_many_many
$this->duplicateRelations($sourceObject, $destinationObject, $name); $this->duplicateRelations($sourceObject, $destinationObject, $name);
} }
@ -666,24 +666,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($this->class == 'DataObject') return; if($this->class == 'DataObject') return;
// Set up accessors for joined items // Set up accessors for joined items
if($manyMany = $this->many_many()) { if($manyMany = $this->manyMany()) {
foreach($manyMany as $relationship => $class) { foreach($manyMany as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getManyManyComponents'); $this->addWrapperMethod($relationship, 'getManyManyComponents');
} }
} }
if($hasMany = $this->has_many()) { if($hasMany = $this->hasMany()) {
foreach($hasMany as $relationship => $class) { foreach($hasMany as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getComponents'); $this->addWrapperMethod($relationship, 'getComponents');
} }
} }
if($hasOne = $this->has_one()) { if($hasOne = $this->hasOne()) {
foreach($hasOne as $relationship => $class) { foreach($hasOne as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getComponent'); $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'); $this->addWrapperMethod($relationship, 'getComponent');
} }
} }
@ -972,7 +972,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// merge relations // merge relations
if($includeRelations) { if($includeRelations) {
if($manyMany = $this->many_many()) { if($manyMany = $this->manyMany()) {
foreach($manyMany as $relationship => $class) { foreach($manyMany as $relationship => $class) {
$leftComponents = $leftObj->getManyManyComponents($relationship); $leftComponents = $leftObj->getManyManyComponents($relationship);
$rightComponents = $rightObj->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) { foreach($hasMany as $relationship => $class) {
$leftComponents = $leftObj->getComponents($relationship); $leftComponents = $leftObj->getComponents($relationship);
$rightComponents = $rightObj->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) { foreach($hasOne as $relationship => $class) {
$leftComponent = $leftObj->getComponent($relationship); $leftComponent = $leftObj->getComponent($relationship);
$rightComponent = $rightObj->getComponent($relationship); $rightComponent = $rightObj->getComponent($relationship);
@ -1130,7 +1130,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->$fieldName = $fieldValue; $this->$fieldName = $fieldValue;
} }
// Set many-many defaults with an array of ids // 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 = $this->$fieldName();
$manyManyJoin->setByIdList($fieldValue); $manyManyJoin->setByIdList($fieldValue);
} }
@ -1497,7 +1497,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $this->components[$componentName]; return $this->components[$componentName];
} }
if($class = $this->has_one($componentName)) { if($class = $this->hasOneComponent($componentName)) {
$joinField = $componentName . 'ID'; $joinField = $componentName . 'ID';
$joinID = $this->getField($joinField); $joinID = $this->getField($joinField);
@ -1514,7 +1514,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(empty($component)) { if(empty($component)) {
$component = $this->model->$class->newObject(); $component = $this->model->$class->newObject();
} }
} elseif($class = $this->belongs_to($componentName)) { } elseif($class = $this->belongsToComponent($componentName)) {
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic); $joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
$joinID = $this->ID; $joinID = $this->ID;
@ -1553,18 +1553,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Returns a one-to-many relation as a HasManyList * Returns a one-to-many relation as a HasManyList
* *
* @param string $componentName Name of the component * @param string $componentName Name of the component
* @param string $filter A filter to be inserted into the WHERE clause * @param string|null $filter Deprecated. 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 * @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
* field $default_sort on the component class will be used. * the static field $default_sort on the component class will be used.
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead * @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. * @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; $result = null;
if(!$componentClass = $this->has_many($componentName)) { if(!$componentClass = $this->hasManyComponent($componentName)) {
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'" user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
. " on class '$this->class'", E_USER_ERROR); . " 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 we haven't been written yet, we can't save these relations, so use a list that handles this case
if(!$this->ID) { if(!$this->ID) {
if(!isset($this->unsavedRelations[$componentName])) { 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) { public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
// Extract relation from current object // 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)) { 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'");
} }
@ -1716,8 +1726,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* *
* @todo Implement query-params * @todo Implement query-params
*/ */
public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") { public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); 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 we haven't been written yet, we can't save these relations, so use a list that handles this case
if(!$this->ID) { if(!$this->ID) {
@ -1730,7 +1747,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$result = ManyManyList::create( $result = ManyManyList::create(
$componentClass, $table, $componentField, $parentField, $componentClass, $table, $componentField, $parentField,
$this->many_many_extraFields($componentName) $this->manyManyExtraFieldsForComponent($componentName)
); );
if($this->model) $result->setDataModel($this->model); if($this->model) $result->setDataModel($this->model);
@ -1743,64 +1760,91 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
->limit($limit); ->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 * 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. * 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 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
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and their * their classes.
* classes.
*/ */
public function has_one($component = null) { public function hasOne($component = null) {
$classes = ClassInfo::ancestry($this); if($component) {
Deprecation::notice(
foreach($classes as $class) { '3.2',
// Wait until after we reach DataObject 'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
if(in_array($class, array('Object', 'ViewableData', 'DataObject'))) continue; Deprecation::SCOPE_GLOBAL
);
if($component) { return $this->hasOneComponent($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;
}
} }
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 * 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. * 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 * @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. * the field data stripped off. It defaults to TRUE.
* @return string|array * @return string|array
*/ */
public function belongs_to($component = null, $classOnly = true) { public function belongsTo($component = null, $classOnly = true) {
$belongsTo = $this->config()->belongs_to;
if($component) { if($component) {
if($belongsTo && array_key_exists($component, $belongsTo)) { Deprecation::notice(
$belongsTo = $belongsTo[$component]; '3.2',
} else { 'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
return false; Deprecation::SCOPE_GLOBAL
} );
return $this->belongsToComponent($component, $classOnly);
} }
$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
if($belongsTo && $classOnly) { if($belongsTo && $classOnly) {
return preg_replace('/(.+)?\..+/', '$1', $belongsTo); return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
} else { } 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. * 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 * @param string $fieldName Limit the output to a specific field name
* @return array The database fields * @return array The database fields
@ -1837,15 +1900,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $dbItems[$fieldName]; return $dbItems[$fieldName];
} }
} else { } 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; $items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
} }
} }
@ -1853,26 +1907,42 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $items; 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 * 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. * 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 * @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. * the field data stripped off. It defaults to TRUE.
* @return string|array * @return string|array|false
*/ */
public function has_many($component = null, $classOnly = true) { public function hasMany($component = null, $classOnly = true) {
$hasMany = $this->config()->has_many;
if($component) { if($component) {
if($hasMany && array_key_exists($component, $hasMany)) { Deprecation::notice(
$hasMany = $hasMany[$component]; '3.2',
} else { 'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
return false; Deprecation::SCOPE_GLOBAL
} );
return $this->hasManyComponent($component, $classOnly);
} }
$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
if($hasMany && $classOnly) { if($hasMany && $classOnly) {
return preg_replace('/(.+)?\..+/', '$1', $hasMany); return preg_replace('/(.+)?\..+/', '$1', $hasMany);
} else { } 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. * Return the many-to-many extra fields specification.
* *
* If you don't specify a component name, it returns all * If you don't specify a component name, it returns all
* extra fields for all components available. * extra fields for all components available.
* *
* @param string $component Name of component * @param string $component Deprecated - Name of component
* @return array * @return array|null
*/ */
public function many_many_extraFields($component = null) { public function manyManyExtraFields($component = null) {
$classes = ClassInfo::ancestry($this); 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) { return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue; }
/**
* 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; $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 we've not already found the relation name from dot notation, we need to find a relation that points
if($component) { // back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
$SNG_class = singleton($class); // so it's safe to assume that it's the correct one
$extraFields = $SNG_class->stat('many_many_extraFields'); if(!$relationName) {
$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
// Extra fields are immediately available on this class foreach($candidateManyManys as $relation => $relatedClass) {
if(isset($extraFields[$component])) { if($relatedClass === $this->class) {
return $extraFields[$component]; $relationName = $relation;
}
$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];
}
} }
} }
}
$manyMany = $SNG_class->stat('belongs_many_many'); // If we've found a matching relation on the target class, see if we can find extra fields for it
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null; $extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
if($candidate) { if(isset($extraFields[$relationName])) {
$SNG_candidate = singleton($candidate); return $extraFields[$relationName];
$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;
} }
} }
return isset($items) ? $items : null; 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. * 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 * The return value is an array of (parentclass, childclass). If $component is null, then all many-many
* components are returned. * components are returned.
* *
* @param string $component Name of component * @see DataObject::manyManyComponent()
* * @param string $component Deprecated - Name of component
* @return array An array of (parentclass, childclass), or an array of all many-many components * @return array|null An array of (parentclass, childclass), or an array of all many-many components
*/ */
public function many_many($component = null) { public function manyMany($component = null) {
$classes = ClassInfo::ancestry($this); if($component) {
Deprecation::notice(
foreach($classes as $class) { '3.2',
// Wait until after we reach DataObject 'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue; Deprecation::SCOPE_GLOBAL
);
if($component) { return $this->manyManyComponent($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;
}
} }
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 ( return (
array_key_exists($field, $this->record) array_key_exists($field, $this->record)
|| $this->db($field) || $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}") || $this->hasMethod("get{$field}")
); );
} }
@ -2624,7 +2741,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
if(Permission::checkMember($member, "ADMIN")) return true; 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($this->ParentID && $this->SecurityType == 'Inherit') {
if(!($p = $this->Parent)) { if(!($p = $this->Parent)) {
return false; return false;
@ -2808,12 +2925,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $obj; return $obj;
// Special case for has_one relationships // 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; $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 // 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(); $val = $this->$fieldName();
return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this); return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
@ -2911,16 +3028,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return String * @return String
*/ */
public function getReverseAssociation($className) { public function getReverseAssociation($className) {
if (is_array($this->many_many())) { if (is_array($this->manyMany())) {
$many_many = array_flip($this->many_many()); $many_many = array_flip($this->manyMany());
if (array_key_exists($className, $many_many)) return $many_many[$className]; if (array_key_exists($className, $many_many)) return $many_many[$className];
} }
if (is_array($this->has_many())) { if (is_array($this->hasMany())) {
$has_many = array_flip($this->has_many()); $has_many = array_flip($this->hasMany());
if (array_key_exists($className, $has_many)) return $has_many[$className]; if (array_key_exists($className, $has_many)) return $has_many[$className];
} }
if (is_array($this->has_one())) { if (is_array($this->hasOne())) {
$has_one = array_flip($this->has_one()); $has_one = array_flip($this->hasOne());
if (array_key_exists($className, $has_one)) return $has_one[$className]; 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(); $indexes = $this->databaseIndexes();
// Validate relationship configuration
$this->validateModelDefinitions();
if($fields) { if($fields) {
$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class)); $hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), 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); $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 * Add default records to database. This function is called whenever the
* database is built, after the database tables have all been created. Overload * 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) { public function hasValue($field, $arguments = null, $cache = true) {
// has_one fields should not use dbObject to check if a value is given // 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(); return $obj->exists();
} else { } else {
return parent::hasValue($field, $arguments, $cache); return parent::hasValue($field, $arguments, $cache);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,8 @@ class DataObjectTest extends SapphireTest {
'DataObjectTest_ValidatedObject', 'DataObjectTest_ValidatedObject',
'DataObjectTest_Player', 'DataObjectTest_Player',
'DataObjectTest_TeamComment', 'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass', 'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass', 'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment', '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() { public function testNewClassInstance() {
$dataObject = $this->objFromFixture('DataObjectTest_Team', 'team1'); $dataObject = $this->objFromFixture('DataObjectTest_Team', 'team1');
$changedDO = $dataObject->newClassInstance('DataObjectTest_SubTeam'); $changedDO = $dataObject->newClassInstance('DataObjectTest_SubTeam');
@ -1078,32 +1160,85 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals($changedDO->ClassName, 'DataObjectTest_SubTeam'); $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() { public function testManyManyExtraFields() {
$player = $this->objFromFixture('DataObjectTest_Player', 'player1'); $player = $this->objFromFixture('DataObjectTest_Player', 'player1');
$team = $this->objFromFixture('DataObjectTest_Team', 'team1'); $team = $this->objFromFixture('DataObjectTest_Team', 'team1');
// Get all extra fields // Get all extra fields
$teamExtraFields = $team->many_many_extraFields(); $teamExtraFields = $team->manyManyExtraFields();
$this->assertEquals(array( $this->assertEquals(array(
'Players' => array('Position' => 'Varchar(100)') 'Players' => array('Position' => 'Varchar(100)')
), $teamExtraFields); ), $teamExtraFields);
// Ensure fields from parent classes are included // Ensure fields from parent classes are included
$subTeam = singleton('DataObjectTest_SubTeam'); $subTeam = singleton('DataObjectTest_SubTeam');
$teamExtraFields = $subTeam->many_many_extraFields(); $teamExtraFields = $subTeam->manyManyExtraFields();
$this->assertEquals(array( $this->assertEquals(array(
'Players' => array('Position' => 'Varchar(100)'), 'Players' => array('Position' => 'Varchar(100)'),
'FormerPlayers' => array('Position' => 'Varchar(100)') 'FormerPlayers' => array('Position' => 'Varchar(100)')
), $teamExtraFields); ), $teamExtraFields);
// Extra fields are immediately available on the Team class (defined in $many_many_extraFields) // 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( $this->assertEquals($teamExtraFields, array(
'Position' => 'Varchar(100)' 'Position' => 'Varchar(100)'
)); ));
// We'll have to go through the relation to get the extra fields on Player // 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( $this->assertEquals($playerExtraFields, array(
'Position' => 'Varchar(100)' '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 // Check that ordering a many-many relation by an aggregate column doesn't fail
$player = $this->objFromFixture('DataObjectTest_Player', 'player2'); $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', 'CurrentStaff' => 'DataObjectTest_Staff',
'PreviousStaff' => 'DataObjectTest_Staff' 'PreviousStaff' => 'DataObjectTest_Staff'
), ),
$company->has_many(), $company->hasMany(),
'has_many strips field name data by default.' 'has_many strips field name data by default.'
); );
$this->assertEquals ( $this->assertEquals (
'DataObjectTest_Staff', 'DataObjectTest_Staff',
$company->has_many('CurrentStaff'), $company->hasManyComponent('CurrentStaff'),
'has_many strips field name data by default on single relationships.' 'has_many strips field name data by default on single relationships.'
); );
@ -1301,13 +1436,13 @@ class DataObjectTest extends SapphireTest {
'CurrentStaff' => 'DataObjectTest_Staff.CurrentCompany', 'CurrentStaff' => 'DataObjectTest_Staff.CurrentCompany',
'PreviousStaff' => 'DataObjectTest_Staff.PreviousCompany' 'PreviousStaff' => 'DataObjectTest_Staff.PreviousCompany'
), ),
$company->has_many(null, false), $company->hasMany(null, false),
'has_many returns field name data when $classOnly is false.' 'has_many returns field name data when $classOnly is false.'
); );
$this->assertEquals ( $this->assertEquals (
'DataObjectTest_Staff.CurrentCompany', '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.' '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( private static $summary_fields = array(
'Title' => 'Custom Title', 'Title' => 'Custom Title',
'Title.UpperCase' => '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 { class DataObjectTest_Staff extends DataObject implements TestOnly {
private static $has_one = array ( private static $has_one = array (
'CurrentCompany' => 'DataObjectTest_Company', '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: DataObjectTest_Team:
team1: team1:
Title: Team 1 Title: Team 1
team2: Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
Title: Team 2 EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany2
team3: team2:
Title: Team 3 Title: Team 2
Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany2,=>DataObjectTest_SubEquipmentCompany.subequipmentcompany1
EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
team3:
Title: Team 3
DataObjectTest_Player: DataObjectTest_Player:
captain1: captain1:
FirstName: Captain FirstName: Captain
ShirtNumber: 007 ShirtNumber: 007
FavouriteTeam: =>DataObjectTest_Team.team1 FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1 Teams: =>DataObjectTest_Team.team1
IsRetired: 1 IsRetired: 1
captain2: captain2:
FirstName: Captain 2 FirstName: Captain 2
Teams: =>DataObjectTest_Team.team2 Teams: =>DataObjectTest_Team.team2
player1: player1:
FirstName: Player 1 FirstName: Player 1
player2: player2:
FirstName: Player 2 FirstName: Player 2
Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2 Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2
DataObjectTest_SubTeam: DataObjectTest_SubTeam:
subteam1: subteam1:
Title: Subteam 1 Title: Subteam 1
SubclassDatabaseField: Subclassed 1 SubclassDatabaseField: Subclassed 1
ExtendedDatabaseField: Extended 1 ExtendedDatabaseField: Extended 1
ParentTeam: =>DataObjectTest_Team.team1 ParentTeam: =>DataObjectTest_Team.team1
subteam2_with_player_relation: Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
Title: Subteam 2 EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany1
SubclassDatabaseField: Subclassed 2 subteam2_with_player_relation:
ExtendedHasOneRelationship: =>DataObjectTest_Player.player1 Title: Subteam 2
subteam3_with_empty_fields: SubclassDatabaseField: Subclassed 2
Title: Subteam 3 ExtendedHasOneRelationship: =>DataObjectTest_Player.player1
subteam3_with_empty_fields:
Title: Subteam 3
DataObjectTest_TeamComment: DataObjectTest_TeamComment:
comment1: comment1:
Name: Joe Name: Joe
Comment: This is a team comment by Joe Comment: This is a team comment by Joe
Team: =>DataObjectTest_Team.team1 Team: =>DataObjectTest_Team.team1
comment2: comment2:
Name: Bob Name: Bob
Comment: This is a team comment by Bob Comment: This is a team comment by Bob
Team: =>DataObjectTest_Team.team1 Team: =>DataObjectTest_Team.team1
comment3: comment3:
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: DataObjectTest_Fan:
fan1: fan1:
Name: Damian Name: Damian
Favourite: =>DataObjectTest_Team.team1 Favourite: =>DataObjectTest_Team.team1
fan2: fan2:
Name: Stephen Name: Stephen
Favourite: =>DataObjectTest_Player.player1 Favourite: =>DataObjectTest_Player.player1
SecondFavourite: =>DataObjectTest_Team.team2 SecondFavourite: =>DataObjectTest_Team.team2
fan3: fan3:
Name: Richard Name: Richard
Favourite: =>DataObjectTest_Team.team1 Favourite: =>DataObjectTest_Team.team1
fan4: fan4:
Name: Mitch Name: Mitch
Favourite: =>DataObjectTest_SubTeam.subteam1 Favourite: =>DataObjectTest_SubTeam.subteam1
DataObjectTest_Company: DataObjectTest_Company:
company1: company1:
Name: Company corp Name: Company corp
Owner: =>DataObjectTest_Player.player1 Owner: =>DataObjectTest_Player.player1
company1: company1:
Name: 'Team co.' Name: 'Team co.'
Owner: =>DataObjectTest_Player.player2 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($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()->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'); 'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount, 'There are no orphaned permissions'); $this->assertEquals(DataObject::get('Permission')->Count(), $baseCount, 'There are no orphaned permissions');
@ -62,14 +62,14 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$untouchable->flushCache(); $untouchable->flushCache();
$this->assertEquals($group->Permissions()->Count(), 2, $this->assertEquals($group->Permissions()->Count(), 2,
'The tested group has two permissions permission'); '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'); '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'); 'The tested group has CMS_ACCESS_AssetAdmin permission');
$this->assertEquals($untouchable->Permissions()->Count(), 1, $this->assertEquals($untouchable->Permissions()->Count(), 1,
'The other group has one permission'); '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'); 'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+2, $this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+2,
@ -85,12 +85,12 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$untouchable->flushCache(); $untouchable->flushCache();
$this->assertEquals($group->Permissions()->Count(), 1, $this->assertEquals($group->Permissions()->Count(), 1,
'The tested group has 1 permission'); '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'); 'The tested group has ADMIN permission');
$this->assertEquals($untouchable->Permissions()->Count(), 1, $this->assertEquals($untouchable->Permissions()->Count(), 1,
'The other group has one permission'); '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'); 'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+1, $this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+1,

View File

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

View File

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