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 = call_user_func($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 paramaterised) */ 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 charactaristics * * @see SS_List::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 charactaristics. * * @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. * * @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; } public function debug() { $val = "