dataClass = $dataClass; $this->dataQuery = new DataQuery($this->dataClass); parent::__construct(); } /** * Set the DataModel * * @param DataModel $model */ public function setDataModel(DataModel $model) { $this->model = $model; } /** * 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 * * todo 3.1: In 3.0 the below is not currently true for backwards compatible reasons, but code should not rely on this * 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() { // TODO 3.1: This method potentially mutates self 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 $callback * @return DataList */ public function alterDataQuery($callback) { if ($this->inAlterDataQueryCall) { $list = $this; $res = $callback($list->dataQuery, $list); if ($res) $list->dataQuery = $res; return $list; } else { $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; } } /** * In 3.0.0 some methods in DataList mutate their list. We don't want to change that in the 3.0.x * line, but we don't want people relying on it either. This does the same as alterDataQuery, but * _does_ mutate the existing list. * * todo 3.1: All methods that call this need to call alterDataQuery instead */ protected function alterDataQuery_30($callback) { Deprecation::notice('3.1', 'DataList will become immutable in 3.1'); if ($this->inAlterDataQueryCall) { $res = $callback($this->dataQuery, $this); if ($res) $this->dataQuery = $res; return $this; } else { $this->inAlterDataQueryCall = true; try { $res = $callback($this->dataQuery, $this); if ($res) $this->dataQuery = $res; } catch (Exception $e) { $this->inAlterDataQueryCall = false; throw $e; } $this->inAlterDataQueryCall = false; return $this; } } /** * Return a new DataList instance with the underlying {@link DataQuery} object changed * * @param DataQuery $dataQuery * @return DataList */ public function setDataQuery(DataQuery $dataQuery) { $clone = clone $this; $clone->dataQuery = $dataQuery; return $clone; } /** * Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-) * * @return SQLQuery */ public function sql() { return $this->dataQuery->query()->sql(); } /** * Return a new DataList instance with a WHERE clause added to this list's query. * * @param string $filter Escaped SQL statement * @return DataList */ public function where($filter) { return $this->alterDataQuery_30(function($query) use ($filter){ $query->where($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); } /** * * @param string $fieldName * @return boolean */ public function canFilterBy($fieldName) { if($t = singleton($this->dataClass)->hasDatabaseField($fieldName)){ return true; } return false; } /** * Return a new DataList instance with a join clause added to this list's query. * * @param type $join Escaped SQL statement * @return DataList * @deprecated 3.0 */ public function join($join) { Deprecation::notice('3.0', 'Use innerJoin() or leftJoin() instead.'); return $this->alterDataQuery_30(function($query) use ($join){ $query->join($join); }); } /** * Return a new DataList instance with the records returned in this query restricted by a limit clause * * @param int $limit * @param int $offset */ public function limit($limit, $offset = 0) { if(!$limit && !$offset) { return $this; } if($limit && !is_numeric($limit)) { Deprecation::notice('3.0', 'Please pass limits as 2 arguments, rather than an array or SQL fragment.', Deprecation::SCOPE_GLOBAL); } return $this->alterDataQuery_30(function($query) use ($limit, $offset){ $query->limit($limit, $offset); }); } /** * Return a new DataList instance as a copy of this data list with the sort order set * * @see SS_List::sort() * @see SQLQuery::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 DataList */ 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'); } $sort = $col = $dir = null; if ($count == 2) { list($col, $dir) = func_get_args(); } else { $sort = func_get_arg(0); } return $this->alterDataQuery_30(function($query, $list) use ($sort, $col, $dir){ if ($col) { // sort('Name','Desc') if(!in_array(strtolower($dir),array('desc','asc'))){ user_error('Second argument to sort must be either ASC or DESC'); } $query->sort($col, $dir); } else if(is_string($sort) && $sort){ // sort('Name ASC') if(stristr($sort, ' asc') || stristr($sort, ' desc')) { $query->sort($sort); } else { $query->sort($sort, 'ASC'); } } else if(is_array($sort)) { // sort(array('Name'=>'desc')); $query->sort(null, null); // wipe the sort foreach($sort as $col => $dir) { // Convert column expressions to SQL fragment, while still allowing the passing of raw SQL fragments. try { $relCol = $list->getRelationName($col); } catch(InvalidArgumentException $e) { $relCol = $col; } $query->sort($relCol, $dir, 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 * * @todo extract the sql from $customQuery into a SQLGenerator class * * @param string|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped. * @return DataList */ public function filter() { // Validate and process arguments $arguments = func_get_args(); switch(sizeof($arguments)) { case 1: $filters = $arguments[0]; break; case 2: $filters = array($arguments[0] => $arguments[1]); break; default: throw new InvalidArgumentException('Incorrect number of arguments passed to filter()'); } // TODO 3.1: Once addFilter doesn't mutate self, this results in a double clone $clone = clone $this; $clone->addFilter($filters); return $clone; } /** * Return a new instance of the list with an added filter */ public function addFilter($filterArray) { $SQL_Statements = array(); foreach($filterArray as $field => $value) { if(is_array($value)) { $customQuery = 'IN (\''.implode('\',\'',Convert::raw2sql($value)).'\')'; } else { $customQuery = '= \''.Convert::raw2sql($value).'\''; } if(stristr($field,':')) { $fieldArgs = explode(':',$field); $field = array_shift($fieldArgs); foreach($fieldArgs as $fieldArg){ $comparisor = $this->applyFilterContext($field, $fieldArg, $value); } } else { if($field == 'ID') { $field = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass)); } else { $field = '"' . Convert::raw2sql($field) . '"'; } $SQL_Statements[] = $field . ' ' . $customQuery; } } if(!count($SQL_Statements)) return $this; return $this->alterDataQuery_30(function($query) use ($SQL_Statements){ foreach($SQL_Statements as $SQL_Statement){ $query->where($SQL_Statement); } }); } /** * Filter this DataList by a callback function. * The function will be passed each record of the DataList in turn, and must return true for the record to be included. * Returns the filtered list. * * Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a future * implementation. */ public function filterByCallback($callback) { if(!is_callable($callback)) throw new LogicException("DataList::filterByCallback() must be passed something callable."); $output = new ArrayList; foreach($this as $item) { if($callback($item)) $output->push($item); } return $output; } /** * Translates a Object relation name to a Database name and apply the relation join to * the query. Throws an InvalidArgumentException if the $field doesn't correspond to a relation * * @param string $field * @return string */ public function getRelationName($field) { if(!preg_match('/^[A-Z0-9._]+$/i', $field)) { throw new InvalidArgumentException("Bad field expression $field"); } if (!$this->inAlterDataQueryCall) { Deprecation::notice('3.1', 'getRelationName is mutating, and must be called inside an alterDataQuery block'); } if(strpos($field,'.') === false) { return '"'.$field.'"'; } $relations = explode('.', $field); $fieldName = array_pop($relations); $relationModelName = $this->dataQuery->applyRelation($field); return '"'.$relationModelName.'"."'.$fieldName.'"'; } /** * Translates the comparisator to the sql query * * @param string $field - the fieldname in the db * @param string $comparisators - example StartsWith, relates to a filtercontext * @param string $value - the value that the filtercontext will use for matching * @todo Deprecated SearchContexts and pull their functionality into the core of the ORM */ private function applyFilterContext($field, $comparisators, $value) { $t = singleton($this->dataClass())->dbObject($field); $className = "{$comparisators}Filter"; if(!class_exists($className)){ throw new InvalidArgumentException('There are no '.$comparisators.' comparisator'); } $t = new $className($field,$value); $t->apply($this->dataQuery()); } /** * Return a copy of this list which does not contain any items with these charactaristics * * @see SS_List::exclude() * @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 Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped. * @return DataList */ public function exclude() { $numberFuncArgs = count(func_get_args()); $whereArguments = array(); 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()'); } $SQL_Statements = array(); foreach($whereArguments as $fieldName => $value) { if($fieldName == 'ID') { $fieldName = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass)); } else { $fieldName = '"' . Convert::raw2sql($fieldName) . '"'; } if(is_array($value)){ $SQL_Statements[] = ($fieldName . ' NOT IN (\''.implode('\',\'', Convert::raw2sql($value)).'\')'); } else { $SQL_Statements[] = ($fieldName . ' != \''.Convert::raw2sql($value).'\''); } } if(!count($SQL_Statements)) return $this; return $this->alterDataQuery_30(function($query) use ($SQL_Statements){ $query->whereAny($SQL_Statements); }); } /** * 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 SS_List $list * @return DataList * @throws BadMethodCallException */ public function subtract(SS_List $list) { if($this->dataclass() != $list->dataclass()) { throw new InvalidArgumentException('The list passed must have the same dataclass as this class'); } return $this->alterDataQuery(function($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 * @return DataList */ public function innerJoin($table, $onClause, $alias = null) { return $this->alterDataQuery_30(function($query) use ($table, $onClause, $alias){ $query->innerJoin($table, $onClause, $alias); }); } /** * 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 * @return DataList */ public function leftJoin($table, $onClause, $alias = null) { return $this->alterDataQuery_30(function($query) use ($table, $onClause, $alias){ $query->leftJoin($table, $onClause, $alias); }); } /** * 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 = array(); 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 type */ public function toNestedArray() { $result = array(); foreach($this as $item) { $result[] = $item->toMap(); } return $result; } public function debug() { $val = "