PageRenderTime 46ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/kirby/toolkit/lib/collection.php

https://gitlab.com/gricelya/Blog-Merry
PHP | 601 lines | 295 code | 108 blank | 198 comment | 50 complexity | 4d78cf233761140311001e9e6e904827 MD5 | raw file
  1. <?php
  2. if(!defined('SORT_NATURAL')) define('SORT_NATURAL', 'natural');
  3. /**
  4. * Collection
  5. *
  6. * @package Kirby Toolkit
  7. * @author Bastian Allgeier <bastian@getkirby.com>
  8. * @link http://getkirby.com
  9. * @copyright Bastian Allgeier
  10. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  11. */
  12. class Collection extends I {
  13. static public $filters = array();
  14. protected $pagination;
  15. /**
  16. * Returns a slice of the collection
  17. *
  18. * @param int $offset The optional index to start the slice from
  19. * @param int $limit The optional number of elements to return
  20. * @return Collection
  21. */
  22. public function slice($offset = null, $limit = null) {
  23. if($offset === null and $limit === null) return $this;
  24. $collection = clone $this;
  25. $collection->data = array_slice($collection->data, $offset, $limit);
  26. return $collection;
  27. }
  28. /**
  29. * Returns a new collection with a limited number of elements
  30. *
  31. * @param int $limit The number of elements to return
  32. * @return Collection
  33. */
  34. public function limit($limit) {
  35. return $this->slice(0, $limit);
  36. }
  37. /**
  38. * Returns a new collection starting from the given offset
  39. *
  40. * @param int $offset The index to start from
  41. * @return Collection
  42. */
  43. public function offset($offset) {
  44. return $this->slice($offset);
  45. }
  46. /**
  47. * Returns the array in reverse order
  48. *
  49. * @return Collection
  50. */
  51. public function flip() {
  52. $collection = clone $this;
  53. $collection->data = array_reverse($collection->data, true);
  54. return $collection;
  55. }
  56. /**
  57. * Counts all elements in the array
  58. *
  59. * @return int
  60. */
  61. public function count() {
  62. return count($this->data);
  63. }
  64. /**
  65. * Returns the first element from the array
  66. *
  67. * @return mixed
  68. */
  69. public function first() {
  70. $array = $this->data;
  71. return array_shift($array);
  72. }
  73. /**
  74. * Returns the last element from the array
  75. *
  76. * @return mixed
  77. */
  78. public function last() {
  79. $array = $this->data;
  80. return array_pop($array);
  81. }
  82. /**
  83. * Returns the nth element from the array
  84. *
  85. * @return mixed
  86. */
  87. public function nth($n) {
  88. $array = array_values($this->data);
  89. return (isset($array[$n])) ? $array[$n] : false;
  90. }
  91. /**
  92. * Converts the current object into an array
  93. *
  94. * @return array
  95. */
  96. public function toArray($callback = null) {
  97. if(is_null($callback)) return $this->data;
  98. return array_map($callback, $this->data);
  99. }
  100. /**
  101. * Converts the current object into a json string
  102. *
  103. * @return string
  104. */
  105. public function toJson() {
  106. return json_encode($this->data);
  107. }
  108. /**
  109. * Appends an element to the data array
  110. *
  111. * @param string $key
  112. * @param mixed $object
  113. * @return Collection
  114. */
  115. public function append($key, $object) {
  116. $this->data = $this->data + array($key => $object);
  117. return $this;
  118. }
  119. /**
  120. * Prepends an element to the data array
  121. *
  122. * @param string $key
  123. * @param mixed $object
  124. * @return Collection
  125. */
  126. public function prepend($key, $object) {
  127. $this->data = array($key => $object) + $this->data;
  128. return $this;
  129. }
  130. /**
  131. * Returns a new collection without the given element(s)
  132. *
  133. * @param args any number of keys, passed as individual arguments
  134. * @return Collection
  135. */
  136. public function not() {
  137. $collection = clone $this;
  138. foreach(func_get_args() as $kill) {
  139. unset($collection->data[$kill]);
  140. }
  141. return $collection;
  142. }
  143. /**
  144. * Returns a new collection without the given element(s)
  145. *
  146. * @param args any number of keys, passed as individual arguments
  147. * @return Collection
  148. */
  149. public function without() {
  150. return call_user_func_array(array($this, 'not'), func_get_args());
  151. }
  152. /**
  153. * Shuffle all elements in the array
  154. *
  155. * @return object a new shuffled collection
  156. */
  157. public function shuffle() {
  158. $collection = clone $this;
  159. $keys = array_keys($collection->data);
  160. shuffle($keys);
  161. $collection->data = array_merge(array_flip($keys), $collection->data);
  162. return $collection;
  163. }
  164. /**
  165. * Returns an array of all keys in the collection
  166. *
  167. * @return array
  168. */
  169. public function keys() {
  170. return array_keys($this->data);
  171. }
  172. /**
  173. * Tries to find the key for the given element
  174. *
  175. * @param mixed $needle the element to search for
  176. * @return mixed the name of the key or false
  177. */
  178. public function keyOf($needle) {
  179. return array_search($needle, $this->data);
  180. }
  181. /**
  182. * Tries to find the index number for the given element
  183. *
  184. * @param mixed $needle the element to search for
  185. * @return mixed the name of the key or false
  186. */
  187. public function indexOf($needle) {
  188. return array_search($needle, array_values($this->data));
  189. }
  190. /**
  191. * Filter the elements in the array by a callback function
  192. *
  193. * @param func $callback the callback function
  194. * @return Collection
  195. */
  196. public function filter($callback) {
  197. $collection = clone $this;
  198. $collection->data = array_filter($collection->data, $callback);
  199. return $collection;
  200. }
  201. /**
  202. * Find a single item by a key and value pair
  203. *
  204. * @param string $key
  205. * @param mixed $value
  206. * @return mixed
  207. */
  208. public function findBy($key, $value) {
  209. foreach($this->data as $item) {
  210. if($this->extractValue($item, $key) == $value) return $item;
  211. }
  212. }
  213. /**
  214. * Filters the current collection by a field, operator and search value
  215. *
  216. * @return Collection
  217. */
  218. public function filterBy() {
  219. $args = func_get_args();
  220. $operator = '==';
  221. $field = @$args[0];
  222. $value = @$args[1];
  223. $split = @$args[2];
  224. $collection = clone $this;
  225. if(is_string($value) and array_key_exists($value, static::$filters)) {
  226. $operator = $value;
  227. $value = @$args[2];
  228. $split = @$args[3];
  229. }
  230. if(array_key_exists($operator, static::$filters)) {
  231. $collection = call_user_func_array(static::$filters[$operator], array(
  232. $collection,
  233. $field,
  234. $value,
  235. $split
  236. ));
  237. }
  238. return $collection;
  239. }
  240. /**
  241. * Makes sure to provide a valid value for each filter method
  242. * no matter if an object or an array is given
  243. *
  244. * @param mixed $item
  245. * @param string $field
  246. * @return mixed
  247. */
  248. static public function extractValue($item, $field) {
  249. if(is_array($item) and isset($item[$field])) {
  250. return $item[$field];
  251. } else if(is_object($item)) {
  252. return $item->$field();
  253. } else {
  254. return false;
  255. }
  256. }
  257. /**
  258. * Sorts the collection by any number of fields
  259. *
  260. * @return Collection
  261. */
  262. public function sortBy() {
  263. $args = func_get_args();
  264. $collection = clone $this;
  265. $array = $collection->data;
  266. $params = array();
  267. if(empty($array)) return $collection;
  268. foreach($args as $i => $param) {
  269. if(is_string($param)) {
  270. if(strtolower($param) === 'desc') {
  271. ${"param_$i"} = SORT_DESC;
  272. } else if(strtolower($param) === 'asc') {
  273. ${"param_$i"} = SORT_ASC;
  274. } else {
  275. ${"param_$i"} = array();
  276. foreach($array as $index => $row) {
  277. ${"param_$i"}[$index] = is_array($row) ? str::lower($row[$param]) : str::lower($row->$param());
  278. }
  279. }
  280. } else {
  281. ${"param_$i"} = $args[$i];
  282. }
  283. $params[] = &${"param_$i"};
  284. }
  285. $params[] = &$array;
  286. call_user_func_array('array_multisort', $params);
  287. $collection->data = $array;
  288. return $collection;
  289. }
  290. /**
  291. * Add pagination
  292. *
  293. * @param int $limit the number of items per page
  294. * @param array $options and optional array with options for the pagination class
  295. * @return object a sliced set of data
  296. */
  297. public function paginate($limit, $options = array()) {
  298. if(is_a($limit, 'Pagination')) {
  299. $this->pagination = $limit;
  300. return $this;
  301. }
  302. $pagination = new Pagination($this->count(), $limit, $options);
  303. $pages = $this->slice($pagination->offset(), $pagination->limit());
  304. $pages->pagination = $pagination;
  305. return $pages;
  306. }
  307. /**
  308. * Get the previously added pagination object
  309. *
  310. * @return object
  311. */
  312. public function pagination() {
  313. return $this->pagination;
  314. }
  315. /**
  316. * Map a function to each item in the collection
  317. *
  318. * @param function $callback
  319. * @return Collection
  320. */
  321. public function map($callback) {
  322. $this->data = array_map($callback, $this->data);
  323. return $this;
  324. }
  325. /**
  326. * Extracts all values for a single field into
  327. * a new array
  328. *
  329. * @param string $field
  330. * @return array
  331. */
  332. public function pluck($field, $split = null, $unique = false) {
  333. $result = array();
  334. foreach($this->data as $item) {
  335. $row = $this->extractValue($item, $field);
  336. if($split) {
  337. $result = array_merge($result, str::split($row, $split));
  338. } else {
  339. $result[] = $row;
  340. }
  341. }
  342. if($unique) {
  343. $result = array_unique($result);
  344. }
  345. return array_values($result);
  346. }
  347. /**
  348. * Groups the collection by a given field
  349. *
  350. * @param string $field
  351. * @return object A new collection with an item for each group and a subcollection in each group
  352. */
  353. public function groupBy($field, $i = true) {
  354. $groups = array();
  355. foreach($this->data as $key => $item) {
  356. // get the value to group by
  357. $value = $this->extractValue($item, $field);
  358. // make sure that there's always a proper value to group by
  359. if(!$value) throw new Exception('Invalid grouping value for key: ' . $key);
  360. // make sure we have a proper key for each group
  361. if(is_object($value) or is_array($value)) throw new Exception('You cannot group by arrays or objects');
  362. // ignore upper/lowercase for group names
  363. if($i) $value = str::lower($value);
  364. if(!isset($groups[$value])) {
  365. // create a new entry for the group if it does not exist yet
  366. $groups[$value] = new static(array($key => $item));
  367. } else {
  368. // add the item to an existing group
  369. $groups[$value]->set($key, $item);
  370. }
  371. }
  372. return new static($groups);
  373. }
  374. public function set($key, $value) {
  375. if(is_array($key)) {
  376. $this->data = array_merge($this->data, $key);
  377. return $this;
  378. }
  379. $this->data[$key] = $value;
  380. return $this;
  381. }
  382. public function __set($key, $value) {
  383. $this->set($key, $value);
  384. }
  385. public function get($key, $default = null) {
  386. if(isset($this->data[$key])) {
  387. return $this->data[$key];
  388. } else {
  389. $lowerkeys = array_change_key_case($this->data, CASE_LOWER);
  390. if(isset($lowerkeys[strtolower($key)])) {
  391. return $lowerkeys[$key];
  392. } else {
  393. return $default;
  394. }
  395. }
  396. }
  397. public function __get($key) {
  398. return $this->get($key);
  399. }
  400. public function __call($key, $arguments) {
  401. return $this->get($key);
  402. }
  403. /**
  404. * Makes it possible to echo the entire object
  405. *
  406. * @return string
  407. */
  408. public function __toString() {
  409. return implode('<br />', array_map(function($item) {
  410. return (string)$item;
  411. }, $this->data));
  412. }
  413. }
  414. /**
  415. * Add all available collection filters
  416. * Those can be extended by creating your own:
  417. * collection::$filters['your operator'] = function($collection, $field, $value, $split = false) {
  418. * // your filter code
  419. * };
  420. */
  421. // take all matching elements
  422. collection::$filters['=='] = function($collection, $field, $value, $split = false) {
  423. foreach($collection->data as $key => $item) {
  424. if($split) {
  425. $values = str::split((string)collection::extractValue($item, $field), $split);
  426. if(!in_array($value, $values)) unset($collection->$key);
  427. } else if(collection::extractValue($item, $field) != $value) {
  428. unset($collection->$key);
  429. }
  430. }
  431. return $collection;
  432. };
  433. // take all elements that won't match
  434. collection::$filters['!='] = function($collection, $field, $value, $split = false) {
  435. foreach($collection->data as $key => $item) {
  436. if($split) {
  437. $values = str::split((string)collection::extractValue($item, $field), $split);
  438. if(in_array($value, $values)) unset($collection->$key);
  439. } else if(collection::extractValue($item, $field) == $value) {
  440. unset($collection->$key);
  441. }
  442. }
  443. return $collection;
  444. };
  445. // take all elements that partly match
  446. collection::$filters['*='] = function($collection, $field, $value, $split = false) {
  447. foreach($collection->data as $key => $item) {
  448. if($split) {
  449. $values = str::split((string)collection::extractValue($item, $field), $split);
  450. foreach($values as $val) {
  451. if(strpos($val, $value) === false) {
  452. unset($collection->$key);
  453. break;
  454. }
  455. }
  456. } else if(strpos(collection::extractValue($item, $field), $value) === false) {
  457. unset($collection->$key);
  458. }
  459. }
  460. return $collection;
  461. };
  462. // greater than
  463. collection::$filters['>'] = function($collection, $field, $value, $split = false) {
  464. foreach($collection->data as $key => $item) {
  465. if(collection::extractValue($item, $field) > $value) continue;
  466. unset($collection->$key);
  467. }
  468. return $collection;
  469. };
  470. // greater and equals
  471. collection::$filters['>='] = function($collection, $field, $value, $split = false) {
  472. foreach($collection->data as $key => $item) {
  473. if(collection::extractValue($item, $field) >= $value) continue;
  474. unset($collection->$key);
  475. }
  476. return $collection;
  477. };
  478. // less than
  479. collection::$filters['<'] = function($collection, $field, $value, $split = false) {
  480. foreach($collection->data as $key => $item) {
  481. if(collection::extractValue($item, $field) < $value) continue;
  482. unset($collection->$key);
  483. }
  484. return $collection;
  485. };
  486. // less and equals
  487. collection::$filters['<='] = function($collection, $field, $value, $split = false) {
  488. foreach($collection->data as $key => $item) {
  489. if(collection::extractValue($item, $field) <= $value) continue;
  490. unset($collection->$key);
  491. }
  492. return $collection;
  493. };