ENHANCEMENT: Created HasManyList and ManyManyList objects that represent relations. API CHANGE: Relation methods no longer cache their results. API CHANGE: ComponentSet is deprecated. API CHANGE: DataObject::getManyManyComponentsQuery() no longer exists; just use the ManyManyList itself. API CHANGE: DataObject::getManyManyJoin() no longer exists; just use the ManyManyList itself. API CHANGE: DataObject::getManyManyFilter() no longer exists; just use the ManyManyList itself.

This commit is contained in:
Sam Minnee 2009-11-22 18:29:24 +13:00
parent de1494e3a8
commit 050e0675ce
5 changed files with 296 additions and 457 deletions

View File

@ -0,0 +1,76 @@
<?php
/**
* Subclass of {@link DataList} representing a has_many relation
*/
class HasManyList extends RelationList {
protected $foreignKey;
/**
* Create a new HasManyList object.
* Generation of the appropriate record set is left up to the caller, using the normal
* {@link DataList} methods. Addition arguments are used to support {@@link add()}
* and {@link remove()} methods.
*
* @param $dataClass The class of the DataObjects that this will list.
* @param $relationFilters A map of key => value filters that define which records
* in the $dataClass table actually belong to this relationship.
*/
function __construct($dataClass, $foreignKey) {
parent::__construct($dataClass);
$this->foreignKey = $foreignKey;
}
protected function foreignIDFilter() {
// Apply relation filter
if(is_array($this->foreignID)) {
return "\"$this->foreignKey\" IN ('" .
implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')";
} else if($this->foreignID){
return "\"$this->foreignKey\" = '" .
Convert::raw2sql($this->foreignID) . "'";
}
}
/**
* Adds the item to this relation.
* It does so by setting the relationFilters.
* @param $item The DataObject to be added, or its ID
*/
function add($item) {
if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item);
else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR);
// Validate foreignID
if(!$this->foreignID) {
user_error("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING);
return;
}
if(is_array($this->foreignID)) {
user_error("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING);
return;
}
$fk = $this->foreignKey;
$item->$fk = $this->foreignID;
$item->write();
}
/**
* Remove an item from this relation.
* Doesn't actually remove the item, it just clears the foreign key value.
* @param $item The DataObject to be removed, or its ID
* @todo Maybe we should delete the object instead?
*/
function remove($item) {
if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item);
else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR);
$fk = $this->foreignKey;
$item->$fk = null;
$item->write();
}
}

166
core/model/ManyManyList.php Normal file
View File

@ -0,0 +1,166 @@
<?php
/**
* Subclass of {@link DataList} representing a many_many relation
*/
class ManyManyList extends RelationList {
protected $joinTable;
protected $localKey;
protected $foreignKey, $foreignID;
protected $extraFields;
/**
* Create a new ManyManyList object.
*
* A ManyManyList object represents a list of DataObject records that correspond to a many-many
* relationship. In addition to,
*
*
*
* Generation of the appropriate record set is left up to the caller, using the normal
* {@link DataList} methods. Addition arguments are used to support {@@link add()}
* and {@link remove()} methods.
*
* @param $dataClass The class of the DataObjects that this will list.
* @param $joinTable The name of the table whose entries define the content of this
* many_many relation.
* @param $localKey The key in the join table that maps to the dataClass' PK.
* @param $foreignKey The key in the join table that maps to joined class' PK.
* @param $extraFields A map of field => fieldtype of extra fields on the join table.
*/
function __construct($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) {
parent::__construct($dataClass);
$this->joinTable = $joinTable;
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->extraFields = $extraFields;
$baseClass = ClassInfo::baseDataClass($dataClass);
// Join to the many-many join table
$this->dataQuery->innerJoin($joinTable, "\"$this->localKey\" = \"$baseClass\".\"ID\"");
// Query the extra fields from the join table
if($extraFields) $this->dataQuery->selectFromTable($joinTable, array_keys($extraFields));
}
/**
* Return a filter expression for the foreign ID.
*/
protected function foreignIDFilter() {
// Apply relation filter
if(is_array($this->foreignID)) {
return "\"$this->joinTable\".\"$this->foreignKey\" IN ('" .
implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')";
} else if($this->foreignID){
return "\"$this->joinTable\".\"$this->foreignKey\" = '" .
Convert::raw2sql($this->foreignID) . "'";
}
}
/**
* Add an item to this many_many relationship
* Does so by adding an entry to the joinTable.
* @param $extraFields A map of additional columns to insert into the joinTable
*/
function add($item, $extraFields = null) {
if(is_numeric($item)) $itemID = $item;
else if($item instanceof $this->dataClass) $itemID = $item->ID;
else throw new InvalidArgumentException("ManyManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR);
// Validate foreignID
if(!$this->foreignID) {
throw new Exception("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING);
}
if(is_array($this->foreignID)) {
throw new Exception("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING);
}
// Delete old entries, to prevent duplication
$this->remove($itemID);
// Insert new entry
$manipulation = array();
$manipulation[$this->joinTable]['command'] = 'insert';
if($extraFields) foreach($extraFields as $k => $v) {
$manipulation[$this->joinTable]['fields'][$k] = "'" . Convert::raw2sql($v) . "'";
}
$manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID;
$manipulation[$this->joinTable]['fields'][$this->foreignKey] = $this->foreignID;
DB::manipulate($manipulation);
}
/**
* Remove the given item from this list.
* Note that for a ManyManyList, the item is never actually deleted, only the join table is affected
* @param $item The data object or its ID
*/
function remove($item) {
if(is_numeric($item)) $itemID = $item;
else if($item instanceof $this->dataClass) $itemID = $item->ID;
else user_eror("ManyManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR);
$query = new SQLQuery("*", array($this->joinTable));
$query->delete = true;
if($filter = $this->foreignIDFilter()) {
$query->where($filter);
} else {
user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING);
}
$query->where("\"$this->localKey\" = {$itemID}");
$query->execute();
}
/**
* Remove all items from this many-many join that match the given filter
* @deprecated this is experimental and will change. Don't use it in your projects.
*/
function removeByFilter($filter) {
$query = new SQLQuery("*", array($this->joinTable));
$query->delete = true;
$query->where($filter);
$query->execute();
}
/**
* Find the extra field data for a single row of the relationship
* join table, given the known child ID.
*
* @todo Add tests for this / refactor it / something
*
* @param string $componentName The name of the component
* @param int $childID The ID of the child for the relationship
* @return array Map of fieldName => fieldValue
*/
function getExtraData($componentName, $childID) {
$ownerObj = $this->ownerObj;
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? 'ChildID' : ($this->childClass . 'ID');
$result = array();
if(!isset($componentName)) {
user_error('ComponentSet::getExtraData() passed a NULL component name', E_USER_ERROR);
}
if(!is_numeric($childID)) {
user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR);
}
// @todo Optimize into a single query instead of one per extra field
if($this->extraFields) {
foreach($this->extraFields as $fieldName => $dbFieldSpec) {
$query = DB::query("SELECT \"$fieldName\" FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$childID}");
$value = $query->value();
$result[$fieldName] = $value;
}
}
return $result;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* A DataList that represents a relation.
* Adds the notion of a foreign ID that can be optionally set.
*
* @todo Is this additional class really necessary?
*/
abstract class RelationList extends DataList {
protected $foreignID;
/**
* Set the ID of the record that this ManyManyList is linking *from*.
* @param $id A single ID, or an array of IDs
*/
function setForeignID($id) {
// Turn a 1-element array into a simple value
if(is_array($id) && sizeof($id) == 1) $id = reset($id);
$this->foreignID = $id;
$this->dataQuery->filter($this->foreignIDFilter());
}
/**
* Returns this ManyMany relationship linked to the given foreign ID.
* @param $id An ID or an array of IDs.
*/
function forForeignID($id) {
$this->setForeignID($id);
return $this;
}
abstract protected function foreignIDFilter();
}

View File

@ -1,307 +1,12 @@
<?php
/**
* This is a special kind of DataObjectSet used to represent the items linked to in a 1-many or many-many
* join. It provides add and remove methods that will update the database.
* @package sapphire
* @subpackage model
* @deprecated 2.5 Use ManyManyList or HasManyList
*/
class ComponentSet extends DataObjectSet {
/**
* Type of relationship (eg '1-1', '1-many').
* @var string
*/
protected $type;
/**
* Object that owns this set.
* @var DataObject
*/
protected $ownerObj;
/**
* Class of object that owns this set.
* @var string
*/
protected $ownerClass;
/**
* Table that holds this relationship.
* @var string
*/
protected $tableName;
/**
* Class of child side of the relationship.
* @var string
*/
protected $childClass;
/**
* Field to join on.
* @var string
*/
protected $joinField;
/**
* Set the ComponentSet specific information.
* @param string $type Type of relationship (eg '1-1', '1-many').
* @param DataObject $ownerObj Object that owns this set.
* @param string $ownerClass Class of object that owns this set.
* @param string $tableName Table that holds this relationship.
* @param string $childClass Class of child side of the relationship.
* @param string $joinField Field to join on.
*/
function setComponentInfo($type, $ownerObj, $ownerClass, $tableName, $childClass, $joinField = null) {
$this->type = $type;
$this->ownerObj = $ownerObj;
$this->ownerClass = $ownerClass ? $ownerClass : $ownerObj->class;
$this->tableName = $tableName;
$this->childClass = $childClass;
$this->joinField = $joinField;
}
/**
* Get the ComponentSet specific information
*
* Returns an array on the format array(
* 'type' => <string>,
* 'ownerObj' => <Object>,
* 'ownerClass' => <string>,
* 'tableName' => <string>,
* 'childClass' => <string>,
* 'joinField' => <string>|null );
*
* @return array
*/
public function getComponentInfo() {
return array(
'type' => $this->type,
'ownerObj' => $this->ownerObj,
'ownerClass' => $this->ownerClass,
'tableName' => $this->tableName,
'childClass' => $this->childClass,
'joinField' => $this->joinField
);
}
/**
* Get an array of all the IDs in this component set, where the keys are the same as the
* values.
* @return array
*/
function getIdList() {
$list = array();
foreach($this->items as $item) {
$list[$item->ID] = $item->ID;
}
return $list;
}
/**
* Add an item to this set.
* @param DataObject|int|string $item Item to add, either as a DataObject or as the ID.
* @param array $extraFields A map of extra fields to add.
*/
function add($item, $extraFields = null) {
if(!isset($item)) {
user_error("ComponentSet::add() Not passed an object or ID", E_USER_ERROR);
}
if(is_object($item)) {
if(!is_a($item, $this->childClass)) {
user_error("ComponentSet::add() Tried to add an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR);
}
} else {
if(!$this->childClass) {
user_error("ComponentSet::add() \$this->childClass not set", E_USER_ERROR);
}
$item = DataObject::get_by_id($this->childClass, $item);
if(!$item) return;
}
// If we've already got a database object, then update the database
if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) {
$this->loadChildIntoDatabase($item, $extraFields);
}
// In either case, add something to $this->items
$this->items[] = $item;
}
/**
* Method to save many-many join data into the database for the given $item.
* Used by add() and write().
* @param DataObject|string|int The item to save, as either a DataObject or the ID.
* @param array $extraFields Map of extra fields.
*/
protected function loadChildIntoDatabase($item, $extraFields = null) {
if($this->type == '1-to-many') {
$child = DataObject::get_by_id($this->childClass,$item->ID);
if (!$child) $child = $item;
$joinField = $this->joinField;
$child->$joinField = $this->ownerObj->ID;
$child->write();
} else {
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID');
DB::query( "DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}" );
$extraKeys = $extraValues = '';
if($extraFields) foreach($extraFields as $k => $v) {
$extraKeys .= ", \"$k\"";
$extraValues .= ", '" . DB::getConn()->addslashes($v) . "'";
}
DB::query("INSERT INTO \"$this->tableName\" (\"$parentField\",\"$childField\" $extraKeys) VALUES ({$this->ownerObj->ID}, {$item->ID} $extraValues)");
}
}
/**
* Add a number of items to the component set.
* @param array $items Items to add, as either DataObjects or IDs.
*/
function addMany($items) {
foreach($items as $item) {
$this->add($item);
}
}
/**
* Sets the ComponentSet to be the given ID list.
* Records will be added and deleted as appropriate.
* @param array $idList List of IDs.
*/
function setByIDList($idList) {
$has = array();
// Index current data
if($this->items) foreach($this->items as $item) {
$has[$item->ID] = true;
}
// Keep track of items to delete
$itemsToDelete = $has;
// add items in the list
// $id is the database ID of the record
if($idList) foreach($idList as $id) {
$itemsToDelete[$id] = false;
if($id && !isset($has[$id])) $this->add($id);
}
// delete items not in the list
$removeList = array();
foreach($itemsToDelete as $id => $actuallyDelete) {
if($actuallyDelete) $removeList[] = $id;
}
$this->removeMany($removeList);
}
/**
* Remove an item from this set.
*
* @param DataObject|string|int $item Item to remove, either as a DataObject or as the ID.
*/
function remove($item) {
if(is_object($item)) {
if(!is_a($item, $this->childClass)) {
user_error("ComponentSet::remove() Tried to remove an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR);
}
} else {
$item = DataObject::get_by_id($this->childClass, $item);
}
// Manipulate the database, if it's in there
if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) {
if($this->type == '1-to-many') {
$child = DataObject::get_by_id($this->childClass,$item->ID);
$joinField = $this->joinField;
if($child->$joinField == $this->ownerObj->ID) {
$child->$joinField = null;
$child->write();
}
} else {
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID');
DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}");
}
}
// Manipulate the in-memory array of items
if($this->items) foreach($this->items as $i => $candidateItem) {
if($candidateItem->ID == $item->ID) {
unset($this->items[$i]);
break;
}
}
}
/**
* Remove many items from this set.
* @param array $itemList The items to remove, as a numerical array with IDs or as a DataObjectSet
*/
function removeMany($itemList) {
if(!count($itemList)) return false;
if($this->type == '1-to-many') {
foreach($itemList as $item) $this->remove($item);
} else {
$itemCSV = implode(", ", $itemList);
$parentField = $this->ownerClass . 'ID';
$childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID');
DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" IN ($itemCSV)");
}
user_error("ComponentSet is deprecated; use HasManyList or ManyManyList", E_USER_WARNING);
}
/**
* Remove all items in this set.
*/
function removeAll() {
if(!empty($this->tableName)) {
$parentField = $this->ownerClass . 'ID';
DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID}");
} else {
foreach($this->items as $item) {
$this->remove($item);
}
}
}
/**
* Write this set to the database.
* Called by DataObject::write().
* @param boolean $firstWrite This should be set to true if it the first time the set is being written.
*/
function write($firstWrite = false) {
if($firstWrite) {
foreach($this->items as $item) {
$this->loadChildIntoDatabase($item);
}
}
}
/**
* Returns information about this set in HTML format for debugging.
*
* @return string
*/
function debug() {
$size = count($this->items);
$output = <<<OUT
<h3>ComponentSet</h3>
<ul>
<li>Type: {$this->type}</li>
<li>Size: $size</li>
</ul>
OUT;
return $output;
}
}
?>

View File

@ -1144,7 +1144,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->extend('onAfterSkippedWrite');
}
// Write ComponentSets as necessary
// Write relations as necessary
if($writeComponents) {
$this->writeComponents(true);
}
@ -1307,10 +1307,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
protected $componentCache;
/**
* Returns a one-to-many component, as a ComponentSet.
* The return value will be cached on this object instance,
* but only when no related objects are found (to avoid unnecessary empty checks in the database).
* If related objects exist, no caching is applied.
* 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
@ -1318,36 +1315,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
* @param string|array $limit A limit expression to be inserted into the LIMIT clause
*
* @return ComponentSet 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 = "") {
$result = null;
$sum = md5("{$filter}_{$sort}_{$join}_{$limit}");
if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) {
return $this->componentCache[$componentName . '_' . $sum];
}
if(!$componentClass = $this->has_many($componentName)) {
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
}
$joinField = $this->getRemoteJoinField($componentName, 'has_many');
$result = new HasManyList($componentClass, $joinField);
if($this->ID) $result->setForeignID($this->ID);
if($this->isInDB()) { //Check to see whether we should query the db
$query = $this->getComponentsQuery($componentName, $filter, $sort, $join, $limit);
$result = $this->buildDataObjectSet($query->execute(), 'ComponentSet', $query, $componentClass);
if($result) $result->parseQueryLimit($query);
}
if(!$result) {
// If this record isn't in the database, then we want to hold onto this specific ComponentSet,
// because it's the only copy of the data that we have.
$result = new ComponentSet();
$this->setComponent($componentName . '_' . $sum, $result);
}
$result->setComponentInfo("1-to-many", $this, null, null, $componentClass, $joinField);
$result = $result->filter($filter)->limit($limit)->sort($sort)->join($join);
return $result;
}
@ -1355,11 +1337,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Get the query object for a $has_many Component.
*
* Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
* resultset you're building with this query.
* Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery}
* object, and pass "ComponentSet" as a $containerClass.
*
* @param string $componentName
* @param string $filter
* @param string|array $sort
@ -1419,149 +1396,30 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* and {@link getManyManyComponents()}.
*
* @param string $componentName Name of the component
* @param DataObject|ComponentSet $componentValue Value of the component
* @param DataObject|HasManyList|ManyManyList $componentValue Value of the component
*/
public function setComponent($componentName, $componentValue) {
$this->componentCache[$componentName] = $componentValue;
}
/**
* Returns a many-to-many component, as a ComponentSet.
* The return value will be cached on this object instance,
* but only when no related objects are found (to avoid unnecessary empty checks in the database).
* If related objects exist, no caching is applied.
*
* Returns a many-to-many component, as a ManyManyList.
* @param string $componentName Name of the many-many component
* @return ComponentSet The set of components
* @return ManyManyList The set of components
*
* @todo Implement query-params
*/
public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
$sum = md5("{$filter}_{$sort}_{$join}_{$limit}");
if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) {
return $this->componentCache[$componentName . '_' . $sum];
}
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
// database inconsistencies
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
if($this->ID && is_numeric($this->ID)) {
if($componentClass) {
$query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit);
$records = $query->execute();
$result = $this->buildDataObjectSet($records, "ComponentSet", $query, $componentBaseClass);
if($result) $result->parseQueryLimit($query); // for pagination support
if(!$result) {
$result = new ComponentSet();
}
}
} else {
$result = new ComponentSet();
}
$result->setComponentInfo("many-to-many", $this, $parentClass, $table, $componentClass);
// If this record isn't in the database, then we want to hold onto this specific ComponentSet,
// because it's the only copy of the data that we have.
if(!$this->isInDB()) {
$this->setComponent($componentName . '_' . $sum, $result);
}
return $result;
}
/**
* Get the query object for a $many_many Component.
* Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
* resultset you're building with this query.
* Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery}
* object, and pass "ComponentSet" as a $containerClass.
*
* @param string $componentName
* @param string $filter
* @param string|array $sort
* @param string $join
* @param string|array $limit
* @return SQLQuery
*/
public function getManyManyComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
$componentObj = singleton($componentClass);
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
// database inconsistencies
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
$query = $componentObj->extendedSQL(
"\"$table\".\"$parentField\" = $this->ID", // filter
$sort,
$limit,
"INNER JOIN \"$table\" ON \"$table\".\"$componentField\" = \"$componentBaseClass\".\"ID\"" // join
);
foreach((array)$this->many_many_extraFields($componentName) as $extraField => $extraFieldType) {
$query->select[] = "\"$table\".\"$extraField\"";
$query->groupby[] = "\"$table\".\"$extraField\"";
}
$result = new ManyManyList($componentClass, $table, $componentField, $parentField,
$this->many_many_extraFields($componentName));
if($filter) $query->where[] = $filter;
if($join) $query->from[] = $join;
return $query;
}
/**
* Pull out a join clause for a many-many relationship.
*
* @param string $componentName The many_many or belongs_many_many relation to join to.
* @param string $baseTable The classtable that will already be included in the SQL query to which this join will be added.
* @return string SQL join clause
*/
function getManyManyJoin($componentName, $baseTable) {
if(!$componentClass = $this->many_many($componentName)) {
user_error("DataObject::getComponents(): Unknown many-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
}
$classes = array_reverse(ClassInfo::ancestry($this->class));
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
$baseComponentClass = ClassInfo::baseDataClass($componentClass);
if($baseTable == $parentClass) {
return "LEFT JOIN \"$table\" ON (\"$table\".\"$parentField\" = \"$parentClass\".\"ID\" AND \"$table\".\"$componentField\" = '{$this->ID}')";
} else {
return "LEFT JOIN \"$table\" ON (\"$table\".\"$componentField\" = \"$baseComponentClass\".\"ID\" AND \"$table\".\"$parentField\" = '{$this->ID}')";
}
}
function getManyManyFilter($componentName, $baseTable) {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
return "\"$table\".\"$parentField\" = '{$this->ID}'";
}
/**
* Return an aggregate object. An aggregate object returns the result of running some SQL aggregate function on a field of
* this dataobject type.
*
* It can be called with no arguments, in which case it returns an object that calculates aggregates on this object's type,
* or with an argument (possibly statically), in which case it returns an object for that type
*/
function Aggregate($type = null, $filter = '') {
return new Aggregate($type ? $type : $this->class, $filter);
}
/**
* Return an relationship aggregate object. A relationship aggregate does the same thing as an aggregate object, but operates
* on a has_many rather than directly on the type specified
*/
function RelationshipAggregate($object = null, $relationship = '', $filter = '') {
if (is_string($object)) { $filter = $relationship; $relationship = $object; $object = $this; }
return new Aggregate_Relationship($object ? $object : $this->owner, $relationship, $filter);
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
// foreignID set elsewhere.
if($this->ID) $result->setForeignID($this->ID);
return $result->filter($filter)->sort($sort)->limit($limit);
}
/**
@ -2560,7 +2418,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$object = $component->dbObject($fieldName);
if (!($object instanceof DBField) && !($object instanceof ComponentSet)) {
if (!($object instanceof DBField) && !($object instanceof DataList)) {
// Todo: come up with a broader range of exception objects to describe differnet kinds of errors programatically
throw new Exception("Unable to traverse to related object field [$fieldPath] on [$this->class]");
}