diff --git a/api/JSONDataFormatter.php b/api/JSONDataFormatter.php index 6f13222b0..295d60cee 100644 --- a/api/JSONDataFormatter.php +++ b/api/JSONDataFormatter.php @@ -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 diff --git a/api/XMLDataFormatter.php b/api/XMLDataFormatter.php index d76ec68c0..2cb848639 100644 --- a/api/XMLDataFormatter.php +++ b/api/XMLDataFormatter.php @@ -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 { . "\">\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 .= "\n"; } - foreach($obj->many_many() as $relName => $relClass) { + foreach($obj->manyMany() as $relName => $relClass) { if(!singleton($relClass)->stat('api_access')) continue; // Field filtering diff --git a/dev/BulkLoader.php b/dev/BulkLoader.php index ac0333f08..53651cc34 100644 --- a/dev/BulkLoader.php +++ b/dev/BulkLoader.php @@ -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; diff --git a/dev/CsvBulkLoader.php b/dev/CsvBulkLoader.php index fed49f334..4d6fea561 100644 --- a/dev/CsvBulkLoader.php +++ b/dev/CsvBulkLoader.php @@ -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(); diff --git a/dev/FixtureBlueprint.php b/dev/FixtureBlueprint.php index b1e79a5d9..388c273a4 100644 --- a/dev/FixtureBlueprint.php +++ b/dev/FixtureBlueprint.php @@ -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') { diff --git a/docs/en/02_Developer_Guides/00_Model/02_Relations.md b/docs/en/02_Developer_Guides/00_Model/02_Relations.md index e07ec169b..73d50d9f9 100644 --- a/docs/en/02_Developer_Guides/00_Model/02_Relations.md +++ b/docs/en/02_Developer_Guides/00_Model/02_Relations.md @@ -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 + '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. diff --git a/forms/FileField.php b/forms/FileField.php index 52d425790..b52ef5960 100644 --- a/forms/FileField.php +++ b/forms/FileField.php @@ -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) { diff --git a/forms/FormScaffolder.php b/forms/FormScaffolder.php index 563ad9b15..1fa632315 100644 --- a/forms/FormScaffolder.php +++ b/forms/FormScaffolder.php @@ -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", diff --git a/forms/UploadField.php b/forms/UploadField.php index 3e6dc2ecc..d76da42d8 100644 --- a/forms/UploadField.php +++ b/forms/UploadField.php @@ -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 diff --git a/model/DataDifferencer.php b/model/DataDifferencer.php index f1771f41f..e7c3ed38a 100644 --- a/model/DataDifferencer.php +++ b/model/DataDifferencer.php @@ -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) { diff --git a/model/DataObject.php b/model/DataObject.php index 37d114ffd..f1ffa71a2 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -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( + * , The class that relation is defined in e.g. "Product" + * , The target class of the relation e.g. "Category" + * , The field name pointing to 's table e.g. "ProductID" + * , The field name pointing to 's table e.g. "CategoryID" + * 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); diff --git a/model/DataQuery.php b/model/DataQuery.php index f518badf6..94cbd7d17 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -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); diff --git a/model/fieldtypes/ForeignKey.php b/model/fieldtypes/ForeignKey.php index c5ea889e8..5850abc72 100644 --- a/model/fieldtypes/ForeignKey.php +++ b/model/fieldtypes/ForeignKey.php @@ -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); diff --git a/security/PermissionCheckboxSetField.php b/security/PermissionCheckboxSetField.php index 83403f2cd..236b26543 100644 --- a/security/PermissionCheckboxSetField.php +++ b/security/PermissionCheckboxSetField.php @@ -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 diff --git a/tests/dev/FixtureBlueprintTest.php b/tests/dev/FixtureBlueprintTest.php index 53c9af684..1e14a9b0c 100644 --- a/tests/dev/FixtureBlueprintTest.php +++ b/tests/dev/FixtureBlueprintTest.php @@ -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( diff --git a/tests/dev/FixtureFactoryTest.php b/tests/dev/FixtureFactoryTest.php index fb26133f1..9d7013e2d 100644 --- a/tests/dev/FixtureFactoryTest.php +++ b/tests/dev/FixtureFactoryTest.php @@ -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" ) ); diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 18fbff30a..3ddd195b6 100755 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -20,6 +20,8 @@ class DataListTest extends SapphireTest { 'DataObjectTest_Player', 'DataObjectTest_TeamComment', 'DataObjectTest_ExtendedTeamComment', + 'DataObjectTest_EquipmentCompany', + 'DataObjectTest_SubEquipmentCompany', 'DataObjectTest\NamespacedClass', 'DataObjectTest_Company', 'DataObjectTest_Fan', diff --git a/tests/model/DataObjectLazyLoadingTest.php b/tests/model/DataObjectLazyLoadingTest.php index cc6c8fe33..79db4d151 100644 --- a/tests/model/DataObjectLazyLoadingTest.php +++ b/tests/model/DataObjectLazyLoadingTest.php @@ -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', diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 142200880..4401fe995 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -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', diff --git a/tests/model/DataObjectTest.yml b/tests/model/DataObjectTest.yml index b3e5b156b..f5065efef 100644 --- a/tests/model/DataObjectTest.yml +++ b/tests/model/DataObjectTest.yml @@ -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 diff --git a/tests/security/PermissionCheckboxSetFieldTest.php b/tests/security/PermissionCheckboxSetFieldTest.php index 192bd9488..5d701d122 100644 --- a/tests/security/PermissionCheckboxSetFieldTest.php +++ b/tests/security/PermissionCheckboxSetFieldTest.php @@ -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, diff --git a/tests/testing/YamlFixtureTest.php b/tests/testing/YamlFixtureTest.php index 975897191..beaa93f3c 100644 --- a/tests/testing/YamlFixtureTest.php +++ b/tests/testing/YamlFixtureTest.php @@ -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" ); } diff --git a/tests/testing/YamlFixtureTest.yml b/tests/testing/YamlFixtureTest.yml index b2a60f7b9..63aed8d6b 100644 --- a/tests/testing/YamlFixtureTest.yml +++ b/tests/testing/YamlFixtureTest.yml @@ -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 \ No newline at end of file + ManyManyRelation: =>YamlFixtureTest_DataObjectRelation.relation1 \ No newline at end of file