PageRenderTime 135ms CodeModel.GetById 17ms RepoModel.GetById 2ms app.codeStats 0ms

/framework/model/DataList.php

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