dataClass = $dataClass;
$this->dataQuery = new DataQuery($this->dataClass);
parent::__construct();
}
/**
* Get the dataClass name for this DataList, ie the DataObject ClassName
*
* @return string
*/
public function dataClass()
{
return $this->dataClass;
}
/**
* When cloning this object, clone the dataQuery object as well
*/
public function __clone()
{
$this->dataQuery = clone $this->dataQuery;
}
/**
* Return a copy of the internal {@link DataQuery} object
*
* Because the returned value is a copy, modifying it won't affect this list's contents. If
* you want to alter the data query directly, use the alterDataQuery method
*
* @return DataQuery
*/
public function dataQuery()
{
return clone $this->dataQuery;
}
/**
* @var bool - Indicates if we are in an alterDataQueryCall already, so alterDataQuery can be re-entrant
*/
protected $inAlterDataQueryCall = false;
/**
* Return a new DataList instance with the underlying {@link DataQuery} object altered
*
* If you want to alter the underlying dataQuery for this list, this wrapper method
* will ensure that you can do so without mutating the existing List object.
*
* It clones this list, calls the passed callback function with the dataQuery of the new
* list as it's first parameter (and the list as it's second), then returns the list
*
* Note that this function is re-entrant - it's safe to call this inside a callback passed to
* alterDataQuery
*
* @param callable $callback
* @return static
* @throws Exception
*/
public function alterDataQuery($callback)
{
if ($this->inAlterDataQueryCall) {
$list = $this;
$res = call_user_func($callback, $list->dataQuery, $list);
if ($res) {
$list->dataQuery = $res;
}
return $list;
}
$list = clone $this;
$list->inAlterDataQueryCall = true;
try {
$res = $callback($list->dataQuery, $list);
if ($res) {
$list->dataQuery = $res;
}
} catch (Exception $e) {
$list->inAlterDataQueryCall = false;
throw $e;
}
$list->inAlterDataQueryCall = false;
return $list;
}
/**
* Return a new DataList instance with the underlying {@link DataQuery} object changed
*
* @param DataQuery $dataQuery
* @return static
*/
public function setDataQuery(DataQuery $dataQuery)
{
$clone = clone $this;
$clone->dataQuery = $dataQuery;
return $clone;
}
/**
* Returns a new DataList instance with the specified query parameter assigned
*
* @param string|array $keyOrArray Either the single key to set, or an array of key value pairs to set
* @param mixed $val If $keyOrArray is not an array, this is the value to set
* @return static
*/
public function setDataQueryParam($keyOrArray, $val = null)
{
$clone = clone $this;
if (is_array($keyOrArray)) {
foreach ($keyOrArray as $key => $value) {
$clone->dataQuery->setQueryParam($key, $value);
}
} else {
$clone->dataQuery->setQueryParam($keyOrArray, $val);
}
return $clone;
}
/**
* Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-)
*
* @param array $parameters Out variable for parameters required for this query
* @return string The resulting SQL query (may be parameterised)
*/
public function sql(&$parameters = [])
{
return $this->dataQuery->query()->sql($parameters);
}
/**
* Return a new DataList instance with a WHERE clause added to this list's query.
*
* Supports parameterised queries.
* See SQLSelect::addWhere() for syntax examples, although DataList
* won't expand multiple method arguments as SQLSelect does.
*
* @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
* paramaterised queries
* @return static
*/
public function where($filter)
{
return $this->alterDataQuery(function (DataQuery $query) use ($filter) {
$query->where($filter);
});
}
/**
* Return a new DataList instance with a WHERE clause added to this list's query.
* All conditions provided in the filter will be joined with an OR
*
* Supports parameterised queries.
* See SQLSelect::addWhere() for syntax examples, although DataList
* won't expand multiple method arguments as SQLSelect does.
*
* @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
* paramaterised queries
* @return static
*/
public function whereAny($filter)
{
return $this->alterDataQuery(function (DataQuery $query) use ($filter) {
$query->whereAny($filter);
});
}
/**
* Returns true if this DataList can be sorted by the given field.
*
* @param string $fieldName
* @return boolean
*/
public function canSortBy($fieldName)
{
return $this->dataQuery()->query()->canSortBy($fieldName);
}
/**
* Returns true if this DataList can be filtered by the given field.
*
* @param string $fieldName (May be a related field in dot notation like Member.FirstName)
* @return boolean
*/
public function canFilterBy($fieldName)
{
$model = singleton($this->dataClass);
$relations = explode(".", $fieldName);
// First validate the relationships
$fieldName = array_pop($relations);
foreach ($relations as $r) {
$relationClass = $model->getRelationClass($r);
if (!$relationClass) {
return false;
}
$model = singleton($relationClass);
if (!$model) {
return false;
}
}
// Then check field
if ($model->hasDatabaseField($fieldName)) {
return true;
}
return false;
}
/**
* Return a new DataList instance with the records returned in this query
* restricted by a limit clause.
*
* @param int $limit
* @param int $offset
* @return static
*/
public function limit($limit, $offset = 0)
{
return $this->alterDataQuery(function (DataQuery $query) use ($limit, $offset) {
$query->limit($limit, $offset);
});
}
/**
* Return a new DataList instance with distinct records or not
*
* @param bool $value
* @return static
*/
public function distinct($value)
{
return $this->alterDataQuery(function (DataQuery $query) use ($value) {
$query->distinct($value);
});
}
/**
* Return a new DataList instance as a copy of this data list with the sort
* order set.
*
* @see SS_List::sort()
* @see SQLSelect::orderby
* @example $list = $list->sort('Name'); // default ASC sorting
* @example $list = $list->sort('Name DESC'); // DESC sorting
* @example $list = $list->sort('Name', 'ASC');
* @example $list = $list->sort(array('Name'=>'ASC', 'Age'=>'DESC'));
*
* @param String|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped.
* @return static
*/
public function sort()
{
$count = func_num_args();
if ($count == 0) {
return $this;
}
if ($count > 2) {
throw new InvalidArgumentException('This method takes zero, one or two arguments');
}
if ($count == 2) {
$col = null;
$dir = null;
list($col, $dir) = func_get_args();
// Validate direction
if (!in_array(strtolower($dir), ['desc', 'asc'])) {
user_error('Second argument to sort must be either ASC or DESC');
}
$sort = [$col => $dir];
} else {
$sort = func_get_arg(0);
}
return $this->alterDataQuery(function (DataQuery $query, DataList $list) use ($sort) {
if (is_string($sort) && $sort) {
if (false !== stripos($sort, ' asc') || false !== stripos($sort, ' desc')) {
$query->sort($sort);
} else {
$list->applyRelation($sort, $column, true);
$query->sort($column, 'ASC');
}
} elseif (is_array($sort)) {
// sort(array('Name'=>'desc'));
$query->sort(null, null); // wipe the sort
foreach ($sort as $column => $direction) {
// Convert column expressions to SQL fragment, while still allowing the passing of raw SQL
// fragments.
$list->applyRelation($column, $relationColumn, true);
$query->sort($relationColumn, $direction, false);
}
}
});
}
/**
* Return a copy of this list which only includes items with these characteristics
*
* @see Filterable::filter()
*
* @example $list = $list->filter('Name', 'bob'); // only bob in the list
* @example $list = $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
* @example $list = $list->filter(array('Name'=>'bob', 'Age'=>21)); // bob with the age 21
* @example $list = $list->filter(array('Name'=>'bob', 'Age'=>array(21, 43))); // bob with the Age 21 or 43
* @example $list = $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
* // aziz with the age 21 or 43 and bob with the Age 21 or 43
*
* Note: When filtering on nullable columns, null checks will be automatically added.
* E.g. ->filter('Field:not', 'value) will generate '... OR "Field" IS NULL', and
* ->filter('Field:not', null) will generate '"Field" IS NOT NULL'
*
* @todo extract the sql from $customQuery into a SQLGenerator class
*
* @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
* @return $this
*/
public function filter()
{
// Validate and process arguments
$arguments = func_get_args();
switch (sizeof($arguments)) {
case 1:
$filters = $arguments[0];
break;
case 2:
$filters = [$arguments[0] => $arguments[1]];
break;
default:
throw new InvalidArgumentException('Incorrect number of arguments passed to filter()');
}
return $this->addFilter($filters);
}
/**
* Return a new instance of the list with an added filter
*
* @param array $filterArray
* @return $this
*/
public function addFilter($filterArray)
{
$list = $this;
foreach ($filterArray as $expression => $value) {
$filter = $this->createSearchFilter($expression, $value);
$list = $list->alterDataQuery([$filter, 'apply']);
}
return $list;
}
/**
* Return a copy of this list which contains items matching any of these characteristics.
*
* @example // only bob in the list
* $list = $list->filterAny('Name', 'bob');
* // SQL: WHERE "Name" = 'bob'
* @example // azis or bob in the list
* $list = $list->filterAny('Name', array('aziz', 'bob');
* // SQL: WHERE ("Name" IN ('aziz','bob'))
* @example // bob or anyone aged 21 in the list
* $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21));
* // SQL: WHERE ("Name" = 'bob' OR "Age" = '21')
* @example // bob or anyone aged 21 or 43 in the list
* $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43)));
* // SQL: WHERE ("Name" = 'bob' OR ("Age" IN ('21', '43'))
* @example // all bobs, phils or anyone aged 21 or 43 in the list
* $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
* // SQL: WHERE (("Name" IN ('bob', 'phil')) OR ("Age" IN ('21', '43'))
*
* @todo extract the sql from this method into a SQLGenerator class
*
* @param string|array See {@link filter()}
* @return static
*/
public function filterAny()
{
$numberFuncArgs = count(func_get_args());
$whereArguments = [];
if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
$whereArguments = func_get_arg(0);
} elseif ($numberFuncArgs == 2) {
$whereArguments[func_get_arg(0)] = func_get_arg(1);
} else {
throw new InvalidArgumentException('Incorrect number of arguments passed to filterAny()');
}
return $this->alterDataQuery(function (DataQuery $query) use ($whereArguments) {
$subquery = $query->disjunctiveGroup();
foreach ($whereArguments as $field => $value) {
$filter = $this->createSearchFilter($field, $value);
$filter->apply($subquery);
}
});
}
/**
* Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a
* future implementation.
* @see Filterable::filterByCallback()
*
* @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
* @param callable $callback
* @return ArrayList (this may change in future implementations)
*/
public function filterByCallback($callback)
{
if (!is_callable($callback)) {
throw new LogicException(sprintf(
"SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
gettype($callback)
));
}
/** @var ArrayList $output */
$output = ArrayList::create();
foreach ($this as $item) {
if (call_user_func($callback, $item, $this)) {
$output->push($item);
}
}
return $output;
}
/**
* Given a field or relation name, apply it safely to this datalist.
*
* Unlike getRelationName, this is immutable and will fallback to the quoted field
* name if not a relation.
*
* Example use (simple WHERE condition on data sitting in a related table):
*
*
* $columnName = null;
* $list = Page::get()
* ->applyRelation('TaxonomyTerms.ID', $columnName)
* ->where([$columnName => 'my value']);
*
*
*
* @param string $field Name of field or relation to apply
* @param string &$columnName Quoted column name
* @param bool $linearOnly Set to true to restrict to linear relations only. Set this
* if this relation will be used for sorting, and should not include duplicate rows.
* @return $this DataList with this relation applied
*/
public function applyRelation($field, &$columnName = null, $linearOnly = false)
{
// If field is invalid, return it without modification
if (!$this->isValidRelationName($field)) {
$columnName = $field;
return $this;
}
// Simple fields without relations are mapped directly
if (strpos($field, '.') === false) {
$columnName = '"' . $field . '"';
return $this;
}
return $this->alterDataQuery(
function (DataQuery $query) use ($field, &$columnName, $linearOnly) {
$relations = explode('.', $field);
$fieldName = array_pop($relations);
// Apply relation
$relationModelName = $query->applyRelation($relations, $linearOnly);
$relationPrefix = $query->applyRelationPrefix($relations);
// Find the db field the relation belongs to
$columnName = DataObject::getSchema()
->sqlColumnForField($relationModelName, $fieldName, $relationPrefix);
}
);
}
/**
* Check if the given field specification could be interpreted as an unquoted relation name
*
* @param string $field
* @return bool
*/
protected function isValidRelationName($field)
{
return preg_match('/^[A-Z0-9._]+$/i', $field);
}
/**
* Given a filter expression and value construct a {@see SearchFilter} instance
*
* @param string $filter E.g. `Name:ExactMatch:not`, `Name:ExactMatch`, `Name:not`, `Name`
* @param mixed $value Value of the filter
* @return SearchFilter
*/
protected function createSearchFilter($filter, $value)
{
// Field name is always the first component
$fieldArgs = explode(':', $filter);
$fieldName = array_shift($fieldArgs);
// Inspect type of second argument to determine context
$secondArg = array_shift($fieldArgs);
$modifiers = $fieldArgs;
if (!$secondArg) {
// Use default filter if none specified. E.g. `->filter(['Name' => $myname])`
$filterServiceName = 'DataListFilter.default';
} else {
// The presence of a second argument is by default ambiguous; We need to query
// Whether this is a valid modifier on the default filter, or a filter itself.
/** @var SearchFilter $defaultFilterInstance */
$defaultFilterInstance = Injector::inst()->get('DataListFilter.default');
if (in_array(strtolower($secondArg), $defaultFilterInstance->getSupportedModifiers())) {
// Treat second (and any subsequent) argument as modifiers, using default filter
$filterServiceName = 'DataListFilter.default';
array_unshift($modifiers, $secondArg);
} else {
// Second argument isn't a valid modifier, so assume is filter identifier
$filterServiceName = "DataListFilter.{$secondArg}";
}
}
// Build instance
return Injector::inst()->create($filterServiceName, $fieldName, $value, $modifiers);
}
/**
* Return a copy of this list which does not contain any items that match all params
*
* @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
* @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
* @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
* @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
* @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
* // bob age 21 or 43, phil age 21 or 43 would be excluded
*
* @todo extract the sql from this method into a SQLGenerator class
*
* @param string|array
* @param string [optional]
*
* @return $this
*/
public function exclude()
{
$numberFuncArgs = count(func_get_args());
$whereArguments = [];
if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
$whereArguments = func_get_arg(0);
} elseif ($numberFuncArgs == 2) {
$whereArguments[func_get_arg(0)] = func_get_arg(1);
} else {
throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
}
return $this->alterDataQuery(function (DataQuery $query) use ($whereArguments) {
$subquery = $query->disjunctiveGroup();
foreach ($whereArguments as $field => $value) {
$filter = $this->createSearchFilter($field, $value);
$filter->exclude($subquery);
}
});
}
/**
* Return a copy of this list which does not contain any items with any of these params
*
* @example $list = $list->excludeAny('Name', 'bob'); // exclude bob from list
* @example $list = $list->excludeAny('Name', array('aziz', 'bob'); // exclude aziz and bob from list
* @example $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>21)); // exclude bob or Age 21
* @example $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob or Age 21 or 43
* @example $list = $list->excludeAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
* // bob, phil, 21 or 43 would be excluded
*
* @param string|array
* @param string [optional]
*
* @return $this
*/
public function excludeAny()
{
$numberFuncArgs = count(func_get_args());
$whereArguments = [];
if ($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
$whereArguments = func_get_arg(0);
} elseif ($numberFuncArgs == 2) {
$whereArguments[func_get_arg(0)] = func_get_arg(1);
} else {
throw new InvalidArgumentException('Incorrect number of arguments passed to excludeAny()');
}
return $this->alterDataQuery(function (DataQuery $dataQuery) use ($whereArguments) {
foreach ($whereArguments as $field => $value) {
$filter = $this->createSearchFilter($field, $value);
$filter->exclude($dataQuery);
}
return $dataQuery;
});
}
/**
* This method returns a copy of this list that does not contain any DataObjects that exists in $list
*
* The $list passed needs to contain the same dataclass as $this
*
* @param DataList $list
* @return static
* @throws InvalidArgumentException
*/
public function subtract(DataList $list)
{
if ($this->dataClass() != $list->dataClass()) {
throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
}
return $this->alterDataQuery(function (DataQuery $query) use ($list) {
$query->subtract($list->dataQuery());
});
}
/**
* Return a new DataList instance with an inner join clause added to this list's query.
*
* @param string $table Table name (unquoted and as escaped SQL)
* @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
* @param string $alias - if you want this table to be aliased under another name
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
* will cause the query to appear first. The default is 20, and joins created automatically by the
* ORM have a value of 10.
* @param array $parameters Any additional parameters if the join is a parameterised subquery
* @return static
*/
public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = [])
{
return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
$query->innerJoin($table, $onClause, $alias, $order, $parameters);
});
}
/**
* Return a new DataList instance with a left join clause added to this list's query.
*
* @param string $table Table name (unquoted and as escaped SQL)
* @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
* @param string $alias - if you want this table to be aliased under another name
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
* will cause the query to appear first. The default is 20, and joins created automatically by the
* ORM have a value of 10.
* @param array $parameters Any additional parameters if the join is a parameterised subquery
* @return static
*/
public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = [])
{
return $this->alterDataQuery(function (DataQuery $query) use ($table, $onClause, $alias, $order, $parameters) {
$query->leftJoin($table, $onClause, $alias, $order, $parameters);
});
}
/**
* Return an array of the actual items that this DataList contains at this stage.
* This is when the query is actually executed.
*
* @return array
*/
public function toArray()
{
$query = $this->dataQuery->query();
$rows = $query->execute();
$results = [];
foreach ($rows as $row) {
$results[] = $this->createDataObject($row);
}
return $results;
}
/**
* Return this list as an array and every object it as an sub array as well
*
* @return array
*/
public function toNestedArray()
{
$result = [];
foreach ($this as $item) {
$result[] = $item->toMap();
}
return $result;
}
/**
* Walks the list using the specified callback
*
* @param callable $callback
* @return $this
*/
public function each($callback)
{
foreach ($this as $row) {
$callback($row);
}
return $this;
}
/**
* Returns a generator for this DataList
*
* @return \Generator&DataObject[]
*/
public function getGenerator()
{
$query = $this->dataQuery->query()->execute();
while ($row = $query->record()) {
yield $this->createDataObject($row);
}
}
public function debug()
{
$val = "