mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-09-28 20:29:15 +02:00
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:
parent
de1494e3a8
commit
050e0675ce
76
core/model/HasManyList.php
Normal file
76
core/model/HasManyList.php
Normal 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
166
core/model/ManyManyList.php
Normal 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;
|
||||
}
|
||||
}
|
34
core/model/RelationList.php
Normal file
34
core/model/RelationList.php
Normal 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();
|
||||
}
|
@ -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;
|
||||
user_error("ComponentSet is deprecated; use HasManyList or ManyManyList", E_USER_WARNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
@ -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');
|
||||
|
||||
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);
|
||||
}
|
||||
$result = new HasManyList($componentClass, $joinField);
|
||||
if($this->ID) $result->setForeignID($this->ID);
|
||||
|
||||
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);
|
||||
$result = new ManyManyList($componentClass, $table, $componentField, $parentField,
|
||||
$this->many_many_extraFields($componentName));
|
||||
|
||||
if($this->ID && is_numeric($this->ID)) {
|
||||
// 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);
|
||||
|
||||
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\"";
|
||||
}
|
||||
|
||||
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);
|
||||
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]");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user