mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
ENHANCEMENT: Implemented DataList as the successor of DataObjectSet. DataList doesn't execute the query until it's actually needed, allowing for a more flexible ORM.
API CHANGE: augmentSQL is now passed a DataQuery object from which query parameters can be extracted. API CHANGE: DataObjectDecorators that manipulate the query can now define augmentDataQueryCreation(). API CHANGE: The container class argument for DataObject::get() is deprecated. API CHANGE: DataObject::buildSQL() and DataObject::extendedSQL() are deprecated; just use DataObject::get() now. API CHANGE: DataObject::instance_get() and DataObject::instance_get_one() are deprecated, and can no longer be overloaded. API CHANGE: DataObject::buildDataObjectSet() is deprecated. API CHANGE: Cant't call manual manipulation methods on DataList such as insertFirst()
This commit is contained in:
parent
2b991629b8
commit
de1494e3a8
@ -109,7 +109,6 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
||||
);
|
||||
// unset 'inlineadd' permission, we don't want inline addition
|
||||
$memberList->setPermissions(array('edit', 'delete', 'add'));
|
||||
$memberList->setRelationAutoSetting(false);
|
||||
|
||||
$fields = new FieldSet(
|
||||
new TabSet(
|
||||
|
383
core/model/DataList.php
Normal file
383
core/model/DataList.php
Normal file
@ -0,0 +1,383 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implements a "lazy loading" DataObjectSet.
|
||||
* Uses {@link DataQuery} to do the actual query generation.
|
||||
*/
|
||||
class DataList extends DataObjectSet {
|
||||
/**
|
||||
* The DataObject class name that this data list is querying
|
||||
*/
|
||||
protected $dataClass;
|
||||
|
||||
/**
|
||||
* The {@link DataQuery} object responsible for getting this DataObjectSet's records
|
||||
*/
|
||||
protected $dataQuery;
|
||||
|
||||
/**
|
||||
* Synonym of the constructor. Can be chained with literate methods.
|
||||
* DataList::create("SiteTree")->sort("Title") is legal, but
|
||||
* new DataList("SiteTree")->sort("Title") is not.
|
||||
*/
|
||||
static function create($dataClass) {
|
||||
return new DataList($dataClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DataList.
|
||||
* No querying is done on construction, but the initial query schema is set up.
|
||||
* @param $dataClass The DataObject class to query.
|
||||
*/
|
||||
public function __construct($dataClass) {
|
||||
$this->dataClass = $dataClass;
|
||||
$this->dataQuery = new DataQuery($this->dataClass);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function dataClass() {
|
||||
return $this->dataClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this object
|
||||
*/
|
||||
function __clone() {
|
||||
$this->dataQuery = clone $this->dataQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the internal {@link DataQuery} object for direct manipulation
|
||||
*/
|
||||
public function dataQuery() {
|
||||
return $this->dataQuery;
|
||||
}
|
||||
/**
|
||||
* Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-)
|
||||
*/
|
||||
public function sql() {
|
||||
return $this->dataQuery->query()->sql();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter this data list by a WHERE clause
|
||||
* @todo Implement array syntax for this. Perhaps the WHERE clause should be $this->where()?
|
||||
*/
|
||||
public function filter($filter) {
|
||||
$this->dataQuery->filter($filter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sort order of this data list
|
||||
*/
|
||||
public function sort($sort, $direction = "ASC") {
|
||||
if($direction && strtoupper($direction) != 'ASC') $sort = "$sort $direction";
|
||||
$this->dataQuery->sort($sort);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an join clause to this data list's query.
|
||||
*/
|
||||
public function join($join) {
|
||||
$this->dataQuery->join($join);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict the records returned in this query by a limit clause
|
||||
*/
|
||||
public function limit($limit) {
|
||||
$this->dataQuery->limit($limit);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an inner join clause to this data list's query.
|
||||
*/
|
||||
public function innerJoin($table, $onClause, $alias = null) {
|
||||
$this->dataQuery->innerJoin($table, $onClause, $alias);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an left join clause to this data list's query.
|
||||
*/
|
||||
public function leftJoin($table, $onClause, $alias = null) {
|
||||
$this->dataQuery->leftJoin($table, $onClause, $alias);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of the actual items that this DataList contains at this stage.
|
||||
* This is when the query is actually executed.
|
||||
*/
|
||||
protected function generateItems() {
|
||||
$query = $this->dataQuery->query();
|
||||
$this->parseQueryLimit($query);
|
||||
$rows = $query->execute();
|
||||
$results = array();
|
||||
foreach($rows as $row) {
|
||||
$results[] = $this->createDataObject($row);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a data object from the given SQL row
|
||||
*/
|
||||
protected function createDataObject($row) {
|
||||
$defaultClass = $this->dataClass;
|
||||
|
||||
// Failover from RecordClassName to ClassName
|
||||
if(empty($row['RecordClassName'])) $row['RecordClassName'] = $row['ClassName'];
|
||||
|
||||
// Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
|
||||
if(class_exists($row['RecordClassName'])) return new $row['RecordClassName']($row);
|
||||
else return new $defaultClass($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Iterator for this DataObjectSet.
|
||||
* This function allows you to use DataObjectSets in foreach loops
|
||||
* @return DataObjectSet_Iterator
|
||||
*/
|
||||
public function getIterator() {
|
||||
return new DataObjectSet_Iterator($this->generateItems());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this DataList to a DataObjectSet.
|
||||
* Useful if you want to push additional records onto the list.
|
||||
*/
|
||||
public function toDataObjectSet() {
|
||||
$array = array();
|
||||
foreach($this as $item) $array[] = $item;
|
||||
return new DataObjectSet($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of items in this DataList
|
||||
*/
|
||||
function Count() {
|
||||
return $this->dataQuery->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum value of the given field in this DataList
|
||||
*/
|
||||
function Max($field) {
|
||||
return $this->dataQuery->max($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum value of the given field in this DataList
|
||||
*/
|
||||
function Min($field) {
|
||||
return $this->dataQuery->min($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the average value of the given field in this DataList
|
||||
*/
|
||||
function Avg($field) {
|
||||
return $this->dataQuery->avg($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sum of the values of the given field in this DataList
|
||||
*/
|
||||
function Sum($field) {
|
||||
return $this->dataQuery->sum($field);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the first item in this DataList
|
||||
*/
|
||||
function First() {
|
||||
foreach($this->dataQuery->firstRow()->execute() as $row) {
|
||||
return $this->createDataObject($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last item in this DataList
|
||||
*/
|
||||
function Last() {
|
||||
foreach($this->dataQuery->lastRow()->execute() as $row) {
|
||||
return $this->createDataObject($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this DataList has items
|
||||
*/
|
||||
function exists() {
|
||||
return $this->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a sub-range of this dataobjectset as an array
|
||||
*/
|
||||
public function getRange($offset, $length) {
|
||||
return $this->limit(array('start' => $offset, 'limit' => $length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an element of this DataList where the given key = value
|
||||
*/
|
||||
public function find($key, $value) {
|
||||
return $this->filter("\"$key\" = '" . Convert::raw2sql($value) . "'")->First();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter this list to only contain the given IDs
|
||||
*/
|
||||
public function byIDs(array $ids) {
|
||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
||||
$this->filter("\"$baseClass\".\"ID\" IN (" . implode(',', $ids) .")");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single column from this DataList.
|
||||
* @param $colNum The DataObject field to return.
|
||||
*/
|
||||
function column($colName = "ID") {
|
||||
return $this->dataQuery->column($colName);
|
||||
}
|
||||
|
||||
|
||||
// Member altering methods
|
||||
/**
|
||||
* 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
|
||||
foreach($this->column() as $id) {
|
||||
$has[$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) {
|
||||
unset($itemsToDelete[$id]);
|
||||
if($id && !isset($has[$id])) $this->add($id);
|
||||
}
|
||||
|
||||
// Remove any items that haven't been mentioned
|
||||
$this->removeMany(array_keys($itemsToDelete));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with both the keys and values set to the IDs of the records in this list.
|
||||
*/
|
||||
function getIDList() {
|
||||
$ids = $this->column("ID");
|
||||
return $ids ? array_combine($ids, $ids) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a HasManyList or ManyMany list representing the querying of a relation across all
|
||||
* objects in this data list. For it to work, the relation must be defined on the data class
|
||||
* that you used to create this DataList.
|
||||
*
|
||||
* Example: Get members from all Groups:
|
||||
*
|
||||
* DataObject::get("Group")->relation("Members")
|
||||
*/
|
||||
|
||||
function relation($relationName) {
|
||||
$ids = $this->column('ID');
|
||||
return singleton($this->dataClass)->$relationName()->forForeignID($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the items from this list with the given IDs
|
||||
*/
|
||||
function removeMany($idList) {
|
||||
foreach($idList as $id) {
|
||||
$this->remove($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove every element in this DataList matching the given $filter.
|
||||
*/
|
||||
function removeByFilter($filter) {
|
||||
foreach($this->filter($filter) as $item) {
|
||||
$this->remove($item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove every element in this DataList.
|
||||
*/
|
||||
function removeAll() {
|
||||
foreach($this as $item) {
|
||||
$this->remove($item);
|
||||
}
|
||||
}
|
||||
|
||||
// These methods are overloaded by HasManyList and ManyMany list to perform
|
||||
// more sophisticated list manipulation
|
||||
|
||||
function add($item) {
|
||||
// Nothing needs to happen by default
|
||||
// TO DO: If a filter is given to this data list then
|
||||
}
|
||||
|
||||
function remove($item) {
|
||||
// TO DO: Allow for amendment of this behaviour - for exmaple, we can remove an item from
|
||||
// an "ActiveItems" DataList by chaning the status to inactive.
|
||||
|
||||
// By default, we remove an item from a DataList by deleting it.
|
||||
if($item instanceof $this->dataClass) $item->delete();
|
||||
|
||||
}
|
||||
|
||||
// Methods that won't function on DataLists
|
||||
|
||||
function push($item) {
|
||||
user_error("Can't call DataList::push() because its data comes from a specific query.", E_USER_ERROR);
|
||||
}
|
||||
function insertFirst($item) {
|
||||
user_error("Can't call DataList::insertFirst() because its data comes from a specific query.", E_USER_ERROR);
|
||||
}
|
||||
function shift() {
|
||||
user_error("Can't call DataList::shift() because its data comes from a specific query.", E_USER_ERROR);
|
||||
}
|
||||
function replace() {
|
||||
user_error("Can't call DataList::replace() because its data comes from a specific query.", E_USER_ERROR);
|
||||
}
|
||||
function merge() {
|
||||
user_error("Can't call DataList::merge() because its data comes from a specific query.", E_USER_ERROR);
|
||||
}
|
||||
function removeDuplicates() {
|
||||
user_error("Can't call DataList::removeDuplicates() because its data comes from a specific query.", E_USER_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
428
core/model/DataQuery.php
Normal file
428
core/model/DataQuery.php
Normal file
@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* An object representing a query of data from the DataObject's supporting database.
|
||||
* Acts as a wrapper over {@link SQLQuery} and performs all of the query generation.
|
||||
* Used extensively by DataList.
|
||||
*/
|
||||
class DataQuery {
|
||||
protected $dataClass;
|
||||
protected $query;
|
||||
|
||||
protected $collidingFields = array();
|
||||
|
||||
private $queryFinalised = false;
|
||||
|
||||
// TODO: replace subclass_access with this
|
||||
protected $querySubclasses = true;
|
||||
// TODO: replace restrictclasses with this
|
||||
protected $filterByClassName = true;
|
||||
|
||||
/**
|
||||
* Create a new DataQuery.
|
||||
* @param $dataClass The name of the DataObject class that you wish to query
|
||||
*/
|
||||
function __construct($dataClass) {
|
||||
$this->dataClass = $dataClass;
|
||||
$this->initialiseQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this object
|
||||
*/
|
||||
function __clone() {
|
||||
$this->query = clone $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link DataObject} class that is being queried.
|
||||
*/
|
||||
function dataClass() {
|
||||
return $this->dataClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link SQLQuery} object that represents the current query; note that it will
|
||||
* be a clone of the object.
|
||||
*/
|
||||
function query() {
|
||||
return $this->getFinalisedQuery();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove a filter from the query
|
||||
*/
|
||||
function removeFilterOn($fieldExpression) {
|
||||
$matched = false;
|
||||
foreach($this->query->where as $i=>$item) {
|
||||
if(strpos($item, $fieldExpression) !== false) {
|
||||
unset($this->query->where[$i]);
|
||||
$matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$matched) user_error("Couldn't find $fieldExpression in the query filter.", E_USER_WARNING);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the simplest intial query
|
||||
*/
|
||||
function initialiseQuery() {
|
||||
// Get the tables to join to
|
||||
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
||||
|
||||
// Error checking
|
||||
if(!$tableClasses) {
|
||||
if(!ManifestBuilder::has_been_included()) {
|
||||
user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR);
|
||||
} else {
|
||||
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->dataClass. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
$baseClass = array_shift($tableClasses);
|
||||
$select = array("\"$baseClass\".*");
|
||||
|
||||
// Build our intial query
|
||||
$this->query = new SQLQuery(array());
|
||||
$this->query->distinct = true;
|
||||
|
||||
if($sort = singleton($this->dataClass)->stat('default_sort')) {
|
||||
$this->sort($sort);
|
||||
}
|
||||
|
||||
$this->query->from("\"$baseClass\"");
|
||||
$this->selectAllFromTable($this->query, $baseClass);
|
||||
|
||||
singleton($this->dataClass)->extend('augmentDataQueryCreation', $this->query, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the query is ready to execute.
|
||||
*/
|
||||
function getFinalisedQuery() {
|
||||
$query = clone $this->query;
|
||||
|
||||
// Get the tables to join to
|
||||
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
||||
$baseClass = array_shift($tableClasses);
|
||||
|
||||
$collidingFields = array();
|
||||
|
||||
// Join all the tables
|
||||
if($this->querySubclasses) {
|
||||
foreach($tableClasses as $tableClass) {
|
||||
$query->leftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ;
|
||||
$this->selectAllFromTable($query, $tableClass);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve colliding fields
|
||||
if($this->collidingFields) {
|
||||
foreach($this->collidingFields as $k => $collisions) {
|
||||
$caseClauses = array();
|
||||
foreach($collisions as $collision) {
|
||||
if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
|
||||
$collisionBase = $matches[1];
|
||||
$collisionClasses = ClassInfo::subclassesFor($collisionBase);
|
||||
$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ('"
|
||||
. implode("', '", $collisionClasses) . "') THEN $collision";
|
||||
} else {
|
||||
user_error("Bad collision item '$collision'", E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
$query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END"
|
||||
. " AS \"$k\"";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($this->filterByClassName) {
|
||||
// If querying the base class, don't bother filtering on class name
|
||||
if($this->dataClass != $baseClass) {
|
||||
// Get the ClassName values to filter to
|
||||
$classNames = ClassInfo::subclassesFor($this->dataClass);
|
||||
if(!$classNames) user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
|
||||
$query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')";
|
||||
}
|
||||
}
|
||||
|
||||
$query->select[] = "\"$baseClass\".\"ID\"";
|
||||
$query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\"";
|
||||
|
||||
// TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses of DataQuery
|
||||
singleton($this->dataClass)->extend('augmentSQL', $query, $this);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and return the result as {@link Query} object.
|
||||
*/
|
||||
function execute() {
|
||||
return $this->getFinalisedQuery()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this query's SQL
|
||||
*/
|
||||
function sql() {
|
||||
return $this->getFinalisedQuery()->sql();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of records in this query.
|
||||
* Note that this will issue a separate SELECT COUNT() query.
|
||||
*/
|
||||
function count() {
|
||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
||||
return $this->getFinalisedQuery()->count("DISTINCT \"$baseClass\".\"ID\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum value of the given field in this DataList
|
||||
*/
|
||||
function Max($field) {
|
||||
return $this->getFinalisedQuery()->aggregate("MAX(\"$field\")")->execute()->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum value of the given field in this DataList
|
||||
*/
|
||||
function Min($field) {
|
||||
return $this->getFinalisedQuery()->aggregate("MIN(\"$field\")")->execute()->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the average value of the given field in this DataList
|
||||
*/
|
||||
function Avg($field) {
|
||||
return $this->getFinalisedQuery()->aggregate("AVG(\"$field\")")->execute()->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sum of the values of the given field in this DataList
|
||||
*/
|
||||
function Sum($field) {
|
||||
return $this->getFinalisedQuery()->aggregate("SUM(\"$field\")")->execute()->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first row that would be returned by this full DataQuery
|
||||
* Note that this will issue a separate SELECT ... LIMIT 1 query.
|
||||
*/
|
||||
function firstRow() {
|
||||
return $this->getFinalisedQuery()->firstRow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last row that would be returned by this full DataQuery
|
||||
* Note that this will issue a separate SELECT ... LIMIT query.
|
||||
*/
|
||||
function lastRow() {
|
||||
return $this->getFinalisedQuery()->lastRow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the SELECT clause of the query with the columns from the given table
|
||||
*/
|
||||
protected function selectAllFromTable(SQLQuery &$query, $tableClass) {
|
||||
// Add SQL for multi-value fields
|
||||
$databaseFields = DataObject::database_fields($tableClass);
|
||||
$compositeFields = DataObject::composite_fields($tableClass, false);
|
||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||
if(!isset($compositeFields[$k])) {
|
||||
// Update $collidingFields if necessary
|
||||
if(isset($query->select[$k])) {
|
||||
if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]);
|
||||
$this->collidingFields[$k][] = "\"$tableClass\".\"$k\"";
|
||||
|
||||
} else {
|
||||
$query->select[$k] = "\"$tableClass\".\"$k\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
if($compositeFields) foreach($compositeFields as $k => $v) {
|
||||
if($v) {
|
||||
$dbO = Object::create_from_string($v, $k);
|
||||
$dbO->addToQuery($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HAVING clause of this query
|
||||
*/
|
||||
function having($having) {
|
||||
if($having) {
|
||||
$clone = $this;
|
||||
$clone->query->having[] = $having;
|
||||
return $clone;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the WHERE clause of this query
|
||||
*/
|
||||
function filter($filter) {
|
||||
if($filter) {
|
||||
$clone = $this;
|
||||
$clone->query->where($filter);
|
||||
return $clone;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ORDER BY clause of this query
|
||||
*/
|
||||
function sort($sort) {
|
||||
if($sort) {
|
||||
$clone = $this;
|
||||
// Add quoting to sort expression if it's a simple column name
|
||||
if(!is_array($sort) && preg_match('/^[A-Z][A-Z0-9_]*$/i', $sort)) $sort = "\"$sort\"";
|
||||
$clone->query->orderby($sort);
|
||||
return $clone;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the limit of this query
|
||||
*/
|
||||
function limit($limit) {
|
||||
if($limit) {
|
||||
$clone = $this;
|
||||
$clone->query->limit($limit);
|
||||
return $clone;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a join clause to this query
|
||||
* @deprecated Use innerJoin() or leftJoin() instead.
|
||||
*/
|
||||
function join($join) {
|
||||
if($join) {
|
||||
$clone = $this;
|
||||
$clone->query->from[] = $join;
|
||||
// TODO: This needs to be resolved for all databases
|
||||
if(DB::getConn() instanceof MySQLDatabase) $clone->query->groupby[] = reset($clone->query->from) . ".\"ID\"";
|
||||
return $clone;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an INNER JOIN clause to this queyr
|
||||
* @param $table The table to join to.
|
||||
* @param $onClause The filter for the join.
|
||||
*/
|
||||
public function innerJoin($table, $onClause, $alias = null) {
|
||||
if($table) {
|
||||
$clone = $this;
|
||||
$clone->query->innerJoin($table, $onClause, $alias);
|
||||
return $clone;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT JOIN clause to this queyr
|
||||
* @param $table The table to join to.
|
||||
* @param $onClause The filter for the join.
|
||||
*/
|
||||
public function leftJoin($table, $onClause, $alias = null) {
|
||||
if($table) {
|
||||
$clone = $this;
|
||||
$clone->query->leftJoin($table, $onClause, $alias);
|
||||
return $clone;
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the given fields from the given table
|
||||
*/
|
||||
public function selectFromTable($table, $fields) {
|
||||
$fieldExpressions = array_map(create_function('$item',
|
||||
"return '\"$table\".\"' . \$item . '\"';"), $fields);
|
||||
|
||||
$this->select($fieldExpressions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the given field column from the database and return as an array.
|
||||
*/
|
||||
public function column($field = 'ID') {
|
||||
$query = $this->getFinalisedQuery();
|
||||
$query->select($this->expressionForField($field, $query));
|
||||
return $query->execute()->column();
|
||||
}
|
||||
|
||||
protected function expressionForField($field, $query) {
|
||||
// Special case for ID
|
||||
if($field == 'ID') {
|
||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
||||
return "\"$baseClass\".\"ID\"";
|
||||
|
||||
} else {
|
||||
return $query->expressionForField($field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the selected fields to start over
|
||||
*/
|
||||
public function clearSelect() {
|
||||
$this->query->select = array();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the given field expressions. You must do your own escaping
|
||||
*/
|
||||
protected function select($fieldExpressions) {
|
||||
$this->query->select = array_merge($this->query->select, $fieldExpressions);
|
||||
}
|
||||
|
||||
//// QUERY PARAMS
|
||||
|
||||
/**
|
||||
* An arbitrary store of query parameters that can be used by decorators.
|
||||
* @todo This will probably be made obsolete if we have subclasses of DataList and/or DataQuery.
|
||||
*/
|
||||
private $queryParams;
|
||||
|
||||
/**
|
||||
* Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
|
||||
* It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'.
|
||||
*/
|
||||
function setQueryParam($key, $value) {
|
||||
$this->queryParams[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query.
|
||||
*/
|
||||
function getQueryParam($key) {
|
||||
if(isset($this->queryParams[$key])) return $this->queryParams[$key];
|
||||
else return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
@ -140,15 +140,7 @@ abstract class BulkLoader extends ViewableData {
|
||||
|
||||
//get all instances of the to be imported data object
|
||||
if($this->deleteExistingRecords) {
|
||||
$q = singleton($this->objectClass)->buildSQL();
|
||||
$q->select = array('"ID"');
|
||||
$ids = $q->execute()->column('ID');
|
||||
foreach($ids as $id) {
|
||||
$obj = DataObject::get_by_id($this->objectClass, $id);
|
||||
$obj->delete();
|
||||
$obj->destroy();
|
||||
unset($obj);
|
||||
}
|
||||
DataObject::get($this->objectClass)->removeAll();
|
||||
}
|
||||
|
||||
return $this->processAll($filepath);
|
||||
|
@ -164,8 +164,7 @@ class CheckboxSetField extends OptionsetField {
|
||||
// If we're not passed a value directly, we can look for it in a relation method on the object passed as a second arg
|
||||
if(!$value && $obj && $obj instanceof DataObject && $obj->hasMethod($this->name)) {
|
||||
$funcName = $this->name;
|
||||
$selected = $obj->$funcName();
|
||||
$value = $selected->toDropdownMap('ID', 'ID');
|
||||
$value = $obj->$funcName()->getIDList();
|
||||
}
|
||||
|
||||
parent::setValue($value, $obj);
|
||||
|
@ -142,8 +142,7 @@ class HtmlEditorField extends TextareaField {
|
||||
// Save file & link tracking data.
|
||||
if(class_exists('SiteTree')) {
|
||||
if($record->ID && $record->many_many('LinkTracking') && $tracker = $record->LinkTracking()) {
|
||||
$filter = sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID);
|
||||
DB::query("DELETE FROM \"$tracker->tableName\" WHERE $filter");
|
||||
$tracker->removeByFilter(sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID));
|
||||
|
||||
if($linkedPages) foreach($linkedPages as $item) {
|
||||
$SQL_fieldName = Convert::raw2sql($this->name);
|
||||
@ -151,10 +150,9 @@ class HtmlEditorField extends TextareaField {
|
||||
VALUES ($record->ID, $item, '$SQL_fieldName')");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($record->ID && $record->many_many('ImageTracking') && $tracker = $record->ImageTracking()) {
|
||||
$filter = sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID);
|
||||
DB::query("DELETE FROM \"$tracker->tableName\" WHERE $filter");
|
||||
$tracker->removeByFilter(sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID));
|
||||
|
||||
$fieldName = $this->name;
|
||||
if($linkedFiles) foreach($linkedFiles as $item) {
|
||||
|
@ -496,12 +496,19 @@ JS
|
||||
$query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin);
|
||||
}
|
||||
|
||||
if(!empty($_REQUEST['ctf'][$this->Name()]['sort'])) {
|
||||
$column = $_REQUEST['ctf'][$this->Name()]['sort'];
|
||||
$dir = 'ASC';
|
||||
if(!empty($_REQUEST['ctf'][$this->Name()]['dir'])) {
|
||||
$dir = $_REQUEST['ctf'][$this->Name()]['dir'];
|
||||
if(strtoupper(trim($dir)) == 'DESC') $dir = 'DESC';
|
||||
if(!$this->dataList) {
|
||||
user_error(get_class($this). ' is missing a DataList', E_USER_ERROR);
|
||||
}
|
||||
|
||||
$dl = clone $this->dataList;
|
||||
|
||||
if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
|
||||
$query = $this->dataList->dataQuery()->query();
|
||||
$SQL_sort = Convert::raw2sql($_REQUEST['ctf'][$this->Name()]['sort']);
|
||||
$sql = $query->sql();
|
||||
// see {isFieldSortable}
|
||||
if(in_array($SQL_sort,$query->select) || stripos($sql,"AS {$SQL_sort}")) {
|
||||
$dl->sort($SQL_sort);
|
||||
}
|
||||
if($query->canSortBy($column)) $query->orderby = $column.' '.$dir;
|
||||
}
|
||||
@ -1210,17 +1217,6 @@ JS
|
||||
return $this->Link();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Int
|
||||
*/
|
||||
function sourceID() {
|
||||
$idField = $this->form->dataFieldByName('ID');
|
||||
if(!isset($idField)) {
|
||||
user_error("TableListField needs a formfield named 'ID' to be present", E_USER_ERROR);
|
||||
}
|
||||
return $idField->Value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to determine permissions for a scaffolded
|
||||
* TableListField (or subclasses) - currently used in {@link ModelAdmin} and {@link DataObject->scaffoldFormFields()}.
|
||||
|
@ -444,7 +444,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
private function duplicateRelations($sourceObject, $destinationObject, $name) {
|
||||
$relations = $sourceObject->$name();
|
||||
if ($relations) {
|
||||
if ($relations instanceOf ComponentSet) { //many-to-something relation
|
||||
if ($relations instanceOf RelationList) { //many-to-something relation
|
||||
if ($relations->Count() > 0) { //with more than one thing it is related to
|
||||
foreach($relations as $relation) {
|
||||
$destinationObject->$name()->add($relation);
|
||||
@ -1192,18 +1192,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
user_error("$this->class has a broken onBeforeDelete() function. Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
|
||||
}
|
||||
|
||||
// Deleting a record without an ID shouldn't do anything
|
||||
if(!$this->ID) throw new Exception("DataObject::delete() called on a DataObject without an ID");
|
||||
|
||||
foreach($this->getClassAncestry() as $ancestor) {
|
||||
if(self::has_own_table($ancestor)) {
|
||||
$sql = new SQLQuery();
|
||||
$sql->delete = true;
|
||||
$sql->from[$ancestor] = "\"$ancestor\"";
|
||||
$sql->where[] = "\"ID\" = $this->ID";
|
||||
$this->extend('augmentSQL', $sql);
|
||||
$sql->execute();
|
||||
}
|
||||
// Deleting a record without an ID shouldn't do anything
|
||||
if(!$this->ID) throw new Exception("DataObject::delete() called on a DataObject without an ID");
|
||||
|
||||
// TODO: This is quite ugly. To improve:
|
||||
// - move the details of the delete code in the DataQuery system
|
||||
// - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
|
||||
// obviously, that means getting requireTable() to configure cascading deletes ;-)
|
||||
$srcQuery = DataList::create($this->class)->filter("ID = $this->ID")->dataQuery()->query();
|
||||
foreach($srcQuery->queriedTables() as $table) {
|
||||
$query = new SQLQuery("*", array('"'.$table.'"'));
|
||||
$query->where("\"ID\" = $this->ID");
|
||||
$query->delete = true;
|
||||
$query->execute();
|
||||
}
|
||||
// Remove this item out of any caches
|
||||
$this->flushCache();
|
||||
@ -2590,162 +2591,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link SQLQuery} object to perform the given query.
|
||||
*
|
||||
* @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, self::$default_sort will be used.
|
||||
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
|
||||
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
|
||||
* @param boolean $restictClasses Restrict results to only objects of either this class of a subclass of this class
|
||||
* @param string $having A filter to be inserted into the HAVING clause.
|
||||
*
|
||||
* @return SQLQuery Query built.
|
||||
* @deprecated 2.5 Use DataObject::get() instead, with the new data mapper there's no reason not to.
|
||||
*/
|
||||
public function buildSQL($filter = "", $sort = "", $limit = "", $join = "", $restrictClasses = true, $having = "") {
|
||||
// Cache the big hairy part of buildSQL
|
||||
if(!isset(self::$cache_buildSQL_query[$this->class])) {
|
||||
// Get the tables to join to
|
||||
$tableClasses = ClassInfo::dataClassesFor($this->class);
|
||||
if(!$tableClasses) {
|
||||
if (!DB::getConn()) {
|
||||
throw new Exception('DataObjects have been requested before'
|
||||
. ' a DB connection has been made. Please ensure you'
|
||||
. ' are not querying the database in _config.php.');
|
||||
} else {
|
||||
user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
user_error("DataObject::buildSQL() deprecated; just use DataObject::get() with the new data mapper", E_USER_NOTICE);
|
||||
return $this->extendedSQL($filter, $sort, $limit, $join, $having);
|
||||
|
||||
$baseClass = array_shift($tableClasses);
|
||||
|
||||
|
||||
// $collidingFields will keep a list fields that appear in mulitple places in the class
|
||||
// heirarchy for this table. They will be dealt with more explicitly in the SQL query
|
||||
// to ensure that junk data from other tables doesn't corrupt data objects
|
||||
$collidingFields = array();
|
||||
|
||||
// Build our intial query
|
||||
$query = new SQLQuery(array());
|
||||
$query->from("\"$baseClass\"");
|
||||
|
||||
// Add SQL for multi-value fields on the base table
|
||||
$databaseFields = self::database_fields($baseClass);
|
||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||
if(!in_array($k, array('ClassName', 'LastEdited', 'Created')) && ClassInfo::classImplements($v, 'CompositeDBField')) {
|
||||
$this->dbObject($k)->addToQuery($query);
|
||||
} else {
|
||||
$query->select[$k] = "\"$baseClass\".\"$k\"";
|
||||
}
|
||||
}
|
||||
// Join all the tables
|
||||
if($tableClasses && self::$subclass_access) {
|
||||
foreach($tableClasses as $tableClass) {
|
||||
$query->from[$tableClass] = "LEFT JOIN \"$tableClass\" ON \"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"";
|
||||
|
||||
// Add SQL for multi-value fields
|
||||
$databaseFields = self::database_fields($tableClass);
|
||||
$compositeFields = self::composite_fields($tableClass, false);
|
||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||
if(!isset($compositeFields[$k])) {
|
||||
// Update $collidingFields if necessary
|
||||
if(isset($query->select[$k])) {
|
||||
if(!isset($collidingFields[$k])) $collidingFields[$k] = array($query->select[$k]);
|
||||
$collidingFields[$k][] = "\"$tableClass\".\"$k\"";
|
||||
|
||||
} else {
|
||||
$query->select[$k] = "\"$tableClass\".\"$k\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
if($compositeFields) foreach($compositeFields as $k => $v) {
|
||||
$dbO = $this->dbObject($k);
|
||||
if($dbO) $dbO->addToQuery($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve colliding fields
|
||||
if($collidingFields) {
|
||||
foreach($collidingFields as $k => $collisions) {
|
||||
$caseClauses = array();
|
||||
foreach($collisions as $collision) {
|
||||
if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
|
||||
$collisionBase = $matches[1];
|
||||
$collisionClasses = ClassInfo::subclassesFor($collisionBase);
|
||||
$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ('"
|
||||
. implode("', '", $collisionClasses) . "') THEN $collision";
|
||||
} else {
|
||||
user_error("Bad collision item '$collision'", E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
$query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END"
|
||||
. " AS \"$k\"";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$query->select[] = "\"$baseClass\".\"ID\"";
|
||||
$query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\"";
|
||||
|
||||
// Get the ClassName values to filter to
|
||||
$classNames = ClassInfo::subclassesFor($this->class);
|
||||
|
||||
if(!$classNames) {
|
||||
user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
|
||||
}
|
||||
|
||||
// If querying the base class, don't bother filtering on class name
|
||||
if($restrictClasses && $this->class != $baseClass) {
|
||||
// Get the ClassName values to filter to
|
||||
$classNames = ClassInfo::subclassesFor($this->class);
|
||||
if(!$classNames) {
|
||||
user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
|
||||
}
|
||||
|
||||
$query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')";
|
||||
}
|
||||
self::$cache_buildSQL_query[$this->class] = clone $query;
|
||||
} else {
|
||||
$query = clone self::$cache_buildSQL_query[$this->class];
|
||||
|
||||
}
|
||||
|
||||
// Find a default sort
|
||||
if(!$sort) {
|
||||
$sort = $this->stat('default_sort');
|
||||
}
|
||||
// Add quoting to sort expression if it's a simple column name
|
||||
if(preg_match('/^[A-Z][A-Z0-9_]*$/i', $sort)) $sort = "\"$sort\"";
|
||||
|
||||
$query->where($filter);
|
||||
$query->orderby($sort);
|
||||
$query->limit($limit);
|
||||
|
||||
|
||||
if($having) {
|
||||
$query->having[] = $having;
|
||||
}
|
||||
|
||||
if($join) {
|
||||
$query->from[] = $join;
|
||||
// In order to group by unique columns we have to group by everything listed in the select
|
||||
foreach($query->select as $field) {
|
||||
// Skip the _SortColumns; these are only going to be aggregate functions
|
||||
if(preg_match('/AS\s+\"?_SortColumn/', $field, $matches)) {
|
||||
|
||||
// Identify columns with aliases, and ignore the alias. Making use of the alias in
|
||||
// group by was causing problems when those queries were subsequently passed into
|
||||
// SQLQuery::unlimitedRowCount.
|
||||
} else if(preg_match('/^(.*)\s+AS\s+(\"[^"]+\")\s*$/', $field, $matches)) {
|
||||
$query->groupby[] = $matches[1];
|
||||
// Otherwise just use the field as is
|
||||
} else {
|
||||
$query->groupby[] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2754,21 +2605,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
private static $cache_buildSQL_query;
|
||||
|
||||
/**
|
||||
* Like {@link buildSQL}, but applies the extension modifications.
|
||||
*
|
||||
* @uses DataExtension->augmentSQL()
|
||||
*
|
||||
* @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, self::$default_sort will be used.
|
||||
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
|
||||
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
|
||||
* @param string $having A filter to be inserted into the HAVING clause.
|
||||
* @return SQLQuery Query built
|
||||
* @deprecated 2.5 Use DataObject::get() instead, with the new data mapper there's no reason not to.
|
||||
*/
|
||||
public function extendedSQL($filter = "", $sort = "", $limit = "", $join = "", $having = ""){
|
||||
$query = $this->buildSQL($filter, $sort, $limit, $join, true, $having);
|
||||
$this->extend('augmentSQL', $query);
|
||||
return $query;
|
||||
public function extendedSQL($filter = "", $sort = "", $limit = "", $join = ""){
|
||||
$dataList = DataObject::get($this->class, $filter, $sort, $join, $limit);
|
||||
return $dataList->dataQuery()->query();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2784,14 +2625,36 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*
|
||||
* @return mixed The objects matching the filter, in the class specified by $containerClass
|
||||
*/
|
||||
public static function get($callerClass, $filter = "", $sort = "", $join = "", $limit = "", $containerClass = "DataObjectSet") {
|
||||
return singleton($callerClass)->instance_get($filter, $sort, $join, $limit, $containerClass);
|
||||
public static function get($callerClass, $filter = "", $sort = "", $join = "", $limit = "", $containerClass = "DataList") {
|
||||
// Deprecated 2.5?
|
||||
// Todo: Make the $containerClass method redundant
|
||||
if($containerClass != "DataList") user_error("The DataObject::get() \$containerClass argument has been deprecated", E_USER_NOTICE);
|
||||
$result = DataList::create($callerClass)->filter($filter)->sort($sort)->join($join)->limit($limit);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function Aggregate($class = null) {
|
||||
if($class) return new DataList($class);
|
||||
else if(isset($this)) return new DataList(get_class($this));
|
||||
else throw new InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed a classname");
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function RelationshipAggregate($relationship) {
|
||||
return $this->$relationship();
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal function that actually performs the querying for get().
|
||||
* DataObject::get("Table","filter") is the same as singleton("Table")->instance_get("filter")
|
||||
*
|
||||
* @deprecated 2.5 Use DataObject::get()
|
||||
*
|
||||
* @param string $filter A filter to be inserted into the WHERE clause.
|
||||
* @param string $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
|
||||
* @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
|
||||
@ -2801,22 +2664,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return mixed The objects matching the filter, in the class specified by $containerClass
|
||||
*/
|
||||
public function instance_get($filter = "", $sort = "", $join = "", $limit="", $containerClass = "DataObjectSet") {
|
||||
if(!DB::isActive()) {
|
||||
user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
|
||||
}
|
||||
|
||||
$query = $this->extendedSQL($filter, $sort, $limit, $join);
|
||||
|
||||
$records = $query->execute();
|
||||
|
||||
$ret = $this->buildDataObjectSet($records, $containerClass, $query, $this->class);
|
||||
if($ret) $ret->parseQueryLimit($query);
|
||||
user_error("instance_get deprecated", E_USER_NOTICE);
|
||||
return self::get($this->class, $filter, $sort, $join, $limit, $containerClass);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a database {@link SS_Query} and instanciate an object for each record.
|
||||
*
|
||||
* @deprecated 2.5 Use DataObject::get(), you don't need to side-step it any more
|
||||
*
|
||||
* @param SS_Query|array $records The database records, a {@link SS_Query} object or an array of maps.
|
||||
* @param string $containerClass The class to place all of the objects into.
|
||||
@ -2824,6 +2680,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return mixed The new objects in an object of type $containerClass
|
||||
*/
|
||||
function buildDataObjectSet($records, $containerClass = "DataObjectSet", $query = null, $baseClass = null) {
|
||||
user_error('buildDataObjectSet is deprecated; use DataList to do your querying', E_USER_NOTICE);
|
||||
|
||||
foreach($records as $record) {
|
||||
if(empty($record['RecordClassName'])) {
|
||||
$record['RecordClassName'] = $record['ClassName'];
|
||||
@ -2879,7 +2737,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
] = false;
|
||||
}
|
||||
if(!$cache || !isset(DataObject::$cache_get_one[$callerClass][$cacheKey])) {
|
||||
$item = $SNG->instance_get_one($filter, $orderby);
|
||||
$dl = DataList::create($callerClass)->filter($filter)->sort($orderby);
|
||||
$item = $dl->First();
|
||||
|
||||
if($cache) {
|
||||
DataObject::$cache_get_one[$callerClass][$cacheKey] = $item;
|
||||
if(!DataObject::$cache_get_one[$callerClass][$cacheKey]) {
|
||||
@ -2935,6 +2795,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Does the hard work for get_one()
|
||||
*
|
||||
* @deprecated 2.5 Use DataObject::get_one() instead
|
||||
*
|
||||
* @uses DataExtension->augmentSQL()
|
||||
*
|
||||
@ -2943,35 +2805,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return DataObject The first item matching the query
|
||||
*/
|
||||
public function instance_get_one($filter, $orderby = null) {
|
||||
if(!DB::isActive()) {
|
||||
user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
|
||||
}
|
||||
|
||||
$query = $this->buildSQL($filter);
|
||||
$query->limit = "1";
|
||||
if($orderby) {
|
||||
$query->orderby = $orderby;
|
||||
}
|
||||
|
||||
$this->extend('augmentSQL', $query);
|
||||
|
||||
$records = $query->execute();
|
||||
$records->rewind();
|
||||
$record = $records->current();
|
||||
|
||||
if($record) {
|
||||
// Mid-upgrade, the database can have invalid RecordClassName values that need to be guarded against.
|
||||
if(class_exists($record['RecordClassName'])) {
|
||||
$record = new $record['RecordClassName']($record);
|
||||
} else {
|
||||
$record = new $this->class($record);
|
||||
}
|
||||
|
||||
// Rather than restrict classes at the SQL-query level, we now check once the object has been instantiated
|
||||
// This lets us check up on weird errors where the class has been incorrectly set, and give warnings to our
|
||||
// developers
|
||||
return $record;
|
||||
}
|
||||
user_error("DataObjct::instance_get_one is deprecated", E_USER_NOTICE);
|
||||
return DataObject::get_one($this->class, $filter, true, $orderby);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,7 +126,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
* Destory all of the DataObjects in this set.
|
||||
*/
|
||||
public function destroy() {
|
||||
foreach($this->items as $item) {
|
||||
foreach($this as $item) {
|
||||
$item->destroy();
|
||||
}
|
||||
}
|
||||
@ -144,14 +144,10 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($index = null) {
|
||||
if(!$index) {
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
$map = array();
|
||||
|
||||
foreach($this->items as $item) {
|
||||
$map[$item->$index] = $item;
|
||||
foreach($this as $item) {
|
||||
if($index) $map[$item->$index] = $item;
|
||||
else $map[] = $item;
|
||||
}
|
||||
|
||||
return $map;
|
||||
@ -169,7 +165,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
|
||||
$map = array();
|
||||
|
||||
foreach( $this->items as $item ) {
|
||||
foreach( $this as $item ) {
|
||||
$map[$item->$index] = $item->getAllFields();
|
||||
}
|
||||
|
||||
@ -584,7 +580,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists() {
|
||||
return (bool)$this->items;
|
||||
return $this->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -616,7 +612,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
* @return int
|
||||
*/
|
||||
public function TotalItems() {
|
||||
return $this->totalSize ? $this->totalSize : sizeof($this->items);
|
||||
return $this->totalSize ? $this->totalSize : $this->Count();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -632,9 +628,9 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
* @return string
|
||||
*/
|
||||
public function UL() {
|
||||
if($this->items) {
|
||||
if($this->exists()) {
|
||||
$result = "<ul id=\"Menu1\">\n";
|
||||
foreach($this->items as $item) {
|
||||
foreach($this as $item) {
|
||||
$result .= "<li onclick=\"location.href = this.getElementsByTagName('a')[0].href\"><a href=\"$item->Link\">$item->Title</a></li>\n";
|
||||
}
|
||||
$result .= "</ul>\n";
|
||||
@ -662,29 +658,27 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
*/
|
||||
public function map($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) {
|
||||
$map = array();
|
||||
|
||||
if($this->items) {
|
||||
foreach($this->items as $item) {
|
||||
$map[$item->$index] = ($item->hasMethod($titleField)) ? $item->$titleField() : $item->$titleField;
|
||||
}
|
||||
foreach($this as $item) {
|
||||
$map[$item->$index] = ($item->hasMethod($titleField))
|
||||
? $item->$titleField() : $item->$titleField;
|
||||
}
|
||||
if($emptyString) $map = array('' => $emptyString) + $map;
|
||||
|
||||
if($emptyString) $map = array('' => "$emptyString") + $map;
|
||||
if($sort) asort($map);
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an item in this list where the field $key is equal to $value
|
||||
* Eg: $doSet->find('ID', 4);
|
||||
* @return ViewableData The first matching item.
|
||||
*/
|
||||
public function find($key, $value) {
|
||||
foreach($this->items as $item) {
|
||||
if($item->$key == $value) return $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an item in this list where the field $key is equal to $value
|
||||
* Eg: $doSet->find('ID', 4);
|
||||
* @return ViewableData The first matching item.
|
||||
*/
|
||||
public function find($key, $value) {
|
||||
foreach($this as $item) {
|
||||
if($item->$key == $value) return $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a column of the given field
|
||||
@ -693,7 +687,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
*/
|
||||
public function column($value = "ID") {
|
||||
$list = array();
|
||||
foreach($this->items as $item ){
|
||||
foreach($this as $item ){
|
||||
$list[] = ($item->hasMethod($value)) ? $item->$value() : $item->$value;
|
||||
}
|
||||
return $list;
|
||||
@ -705,11 +699,9 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
* @param string $index The field name to index the array by.
|
||||
* @return array
|
||||
*/
|
||||
public function groupBy($index) {
|
||||
$result = array();
|
||||
foreach($this->items as $item) {
|
||||
public function groupBy($index){
|
||||
foreach($this as $item ){
|
||||
$key = ($item->hasMethod($index)) ? $item->$index() : $item->$index;
|
||||
|
||||
if(!isset($result[$key])) {
|
||||
$result[$key] = new DataObjectSet();
|
||||
}
|
||||
@ -898,7 +890,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
|
||||
// Put this item into the array indexed by $groupField.
|
||||
// the keys are later used to retrieve the top-level records
|
||||
foreach( $this->items as $item ) {
|
||||
foreach( $this as $item ) {
|
||||
$groupedSet[$item->$groupField][] = $item;
|
||||
}
|
||||
|
||||
@ -994,7 +986,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
*/
|
||||
function containsIDs($idList) {
|
||||
foreach($idList as $item) $wants[$item] = true;
|
||||
foreach($this->items as $item) if($item) unset($wants[$item->ID]);
|
||||
foreach($this as $item) if($item) unset($wants[$item->ID]);
|
||||
return !$wants;
|
||||
}
|
||||
|
||||
@ -1004,7 +996,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable
|
||||
* @param $idList An array of object IDs
|
||||
*/
|
||||
function onlyContainsIDs($idList) {
|
||||
return $this->containsIDs($idList) && sizeof($idList) == sizeof($this->items);
|
||||
return $this->containsIDs($idList) && sizeof($idList) == $this->count();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -484,11 +484,9 @@ class Hierarchy extends DataExtension {
|
||||
*/
|
||||
public function numHistoricalChildren() {
|
||||
if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
|
||||
|
||||
$query = Versioned::get_including_deleted_query(ClassInfo::baseDataClass($this->owner->class),
|
||||
"\"ParentID\" = " . (int)$this->owner->ID);
|
||||
|
||||
return $query->unlimitedRowCount();
|
||||
|
||||
return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class),
|
||||
"\"ParentID\" = " . (int)$this->owner->ID)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -500,20 +498,11 @@ class Hierarchy extends DataExtension {
|
||||
* @return int
|
||||
*/
|
||||
public function numChildren($cache = true) {
|
||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
||||
|
||||
// Build the cache for this class if it doesn't exist.
|
||||
if(!$cache || !is_numeric($this->_cache_numChildren)) {
|
||||
// We build the query in an extension-friendly way.
|
||||
$query = new SQLQuery(
|
||||
"COUNT(*)",
|
||||
"\"$baseClass\"",
|
||||
sprintf('"ParentID" = %d', $this->owner->ID)
|
||||
);
|
||||
$this->owner->extend('augmentSQL', $query);
|
||||
$this->owner->extend('augmentNumChildrenCountQuery', $query);
|
||||
|
||||
$this->_cache_numChildren = (int)$query->execute()->value();
|
||||
// Hey, this is efficient now!
|
||||
// We call stageChildren(), because Children() has canView() filtering
|
||||
$this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count();
|
||||
}
|
||||
|
||||
// If theres no value in the cache, it just means that it doesn't have any children.
|
||||
@ -540,7 +529,6 @@ class Hierarchy extends DataExtension {
|
||||
. (int)$this->owner->ID . " AND \"{$baseClass}\".\"ID\" != " . (int)$this->owner->ID
|
||||
. $extraFilter, "");
|
||||
|
||||
if(!$staged) $staged = new DataObjectSet();
|
||||
$this->owner->extend("augmentStageChildren", $staged, $showAll);
|
||||
return $staged;
|
||||
}
|
||||
|
@ -127,6 +127,31 @@ class SQLQuery {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add addition columns to the select clause
|
||||
*/
|
||||
public function selectMore($fields) {
|
||||
if (func_num_args() > 1) $fields = func_get_args();
|
||||
if(is_array($fields)) {
|
||||
foreach($fields as $field) $this->select[] = $field;
|
||||
} else {
|
||||
$this->select[] = $fields;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SQL expression for the given field
|
||||
* @todo This should be refactored after $this->select is changed to make that easier
|
||||
*/
|
||||
public function expressionForField($field) {
|
||||
foreach($this->select as $sel) {
|
||||
if(preg_match('/AS +"?([^"]*)"?/i', $sel, $matches)) $selField = $matches[1];
|
||||
else if(preg_match('/"([^"]*)"\."([^"]*)"/', $sel, $matches)) $selField = $matches[2];
|
||||
else if(preg_match('/"?([^"]*)"?/', $sel, $matches)) $selField = $matches[2];
|
||||
if($selField == $field) return $sel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the target table to select from.
|
||||
*
|
||||
@ -156,7 +181,7 @@ class SQLQuery {
|
||||
if( !$tableAlias ) {
|
||||
$tableAlias = $table;
|
||||
}
|
||||
$this->from[$tableAlias] = "LEFT JOIN \"$table\" AS \"$tableAlias\" ON $onPredicate";
|
||||
$this->from[$tableAlias] = array('type' => 'LEFT', 'table' => $table, 'filter' => array($onPredicate));
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -173,10 +198,25 @@ class SQLQuery {
|
||||
if( !$tableAlias ) {
|
||||
$tableAlias = $table;
|
||||
}
|
||||
$this->from[$tableAlias] = "INNER JOIN \"$table\" AS \"$tableAlias\" ON $onPredicate";
|
||||
$this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an additional filter (part of the ON clause) on a join
|
||||
*/
|
||||
public function addFilterToJoin($tableAlias, $filter) {
|
||||
$this->from[$tableAlias]['filter'][] = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the existing filter (ON clause) on a join
|
||||
*/
|
||||
public function setJoinFilter($tableAlias, $filter) {
|
||||
if(is_string($this->from[$tableAlias])) {Debug::message($tableAlias); Debug::dump($this->from);}
|
||||
$this->from[$tableAlias]['filter'] = array($filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are already joining to the given table alias
|
||||
*/
|
||||
@ -184,6 +224,27 @@ class SQLQuery {
|
||||
return isset($this->from[$tableAlias]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of tables that this query is selecting from.
|
||||
*/
|
||||
public function queriedTables() {
|
||||
$tables = array();
|
||||
foreach($this->from as $key => $tableClause) {
|
||||
if(is_array($tableClause)) $table = $tableClause['table'];
|
||||
else if(is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) $table = $matches[1];
|
||||
else $table = $tableClause;
|
||||
|
||||
// Handle string replacements
|
||||
if($this->replacementsOld) $table = str_replace($this->replacementsOld, $this->replacementsNew, $table);
|
||||
|
||||
$tables[] = preg_replace('/^"|"$/','',$table);
|
||||
}
|
||||
return $tables;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Pass LIMIT clause either as SQL snippet or in array format.
|
||||
* Internally, limit will always be stored as a map containing the keys 'start' and 'limit'
|
||||
@ -394,6 +455,20 @@ class SQLQuery {
|
||||
* @return string
|
||||
*/
|
||||
function sql() {
|
||||
// Build from clauses
|
||||
foreach($this->from as $alias => $join) {
|
||||
// $join can be something like this array structure
|
||||
// array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1", "Status = 'approved'"))
|
||||
if(is_array($join)) {
|
||||
if(is_string($join['filter'])) $filter = $join['filter'];
|
||||
else if(sizeof($join['filter']) == 1) $filter = $join['filter'][0];
|
||||
else $filter = "(" . implode(") AND (", $join['filter']) . ")";
|
||||
|
||||
$this->from[$alias] = strtoupper($join['type']) . " JOIN \"{$join['table']}\" AS \"$alias\" ON $filter";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$sql = DB::getConn()->sqlQueryToString($this);
|
||||
if($this->replacementsOld) $sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql);
|
||||
return $sql;
|
||||
@ -539,6 +614,26 @@ class SQLQuery {
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new SQLQuery that calls the given aggregate functions on this data.
|
||||
* @param $columns An aggregate expression, such as 'MAX("Balance")', or an array of them.
|
||||
*/
|
||||
function aggregate($columns) {
|
||||
if(!is_array($columns)) $columns = array($columns);
|
||||
|
||||
if($this->groupby || $this->limit) {
|
||||
throw new Exception("SQLQuery::aggregate() doesn't work with groupby or limit, yet");
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->limit = null;
|
||||
$clone->orderby = null;
|
||||
$clone->groupby = null;
|
||||
$clone->select = $columns;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query that returns only the first row of this query
|
||||
|
@ -106,9 +106,31 @@ class Versioned extends DataExtension {
|
||||
);
|
||||
}
|
||||
|
||||
function augmentSQL(SQLQuery &$query) {
|
||||
// Get the content at a specific date
|
||||
if($date = Versioned::current_archived_date()) {
|
||||
|
||||
/**
|
||||
* Amend freshly created DataQuery objects with versioned-specific information
|
||||
*/
|
||||
function augmentDataQueryCreation(SQLQuery &$query, DataQuery &$dataQuery) {
|
||||
if($date = Versioned::$reading_archived_date) {
|
||||
$dataQuery->setQueryParam('Versioned.mode', 'archive');
|
||||
$dataQuery->setQueryParam('Versioned.date', Versioned::$reading_archived_date);
|
||||
|
||||
} else if(Versioned::$reading_stage && Versioned::$reading_stage != $this->defaultStage && array_search(Versioned::$reading_stage,$this->stages) !== false) {
|
||||
$dataQuery->setQueryParam('Versioned.mode', 'stage');
|
||||
$dataQuery->setQueryParam('Versioned.stage', Versioned::$reading_stage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment the the SQLQuery that is created by the DataQuery
|
||||
* @todo Should this all go into VersionedDataQuery?
|
||||
*/
|
||||
function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery) {
|
||||
switch($dataQuery->getQueryParam('Versioned.mode')) {
|
||||
// Reading a specific data from the archive
|
||||
case 'archive':
|
||||
$date = $dataQuery->getQueryParam('Versioned.date');
|
||||
foreach($query->from as $table => $dummy) {
|
||||
if(!isset($baseTable)) {
|
||||
$baseTable = $table;
|
||||
@ -132,13 +154,24 @@ class Versioned extends DataExtension {
|
||||
$query->from[$archiveTable] = "INNER JOIN \"$archiveTable\"
|
||||
ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\"
|
||||
AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
|
||||
|
||||
// Get a specific stage
|
||||
} else if(Versioned::current_stage() && Versioned::current_stage() != $this->defaultStage
|
||||
&& array_search(Versioned::current_stage(), $this->stages) !== false) {
|
||||
foreach($query->from as $table => $dummy) {
|
||||
$query->renameTable($table, $table . '_' . Versioned::current_stage());
|
||||
break;
|
||||
|
||||
// Reading a specific stage (Stage or Live)
|
||||
case 'stage':
|
||||
$stage = $dataQuery->getQueryParam('Versioned.stage');
|
||||
if($stage && ($stage != $this->defaultStage)) {
|
||||
foreach($query->from as $table => $dummy) {
|
||||
// Only rewrite table names that are actually part of the subclass tree
|
||||
// This helps prevent rewriting of other tables that get joined in, in
|
||||
// particular, many_many tables
|
||||
if(class_exists($table) && ($table == $this->owner->class
|
||||
|| is_subclass_of($table, $this->owner->class)
|
||||
|| is_subclass_of($this->owner->class, $table))) {
|
||||
$query->renameTable($table, $table . '_' . $stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -846,11 +879,11 @@ class Versioned extends DataExtension {
|
||||
* @param string $containerClass The container class for the result set (default is DataObjectSet)
|
||||
* @return DataObjectSet
|
||||
*/
|
||||
static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', $containerClass = 'DataObjectSet') {
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::reading_stage($stage);
|
||||
static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', $containerClass = 'DataList') {
|
||||
$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
$dq = $result->dataQuery();
|
||||
$dq->setQueryParam('Versioned.mode', 'stage');
|
||||
$dq->setQueryParam('Versioned.stage', $stage);
|
||||
return $result;
|
||||
}
|
||||
|
||||
@ -959,20 +992,6 @@ class Versioned extends DataExtension {
|
||||
* In particular, this will query deleted records as well as active ones.
|
||||
*/
|
||||
static function get_including_deleted($class, $filter = "", $sort = "") {
|
||||
$query = self::get_including_deleted_query($class, $filter, $sort);
|
||||
|
||||
// Process into a DataObjectSet
|
||||
$SNG = singleton($class);
|
||||
return $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query for the equivalent of a DataObject::get() call, querying the latest
|
||||
* version of each page stored in the (class)_versions tables.
|
||||
*
|
||||
* In particular, this will query deleted records as well as active ones.
|
||||
*/
|
||||
static function get_including_deleted_query($class, $filter = "", $sort = "") {
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::set_reading_mode('');
|
||||
|
||||
@ -985,6 +1004,9 @@ class Versioned extends DataExtension {
|
||||
$query->from[$archiveTable] = "INNER JOIN \"$archiveTable\"
|
||||
ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\"
|
||||
AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
|
||||
|
||||
// Process into a DataObjectSet
|
||||
$result = $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class);
|
||||
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
return $query;
|
||||
|
@ -155,7 +155,6 @@ class Group extends DataObject {
|
||||
$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
|
||||
$memberList->setParentClass('Group');
|
||||
$memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User'));
|
||||
$memberList->setRelationAutoSetting(false);
|
||||
|
||||
$fields->push($idField = new HiddenField("ID"));
|
||||
|
||||
|
@ -139,16 +139,15 @@ class Member extends DataObject {
|
||||
// Default groups should've been built by Group->requireDefaultRecords() already
|
||||
|
||||
// Find or create ADMIN group
|
||||
$adminGroups = Permission::get_groups_by_permission('ADMIN');
|
||||
if(!$adminGroups) {
|
||||
$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
|
||||
if(!$adminGroup) {
|
||||
singleton('Group')->requireDefaultRecords();
|
||||
$adminGroups = Permission::get_groups_by_permission('ADMIN');
|
||||
$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
|
||||
}
|
||||
$adminGroup = $adminGroups->First();
|
||||
|
||||
// Add a default administrator to the first ADMIN group found (most likely the default
|
||||
// group created through Group->requireDefaultRecords()).
|
||||
$admins = Permission::get_members_by_permission('ADMIN');
|
||||
$admins = Permission::get_members_by_permission('ADMIN')->First();
|
||||
if(!$admins) {
|
||||
// Leave 'Email' and 'Password' are not set to avoid creating
|
||||
// persistent logins in the database. See Security::setDefaultAdmin().
|
||||
@ -156,7 +155,7 @@ class Member extends DataObject {
|
||||
$admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
|
||||
$admin->write();
|
||||
$admin->Groups()->add($adminGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -379,7 +379,7 @@ class Permission extends DataObject {
|
||||
*/
|
||||
public static function get_members_by_permission($code) {
|
||||
$toplevelGroups = self::get_groups_by_permission($code);
|
||||
if (!$toplevelGroups) return false;
|
||||
if (!$toplevelGroups) return new DataObjectSet();
|
||||
|
||||
$groupIDs = array();
|
||||
foreach($toplevelGroups as $group) {
|
||||
@ -389,8 +389,7 @@ class Permission extends DataObject {
|
||||
}
|
||||
}
|
||||
|
||||
if(!count($groupIDs))
|
||||
return false;
|
||||
if(!count($groupIDs)) return new DataObjectSet();
|
||||
|
||||
$members = DataObject::get(
|
||||
Object::getCustomClass('Member'),
|
||||
|
@ -668,32 +668,30 @@ class Security extends Controller {
|
||||
Subsite::changeSubsite(0);
|
||||
}
|
||||
|
||||
$member = null;
|
||||
|
||||
// find a group with ADMIN permission
|
||||
$adminGroup = DataObject::get('Group',
|
||||
"\"Permission\".\"Code\" = 'ADMIN'",
|
||||
"\"Group\".\"ID\"",
|
||||
"JOIN \"Permission\" ON \"Group\".\"ID\"=\"Permission\".\"GroupID\"",
|
||||
'1');
|
||||
'1')->First();
|
||||
|
||||
if(is_callable('Subsite::changeSubsite')) {
|
||||
Subsite::changeSubsite($origSubsite);
|
||||
}
|
||||
|
||||
if ($adminGroup) {
|
||||
$adminGroup = $adminGroup->First();
|
||||
|
||||
if($adminGroup->Members()->First()) {
|
||||
$member = $adminGroup->Members()->First();
|
||||
}
|
||||
$member = $adminGroup->Members()->First();
|
||||
}
|
||||
|
||||
if(!$adminGroup) {
|
||||
singleton('Group')->requireDefaultRecords();
|
||||
}
|
||||
|
||||
if(!isset($member)) {
|
||||
if(!$member) {
|
||||
singleton('Member')->requireDefaultRecords();
|
||||
$members = Permission::get_members_by_permission('ADMIN');
|
||||
$member = $members->First();
|
||||
$member = Permission::get_members_by_permission('ADMIN')->First();
|
||||
}
|
||||
|
||||
return $member;
|
||||
|
14
tests/DataQueryTest.php
Normal file
14
tests/DataQueryTest.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
class DataQueryTest extends SapphireTest {
|
||||
/**
|
||||
* Test the join() method of the DataQuery object
|
||||
*/
|
||||
function testJoin() {
|
||||
$dq = new DataQuery('Member');
|
||||
$dq->join("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
||||
$this->assertContains("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"", $dq->sql());
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -121,7 +121,7 @@ class RestfulServerTest extends SapphireTest {
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 200);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$arr = Convert::xml2array($response->getBody());
|
||||
$authorsArr = $arr['RestfulServerTest_Author'];
|
||||
|
||||
|
@ -110,7 +110,7 @@ class TableFieldTest extends SapphireTest {
|
||||
new FieldSet()
|
||||
);
|
||||
|
||||
$this->assertEquals($tableField->sourceItems()->Count(), 2);
|
||||
$this->assertEquals(2, $tableField->sourceItems()->Count());
|
||||
|
||||
// We have replicated the array structure that the specific layout of the form generates.
|
||||
$tableField->setValue(array(
|
||||
|
@ -112,7 +112,7 @@ class DataObjectSetTest extends SapphireTest {
|
||||
$commArr = $comments->toArray();
|
||||
$multiplesOf3 = 0;
|
||||
|
||||
foreach($comments as $comment) {
|
||||
foreach($commArr as $comment) {
|
||||
if($comment->MultipleOf(3)) {
|
||||
$comment->IsMultipleOf3 = true;
|
||||
$multiplesOf3++;
|
||||
@ -295,6 +295,8 @@ class DataObjectSetTest extends SapphireTest {
|
||||
* Test {@link DataObjectSet->insertFirst()}
|
||||
*/
|
||||
function testInsertFirst() {
|
||||
// inserFirst doesn't work with DataLists any more, because of new ORM.
|
||||
/*
|
||||
// Get one comment
|
||||
$comment = DataObject::get_one('DataObjectSetTest_TeamComment', "\"Name\" = 'Joe'");
|
||||
|
||||
@ -316,6 +318,7 @@ class DataObjectSetTest extends SapphireTest {
|
||||
// insert with a non-numeric key
|
||||
$set->insertFirst($comment, 'SomeRandomKey');
|
||||
$this->assertEquals($comment, $set->First(), 'Comment should be first');
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,14 +137,6 @@ class DataObjectTest extends SapphireTest {
|
||||
$this->assertEquals('Joe', $comments->First()->Name);
|
||||
$this->assertEquals('Phil', $comments->Last()->Name);
|
||||
|
||||
// Test container class
|
||||
$comments = DataObject::get('DataObjectTest_TeamComment', '', '', '', '', 'DataObjectSet');
|
||||
$this->assertEquals('DataObjectSet', get_class($comments));
|
||||
|
||||
$comments = DataObject::get('DataObjectTest_TeamComment', '', '', '', '', 'ComponentSet');
|
||||
$this->assertEquals('ComponentSet', get_class($comments));
|
||||
|
||||
|
||||
// Test get_by_id()
|
||||
$captain1ID = $this->idFromFixture('DataObjectTest_Player', 'captain1');
|
||||
$captain1 = DataObject::get_by_id('DataObjectTest_Player', $captain1ID);
|
||||
@ -182,7 +174,7 @@ class DataObjectTest extends SapphireTest {
|
||||
/* Test that fields / has_one relations from the parent table and the subclass tables are extracted */
|
||||
$captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1");
|
||||
// Base field
|
||||
$this->assertEquals('Captain 1', $captain1->FirstName);
|
||||
$this->assertEquals('Captain', $captain1->FirstName);
|
||||
// Subclass field
|
||||
$this->assertEquals('007', $captain1->ShirtNumber);
|
||||
// Subclass has_one relation
|
||||
@ -197,6 +189,25 @@ class DataObjectTest extends SapphireTest {
|
||||
$this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeam()->ID);
|
||||
}
|
||||
|
||||
function testLimitAndCount() {
|
||||
$players = DataObject::get("DataObjectTest_Player");
|
||||
|
||||
// There's 4 records in total
|
||||
$this->assertEquals(4, $players->count());
|
||||
|
||||
// Testing "## offset ##" syntax
|
||||
$this->assertEquals(4, $players->limit("20 OFFSET 0")->count());
|
||||
$this->assertEquals(0, $players->limit("20 OFFSET 20")->count());
|
||||
$this->assertEquals(2, $players->limit("2 OFFSET 0")->count());
|
||||
$this->assertEquals(1, $players->limit("5 OFFSET 3")->count());
|
||||
|
||||
// Testing "##, ##" syntax
|
||||
$this->assertEquals(4, $players->limit("20")->count());
|
||||
$this->assertEquals(4, $players->limit("0, 20")->count());
|
||||
$this->assertEquals(0, $players->limit("20, 20")->count());
|
||||
$this->assertEquals(2, $players->limit("0, 2")->count());
|
||||
$this->assertEquals(1, $players->limit("3, 5")->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test writing of database columns which don't correlate to a DBField,
|
||||
@ -221,26 +232,26 @@ class DataObjectTest extends SapphireTest {
|
||||
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||
|
||||
// Test getComponents() gets the ComponentSet of the other side of the relation
|
||||
$this->assertTrue($page->Comments()->Count() == 2);
|
||||
$this->assertTrue($team->Comments()->Count() == 2);
|
||||
|
||||
// Test the IDs on the DataObjects are set correctly
|
||||
foreach($page->Comments() as $comment) {
|
||||
$this->assertTrue($comment->ParentID == $page->ID);
|
||||
foreach($team->Comments() as $comment) {
|
||||
$this->assertEquals($team->ID, $comment->TeamID);
|
||||
}
|
||||
|
||||
// Test that we can add and remove items that already exist in the database
|
||||
$newComment = new PageComment();
|
||||
$newComment = new DataObjectTest_TeamComment();
|
||||
$newComment->Name = "Automated commenter";
|
||||
$newComment->Comment = "This is a new comment";
|
||||
$newComment->write();
|
||||
$page->Comments()->add($newComment);
|
||||
$this->assertEquals($page->ID, $newComment->ParentID);
|
||||
$team->Comments()->add($newComment);
|
||||
$this->assertEquals($team->ID, $newComment->TeamID);
|
||||
|
||||
$comment1 = $this->fixture->objFromFixture('PageComment', 'comment1');
|
||||
$comment2 = $this->fixture->objFromFixture('PageComment', 'comment2');
|
||||
$page->Comments()->remove($comment2);
|
||||
$comment1 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment1');
|
||||
$comment2 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment2');
|
||||
$team->Comments()->remove($comment2);
|
||||
|
||||
$commentIDs = $page->Comments()->column('ID');
|
||||
$commentIDs = $team->Comments()->column('ID');
|
||||
$this->assertEquals(array($comment1->ID, $newComment->ID), $commentIDs);
|
||||
}
|
||||
|
||||
@ -421,17 +432,22 @@ class DataObjectTest extends SapphireTest {
|
||||
$obj->FirstName = "New Player";
|
||||
$this->assertTrue($obj->isChanged());
|
||||
|
||||
$page->write();
|
||||
$this->assertFalse($page->isChanged());
|
||||
$obj->write();
|
||||
$this->assertFalse($obj->isChanged());
|
||||
|
||||
/* If we perform the same random query twice, it shouldn't return the same results */
|
||||
$itemsA = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
foreach($itemsA as $item) $keysA[] = $item->ID;
|
||||
|
||||
$itemsB = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
$itemsC = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
$itemsD = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
foreach($itemsA as $item) $keysA[] = $item->ID;
|
||||
foreach($itemsB as $item) $keysB[] = $item->ID;
|
||||
foreach($itemsC as $item) $keysC[] = $item->ID;
|
||||
foreach($itemsD as $item) $keysD[] = $item->ID;
|
||||
|
||||
$this->assertNotEquals($keysA, $keysB);
|
||||
// These shouldn't all be the same (run it 4 times to minimise chance of an accidental collision)
|
||||
// There's about a 1 in a billion chance of an accidental collision
|
||||
$this->assertTrue($keysA != $keysB || $keysB != $keysC || $keysC != $keysD);
|
||||
}
|
||||
|
||||
function testWriteSavesToHasOneRelations() {
|
||||
@ -815,8 +831,8 @@ class DataObjectTest extends SapphireTest {
|
||||
*/
|
||||
function testManyManyUnlimitedRowCount() {
|
||||
$player = $this->objFromFixture('DataObjectTest_Player', 'player2');
|
||||
$query = $player->getManyManyComponentsQuery('Teams');
|
||||
$this->assertEquals(2, $query->unlimitedRowCount());
|
||||
// TODO: What's going on here?
|
||||
$this->assertEquals(2, $player->Teams()->dataQuery()->query()->unlimitedRowCount());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,7 +6,7 @@ DataObjectTest_Team:
|
||||
|
||||
DataObjectTest_Player:
|
||||
captain1:
|
||||
FirstName: Captain 1
|
||||
FirstName: Captain
|
||||
ShirtNumber: 007
|
||||
FavouriteTeam: =>DataObjectTest_Team.team1
|
||||
Teams: =>DataObjectTest_Team.team1
|
||||
|
@ -171,10 +171,10 @@ class VersionedTest extends SapphireTest {
|
||||
$page->write();
|
||||
|
||||
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
|
||||
$this->assertNull($live);
|
||||
$this->assertEquals(0, $live->count());
|
||||
|
||||
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
|
||||
$this->assertNotNull($stage);
|
||||
$this->assertEquals(1, $stage->count());
|
||||
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
|
||||
|
||||
Versioned::reading_stage($origStage);
|
||||
@ -195,11 +195,11 @@ class VersionedTest extends SapphireTest {
|
||||
$page->write();
|
||||
|
||||
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
|
||||
$this->assertNotNull($live->First());
|
||||
$this->assertEquals(1, $live->count());
|
||||
$this->assertEquals($live->First()->Title, 'testWritingNewToLive');
|
||||
|
||||
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
|
||||
$this->assertNull($stage);
|
||||
$this->assertEquals(0, $stage->count());
|
||||
|
||||
Versioned::reading_stage($origStage);
|
||||
}
|
||||
|
@ -93,8 +93,8 @@ class GroupTest extends FunctionalTest {
|
||||
|
||||
$adminGroup->delete();
|
||||
|
||||
$this->assertNull(DataObject::get('Group', "\"ID\"={$adminGroup->ID}"), 'Group is removed');
|
||||
$this->assertNull(DataObject::get('Permission',"\"GroupID\"={$adminGroup->ID}"), 'Permissions removed along with the group');
|
||||
$this->assertEquals(0, DataObject::get('Group', "\"ID\"={$adminGroup->ID}")->count(), 'Group is removed');
|
||||
$this->assertEquals(0, DataObject::get('Permission',"\"GroupID\"={$adminGroup->ID}")->count(), 'Permissions removed along with the group');
|
||||
}
|
||||
|
||||
function testCollateAncestorIDs() {
|
||||
|
@ -11,7 +11,7 @@ class PermissionRoleTest extends FunctionalTest {
|
||||
|
||||
$role->delete();
|
||||
|
||||
$this->assertNull(DataObject::get('PermissionRole', "\"ID\"={$role->ID}"), 'Role is removed');
|
||||
$this->assertNull(DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}"), 'Permissions removed along with the role');
|
||||
$this->assertEquals(0, DataObject::get('PermissionRole', "\"ID\"={$role->ID}")->count(), 'Role is removed');
|
||||
$this->assertEquals(0, DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}")->count(), 'Permissions removed along with the role');
|
||||
}
|
||||
}
|
||||
|
@ -64,4 +64,19 @@ class PermissionTest extends SapphireTest {
|
||||
'Member is found via a permission attached to a role');
|
||||
$this->assertNotContains($accessAuthor->ID, $resultIDs);
|
||||
}
|
||||
|
||||
|
||||
function testHiddenPermissions(){
|
||||
$permissionCheckboxSet = new PermissionCheckboxSetField('Permissions','Permissions','Permission','GroupID');
|
||||
$this->assertContains('CMS_ACCESS_CMSMain', $permissionCheckboxSet->Field());
|
||||
$this->assertContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field());
|
||||
|
||||
Permission::add_to_hidden_permissions('CMS_ACCESS_CMSMain');
|
||||
Permission::add_to_hidden_permissions('CMS_ACCESS_AssetAdmin');
|
||||
$this->assertNotContains('CMS_ACCESS_CMSMain', $permissionCheckboxSet->Field());
|
||||
$this->assertNotContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field());
|
||||
|
||||
Permission::remove_from_hidden_permissions('CMS_ACCESS_AssetAdmin');
|
||||
$this->assertContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field());
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ class SecurityDefaultAdminTest extends SapphireTest {
|
||||
|
||||
function testFindAnAdministratorCreatesNewUser() {
|
||||
$adminMembers = Permission::get_members_by_permission('ADMIN');
|
||||
$this->assertFalse($adminMembers);
|
||||
$this->assertEquals(0, $adminMembers->count());
|
||||
|
||||
$admin = Security::findAnAdministrator();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user