* @link http://getkirby.com * @copyright Bastian Allgeier * @license http://www.opensource.org/licenses/mit-license.php MIT License */ class Collection extends I { public static $filters = array(); protected $pagination; /** * Returns a slice of the collection * * @param int $offset The optional index to start the slice from * @param int $limit The optional number of elements to return * @return Collection */ public function slice($offset = null, $limit = null) { if($offset === null && $limit === null) return $this; $collection = clone $this; $collection->data = array_slice($collection->data, $offset, $limit); return $collection; } /** * Returns a new combined collection * * @return Collection */ public function merge($collection2) { $collection = clone $this; $collection->data = a::merge($collection->data, $collection2->data); return $collection; } /** * Returns a new collection with a limited number of elements * * @param int $limit The number of elements to return * @return Collection */ public function limit($limit) { return $this->slice(0, $limit); } /** * Returns a new collection starting from the given offset * * @param int $offset The index to start from * @return Collection */ public function offset($offset) { return $this->slice($offset); } /** * Returns the array in reverse order * * @return Collection */ public function flip() { $collection = clone $this; $collection->data = array_reverse($collection->data, true); return $collection; } /** * Counts all elements in the array * * @return int */ public function count() { return count($this->data); } /** * Returns the first element from the array * * @return mixed */ public function first() { $array = $this->data; return array_shift($array); } /** * Checks if an element is in the collection by key. * * @param string $key * @return boolean */ public function has($key) { return isset($this->data[$key]); } /** * Returns the last element from the array * * @return mixed */ public function last() { $array = $this->data; return array_pop($array); } /** * Returns the nth element from the array * * @return mixed */ public function nth($n) { $array = array_values($this->data); return (isset($array[$n])) ? $array[$n] : false; } /** * Converts the current object into an array * * @return array */ public function toArray($callback = null) { if(is_null($callback)) return $this->data; return array_map($callback, $this->data); } /** * Converts the current object into a json string * * @return string */ public function toJson() { return json_encode($this->data); } /** * Appends an element to the data array * * @param string $key * @param mixed $object * @return Collection */ public function append($key, $object) { $this->data = $this->data + array($key => $object); return $this; } /** * Prepends an element to the data array * * @param string $key * @param mixed $object * @return Collection */ public function prepend($key, $object) { $this->data = array($key => $object) + $this->data; return $this; } /** * Returns a new collection without the given element(s) * * @param args any number of keys, passed as individual arguments * @return Collection */ public function not() { $collection = clone $this; foreach(func_get_args() as $kill) { unset($collection->data[$kill]); } return $collection; } /** * Returns a new collection without the given element(s) * * @param args any number of keys, passed as individual arguments * @return Collection */ public function without() { return call_user_func_array(array($this, 'not'), func_get_args()); } /** * Shuffle all elements in the array * * @return object a new shuffled collection */ public function shuffle() { $collection = clone $this; $keys = array_keys($collection->data); shuffle($keys); $collection->data = array_merge(array_flip($keys), $collection->data); return $collection; } /** * Returns an array of all keys in the collection * * @return array */ public function keys() { return array_keys($this->data); } /** * Tries to find the key for the given element * * @param mixed $needle the element to search for * @return mixed the name of the key or false */ public function keyOf($needle) { return array_search($needle, $this->data); } /** * Tries to find the index number for the given element * * @param mixed $needle the element to search for * @return mixed the name of the key or false */ public function indexOf($needle) { return array_search($needle, array_values($this->data)); } /** * Filter the elements in the array by a callback function * * @param func $callback the callback function * @return Collection */ public function filter($callback) { $collection = clone $this; $collection->data = array_filter($collection->data, $callback); return $collection; } /** * Find a single item by a key and value pair * * @param string $key * @param mixed $value * @return mixed */ public function findBy($key, $value) { foreach($this->data as $item) { if($this->extractValue($item, $key) == $value) return $item; } } /** * Filters the current collection by a field, operator and search value * * @return Collection */ public function filterBy() { $args = func_get_args(); $operator = '=='; $field = @$args[0]; $value = @$args[1]; $split = @$args[2]; $collection = clone $this; if(is_string($value) && array_key_exists($value, static::$filters)) { $operator = $value; $value = @$args[2]; $split = @$args[3]; } if(is_object($value)) { $value = (string)$value; } if(array_key_exists($operator, static::$filters)) { $collection = call_user_func_array(static::$filters[$operator], array( $collection, $field, $value, $split )); } return $collection; } /** * Makes sure to provide a valid value for each filter method * no matter if an object or an array is given * * @param mixed $item * @param string $field * @return mixed */ static public function extractValue($item, $field) { if(is_array($item) && isset($item[$field])) { return $item[$field]; } else if(is_object($item)) { return $item->$field(); } else { return false; } } /** * Sorts the collection by any number of fields * * @return Collection */ public function sortBy() { $args = func_get_args(); $collection = clone $this; $array = $collection->data; $params = array(); if(empty($array)) return $collection; foreach($args as $i => $param) { if(is_string($param)) { if(strtolower($param) === 'desc') { ${"param_$i"} = SORT_DESC; } else if(strtolower($param) === 'asc') { ${"param_$i"} = SORT_ASC; } else { ${"param_$i"} = array(); foreach($array as $index => $row) { ${"param_$i"}[$index] = is_array($row) ? str::lower($row[$param]) : str::lower($row->$param()); } } } else { ${"param_$i"} = $args[$i]; } $params[] = &${"param_$i"}; } $params[] = &$array; call_user_func_array('array_multisort', $params); $collection->data = $array; return $collection; } /** * Add pagination * * @param int $limit the number of items per page * @param array $options and optional array with options for the pagination class * @return object a sliced set of data */ public function paginate($limit, $options = array()) { if(is_a($limit, 'Pagination')) { $this->pagination = $limit; return $this; } $pagination = new Pagination($this->count(), $limit, $options); $pages = $this->slice($pagination->offset(), $pagination->limit()); $pages->pagination = $pagination; return $pages; } /** * Get the previously added pagination object * * @return object */ public function pagination() { return $this->pagination; } /** * Map a function to each item in the collection * * @param function $callback * @return Collection */ public function map($callback) { $this->data = array_map($callback, $this->data); return $this; } /** * Extracts all values for a single field into * a new array * * @param string $field * @return array */ public function pluck($field, $split = null, $unique = false) { $result = array(); foreach($this->data as $item) { $row = $this->extractValue($item, $field); if($split) { $result = array_merge($result, str::split($row, $split)); } else { $result[] = $row; } } if($unique) { $result = array_unique($result); } return array_values($result); } /** * Groups the collection by a given callback * * @param callable $callback * @return object A new collection with an item for each group and a subcollection in each group */ public function group($callback) { if (!is_callable($callback)) throw new Exception($callback . ' is not callable. Did you mean to use groupBy()?'); $groups = array(); foreach($this->data as $key => $item) { // get the value to group by $value = call_user_func($callback, $item); // make sure that there's always a proper value to group by if(!$value) throw new Exception('Invalid grouping value for key: ' . $key); // make sure we have a proper key for each group if(is_array($value)) { throw new Exception('You cannot group by arrays or objects'); } else if(is_object($value)) { if(!method_exists($value, '__toString')) { throw new Exception('You cannot group by arrays or objects'); } else { $value = (string)$value; } } if(!isset($groups[$value])) { // create a new entry for the group if it does not exist yet $groups[$value] = new static(array($key => $item)); } else { // add the item to an existing group $groups[$value]->set($key, $item); } } return new Collection($groups); } /** * Groups the collection by a given field * * @param string $field * @return object A new collection with an item for each group and a subcollection in each group */ public function groupBy($field, $i = true) { if (!is_string($field)) throw new Exception('Cannot group by non-string values. Did you mean to call group()?'); return $this->group(function($item) use ($field, $i) { $value = $this->extractValue($item, $field); // ignore upper/lowercase for group names return ($i == true) ? str::lower($value) : $value; }); } public function set($key, $value) { if(is_array($key)) { $this->data = array_merge($this->data, $key); return $this; } $this->data[$key] = $value; return $this; } public function __set($key, $value) { $this->set($key, $value); } public function get($key, $default = null) { if(isset($this->data[$key])) { return $this->data[$key]; } else { $lowerkeys = array_change_key_case($this->data, CASE_LOWER); if(isset($lowerkeys[strtolower($key)])) { return $lowerkeys[$key]; } else { return $default; } } } public function __get($key) { return $this->get($key); } public function __call($key, $arguments) { return $this->get($key); } /** * Makes it possible to echo the entire object * * @return string */ public function __toString() { return implode('
', array_map(function($item) { return (string)$item; }, $this->data)); } } /** * Add all available collection filters * Those can be extended by creating your own: * collection::$filters['your operator'] = function($collection, $field, $value, $split = false) { * // your filter code * }; */ // take all matching elements collection::$filters['=='] = function($collection, $field, $value, $split = false) { foreach($collection->data as $key => $item) { if($split) { $values = str::split((string)collection::extractValue($item, $field), $split); if(!in_array($value, $values)) unset($collection->$key); } else if(collection::extractValue($item, $field) != $value) { unset($collection->$key); } } return $collection; }; // take all elements that won't match collection::$filters['!='] = function($collection, $field, $value, $split = false) { foreach($collection->data as $key => $item) { if($split) { $values = str::split((string)collection::extractValue($item, $field), $split); if(in_array($value, $values)) unset($collection->$key); } else if(collection::extractValue($item, $field) == $value) { unset($collection->$key); } } return $collection; }; // take all elements that partly match collection::$filters['*='] = function($collection, $field, $value, $split = false) { foreach($collection->data as $key => $item) { if($split) { $values = str::split((string)collection::extractValue($item, $field), $split); foreach($values as $val) { if(strpos($val, $value) === false) { unset($collection->$key); break; } } } else if(strpos(collection::extractValue($item, $field), $value) === false) { unset($collection->$key); } } return $collection; }; // greater than collection::$filters['>'] = function($collection, $field, $value) { foreach($collection->data as $key => $item) { if(collection::extractValue($item, $field) > $value) continue; unset($collection->$key); } return $collection; }; // greater and equals collection::$filters['>='] = function($collection, $field, $value) { foreach($collection->data as $key => $item) { if(collection::extractValue($item, $field) >= $value) continue; unset($collection->$key); } return $collection; }; // less than collection::$filters['<'] = function($collection, $field, $value) { foreach($collection->data as $key => $item) { if(collection::extractValue($item, $field) < $value) continue; unset($collection->$key); } return $collection; }; // less and equals collection::$filters['<='] = function($collection, $field, $value) { foreach($collection->data as $key => $item) { if(collection::extractValue($item, $field) <= $value) continue; unset($collection->$key); } return $collection; };