PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/ORM/DataList.php

http://github.com/silverstripe/sapphire
PHP | 1135 lines | 475 code | 126 blank | 534 comment | 53 complexity | b24d9a6a3449ca39b0469df12e0ab7e7 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. <?php
  2. namespace SilverStripe\ORM;
  3. use ViewableData;
  4. use Exception;
  5. use InvalidArgumentException;
  6. use Injector;
  7. use LogicException;
  8. use Debug;
  9. use ArrayIterator;
  10. /**
  11. * Implements a "lazy loading" DataObjectSet.
  12. * Uses {@link DataQuery} to do the actual query generation.
  13. *
  14. * DataLists are _immutable_ as far as the query they represent is concerned. When you call a method that
  15. * alters the query, a new DataList instance is returned, rather than modifying the existing instance
  16. *
  17. * When you add or remove an element to the list the query remains the same, but because you have modified
  18. * the underlying data the contents of the list changes. These are some of those methods:
  19. *
  20. * - add
  21. * - addMany
  22. * - remove
  23. * - removeMany
  24. * - removeByID
  25. * - removeByFilter
  26. * - removeAll
  27. *
  28. * Subclasses of DataList may add other methods that have the same effect.
  29. *
  30. * @package framework
  31. * @subpackage orm
  32. */
  33. class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortable, SS_Limitable {
  34. /**
  35. * The DataObject class name that this data list is querying
  36. *
  37. * @var string
  38. */
  39. protected $dataClass;
  40. /**
  41. * The {@link DataQuery} object responsible for getting this DataList's records
  42. *
  43. * @var DataQuery
  44. */
  45. protected $dataQuery;
  46. /**
  47. * The DataModel from which this DataList comes.
  48. *
  49. * @var DataModel
  50. */
  51. protected $model;
  52. /**
  53. * Create a new DataList.
  54. * No querying is done on construction, but the initial query schema is set up.
  55. *
  56. * @param string $dataClass - The DataObject class to query.
  57. */
  58. public function __construct($dataClass) {
  59. $this->dataClass = $dataClass;
  60. $this->dataQuery = new DataQuery($this->dataClass);
  61. parent::__construct();
  62. }
  63. /**
  64. * Set the DataModel
  65. *
  66. * @param DataModel $model
  67. */
  68. public function setDataModel(DataModel $model) {
  69. $this->model = $model;
  70. }
  71. /**
  72. * Get the dataClass name for this DataList, ie the DataObject ClassName
  73. *
  74. * @return string
  75. */
  76. public function dataClass() {
  77. return $this->dataClass;
  78. }
  79. /**
  80. * When cloning this object, clone the dataQuery object as well
  81. */
  82. public function __clone() {
  83. $this->dataQuery = clone $this->dataQuery;
  84. }
  85. /**
  86. * Return a copy of the internal {@link DataQuery} object
  87. *
  88. * Because the returned value is a copy, modifying it won't affect this list's contents. If
  89. * you want to alter the data query directly, use the alterDataQuery method
  90. *
  91. * @return DataQuery
  92. */
  93. public function dataQuery() {
  94. return clone $this->dataQuery;
  95. }
  96. /**
  97. * @var bool - Indicates if we are in an alterDataQueryCall already, so alterDataQuery can be re-entrant
  98. */
  99. protected $inAlterDataQueryCall = false;
  100. /**
  101. * Return a new DataList instance with the underlying {@link DataQuery} object altered
  102. *
  103. * If you want to alter the underlying dataQuery for this list, this wrapper method
  104. * will ensure that you can do so without mutating the existing List object.
  105. *
  106. * It clones this list, calls the passed callback function with the dataQuery of the new
  107. * list as it's first parameter (and the list as it's second), then returns the list
  108. *
  109. * Note that this function is re-entrant - it's safe to call this inside a callback passed to
  110. * alterDataQuery
  111. *
  112. * @param callable $callback
  113. * @return DataList
  114. * @throws Exception
  115. */
  116. public function alterDataQuery($callback) {
  117. if ($this->inAlterDataQueryCall) {
  118. $list = $this;
  119. $res = call_user_func($callback, $list->dataQuery, $list);
  120. if ($res) $list->dataQuery = $res;
  121. return $list;
  122. }
  123. else {
  124. $list = clone $this;
  125. $list->inAlterDataQueryCall = true;
  126. try {
  127. $res = call_user_func($callback, $list->dataQuery, $list);
  128. if ($res) $list->dataQuery = $res;
  129. }
  130. catch (Exception $e) {
  131. $list->inAlterDataQueryCall = false;
  132. throw $e;
  133. }
  134. $list->inAlterDataQueryCall = false;
  135. return $list;
  136. }
  137. }
  138. /**
  139. * Return a new DataList instance with the underlying {@link DataQuery} object changed
  140. *
  141. * @param DataQuery $dataQuery
  142. * @return DataList
  143. */
  144. public function setDataQuery(DataQuery $dataQuery) {
  145. $clone = clone $this;
  146. $clone->dataQuery = $dataQuery;
  147. return $clone;
  148. }
  149. /**
  150. * Returns a new DataList instance with the specified query parameter assigned
  151. *
  152. * @param string|array $keyOrArray Either the single key to set, or an array of key value pairs to set
  153. * @param mixed $val If $keyOrArray is not an array, this is the value to set
  154. * @return static
  155. */
  156. public function setDataQueryParam($keyOrArray, $val = null) {
  157. $clone = clone $this;
  158. if(is_array($keyOrArray)) {
  159. foreach($keyOrArray as $key => $val) {
  160. $clone->dataQuery->setQueryParam($key, $val);
  161. }
  162. }
  163. else {
  164. $clone->dataQuery->setQueryParam($keyOrArray, $val);
  165. }
  166. return $clone;
  167. }
  168. /**
  169. * Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-)
  170. *
  171. * @param array $parameters Out variable for parameters required for this query
  172. * @return string The resulting SQL query (may be paramaterised)
  173. */
  174. public function sql(&$parameters = array()) {
  175. return $this->dataQuery->query()->sql($parameters);
  176. }
  177. /**
  178. * Return a new DataList instance with a WHERE clause added to this list's query.
  179. *
  180. * Supports parameterised queries.
  181. * See SQLSelect::addWhere() for syntax examples, although DataList
  182. * won't expand multiple method arguments as SQLSelect does.
  183. *
  184. * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
  185. * paramaterised queries
  186. * @return DataList
  187. */
  188. public function where($filter) {
  189. return $this->alterDataQuery(function(DataQuery $query) use ($filter){
  190. $query->where($filter);
  191. });
  192. }
  193. /**
  194. * Return a new DataList instance with a WHERE clause added to this list's query.
  195. * All conditions provided in the filter will be joined with an OR
  196. *
  197. * Supports parameterised queries.
  198. * See SQLSelect::addWhere() for syntax examples, although DataList
  199. * won't expand multiple method arguments as SQLSelect does.
  200. *
  201. * @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
  202. * paramaterised queries
  203. * @return DataList
  204. */
  205. public function whereAny($filter) {
  206. return $this->alterDataQuery(function(DataQuery $query) use ($filter){
  207. $query->whereAny($filter);
  208. });
  209. }
  210. /**
  211. * Returns true if this DataList can be sorted by the given field.
  212. *
  213. * @param string $fieldName
  214. * @return boolean
  215. */
  216. public function canSortBy($fieldName) {
  217. return $this->dataQuery()->query()->canSortBy($fieldName);
  218. }
  219. /**
  220. *
  221. * @param string $fieldName
  222. * @return boolean
  223. */
  224. public function canFilterBy($fieldName) {
  225. if($t = singleton($this->dataClass)->hasDatabaseField($fieldName)){
  226. return true;
  227. }
  228. return false;
  229. }
  230. /**
  231. * Return a new DataList instance with the records returned in this query
  232. * restricted by a limit clause.
  233. *
  234. * @param int $limit
  235. * @param int $offset
  236. * @return DataList
  237. */
  238. public function limit($limit, $offset = 0) {
  239. return $this->alterDataQuery(function(DataQuery $query) use ($limit, $offset){
  240. $query->limit($limit, $offset);
  241. });
  242. }
  243. /**
  244. * Return a new DataList instance with distinct records or not
  245. *
  246. * @param bool $value
  247. * @return DataList
  248. */
  249. public function distinct($value) {
  250. return $this->alterDataQuery(function(DataQuery $query) use ($value){
  251. $query->distinct($value);
  252. });
  253. }
  254. /**
  255. * Return a new DataList instance as a copy of this data list with the sort
  256. * order set.
  257. *
  258. * @see SS_List::sort()
  259. * @see SQLSelect::orderby
  260. * @example $list = $list->sort('Name'); // default ASC sorting
  261. * @example $list = $list->sort('Name DESC'); // DESC sorting
  262. * @example $list = $list->sort('Name', 'ASC');
  263. * @example $list = $list->sort(array('Name'=>'ASC', 'Age'=>'DESC'));
  264. *
  265. * @param String|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped.
  266. * @return DataList
  267. */
  268. public function sort() {
  269. $count = func_num_args();
  270. if($count == 0) {
  271. return $this;
  272. }
  273. if($count > 2) {
  274. throw new InvalidArgumentException('This method takes zero, one or two arguments');
  275. }
  276. if ($count == 2) {
  277. $col = null;
  278. $dir = null;
  279. list($col, $dir) = func_get_args();
  280. // Validate direction
  281. if(!in_array(strtolower($dir),array('desc','asc'))){
  282. user_error('Second argument to sort must be either ASC or DESC');
  283. }
  284. $sort = array($col => $dir);
  285. }
  286. else {
  287. $sort = func_get_arg(0);
  288. }
  289. return $this->alterDataQuery(function(DataQuery $query, DataList $list) use ($sort){
  290. if(is_string($sort) && $sort){
  291. if(stristr($sort, ' asc') || stristr($sort, ' desc')) {
  292. $query->sort($sort);
  293. } else {
  294. $list->applyRelation($sort, $column, true);
  295. $query->sort($column, 'ASC');
  296. }
  297. }
  298. else if(is_array($sort)) {
  299. // sort(array('Name'=>'desc'));
  300. $query->sort(null, null); // wipe the sort
  301. foreach($sort as $column => $direction) {
  302. // Convert column expressions to SQL fragment, while still allowing the passing of raw SQL
  303. // fragments.
  304. $list->applyRelation($column, $relationColumn, true);
  305. $query->sort($relationColumn, $direction, false);
  306. }
  307. }
  308. });
  309. }
  310. /**
  311. * Return a copy of this list which only includes items with these charactaristics
  312. *
  313. * @see SS_List::filter()
  314. *
  315. * @example $list = $list->filter('Name', 'bob'); // only bob in the list
  316. * @example $list = $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
  317. * @example $list = $list->filter(array('Name'=>'bob', 'Age'=>21)); // bob with the age 21
  318. * @example $list = $list->filter(array('Name'=>'bob', 'Age'=>array(21, 43))); // bob with the Age 21 or 43
  319. * @example $list = $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
  320. * // aziz with the age 21 or 43 and bob with the Age 21 or 43
  321. *
  322. * Note: When filtering on nullable columns, null checks will be automatically added.
  323. * E.g. ->filter('Field:not', 'value) will generate '... OR "Field" IS NULL', and
  324. * ->filter('Field:not', null) will generate '"Field" IS NOT NULL'
  325. *
  326. * @todo extract the sql from $customQuery into a SQLGenerator class
  327. *
  328. * @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
  329. * @return DataList
  330. */
  331. public function filter() {
  332. // Validate and process arguments
  333. $arguments = func_get_args();
  334. switch(sizeof($arguments)) {
  335. case 1: $filters = $arguments[0]; break;
  336. case 2: $filters = array($arguments[0] => $arguments[1]); break;
  337. default:
  338. throw new InvalidArgumentException('Incorrect number of arguments passed to filter()');
  339. }
  340. return $this->addFilter($filters);
  341. }
  342. /**
  343. * Return a new instance of the list with an added filter
  344. *
  345. * @param array $filterArray
  346. * @return DataList
  347. */
  348. public function addFilter($filterArray) {
  349. $list = $this;
  350. foreach($filterArray as $field => $value) {
  351. $fieldArgs = explode(':', $field);
  352. $field = array_shift($fieldArgs);
  353. $filterType = array_shift($fieldArgs);
  354. $modifiers = $fieldArgs;
  355. $list = $list->applyFilterContext($field, $filterType, $modifiers, $value);
  356. }
  357. return $list;
  358. }
  359. /**
  360. * Return a copy of this list which contains items matching any of these charactaristics.
  361. *
  362. * @example // only bob in the list
  363. * $list = $list->filterAny('Name', 'bob');
  364. * // SQL: WHERE "Name" = 'bob'
  365. * @example // azis or bob in the list
  366. * $list = $list->filterAny('Name', array('aziz', 'bob');
  367. * // SQL: WHERE ("Name" IN ('aziz','bob'))
  368. * @example // bob or anyone aged 21 in the list
  369. * $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21));
  370. * // SQL: WHERE ("Name" = 'bob' OR "Age" = '21')
  371. * @example // bob or anyone aged 21 or 43 in the list
  372. * $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43)));
  373. * // SQL: WHERE ("Name" = 'bob' OR ("Age" IN ('21', '43'))
  374. * @example // all bobs, phils or anyone aged 21 or 43 in the list
  375. * $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
  376. * // SQL: WHERE (("Name" IN ('bob', 'phil')) OR ("Age" IN ('21', '43'))
  377. *
  378. * @todo extract the sql from this method into a SQLGenerator class
  379. *
  380. * @param string|array See {@link filter()}
  381. * @return DataList
  382. */
  383. public function filterAny() {
  384. $numberFuncArgs = count(func_get_args());
  385. $whereArguments = array();
  386. if($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
  387. $whereArguments = func_get_arg(0);
  388. } elseif($numberFuncArgs == 2) {
  389. $whereArguments[func_get_arg(0)] = func_get_arg(1);
  390. } else {
  391. throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
  392. }
  393. return $this->alterDataQuery(function(DataQuery $query) use ($whereArguments) {
  394. $subquery = $query->disjunctiveGroup();
  395. foreach($whereArguments as $field => $value) {
  396. $fieldArgs = explode(':',$field);
  397. $field = array_shift($fieldArgs);
  398. $filterType = array_shift($fieldArgs);
  399. $modifiers = $fieldArgs;
  400. if($filterType) {
  401. $className = "{$filterType}Filter";
  402. } else {
  403. $className = 'ExactMatchFilter';
  404. }
  405. if(!class_exists($className)){
  406. $className = 'ExactMatchFilter';
  407. array_unshift($modifiers, $filterType);
  408. }
  409. $filter = Injector::inst()->create($className, $field, $value, $modifiers);
  410. $filter->apply($subquery);
  411. }
  412. });
  413. }
  414. /**
  415. * Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a
  416. * future implementation.
  417. * @see SS_Filterable::filterByCallback()
  418. *
  419. * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
  420. * @param callable $callback
  421. * @return ArrayList (this may change in future implementations)
  422. */
  423. public function filterByCallback($callback) {
  424. if(!is_callable($callback)) {
  425. throw new LogicException(sprintf(
  426. "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
  427. gettype($callback)
  428. ));
  429. }
  430. /** @var ArrayList $output */
  431. $output = ArrayList::create();
  432. foreach($this as $item) {
  433. if(call_user_func($callback, $item, $this)) {
  434. $output->push($item);
  435. }
  436. }
  437. return $output;
  438. }
  439. /**
  440. * Given a field or relation name, apply it safely to this datalist.
  441. *
  442. * Unlike getRelationName, this is immutable and will fallback to the quoted field
  443. * name if not a relation.
  444. *
  445. * @param string $field Name of field or relation to apply
  446. * @param string &$columnName Quoted column name
  447. * @param bool $linearOnly Set to true to restrict to linear relations only. Set this
  448. * if this relation will be used for sorting, and should not include duplicate rows.
  449. * @return DataList DataList with this relation applied
  450. */
  451. public function applyRelation($field, &$columnName = null, $linearOnly = false) {
  452. // If field is invalid, return it without modification
  453. if(!$this->isValidRelationName($field)) {
  454. $columnName = $field;
  455. return $this;
  456. }
  457. // Simple fields without relations are mapped directly
  458. if(strpos($field,'.') === false) {
  459. $columnName = '"'.$field.'"';
  460. return $this;
  461. }
  462. return $this->alterDataQuery(
  463. function(DataQuery $query) use ($field, &$columnName, $linearOnly) {
  464. $relations = explode('.', $field);
  465. $fieldName = array_pop($relations);
  466. // Apply
  467. $relationModelName = $query->applyRelation($relations, $linearOnly);
  468. // Find the db field the relation belongs to
  469. $columnName = DataObject::getSchema()->sqlColumnForField($relationModelName, $fieldName);
  470. }
  471. );
  472. }
  473. /**
  474. * Check if the given field specification could be interpreted as an unquoted relation name
  475. *
  476. * @param string $field
  477. * @return bool
  478. */
  479. protected function isValidRelationName($field) {
  480. return preg_match('/^[A-Z0-9._]+$/i', $field);
  481. }
  482. /**
  483. * Translates a filter type to a SQL query.
  484. *
  485. * @param string $field - the fieldname in the db
  486. * @param string $filter - example StartsWith, relates to a filtercontext
  487. * @param array $modifiers - Modifiers to pass to the filter, ie not,nocase
  488. * @param string $value - the value that the filtercontext will use for matching
  489. * @return DataList
  490. */
  491. private function applyFilterContext($field, $filter, $modifiers, $value) {
  492. if($filter) {
  493. $className = "{$filter}Filter";
  494. } else {
  495. $className = 'ExactMatchFilter';
  496. }
  497. if(!class_exists($className)) {
  498. $className = 'ExactMatchFilter';
  499. array_unshift($modifiers, $filter);
  500. }
  501. $t = new $className($field, $value, $modifiers);
  502. return $this->alterDataQuery(array($t, 'apply'));
  503. }
  504. /**
  505. * Return a copy of this list which does not contain any items with these charactaristics
  506. *
  507. * @see SS_List::exclude()
  508. * @example $list = $list->exclude('Name', 'bob'); // exclude bob from list
  509. * @example $list = $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
  510. * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
  511. * @example $list = $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
  512. * @example $list = $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
  513. * // bob age 21 or 43, phil age 21 or 43 would be excluded
  514. *
  515. * @todo extract the sql from this method into a SQLGenerator class
  516. *
  517. * @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
  518. * @return DataList
  519. */
  520. public function exclude() {
  521. $numberFuncArgs = count(func_get_args());
  522. $whereArguments = array();
  523. if($numberFuncArgs == 1 && is_array(func_get_arg(0))) {
  524. $whereArguments = func_get_arg(0);
  525. } elseif($numberFuncArgs == 2) {
  526. $whereArguments[func_get_arg(0)] = func_get_arg(1);
  527. } else {
  528. throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
  529. }
  530. return $this->alterDataQuery(function(DataQuery $query) use ($whereArguments) {
  531. $subquery = $query->disjunctiveGroup();
  532. foreach($whereArguments as $field => $value) {
  533. $fieldArgs = explode(':', $field);
  534. $field = array_shift($fieldArgs);
  535. $filterType = array_shift($fieldArgs);
  536. $modifiers = $fieldArgs;
  537. if($filterType) {
  538. $className = "{$filterType}Filter";
  539. } else {
  540. $className = 'ExactMatchFilter';
  541. }
  542. if(!class_exists($className)){
  543. $className = 'ExactMatchFilter';
  544. array_unshift($modifiers, $filterType);
  545. }
  546. $filter = Injector::inst()->create($className, $field, $value, $modifiers);
  547. $filter->exclude($subquery);
  548. }
  549. });
  550. }
  551. /**
  552. * This method returns a copy of this list that does not contain any DataObjects that exists in $list
  553. *
  554. * The $list passed needs to contain the same dataclass as $this
  555. *
  556. * @param DataList $list
  557. * @return DataList
  558. * @throws BadMethodCallException
  559. */
  560. public function subtract(DataList $list) {
  561. if($this->dataClass() != $list->dataClass()) {
  562. throw new InvalidArgumentException('The list passed must have the same dataclass as this class');
  563. }
  564. return $this->alterDataQuery(function(DataQuery $query) use ($list){
  565. $query->subtract($list->dataQuery());
  566. });
  567. }
  568. /**
  569. * Return a new DataList instance with an inner join clause added to this list's query.
  570. *
  571. * @param string $table Table name (unquoted and as escaped SQL)
  572. * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
  573. * @param string $alias - if you want this table to be aliased under another name
  574. * @param int $order A numerical index to control the order that joins are added to the query; lower order values
  575. * will cause the query to appear first. The default is 20, and joins created automatically by the
  576. * ORM have a value of 10.
  577. * @param array $parameters Any additional parameters if the join is a parameterised subquery
  578. * @return DataList
  579. */
  580. public function innerJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) {
  581. return $this->alterDataQuery(function(DataQuery $query) use ($table, $onClause, $alias, $order, $parameters){
  582. $query->innerJoin($table, $onClause, $alias, $order, $parameters);
  583. });
  584. }
  585. /**
  586. * Return a new DataList instance with a left join clause added to this list's query.
  587. *
  588. * @param string $table Table name (unquoted and as escaped SQL)
  589. * @param string $onClause Escaped SQL statement, e.g. '"Table1"."ID" = "Table2"."ID"'
  590. * @param string $alias - if you want this table to be aliased under another name
  591. * @param int $order A numerical index to control the order that joins are added to the query; lower order values
  592. * will cause the query to appear first. The default is 20, and joins created automatically by the
  593. * ORM have a value of 10.
  594. * @param array $parameters Any additional parameters if the join is a parameterised subquery
  595. * @return DataList
  596. */
  597. public function leftJoin($table, $onClause, $alias = null, $order = 20, $parameters = array()) {
  598. return $this->alterDataQuery(function(DataQuery $query) use ($table, $onClause, $alias, $order, $parameters){
  599. $query->leftJoin($table, $onClause, $alias, $order, $parameters);
  600. });
  601. }
  602. /**
  603. * Return an array of the actual items that this DataList contains at this stage.
  604. * This is when the query is actually executed.
  605. *
  606. * @return array
  607. */
  608. public function toArray() {
  609. $query = $this->dataQuery->query();
  610. $rows = $query->execute();
  611. $results = array();
  612. foreach($rows as $row) {
  613. $results[] = $this->createDataObject($row);
  614. }
  615. return $results;
  616. }
  617. /**
  618. * Return this list as an array and every object it as an sub array as well
  619. *
  620. * @return array
  621. */
  622. public function toNestedArray() {
  623. $result = array();
  624. foreach($this as $item) {
  625. $result[] = $item->toMap();
  626. }
  627. return $result;
  628. }
  629. /**
  630. * Walks the list using the specified callback
  631. *
  632. * @param callable $callback
  633. * @return DataList
  634. */
  635. public function each($callback) {
  636. foreach($this as $row) {
  637. $callback($row);
  638. }
  639. return $this;
  640. }
  641. public function debug() {
  642. $val = "<h2>" . $this->class . "</h2><ul>";
  643. foreach($this->toNestedArray() as $item) {
  644. $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
  645. }
  646. $val .= "</ul>";
  647. return $val;
  648. }
  649. /**
  650. * Returns a map of this list
  651. *
  652. * @param string $keyField - the 'key' field of the result array
  653. * @param string $titleField - the value field of the result array
  654. * @return SS_Map
  655. */
  656. public function map($keyField = 'ID', $titleField = 'Title') {
  657. return new SS_Map($this, $keyField, $titleField);
  658. }
  659. /**
  660. * Create a DataObject from the given SQL row
  661. *
  662. * @param array $row
  663. * @return DataObject
  664. */
  665. protected function createDataObject($row) {
  666. $class = $this->dataClass;
  667. // Failover from RecordClassName to ClassName
  668. if(empty($row['RecordClassName'])) {
  669. $row['RecordClassName'] = $row['ClassName'];
  670. }
  671. // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
  672. if(class_exists($row['RecordClassName'])) {
  673. $class = $row['RecordClassName'];
  674. }
  675. $item = Injector::inst()->create($class, $row, false, $this->model, $this->getQueryParams());
  676. return $item;
  677. }
  678. /**
  679. * Get query parameters for this list.
  680. * These values will be assigned as query parameters to newly created objects from this list.
  681. *
  682. * @return array
  683. */
  684. public function getQueryParams() {
  685. return $this->dataQuery()->getQueryParams();
  686. }
  687. /**
  688. * Returns an Iterator for this DataList.
  689. * This function allows you to use DataLists in foreach loops
  690. *
  691. * @return ArrayIterator
  692. */
  693. public function getIterator() {
  694. return new ArrayIterator($this->toArray());
  695. }
  696. /**
  697. * Return the number of items in this DataList
  698. *
  699. * @return int
  700. */
  701. public function count() {
  702. return $this->dataQuery->count();
  703. }
  704. /**
  705. * Return the maximum value of the given field in this DataList
  706. *
  707. * @param string $fieldName
  708. * @return mixed
  709. */
  710. public function max($fieldName) {
  711. return $this->dataQuery->max($fieldName);
  712. }
  713. /**
  714. * Return the minimum value of the given field in this DataList
  715. *
  716. * @param string $fieldName
  717. * @return mixed
  718. */
  719. public function min($fieldName) {
  720. return $this->dataQuery->min($fieldName);
  721. }
  722. /**
  723. * Return the average value of the given field in this DataList
  724. *
  725. * @param string $fieldName
  726. * @return mixed
  727. */
  728. public function avg($fieldName) {
  729. return $this->dataQuery->avg($fieldName);
  730. }
  731. /**
  732. * Return the sum of the values of the given field in this DataList
  733. *
  734. * @param string $fieldName
  735. * @return mixed
  736. */
  737. public function sum($fieldName) {
  738. return $this->dataQuery->sum($fieldName);
  739. }
  740. /**
  741. * Returns the first item in this DataList
  742. *
  743. * @return DataObject
  744. */
  745. public function first() {
  746. foreach($this->dataQuery->firstRow()->execute() as $row) {
  747. return $this->createDataObject($row);
  748. }
  749. return null;
  750. }
  751. /**
  752. * Returns the last item in this DataList
  753. *
  754. * @return DataObject
  755. */
  756. public function last() {
  757. foreach($this->dataQuery->lastRow()->execute() as $row) {
  758. return $this->createDataObject($row);
  759. }
  760. return null;
  761. }
  762. /**
  763. * Returns true if this DataList has items
  764. *
  765. * @return bool
  766. */
  767. public function exists() {
  768. return $this->count() > 0;
  769. }
  770. /**
  771. * Find the first DataObject of this DataList where the given key = value
  772. *
  773. * @param string $key
  774. * @param string $value
  775. * @return DataObject|null
  776. */
  777. public function find($key, $value) {
  778. return $this->filter($key, $value)->first();
  779. }
  780. /**
  781. * Restrict the columns to fetch into this DataList
  782. *
  783. * @param array $queriedColumns
  784. * @return DataList
  785. */
  786. public function setQueriedColumns($queriedColumns) {
  787. return $this->alterDataQuery(function(DataQuery $query) use ($queriedColumns){
  788. $query->setQueriedColumns($queriedColumns);
  789. });
  790. }
  791. /**
  792. * Filter this list to only contain the given Primary IDs
  793. *
  794. * @param array $ids Array of integers
  795. * @return DataList
  796. */
  797. public function byIDs($ids) {
  798. return $this->filter('ID', $ids);
  799. }
  800. /**
  801. * Return the first DataObject with the given ID
  802. *
  803. * @param int $id
  804. * @return DataObject
  805. */
  806. public function byID($id) {
  807. return $this->filter('ID', $id)->first();
  808. }
  809. /**
  810. * Returns an array of a single field value for all items in the list.
  811. *
  812. * @param string $colName
  813. * @return array
  814. */
  815. public function column($colName = "ID") {
  816. return $this->dataQuery->column($colName);
  817. }
  818. // Member altering methods
  819. /**
  820. * Sets the ComponentSet to be the given ID list.
  821. * Records will be added and deleted as appropriate.
  822. *
  823. * @param array $idList List of IDs.
  824. */
  825. public function setByIDList($idList) {
  826. $has = array();
  827. // Index current data
  828. foreach($this->column() as $id) {
  829. $has[$id] = true;
  830. }
  831. // Keep track of items to delete
  832. $itemsToDelete = $has;
  833. // add items in the list
  834. // $id is the database ID of the record
  835. if($idList) foreach($idList as $id) {
  836. unset($itemsToDelete[$id]);
  837. if($id && !isset($has[$id])) {
  838. $this->add($id);
  839. }
  840. }
  841. // Remove any items that haven't been mentioned
  842. $this->removeMany(array_keys($itemsToDelete));
  843. }
  844. /**
  845. * Returns an array with both the keys and values set to the IDs of the records in this list.
  846. * Does not respect sort order. Use ->column("ID") to get an ID list with the current sort.
  847. *
  848. * @return array
  849. */
  850. public function getIDList() {
  851. $ids = $this->column("ID");
  852. return $ids ? array_combine($ids, $ids) : array();
  853. }
  854. /**
  855. * Returns a HasManyList or ManyMany list representing the querying of a relation across all
  856. * objects in this data list. For it to work, the relation must be defined on the data class
  857. * that you used to create this DataList.
  858. *
  859. * Example: Get members from all Groups:
  860. *
  861. * DataList::Create("Group")->relation("Members")
  862. *
  863. * @param string $relationName
  864. * @return HasManyList|ManyManyList
  865. */
  866. public function relation($relationName) {
  867. $ids = $this->column('ID');
  868. return singleton($this->dataClass)->$relationName()->forForeignID($ids);
  869. }
  870. public function dbObject($fieldName) {
  871. return singleton($this->dataClass)->dbObject($fieldName);
  872. }
  873. /**
  874. * Add a number of items to the component set.
  875. *
  876. * @param array $items Items to add, as either DataObjects or IDs.
  877. * @return DataList
  878. */
  879. public function addMany($items) {
  880. foreach($items as $item) {
  881. $this->add($item);
  882. }
  883. return $this;
  884. }
  885. /**
  886. * Remove the items from this list with the given IDs
  887. *
  888. * @param array $idList
  889. * @return DataList
  890. */
  891. public function removeMany($idList) {
  892. foreach($idList as $id) {
  893. $this->removeByID($id);
  894. }
  895. return $this;
  896. }
  897. /**
  898. * Remove every element in this DataList matching the given $filter.
  899. *
  900. * @param string $filter - a sql type where filter
  901. * @return DataList
  902. */
  903. public function removeByFilter($filter) {
  904. foreach($this->where($filter) as $item) {
  905. $this->remove($item);
  906. }
  907. return $this;
  908. }
  909. /**
  910. * Remove every element in this DataList.
  911. *
  912. * @return DataList
  913. */
  914. public function removeAll() {
  915. foreach($this as $item) {
  916. $this->remove($item);
  917. }
  918. return $this;
  919. }
  920. /**
  921. * This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
  922. * list manipulation
  923. *
  924. * @param mixed $item
  925. */
  926. public function add($item) {
  927. // Nothing needs to happen by default
  928. // TO DO: If a filter is given to this data list then
  929. }
  930. /**
  931. * Return a new item to add to this DataList.
  932. *
  933. * @todo This doesn't factor in filters.
  934. * @param array $initialFields
  935. * @return DataObject
  936. */
  937. public function newObject($initialFields = null) {
  938. $class = $this->dataClass;
  939. return Injector::inst()->create($class, $initialFields, false, $this->model);
  940. }
  941. /**
  942. * Remove this item by deleting it
  943. *
  944. * @param DataObject $item
  945. * @todo Allow for amendment of this behaviour - for example, we can remove an item from
  946. * an "ActiveItems" DataList by chaning the status to inactive.
  947. */
  948. public function remove($item) {
  949. // By default, we remove an item from a DataList by deleting it.
  950. $this->removeByID($item->ID);
  951. }
  952. /**
  953. * Remove an item from this DataList by ID
  954. *
  955. * @param int $itemID The primary ID
  956. */
  957. public function removeByID($itemID) {
  958. $item = $this->byID($itemID);
  959. if($item) {
  960. $item->delete();
  961. }
  962. }
  963. /**
  964. * Reverses a list of items.
  965. *
  966. * @return DataList
  967. */
  968. public function reverse() {
  969. return $this->alterDataQuery(function(DataQuery $query){
  970. $query->reverseSort();
  971. });
  972. }
  973. /**
  974. * Returns whether an item with $key exists
  975. *
  976. * @param mixed $key
  977. * @return bool
  978. */
  979. public function offsetExists($key) {
  980. return ($this->limit(1,$key)->first() != null);
  981. }
  982. /**
  983. * Returns item stored in list with index $key
  984. *
  985. * @param mixed $key
  986. * @return DataObject
  987. */
  988. public function offsetGet($key) {
  989. return $this->limit(1, $key)->first();
  990. }
  991. /**
  992. * Set an item with the key in $key
  993. *
  994. * @param mixed $key
  995. * @param mixed $value
  996. */
  997. public function offsetSet($key, $value) {
  998. user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
  999. }
  1000. /**
  1001. * Unset an item with the key in $key
  1002. *
  1003. * @param mixed $key
  1004. */
  1005. public function offsetUnset($key) {
  1006. user_error("Can't alter items in a DataList using array-access", E_USER_ERROR);
  1007. }
  1008. }