Last active
October 10, 2021 03:53
-
-
Save WinterSilence/b1884de728d3023591bf6e0c53181742 to your computer and use it in GitHub Desktop.
Yii2 Collection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace app\core; | |
use ArrayAccess; | |
use Closure; | |
use Countable; | |
use Iterator; | |
use IteratorAggregate; | |
use yii\base\ArrayAccessTrait; | |
use yii\base\Component; | |
use yii\base\InvalidCallException; | |
use yii\data\Pagination; | |
use yii\helpers\ArrayHelper; | |
use function array_filter; | |
use function array_flip; | |
use function array_keys; | |
use function array_map; | |
use function array_merge; | |
use function array_reduce; | |
use function array_reverse; | |
use function array_slice; | |
use function array_values; | |
use function arsort; | |
use function asort; | |
use function call_user_func_array; | |
use function is_object; | |
use function iterator_apply; | |
use function iterator_to_array; | |
use function krsort; | |
use function ksort; | |
use function max; | |
use function natcasesort; | |
use function natsort; | |
use const ARRAY_FILTER_USE_BOTH; | |
use const SORT_ASC; | |
use const SORT_REGULAR; | |
/** | |
* Collection is a container for a set of data. | |
* | |
* It provides methods for transforming and filtering the data as well as sorting methods, which can be applied | |
* using a chained interface. All these operations will return a new collection containing the modified data | |
* keeping the original collection as it was as long as containing objects state is not changed. | |
* | |
* ```php | |
* $collection = new Collection([1, 2, 3]); | |
* echo $collection->map(function($i) { // [2, 3, 4] | |
* return $i + 1; | |
* })->filter(function($i) { // [2, 3] | |
* return $i < 4; | |
* })->sum(); // 5 | |
* ``` | |
* | |
* The collection implements [[ArrayAccessTrait]], so you can access it in the same way you use a PHP array. | |
* A collection however is read-only, you can not manipulate single data. | |
* | |
* ```php | |
* $collection = new Collection([1, 2, 3]); | |
* echo $collection[1]; // 2 | |
* foreach($collection as $item) { | |
* echo $item . ' '; | |
* } // will print 1 2 3 | |
* ``` | |
* | |
* Note: The original collection will not be changed, a new collection with modified data is returned. | |
* | |
* @property-read array $data The data contained in this collection. | |
*/ | |
class Collection extends Component implements ArrayAccess, Countable, IteratorAggregate | |
{ | |
use ArrayAccessTrait; | |
/** | |
* @var array The data contained in this collection. | |
*/ | |
private array $data; | |
/** | |
* Create new instance. | |
* | |
* @param array $data the collection data | |
* @param array $config collection configuration | |
*/ | |
public function __construct(array $data, array $config = []) | |
{ | |
$this->setData($data); | |
parent::__construct($config); | |
} | |
/** | |
* Returns by reference data contained in collection. | |
* | |
* @return array | |
*/ | |
public function &getData(): array | |
{ | |
return $this->data; | |
} | |
/** | |
* Sets new collection data. | |
* | |
* @param array $data collection data | |
* @return void | |
* @internal | |
*/ | |
protected function setData(array $data): void | |
{ | |
$this->data = $data; | |
} | |
/** | |
* Whether the collection is empty. | |
* | |
* @return bool | |
*/ | |
public function isEmpty(): bool | |
{ | |
return $this->count() === 0; | |
} | |
/** | |
* Apply reduce operation to data from the collection. | |
* | |
* @param callable $callable the callback to compute the reduce value, syntax: | |
* `function (mixed $carry, object $value): mixed` | |
* @param mixed $initialValue initial value to pass to the callback on first item. | |
* @return mixed the result of the reduce operation. | |
*/ | |
public function reduce(callable $callable, $initialValue = null) | |
{ | |
return array_reduce($this->getData(), $callable, $initialValue); | |
} | |
/** | |
* Merges all sub arrays into one array. | |
* | |
* ```php | |
* $collection = new Collection([[1,2], [3,4], [5,6]]); | |
* $collapsed = $collection->collapse(); // [1,2,3,4,5,6]; | |
* ``` | |
* | |
* @return static a new collection containing the collapsed array result. | |
*/ | |
public function collapse(): self | |
{ | |
$clone = clone $this; | |
$clone->setData($clone->reduce('array_merge', [])); | |
return $clone; | |
} | |
/** | |
* Apply callback to all data in the collection. | |
* | |
* @param callable $callable the callback function to apply, syntax: `function(object $value): mixed`. | |
* @return static a new collection with data returned from the callback. | |
*/ | |
public function map(callable $callable): self | |
{ | |
$clone = clone $this; | |
$clone->setData(array_map($callable, $clone->getData())); | |
return $clone; | |
} | |
/** | |
* Apply callback to all data in the collection and return a new collection containing all data returned by | |
* the callback. | |
* | |
* @param callable $callable the callback function to apply, syntax: `function (object $value): array` | |
* @return static a new collection with data returned from the callback. | |
*/ | |
public function flatMap(callable $callable): self | |
{ | |
return $this->map($callable)->collapse(); | |
} | |
/** | |
* Filter data from the collection. | |
* | |
* @param callable $callable the callback to decide which data to remove, syntax: | |
* `function (object $value, string|int $key): bool` | |
* @return static a new collection containing the filtered data. | |
*/ | |
public function filter(callable $callable): self | |
{ | |
$clone = clone $this; | |
$clone->setData(array_filter($clone->getData(), $callable, ARRAY_FILTER_USE_BOTH)); | |
return $clone; | |
} | |
/** | |
* Calculate the sum of a field of the data in the collection. | |
* | |
* @param string|string[]|Closure|null $field the name of the field to calculate. | |
* @return mixed the calculated sum. | |
*/ | |
public function sum($field = null) | |
{ | |
return $this->reduce( | |
/** | |
* @param mixed $carry | |
* @param mixed $value | |
* @return mixed | |
* @throws \Exception | |
*/ | |
static function ($carry, $value) use ($field) { | |
return $carry + (empty($field) ? $value : ArrayHelper::getValue($value, $field, 0)); | |
}, | |
0 | |
); | |
} | |
/** | |
* Calculate the maximum value of a field of the data in the collection. | |
* | |
* @param string|string[]|Closure|null $field the name of the field to calculate. | |
* @return mixed the calculated maximum value. 0 if the collection is empty. | |
*/ | |
public function max($field = null) | |
{ | |
return $this->reduce( | |
/** | |
* @param mixed $carry | |
* @param mixed $value | |
* @return mixed | |
* @throws \Exception | |
*/ | |
static function ($carry, $value) use ($field) { | |
$value = empty($field) ? $value : ArrayHelper::getValue($value, $field, 0); | |
return max($value, $carry); | |
} | |
); | |
} | |
/** | |
* Calculate the minimum value of a field of the data in the collection. | |
* | |
* @param string|string[]|Closure|null $field the name of the field to calculate. | |
* @return mixed the calculated minimum value. 0 if the collection is empty. | |
*/ | |
public function min($field = null) | |
{ | |
return $this->reduce( | |
/** | |
* @param mixed $carry | |
* @param mixed $value | |
* @return mixed | |
* @throws \Exception | |
*/ | |
static function ($carry, $value) use ($field) { | |
$value = empty($field) ? $value : ArrayHelper::getValue($value, $field, 0); | |
return ($carry === null || $value < $carry) ? $value : $carry; | |
} | |
); | |
} | |
/** | |
* Sort collection data by value. | |
* | |
* If the collection values are not scalar types, use [[sortBy()]] instead. | |
* | |
* @param bool $ascendingOrder if true, sort in ascending order, else in descending order. | |
* @param int $sortFlag type of comparison, either `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, | |
* `SORT_LOCALE_STRING`, `SORT_NATURAL` or `SORT_FLAG_CASE`. | |
* @return static a new collection containing the sorted data. | |
* @see https://www.php.net/manual/en/function.sort#refsect1-function.sort-parameters | |
*/ | |
public function sort(bool $ascendingOrder = true, int $sortFlag = SORT_REGULAR): self | |
{ | |
$clone = clone $this; | |
if ($ascendingOrder) { | |
asort($clone->getData(), $sortFlag); | |
} else { | |
arsort($clone->getData(), $sortFlag); | |
} | |
return $clone; | |
} | |
/** | |
* Sort collection data by key. | |
* | |
* @param bool $ascendingOrder if true, sort in ascending order, else in descending order. | |
* @param int $sortFlag type of comparison, either `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, | |
* `SORT_LOCALE_STRING`, `SORT_NATURAL` or `SORT_FLAG_CASE`. | |
* @return static a new collection containing the sorted data. | |
* @see https://www.php.net/manual/en/function.sort#refsect1-function.sort-parameters | |
*/ | |
public function sortByKey(bool $ascendingOrder = true, int $sortFlag = SORT_REGULAR): self | |
{ | |
$clone = clone $this; | |
if ($ascendingOrder) { | |
ksort($clone->getData(), $sortFlag); | |
} else { | |
krsort($clone->getData(), $sortFlag); | |
} | |
return $clone; | |
} | |
/** | |
* Sort collection data by value using natural sort comparsion. | |
* | |
* If the collection values are not scalar types, use [[sortBy()]] instead. | |
* | |
* @param bool $caseSensitive whether comparison should be done in a case-sensitive manner. Defaults to `false`. | |
* @return static a new collection containing the sorted data. | |
*/ | |
public function sortNatural(bool $caseSensitive = false): self | |
{ | |
$clone = clone $this; | |
if ($caseSensitive) { | |
natsort($clone->getData()); | |
} else { | |
natcasesort($clone->getData()); | |
} | |
return $clone; | |
} | |
/** | |
* Sort collection data by one or multiple values. | |
* | |
* Note that keys will not be preserved by this method. | |
* | |
* @param string|int|Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array | |
* elements, a property name of the objects, or an anonymous function returning the values for comparison | |
* purpose. The anonymous function signature should be: `function ($item)`. To sort by multiple keys, provide | |
* an array of keys here. | |
* @param int|int[] $direction the sorting direction. It can be either `SORT_ASC` or `SORT_DESC`. When sorting by | |
* multiple keys with different sorting directions, use an array of sorting directions. | |
* @param int|int[] $sortFlag the PHP sort flag. Valid values include `SORT_REGULAR`, `SORT_NUMERIC`, | |
* `SORT_STRING`, `SORT_LOCALE_STRING`, `SORT_NATURAL` and `SORT_FLAG_CASE`. When sorting by multiple keys with | |
* different sort flags, use an array of sort flags. | |
* @return static a new collection containing the sorted data. | |
* @throws \yii\base\InvalidArgumentException if the `$direction` or $sortFlag parameters do not have correct number of | |
* elements as that of `$key`. | |
* @see [[ArrayHelper::multisort()]] | |
*/ | |
public function sortBy($key, $direction = SORT_ASC, $sortFlag = SORT_REGULAR): self | |
{ | |
$clone = clone $this; | |
ArrayHelper::multisort($clone->getData(), $key, $direction, $sortFlag); | |
return $clone; | |
} | |
/** | |
* Reverse the order of data. | |
* | |
* @param bool $preserveKeys if true, numeric keys are preserved. Non-numeric keys are not affected by this setting | |
* and will always be preserved. | |
* @return static a new collection containing the data in reverse order. | |
*/ | |
public function reverse(bool $preserveKeys = true): self | |
{ | |
$clone = clone $this; | |
$clone->setData(array_reverse($this->getData(), $preserveKeys)); | |
return $clone; | |
} | |
/** | |
* Return data without keys. | |
* | |
* @return static a new collection containing the values of this collections data. | |
*/ | |
public function values(): self | |
{ | |
$clone = clone $this; | |
$clone->setData(array_values($this->getData())); | |
return $clone; | |
} | |
/** | |
* Return keys of all collection data. | |
* | |
* @return array | |
*/ | |
public function keys(): array | |
{ | |
return array_keys($this->getData()); | |
} | |
/** | |
* Flip keys and values of all collection data. | |
* | |
* @return static a new collection containing the data of this collections flipped by key and value. | |
*/ | |
public function flip(): self | |
{ | |
$clone = clone $this; | |
$clone->setData(array_flip($this->getData())); | |
return $clone; | |
} | |
/** | |
* Merge two collections or this collection with an array. | |
* | |
* @param array|\Traversable $collection the collection or array to merge with. | |
* @return static a new collection containing the merged data. | |
*/ | |
public function merge($collection): self | |
{ | |
if (is_object($collection)) { | |
$collection = $collection instanceof self ? $collection->getData() : iterator_to_array($collection); | |
} | |
$clone = clone $this; | |
$clone->setData(array_merge($clone->getData(), $collection)); | |
return $clone; | |
} | |
/** | |
* Convert collection data by selecting a new key and a new value for each item. | |
* | |
* Builds a map (key-value pairs) from a multidimensional array or an array of objects. | |
* The `$from` and `$to` parameters specify the key names or property names to set up the map. | |
* | |
* @param string|Closure $from the field of the item to use as the key of the created map. This can be a closure | |
* that returns such a value. | |
* @param string|Closure $to the field of the item to use as the value of the created map. This can be a closure | |
* that returns such a value. | |
* @return static a new collection containing the mapped data. | |
*/ | |
public function remap($from, $to): self | |
{ | |
$clone = clone $this; | |
$clone->setData(ArrayHelper::map($clone->getData(), $from, $to)); | |
return $clone; | |
} | |
/** | |
* Assign a new key to each item in the collection. | |
* | |
* @param string|Closure $key the field of the item to use as the new key. This can be a closure that returns such | |
* a value. | |
* @return static a new collection containing the newly index data. | |
*/ | |
public function indexBy($key): self | |
{ | |
return $this->remap( | |
$key, | |
static function ($value) { | |
return $value; | |
} | |
); | |
} | |
/** | |
* Group data by a specified value. | |
* | |
* @param string|Closure $groupField the field of the item to use as the group value. This can be a closure that | |
* returns such a value. | |
* @param bool $preserveKeys whether to preserve item keys in the groups. Defaults to `true`. | |
* @return static a new collection containing the grouped data. | |
* @throws \Exception | |
* @see [[ArrayHelper::getValue()]] | |
*/ | |
public function groupBy($groupField, bool $preserveKeys = true): self | |
{ | |
$clone = clone $this; | |
$data = []; | |
if ($preserveKeys) { | |
foreach ($clone->getData() as $key => $value) { | |
$data[ArrayHelper::getValue($value, $groupField)][$key] = $value; | |
} | |
} else { | |
foreach ($clone->getData() as $value) { | |
$data[ArrayHelper::getValue($value, $groupField)][] = $value; | |
} | |
} | |
$clone->setData($data); | |
return $clone; | |
} | |
/** | |
* Check whether the collection contains a specific item. | |
* | |
* @param mixed $item the item to search for. You may also pass a closure that returns a boolean. The closure will | |
* be called on each item and in case it returns `true`, the item will be considered to be found. In case a | |
* closure is passed, `$strict` parameter has no effect. | |
* @param bool $strict whether comparison should be compared strict (`===`) or not (`==`). Defaults to `false`. | |
* @return bool `true` if the collection contains at least one item that matches, `false` if not. | |
*/ | |
public function contains($item, bool $strict = false): bool | |
{ | |
if ($item instanceof Closure) { | |
foreach ($this->getData() as $i) { | |
if ($item($i)) { | |
return true; | |
} | |
} | |
} else { | |
foreach ($this->getData() as $i) { | |
if ($strict ? $i === $item : $i == $item) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* Remove a specific item from the collection. | |
* | |
* @param mixed|Closure $item the item to search for. You may also pass a closure that returns a boolean. | |
* The closure will be called on each item and in case it returns `true`, the item will be removed. In case | |
* a closure is passed, `$strict` parameter has no effect. | |
* @param bool $strict whether comparison should be compared strict (`===`) or not (`==`). Defaults to `false`. | |
* @return static a new collection containing the filtered data. | |
* @see [[filter()]] | |
*/ | |
public function remove($item, bool $strict = false): self | |
{ | |
if ($item instanceof Closure) { | |
$func = static function ($i) use ($item) { | |
return !$item($i); | |
}; | |
} elseif ($strict) { | |
$func = static function ($i) use ($item) { | |
return $i !== $item; | |
}; | |
} else { | |
$func = static function ($i) use ($item) { | |
return $i != $item; | |
}; | |
} | |
return $this->filter($func); | |
} | |
/** | |
* Replace a specific item in the collection with another one. | |
* | |
* @param mixed $item the item to search for. | |
* @param mixed $replacement the replacement to insert instead of the item. | |
* @param bool $strict whether comparison should be compared strict (`===`) or not (`==`). Defaults to `false`. | |
* @return static a new collection containing the new set of data. | |
* @see [[map()]] | |
*/ | |
public function replace($item, $replacement, bool $strict = false): self | |
{ | |
return $this->map(static function ($i) use ($item, $replacement, $strict) { | |
if ($strict ? $i === $item : $i == $item) { | |
return $replacement; | |
} | |
return $i; | |
}); | |
} | |
/** | |
* Slice the set of elements by an offset and number of data to return. | |
* | |
* @param int|string $offset starting offset for the slice. | |
* @param int|null $limit the number of elements to return at maximum. | |
* @param bool $preserveKeys whether to preserve item keys. | |
* @return static a new collection containing the new set of data. | |
*/ | |
public function slice($offset, int $limit = null, bool $preserveKeys = true): self | |
{ | |
$clone = clone $this; | |
$clone->setData(array_slice($this->getData(), $offset, $limit, $preserveKeys)); | |
return $clone; | |
} | |
/** | |
* Apply `Pagination` to the collection. | |
* | |
* ```php | |
* $collection = new Collection($models); | |
* $pagination = new Pagination(['totalCount' => $collection->count(), 'pageSize' => 3]); | |
* // the current page will be determined from request parameters | |
* $pageData = $collection->paginate($pagination)->getData(); | |
* ``` | |
* | |
* @param Pagination $pagination the pagination object to retrieve page information from. | |
* @return static a new collection containing the data for the current page. | |
* @see Pagination | |
*/ | |
public function paginate(Pagination $pagination, bool $preserveKeys = false): self | |
{ | |
return $this->slice($pagination->getOffset(), $pagination->getLimit() ?: null, $preserveKeys); | |
} | |
/** | |
* Calls given method for current object in an iterator. | |
* | |
* @param \Iterator $iterator the `Iterator` instance to iterate over | |
* @param string $method the method called on each object | |
* @param array $arguments an array of arguments, each element is passed to method as separate argument | |
* @param array $results the execution results | |
* @return bool | |
* @see [[apply()]] | |
*/ | |
protected function applyCallback(Iterator $iterator, string $method, array $arguments, array &$results): bool | |
{ | |
$results[$iterator->key()] = call_user_func_array([$iterator->current(), $method], $arguments); | |
return true; | |
} | |
/** | |
* Calls given method for each objects in an iterator. | |
* | |
* Note: this method works only with object collection! | |
* | |
* @param string $method the method called on each object | |
* @param array $arguments an array of arguments, each element is passed to method as separate argument | |
* @param int $iterations the iteration count returning by reference | |
* @return array the execution results | |
*/ | |
public function apply(string $method, array $arguments = [], int &$iterations = 0): array | |
{ | |
$results = []; | |
$clone = clone $this; | |
$iterator = $clone->getIterator(); | |
$iterations = iterator_apply( | |
$iterator, | |
[$this, 'applyCallback'], | |
[$iterator, $method, $arguments, &$results] | |
); | |
return $results; | |
} | |
/** | |
* @inheritdoc | |
* @throws InvalidCallException Read only collection | |
*/ | |
public function offsetSet($offset, $item): void | |
{ | |
throw new InvalidCallException('Read only collection'); | |
} | |
/** | |
* Clones collection objects. | |
* | |
* @return void | |
*/ | |
public function __clone() | |
{ | |
parent::__clone(); | |
foreach ($this->getData() as &$value) { | |
if (is_object($value)) { | |
$value = clone $value; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment