652 lines
15 KiB
PHP
652 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Collection
|
|
*
|
|
* @package Kirby Toolkit
|
|
* @author Bastian Allgeier <bastian@getkirby.com>
|
|
* @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('<br />', 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;
|
|
|
|
};
|