PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/model/ArrayList.php

https://github.com/daslicht/SilverStripe-cms-v3.1.5
PHP | 639 lines | 426 code | 43 blank | 170 comment | 19 complexity | d0a540e548275f8c17cb4fbe468b920d MD5 | raw file
Possible License(s): GPL-2.0, AGPL-1.0, LGPL-2.1, MIT, BSD-3-Clause
  1. <?php
  2. /**
  3. * A list object that wraps around an array of objects or arrays.
  4. *
  5. * Note that (like DataLists), the implementations of the methods from SS_Filterable, SS_Sortable and
  6. * SS_Limitable return a new instance of ArrayList, rather than modifying the existing instance.
  7. *
  8. * For easy reference, methods that operate in this way are:
  9. *
  10. * - limit
  11. * - reverse
  12. * - sort
  13. * - filter
  14. * - exclude
  15. *
  16. * @package framework
  17. * @subpackage model
  18. */
  19. class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sortable, SS_Limitable {
  20. /**
  21. * Holds the items in the list
  22. *
  23. * @var array
  24. */
  25. protected $items = array();
  26. /**
  27. *
  28. * @param array $items - an initial array to fill this object with
  29. */
  30. public function __construct(array $items = array()) {
  31. $this->items = array_values($items);
  32. parent::__construct();
  33. }
  34. /**
  35. * Return the class of items in this list, by looking at the first item inside it.
  36. */
  37. public function dataClass() {
  38. if(count($this->items) > 0) return get_class($this->items[0]);
  39. }
  40. /**
  41. * Return the number of items in this list
  42. *
  43. * @return int
  44. */
  45. public function count() {
  46. return count($this->items);
  47. }
  48. /**
  49. * Returns true if this list has items
  50. *
  51. * @return bool
  52. */
  53. public function exists() {
  54. return (bool) count($this);
  55. }
  56. /**
  57. * Returns an Iterator for this ArrayList.
  58. * This function allows you to use ArrayList in foreach loops
  59. *
  60. * @return ArrayIterator
  61. */
  62. public function getIterator() {
  63. foreach($this->items as $i => $item) {
  64. if(is_array($item)) $this->items[$i] = new ArrayData($item);
  65. }
  66. return new ArrayIterator($this->items);
  67. }
  68. /**
  69. * Return an array of the actual items that this ArrayList contains.
  70. *
  71. * @return array
  72. */
  73. public function toArray() {
  74. return $this->items;
  75. }
  76. /**
  77. * Walks the list using the specified callback
  78. *
  79. * @param callable $callback
  80. * @return DataList
  81. */
  82. public function each($callback) {
  83. foreach($this as $item) {
  84. $callback($item);
  85. }
  86. }
  87. public function debug() {
  88. $val = "<h2>" . $this->class . "</h2><ul>";
  89. foreach($this->toNestedArray() as $item) {
  90. $val .= "<li style=\"list-style-type: disc; margin-left: 20px\">" . Debug::text($item) . "</li>";
  91. }
  92. $val .= "</ul>";
  93. return $val;
  94. }
  95. /**
  96. * Return this list as an array and every object it as an sub array as well
  97. *
  98. * @return array
  99. */
  100. public function toNestedArray() {
  101. $result = array();
  102. foreach ($this->items as $item) {
  103. if (is_object($item)) {
  104. if (method_exists($item, 'toMap')) {
  105. $result[] = $item->toMap();
  106. } else {
  107. $result[] = (array) $item;
  108. }
  109. } else {
  110. $result[] = $item;
  111. }
  112. }
  113. return $result;
  114. }
  115. /**
  116. * Get a sub-range of this dataobjectset as an array
  117. *
  118. * @param int $offset
  119. * @param int $length
  120. * @return ArrayList
  121. */
  122. public function limit($length, $offset = 0) {
  123. if(!$length) {
  124. $length = count($this->items);
  125. }
  126. $list = clone $this;
  127. $list->items = array_slice($this->items, $offset, $length);
  128. return $list;
  129. }
  130. /**
  131. * Add this $item into this list
  132. *
  133. * @param mixed $item
  134. */
  135. public function add($item) {
  136. $this->push($item);
  137. }
  138. /**
  139. * Remove this item from this list
  140. *
  141. * @param mixed $item
  142. */
  143. public function remove($item) {
  144. $renumberKeys = false;
  145. foreach ($this->items as $key => $value) {
  146. if ($item === $value) {
  147. $renumberKeys = true;
  148. unset($this->items[$key]);
  149. }
  150. }
  151. if($renumberKeys) $this->items = array_values($this->items);
  152. }
  153. /**
  154. * Replaces an item in this list with another item.
  155. *
  156. * @param array|object $item
  157. * @param array|object $with
  158. * @return void;
  159. */
  160. public function replace($item, $with) {
  161. foreach ($this->items as $key => $candidate) {
  162. if ($candidate === $item) {
  163. $this->items[$key] = $with;
  164. return;
  165. }
  166. }
  167. }
  168. /**
  169. * Merges with another array or list by pushing all the items in it onto the
  170. * end of this list.
  171. *
  172. * @param array|object $with
  173. */
  174. public function merge($with) {
  175. foreach ($with as $item) $this->push($item);
  176. }
  177. /**
  178. * Removes items from this list which have a duplicate value for a certain
  179. * field. This is especially useful when combining lists.
  180. *
  181. * @param string $field
  182. */
  183. public function removeDuplicates($field = 'ID') {
  184. $seen = array();
  185. $renumberKeys = false;
  186. foreach ($this->items as $key => $item) {
  187. $value = $this->extractValue($item, $field);
  188. if (array_key_exists($value, $seen)) {
  189. $renumberKeys = true;
  190. unset($this->items[$key]);
  191. }
  192. $seen[$value] = true;
  193. }
  194. if($renumberKeys) $this->items = array_values($this->items);
  195. }
  196. /**
  197. * Pushes an item onto the end of this list.
  198. *
  199. * @param array|object $item
  200. */
  201. public function push($item) {
  202. $this->items[] = $item;
  203. }
  204. /**
  205. * Pops the last element off the end of the list and returns it.
  206. *
  207. * @return array|object
  208. */
  209. public function pop() {
  210. return array_pop($this->items);
  211. }
  212. /**
  213. * Add an item onto the beginning of the list.
  214. *
  215. * @param array|object $item
  216. */
  217. public function unshift($item) {
  218. array_unshift($this->items, $item);
  219. }
  220. /**
  221. * Shifts the item off the beginning of the list and returns it.
  222. *
  223. * @return array|object
  224. */
  225. public function shift() {
  226. return array_shift($this->items);
  227. }
  228. /**
  229. * Returns the first item in the list
  230. *
  231. * @return mixed
  232. */
  233. public function first() {
  234. return reset($this->items);
  235. }
  236. /**
  237. * Returns the last item in the list
  238. *
  239. * @return mixed
  240. */
  241. public function last() {
  242. return end($this->items);
  243. }
  244. /**
  245. * Returns a map of this list
  246. *
  247. * @param type $keyfield - the 'key' field of the result array
  248. * @param type $titlefield - the value field of the result array
  249. * @return array
  250. */
  251. public function map($keyfield = 'ID', $titlefield = 'Title') {
  252. $map = array();
  253. foreach ($this->items as $item) {
  254. $map[$this->extractValue($item, $keyfield)] = $this->extractValue(
  255. $item,
  256. $titlefield
  257. );
  258. }
  259. return $map;
  260. }
  261. /**
  262. * Find the first item of this list where the given key = value
  263. *
  264. * @param type $key
  265. * @param type $value
  266. * @return type
  267. */
  268. public function find($key, $value) {
  269. foreach ($this->items as $item) {
  270. if ($this->extractValue($item, $key) == $value) {
  271. return $item;
  272. }
  273. }
  274. }
  275. /**
  276. * Returns an array of a single field value for all items in the list.
  277. *
  278. * @param string $colName
  279. * @return array
  280. */
  281. public function column($colName = 'ID') {
  282. $result = array();
  283. foreach ($this->items as $item) {
  284. $result[] = $this->extractValue($item, $colName);
  285. }
  286. return $result;
  287. }
  288. /**
  289. * You can always sort a ArrayList
  290. *
  291. * @param string $by
  292. * @return bool
  293. */
  294. public function canSortBy($by) {
  295. return true;
  296. }
  297. /**
  298. * Reverses an {@link ArrayList}
  299. *
  300. * @return ArrayList
  301. */
  302. public function reverse() {
  303. $list = clone $this;
  304. $list->items = array_reverse($this->items);
  305. return $list;
  306. }
  307. /**
  308. * Sorts this list by one or more fields. You can either pass in a single
  309. * field name and direction, or a map of field names to sort directions.
  310. *
  311. * @return DataList
  312. * @see SS_List::sort()
  313. * @example $list->sort('Name'); // default ASC sorting
  314. * @example $list->sort('Name DESC'); // DESC sorting
  315. * @example $list->sort('Name', 'ASC');
  316. * @example $list->sort(array('Name'=>'ASC,'Age'=>'DESC'));
  317. */
  318. public function sort() {
  319. $args = func_get_args();
  320. if(count($args)==0){
  321. return $this;
  322. }
  323. if(count($args)>2){
  324. throw new InvalidArgumentException('This method takes zero, one or two arguments');
  325. }
  326. // One argument and it's a string
  327. if(count($args)==1 && is_string($args[0])){
  328. $column = $args[0];
  329. if(strpos($column, ' ') !== false) {
  330. throw new InvalidArgumentException("You can't pass SQL fragments to sort()");
  331. }
  332. $columnsToSort[$column] = SORT_ASC;
  333. } else if(count($args)==2){
  334. $columnsToSort[$args[0]]=(strtolower($args[1])=='desc')?SORT_DESC:SORT_ASC;
  335. } else if(is_array($args[0])) {
  336. foreach($args[0] as $column => $sort_order){
  337. $columnsToSort[$column] = (strtolower($sort_order)=='desc')?SORT_DESC:SORT_ASC;
  338. }
  339. } else {
  340. throw new InvalidArgumentException("Bad arguments passed to sort()");
  341. }
  342. // This the main sorting algorithm that supports infinite sorting params
  343. $multisortArgs = array();
  344. $values = array();
  345. foreach($columnsToSort as $column => $direction ) {
  346. // The reason these are added to columns is of the references, otherwise when the foreach
  347. // is done, all $values and $direction look the same
  348. $values[$column] = array();
  349. $sortDirection[$column] = $direction;
  350. // We need to subtract every value into a temporary array for sorting
  351. foreach($this->items as $index => $item) {
  352. $values[$column][] = $this->extractValue($item, $column);
  353. }
  354. // PHP 5.3 requires below arguments to be reference when using array_multisort together
  355. // with call_user_func_array
  356. // First argument is the 'value' array to be sorted
  357. $multisortArgs[] = &$values[$column];
  358. // First argument is the direction to be sorted,
  359. $multisortArgs[] = &$sortDirection[$column];
  360. }
  361. $list = clone $this;
  362. // As the last argument we pass in a reference to the items that all the sorting will be applied upon
  363. $multisortArgs[] = &$list->items;
  364. call_user_func_array('array_multisort', $multisortArgs);
  365. return $list;
  366. }
  367. /**
  368. * Returns true if the given column can be used to filter the records.
  369. *
  370. * It works by checking the fields available in the first record of the list.
  371. */
  372. public function canFilterBy($by) {
  373. $firstRecord = $this->first();
  374. if ($firstRecord === false) {
  375. return false;
  376. }
  377. return array_key_exists($by, $firstRecord);
  378. }
  379. /**
  380. * Filter the list to include items with these charactaristics
  381. *
  382. * @return ArrayList
  383. * @see SS_List::filter()
  384. * @example $list->filter('Name', 'bob'); // only bob in the list
  385. * @example $list->filter('Name', array('aziz', 'bob'); // aziz and bob in list
  386. * @example $list->filter(array('Name'=>'bob, 'Age'=>21)); // bob with the Age 21 in list
  387. * @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43
  388. * @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43)));
  389. * // aziz with the age 21 or 43 and bob with the Age 21 or 43
  390. */
  391. public function filter() {
  392. if(count(func_get_args())>2){
  393. throw new InvalidArgumentException('filter takes one array or two arguments');
  394. }
  395. if(count(func_get_args()) == 1 && !is_array(func_get_arg(0))){
  396. throw new InvalidArgumentException('filter takes one array or two arguments');
  397. }
  398. $keepUs = array();
  399. if(count(func_get_args())==2){
  400. $keepUs[func_get_arg(0)] = func_get_arg(1);
  401. }
  402. if(count(func_get_args())==1 && is_array(func_get_arg(0))){
  403. foreach(func_get_arg(0) as $column => $value) {
  404. $keepUs[$column] = $value;
  405. }
  406. }
  407. $itemsToKeep = array();
  408. foreach($this->items as $item){
  409. $keepItem = true;
  410. foreach($keepUs as $column => $value ) {
  411. if(is_array($value) && !in_array($this->extractValue($item, $column), $value)) {
  412. $keepItem = false;
  413. } elseif(!is_array($value) && $this->extractValue($item, $column) != $value) {
  414. $keepItem = false;
  415. }
  416. }
  417. if($keepItem) {
  418. $itemsToKeep[] = $item;
  419. }
  420. }
  421. $list = clone $this;
  422. $list->items = $itemsToKeep;
  423. return $list;
  424. }
  425. public function byID($id) {
  426. $firstElement = $this->filter("ID", $id)->first();
  427. if ($firstElement === false) {
  428. return null;
  429. }
  430. return $firstElement;
  431. }
  432. /**
  433. * @see SS_Filterable::filterByCallback()
  434. *
  435. * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; })
  436. * @param callable $callback
  437. * @return ArrayList
  438. */
  439. public function filterByCallback($callback) {
  440. if(!is_callable($callback)) {
  441. throw new LogicException(sprintf(
  442. "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given",
  443. gettype($callback)
  444. ));
  445. }
  446. $output = ArrayList::create();
  447. foreach($this as $item) {
  448. if(call_user_func($callback, $item, $this)) $output->push($item);
  449. }
  450. return $output;
  451. }
  452. /**
  453. * Exclude the list to not contain items with these charactaristics
  454. *
  455. * @return ArrayList
  456. * @see SS_List::exclude()
  457. * @example $list->exclude('Name', 'bob'); // exclude bob from list
  458. * @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list
  459. * @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21
  460. * @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43
  461. * @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43)));
  462. * // bob age 21 or 43, phil age 21 or 43 would be excluded
  463. */
  464. public function exclude() {
  465. if(count(func_get_args())>2){
  466. throw new InvalidArgumentException('exclude() takes one array or two arguments');
  467. }
  468. if(count(func_get_args()) == 1 && !is_array(func_get_arg(0))){
  469. throw new InvalidArgumentException('exclude() takes one array or two arguments');
  470. }
  471. $removeUs = array();
  472. if(count(func_get_args())==2){
  473. $removeUs[func_get_arg(0)] = func_get_arg(1);
  474. }
  475. if(count(func_get_args())==1 && is_array(func_get_arg(0))){
  476. foreach(func_get_arg(0) as $column => $excludeValue) {
  477. $removeUs[$column] = $excludeValue;
  478. }
  479. }
  480. $hitsRequiredToRemove = count($removeUs);
  481. $matches = array();
  482. foreach($removeUs as $column => $excludeValue) {
  483. foreach($this->items as $key => $item){
  484. if(!is_array($excludeValue) && $this->extractValue($item, $column) == $excludeValue) {
  485. $matches[$key]=isset($matches[$key])?$matches[$key]+1:1;
  486. } elseif(is_array($excludeValue) && in_array($this->extractValue($item, $column), $excludeValue)) {
  487. $matches[$key]=isset($matches[$key])?$matches[$key]+1:1;
  488. }
  489. }
  490. }
  491. $keysToRemove = array_keys($matches,$hitsRequiredToRemove);
  492. $itemsToKeep = array();
  493. foreach($this->items as $key => $value) {
  494. if(!in_array($key, $keysToRemove)) {
  495. $itemsToKeep[] = $value;
  496. }
  497. }
  498. $list = clone $this;
  499. $list->items = $itemsToKeep;
  500. return $list;
  501. }
  502. protected function shouldExclude($item, $args) {
  503. }
  504. /**
  505. * Returns whether an item with $key exists
  506. *
  507. * @param mixed $key
  508. * @return bool
  509. */
  510. public function offsetExists($offset) {
  511. return array_key_exists($offset, $this->items);
  512. }
  513. /**
  514. * Returns item stored in list with index $key
  515. *
  516. * @param mixed $key
  517. * @return DataObject
  518. */
  519. public function offsetGet($offset) {
  520. if ($this->offsetExists($offset)) return $this->items[$offset];
  521. }
  522. /**
  523. * Set an item with the key in $key
  524. *
  525. * @param mixed $key
  526. * @param mixed $value
  527. */
  528. public function offsetSet($offset, $value) {
  529. if($offset == null) {
  530. $this->items[] = $value;
  531. } else {
  532. $this->items[$offset] = $value;
  533. }
  534. }
  535. /**
  536. * Unset an item with the key in $key
  537. *
  538. * @param mixed $key
  539. */
  540. public function offsetUnset($offset) {
  541. unset($this->items[$offset]);
  542. }
  543. /**
  544. * Extracts a value from an item in the list, where the item is either an
  545. * object or array.
  546. *
  547. * @param array|object $item
  548. * @param string $key
  549. * @return mixed
  550. */
  551. protected function extractValue($item, $key) {
  552. if (is_object($item)) {
  553. return $item->$key;
  554. } else {
  555. if (array_key_exists($key, $item)) return $item[$key];
  556. }
  557. }
  558. }