PageRenderTime 27ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/model/ArrayList.php

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