├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Collection.php ├── CollectionBehavior.php ├── GeneratorCollection.php └── ModelCollection.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Yii Framework 2 collection extension Change Log 2 | =============================================== 3 | 4 | 1.0.0 under development 5 | ----------------------- 6 | 7 | - Initial release. 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software LLC nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

ActiveRecord Collection Extension for Yii 2

6 |
7 |

8 | 9 | This extension provides a generic data collection as well as a collection for the ActiveRecord DB layer of Yii 2. 10 | 11 | **Development is currently in experimental state. It is not ready for production use and may change significantly.** 12 | 13 | For license information check the [LICENSE](LICENSE.md)-file. 14 | 15 | Documentation is at [docs/guide/README.md](docs/guide/README.md). 16 | 17 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-collection/v/stable.png)](https://packagist.org/packages/yiisoft/yii2-collection) 18 | [![Total Downloads](https://poser.pugx.org/yiisoft/yii2-collection/downloads.png)](https://packagist.org/packages/yiisoft/yii2-collection) 19 | [![Build Status](https://travis-ci.org/yiisoft/yii2-collection.svg?branch=master)](https://travis-ci.org/yiisoft/yii2-collection) 20 | 21 | Installation 22 | ------------ 23 | 24 | The preferred way to install this extension is through [composer](https://getcomposer.org/download/). 25 | 26 | Either run 27 | 28 | ``` 29 | php composer.phar require --prefer-dist yiisoft/yii2-collection 30 | ``` 31 | 32 | or add 33 | 34 | ```json 35 | "yiisoft/yii2-collection": "~1.0.0" 36 | ``` 37 | 38 | to the require section of your composer.json. 39 | 40 | 41 | Configuration 42 | ------------- 43 | 44 | To use this extension, you have to attach the `yii\collection\CollectionBehavior` to the `ActiveQuery` instance of 45 | your `ActiveRecord` classes by overriding the `find()` method: 46 | 47 | ```php 48 | /** 49 | * {@inheritdoc} 50 | * @return \yii\db\ActiveQuery|\yii\collection\CollectionBehavior 51 | */ 52 | public static function find() 53 | { 54 | $query = parent::find(); 55 | $query->attachBehavior('collection', \yii\collection\CollectionBehavior::class); 56 | return $query; 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/yii2-collection", 3 | "description": "Active Record Collection implementation for the Yii framework", 4 | "keywords": ["yii2", "collection", "active-record"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "support": { 8 | "issues": "https://github.com/yiisoft/yii2-collection/issues", 9 | "forum": "https://www.yiiframework.com/forum/", 10 | "wiki": "https://www.yiiframework.com/wiki/", 11 | "irc": "ircs://irc.libera.chat:6697/yii", 12 | "source": "https://github.com/yiisoft/yii2-collection" 13 | }, 14 | "authors": [ 15 | { 16 | "name": "Carsten Brandt", 17 | "email": "mail@cebe.cc" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.6.0", 22 | "yiisoft/yii2": "~2.0.14" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "<7" 26 | }, 27 | "repositories": [ 28 | { 29 | "type": "composer", 30 | "url": "https://asset-packagist.org" 31 | } 32 | ], 33 | "autoload": { 34 | "psr-4": {"yii\\collection\\": "src"} 35 | }, 36 | "autoload-dev": { 37 | "psr-4": {"yiiunit\\collection\\": "tests"} 38 | }, 39 | "extra": { 40 | "branch-alias": { 41 | "dev-master": "1.0.x-dev" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | map(function($i) { // [2, 3, 4] 31 | * return $i + 1; 32 | * })->filter(function($i) { // [2, 3] 33 | * return $i < 4; 34 | * })->sum(); // 5 35 | * ``` 36 | * 37 | * The collection use [[ArrayAccessTrait]], so you can access it in the same way you use a PHP array. 38 | * A collection however is read-only, you can not manipulate single items. 39 | * 40 | * ```php 41 | * $collection = new Collection([1, 2, 3]); 42 | * echo $collection[1]; // 2 43 | * foreach($collection as $item) { 44 | * echo $item . ' '; 45 | * } // will print 1 2 3 46 | * ``` 47 | * 48 | * @author Carsten Brandt 49 | * @since 1.0 50 | */ 51 | class Collection extends Component implements ArrayAccess, IteratorAgregate, Countable 52 | { 53 | use ArrayAccessTrait; 54 | 55 | /** 56 | * @var array the collection data 57 | */ 58 | private $data; 59 | 60 | /** 61 | * Creates new collection. 62 | * 63 | * @param array $data the collection data. 64 | * @param array $config the configuration. 65 | */ 66 | public function __construct(array $data, array $config = []) 67 | { 68 | $this->setData($data); 69 | parent::__construct($config); 70 | } 71 | 72 | /** 73 | * Returns the data contained in this collection. 74 | * 75 | * @return array the reference to collection data. 76 | */ 77 | public function &getData() 78 | { 79 | return $this->data; 80 | } 81 | 82 | /** 83 | * Sets the collection data. 84 | * 85 | * @param array $data the collection data. 86 | * @return void 87 | */ 88 | protected function setData(array $data) 89 | { 90 | $this->data = $data; 91 | } 92 | 93 | /** 94 | * @return bool a value indicating whether the collection is empty. 95 | */ 96 | public function isEmpty() 97 | { 98 | return $this->count() === 0; 99 | } 100 | 101 | /** 102 | * Apply callback to all items in the collection. 103 | * 104 | * The original collection will not be changed, a new collection with modified data is returned. 105 | * @param callable $callable the callback function to apply, syntax: `function (mixed $value): mixed`. 106 | * @return static a new collection with items returned from the callback. 107 | */ 108 | public function map($callable) 109 | { 110 | return new static(\array_map($callable, $this->getData())); 111 | } 112 | 113 | /** 114 | * Apply callback to all items and return multiple results. 115 | * 116 | * Apply callback to all items in the collection and return a new collection containing all items 117 | * returned by the callback. 118 | * 119 | * The original collection will not be changed, a new collection with modified data is returned. 120 | * @param callable $callable the callback function to apply, syntax: `function (mixed $value): mixed`. 121 | * @return static a new collection with items returned from the callback. 122 | */ 123 | public function flatMap($callable) 124 | { 125 | return $this->map($callable)->collapse(); 126 | } 127 | 128 | /** 129 | * Merges all sub arrays into one array. 130 | * 131 | * For example: 132 | * 133 | * ```php 134 | * $collection = new Collection([[1,2], [3,4], [5,6]]); 135 | * $collapsed = $collection->collapse(); // [1,2,3,4,5,6]; 136 | * ``` 137 | * 138 | * This method can only be called on a collection which contains arrays. 139 | * The original collection will not be changed, a new collection with modified data is returned. 140 | * @return static a new collection containing the collapsed array result. 141 | */ 142 | public function collapse() 143 | { 144 | return new static($this->reduce('\array_merge', [])); 145 | } 146 | 147 | /** 148 | * Filter items from the collection. 149 | * 150 | * The original collection will not be changed, a new collection with modified data is returned. 151 | * @param callable $callable the callback function to decide which items to remove. Signature: `function($model, $key)`. 152 | * Should return `true` to keep an item and return `false` to remove them. 153 | * @return static a new collection containing the filtered items. 154 | */ 155 | public function filter($callable) 156 | { 157 | return new static(\array_filter($this->getData(), $callable, ARRAY_FILTER_USE_BOTH)); 158 | } 159 | 160 | /** 161 | * Apply reduce operation to items from the collection. 162 | * @param callable $callable the callback function to compute the reduce value. Signature: `function($carry, $model)`. 163 | * @param mixed $initialValue initial value to pass to the callback on first item. 164 | * @return mixed the result of the reduce operation. 165 | */ 166 | public function reduce($callable, $initialValue = null) 167 | { 168 | return \array_reduce($this->getData(), $callable, $initialValue); 169 | } 170 | 171 | /** 172 | * Calculate the sum of a field of the models in the collection. 173 | * @param string|Closure|array $field the name of the field to calculate. 174 | * This will be passed to [[ArrayHelper::getValue()]]. 175 | * @return mixed the calculated sum. 176 | */ 177 | public function sum($field = null) 178 | { 179 | return $this->reduce(function ($carry, $model) use ($field) { 180 | return $carry + ($field === null ? $model : ArrayHelper::getValue($model, $field, 0)); 181 | }, 0); 182 | } 183 | 184 | /** 185 | * Calculate the maximum value of a field of the models in the collection 186 | * @param string|Closure|array $field the name of the field to calculate. 187 | * This will be passed to [[ArrayHelper::getValue()]]. 188 | * @return mixed the calculated maximum value. 0 if the collection is empty. 189 | */ 190 | public function max($field = null) 191 | { 192 | return $this->reduce(function ($carry, $model) use ($field) { 193 | $value = ($field === null ? $model : ArrayHelper::getValue($model, $field, 0)); 194 | if ($carry === null) { 195 | return $value; 196 | } 197 | return $value > $carry ? $value : $carry; 198 | }); 199 | } 200 | 201 | /** 202 | * Calculate the minimum value of a field of the models in the collection 203 | * @param string|Closure|array $field the name of the field to calculate. 204 | * This will be passed to [[ArrayHelper::getValue()]]. 205 | * @return mixed the calculated minimum value. 0 if the collection is empty. 206 | */ 207 | public function min($field = null) 208 | { 209 | return $this->reduce(function ($carry, $model) use ($field) { 210 | $value = ($field === null ? $model : ArrayHelper::getValue($model, $field, 0)); 211 | if ($carry === null) { 212 | return $value; 213 | } 214 | return $value < $carry ? $value : $carry; 215 | }); 216 | } 217 | 218 | /** 219 | * Sort collection data by value. 220 | * 221 | * If the collection values are not scalar types, use [[sortBy()]] instead. 222 | * 223 | * The original collection will not be changed, a new collection with sorted data is returned. 224 | * @param int $direction sort direction, either `SORT_ASC` or `SORT_DESC`. 225 | * @param int $sortFlag type of comparison, either `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, 226 | * `SORT_LOCALE_STRING`, `SORT_NATURAL` or `SORT_FLAG_CASE`. 227 | * See [the PHP manual](https://php.net/manual/en/function.sort.php#refsect1-function.sort-parameters) 228 | * for details. 229 | * @return static a new collection containing the sorted items. 230 | * @see https://php.net/manual/en/function.asort.php 231 | * @see https://php.net/manual/en/function.arsort.php 232 | */ 233 | public function sort($direction = SORT_ASC, $sortFlag = SORT_REGULAR) 234 | { 235 | $data = $this->getData(); 236 | if ($direction === SORT_ASC) { 237 | \asort($data, $sortFlag); 238 | } else { 239 | \arsort($data, $sortFlag); 240 | } 241 | return new static($data); 242 | } 243 | 244 | /** 245 | * Sort collection data by key. 246 | * 247 | * The original collection will not be changed, a new collection with sorted data is returned. 248 | * @param int $direction sort direction, either `SORT_ASC` or `SORT_DESC`. 249 | * @param int $sortFlag type of comparison, either `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, 250 | * `SORT_LOCALE_STRING`, `SORT_NATURAL` or `SORT_FLAG_CASE`. 251 | * See [the PHP manual](https://php.net/manual/en/function.sort.php#refsect1-function.sort-parameters) 252 | * for details. 253 | * @return static a new collection containing the sorted items. 254 | * @see https://php.net/manual/en/function.ksort.php 255 | * @see https://php.net/manual/en/function.krsort.php 256 | */ 257 | public function sortByKey($direction = SORT_ASC, $sortFlag = SORT_REGULAR) 258 | { 259 | $data = $this->getData(); 260 | if ($direction === SORT_ASC) { 261 | \ksort($data, $sortFlag); 262 | } else { 263 | \krsort($data, $sortFlag); 264 | } 265 | return new static($data); 266 | } 267 | 268 | /** 269 | * Sort collection data by value using natural sort comparsion. 270 | * 271 | * If the collection values are not scalar types, use [[sortBy()]] instead. 272 | * 273 | * The original collection will not be changed, a new collection with sorted data is returned. 274 | * @param bool $caseSensitive whether comparison should be done in a case-sensitive manner. Defaults to `false`. 275 | * @return static a new collection containing the sorted items. 276 | * @see https://php.net/manual/en/function.natsort.php 277 | * @see https://php.net/manual/en/function.natcasesort.php 278 | */ 279 | public function sortNatural($caseSensitive = false) 280 | { 281 | $data = $this->getData(); 282 | if ($caseSensitive) { 283 | \natsort($data); 284 | } else { 285 | \natcasesort($data); 286 | } 287 | return new static($data); 288 | } 289 | 290 | /** 291 | * Sort collection data by one or multiple values. 292 | * 293 | * This method uses [[ArrayHelper::multisort()]] on the collection data. 294 | * 295 | * Note that keys will not be preserved by this method. 296 | * 297 | * The original collection will not be changed, a new collection with sorted data is returned. 298 | * @param string|Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array 299 | * elements, a property name of the objects, or an anonymous function returning the values for comparison 300 | * purpose. The anonymous function signature should be: `function($item)`. 301 | * To sort by multiple keys, provide an array of keys here. 302 | * @param int|array $direction the sorting direction. It can be either `SORT_ASC` or `SORT_DESC`. 303 | * When sorting by multiple keys with different sorting directions, use an array of sorting directions. 304 | * @param int|array $sortFlag the PHP sort flag. Valid values include 305 | * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, `SORT_LOCALE_STRING`, `SORT_NATURAL` and `SORT_FLAG_CASE`. 306 | * Please refer to the [PHP manual](https://php.net/manual/en/function.sort.php) 307 | * for more details. When sorting by multiple keys with different sort flags, use an array of sort flags. 308 | * @return static a new collection containing the sorted items. 309 | * @throws InvalidArgumentException if the $direction or $sortFlag parameters do not have 310 | * correct number of elements as that of $key. 311 | * @see ArrayHelper::multisort() 312 | */ 313 | public function sortBy($key, $direction = SORT_ASC, $sortFlag = SORT_REGULAR) 314 | { 315 | $data = $this->getData(); 316 | ArrayHelper::multisort($data, $key, $direction, $sortFlag); 317 | return new static($data); 318 | } 319 | 320 | /** 321 | * Reverse the order of items. 322 | * 323 | * The original collection will not be changed, a new collection with items in reverse order is returned. 324 | * @return static a new collection containing the items in reverse order. 325 | */ 326 | public function reverse() 327 | { 328 | return new static(\array_reverse($this->getData(), true)); 329 | } 330 | 331 | /** 332 | * Return items without keys. 333 | * @return static a new collection containing the values of this collections data. 334 | */ 335 | public function values() 336 | { 337 | return new static(\array_values($this->getData())); 338 | } 339 | 340 | /** 341 | * Return keys of all collection items. 342 | * @return static a new collection containing the keys of this collections data. 343 | */ 344 | public function keys() 345 | { 346 | return new static(\array_keys($this->getData())); 347 | } 348 | 349 | /** 350 | * Flip keys and values of all collection items. 351 | * @return static a new collection containing the data of this collections flipped by key and value. 352 | */ 353 | public function flip() 354 | { 355 | return new static(\array_flip($this->getData())); 356 | } 357 | 358 | /** 359 | * Merge two collections or this collection with an array. 360 | * 361 | * Data in this collection will be overwritten if non-integer keys exist in the merged collection. 362 | * 363 | * The original collection will not be changed, a new collection with items in reverse order is returned. 364 | * @param array|Collection $collection the collection or array to merge with. 365 | * @return static a new collection containing the merged data. 366 | */ 367 | public function merge($collection) 368 | { 369 | if ($collection instanceof Collection) { 370 | return new static(\array_merge($this->getData(), $collection->getData())); 371 | } elseif (\is_array($collection)) { 372 | return new static(\array_merge($this->getData(), $collection)); 373 | } 374 | throw new InvalidArgumentException('Collection can only be merged with an array or other collections.'); 375 | } 376 | 377 | /** 378 | * Convert collection data by selecting a new key and a new value for each item. 379 | * 380 | * Builds a map (key-value pairs) from a multidimensional array or an array of objects. 381 | * The `$from` and `$to` parameters specify the key names or property names to set up the map. 382 | * 383 | * The original collection will not be changed, a new collection with newly mapped data is returned. 384 | * @param string|Closure $from the field of the item to use as the key of the created map. 385 | * This can be a closure that returns such a value. 386 | * @param string|Closure $to the field of the item to use as the value of the created map. 387 | * This can be a closure that returns such a value. 388 | * @return static a new collection containing the mapped data. 389 | * @see ArrayHelper::map() 390 | */ 391 | public function remap($from, $to) 392 | { 393 | return new static(ArrayHelper::map($this->getData(), $from, $to)); 394 | } 395 | 396 | /** 397 | * Assign a new key to each item in the collection. 398 | * 399 | * The original collection will not be changed, a new collection with newly mapped data is returned. 400 | * @param string|Closure $key the field of the item to use as the new key. 401 | * This can be a closure that returns such a value. 402 | * @return static a new collection containing the newly index data. 403 | * @see ArrayHelper::map() 404 | */ 405 | public function indexBy($key) 406 | { 407 | return $this->remap($key, function ($model) { 408 | return $model; 409 | }); 410 | } 411 | 412 | /** 413 | * Group items by a specified value. 414 | * 415 | * The original collection will not be changed, a new collection with grouped data is returned. 416 | * @param string|Closure $groupField the field of the item to use as the group value. 417 | * This can be a closure that returns such a value. 418 | * @param bool $preserveKeys whether to preserve item keys in the groups. Defaults to `true`. 419 | * @return static a new collection containing the grouped data. 420 | * @see ArrayHelper::map() 421 | */ 422 | public function groupBy($groupField, $preserveKeys = true) 423 | { 424 | $result = []; 425 | if ($preserveKeys) { 426 | foreach ($this->getData() as $key => $element) { 427 | $result[ArrayHelper::getValue($element, $groupField)][$key] = $element; 428 | } 429 | } else { 430 | foreach ($this->getData() as $key => $element) { 431 | $result[ArrayHelper::getValue($element, $groupField)][] = $element; 432 | } 433 | } 434 | return new static($result); 435 | } 436 | 437 | /** 438 | * Check whether the collection contains a specific item. 439 | * @param mixed|Closure $item the item to search for. You may also pass a closure that returns a boolean. 440 | * The closure will be called on each item and in case it returns `true`, the item will be considered to 441 | * be found. In case a closure is passed, `$strict` parameter has no effect. 442 | * @param bool $strict whether comparison should be compared strict (`===`) or not (`==`). 443 | * Defaults to `false`. 444 | * @return bool `true` if the collection contains at least one item that matches, `false` if not. 445 | */ 446 | public function contains($item, $strict = false) 447 | { 448 | if ($item instanceof Closure) { 449 | foreach ($this->getData() as $i) { 450 | if ($item($i)) { 451 | return true; 452 | } 453 | } 454 | } else { 455 | foreach ($this->getData() as $i) { 456 | if ($strict ? $i === $item : $i == $item) { 457 | return true; 458 | } 459 | } 460 | } 461 | return false; 462 | } 463 | 464 | /** 465 | * Remove a specific item from the collection. 466 | * 467 | * The original collection will not be changed, a new collection with modified data is returned. 468 | * @param mixed|Closure $item the item to search for. You may also pass a closure that returns a boolean. 469 | * The closure will be called on each item and in case it returns `true`, the item will be removed. 470 | * In case a closure is passed, `$strict` parameter has no effect. 471 | * @param bool $strict whether comparison should be compared strict (`===`) or not (`==`). 472 | * Defaults to `false`. 473 | * @return static a new collection containing the filtered items. 474 | * @see filter() 475 | */ 476 | public function remove($item, $strict = false) 477 | { 478 | if ($item instanceof Closure) { 479 | $fun = function ($i) use ($item) { 480 | return !$item($i); 481 | }; 482 | } elseif ($strict) { 483 | $fun = function ($i) use ($item) { 484 | return $i !== $item; 485 | }; 486 | } else { 487 | $fun = function ($i) use ($item) { 488 | return $i != $item; 489 | }; 490 | } 491 | return $this->filter($fun); 492 | } 493 | 494 | /** 495 | * Replace a specific item in the collection with another one. 496 | * 497 | * The original collection will not be changed, a new collection with modified data is returned. 498 | * @param mixed $item the item to search for. 499 | * @param mixed $replacement the replacement to insert instead of the item. 500 | * @param bool $strict whether comparison should be compared strict (`===`) or not (`==`). 501 | * Defaults to `false`. 502 | * @return static a new collection containing the new set of items. 503 | * @see map() 504 | */ 505 | public function replace($item, $replacement, $strict = false) 506 | { 507 | return $this->map(function ($i) use ($item, $replacement, $strict) { 508 | if ($strict ? $i === $item : $i == $item) { 509 | return $replacement; 510 | } 511 | return $i; 512 | }); 513 | } 514 | 515 | /** 516 | * Slice the set of elements by an offset and number of items to return. 517 | * 518 | * The original collection will not be changed, a new collection with the selected data is returned. 519 | * @param int $offset starting offset for the slice. 520 | * @param int|null $limit the number of elements to return at maximum. 521 | * @param bool $preserveKeys whether to preserve item keys. 522 | * @return static a new collection containing the new set of items. 523 | */ 524 | public function slice($offset, $limit = null, $preserveKeys = true) 525 | { 526 | return new static(\array_slice($this->getData(), $offset, $limit, $preserveKeys)); 527 | } 528 | 529 | /** 530 | * Apply Pagination to the collection. 531 | * 532 | * This will return a portion of the data that maps the the page calculated by the pagination object. 533 | * 534 | * Usage example: 535 | * 536 | * ```php 537 | * $collection = new Collection($data); 538 | * $pagination = new Pagination([ 539 | * 'totalCount' => $collection->count(), 540 | * 'pageSize' => 3, 541 | * ]); 542 | * // the current page will be determined from request parameters 543 | * $pageData = $collection->paginate($pagination)->getData()); 544 | * ``` 545 | * 546 | * The original collection will not be changed, a new collection with the selected data is returned. 547 | * @param Pagination $pagination the pagination object to retrieve page information from. 548 | * @return static a new collection containing the items for the current page. 549 | * @see Pagination 550 | */ 551 | public function paginate(Pagination $pagination, $preserveKeys = false) 552 | { 553 | $limit = $pagination->getLimit(); 554 | return $this->slice($pagination->getOffset(), $limit > 0 ? $limit : null, $preserveKeys); 555 | } 556 | 557 | /** 558 | * @param int|string $offset Offset to set. 559 | * @param mixed $value The value to set. 560 | * @return void 561 | * @throws InvalidCallException 562 | */ 563 | public function offsetSet($offset, $value) 564 | { 565 | throw new InvalidCallException('Read only collection'); 566 | } 567 | 568 | /** 569 | * Clones collection objects. 570 | * 571 | * @return void 572 | */ 573 | public function __clone() 574 | { 575 | foreach ($this->getData() as &$value) { 576 | if (\is_object($value)) { 577 | $value = clone $value; 578 | } 579 | } 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/CollectionBehavior.php: -------------------------------------------------------------------------------- 1 | attachBehavior('collection', \yii\collection\CollectionBehavior::class); 29 | * return $query; 30 | * } 31 | * } 32 | * ``` 33 | * 34 | * In case you already define custom query class for your active record, you can move behavior attachment there. 35 | * For example: 36 | * 37 | * ```php 38 | * class Item extend \yii\db\ActiveRecord 39 | * { 40 | * // ... 41 | * public static function find() 42 | * { 43 | * return new ItemQuery(get_called_class()); 44 | * } 45 | * } 46 | * 47 | * class ItemQuery extends \yii\db\ActiveQuery 48 | * { 49 | * public function behaviors() 50 | * { 51 | * return [ 52 | * 'collection' => [ 53 | * 'class' => \yii\collection\CollectionBehavior::class 54 | * ], 55 | * ]; 56 | * } 57 | * } 58 | * ``` 59 | * 60 | * @see Collection 61 | * 62 | * @author Carsten Brandt 63 | * @since 1.0 64 | */ 65 | class CollectionBehavior extends Behavior 66 | { 67 | /** 68 | * @var string default collection class to be used at [[collect()]] method. 69 | */ 70 | public $collectionClass = ModelCollection::class; 71 | /** 72 | * @var string default collection class to be used at [[batchCollect()]] method. 73 | */ 74 | public $batchCollectionClass = GeneratorCollection::class; 75 | 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function attach($owner) 81 | { 82 | if (!$owner instanceof ActiveQueryInterface) { 83 | throw new InvalidConfigException('CollectionBehavior can only be attached to an ActiveQuery.'); 84 | } 85 | parent::attach($owner); 86 | } 87 | 88 | /** 89 | * Returns query result as a collection object. 90 | * @param string|null $collectionClass collection class, if not set - [[collectionClass]] will be used. 91 | * @return ModelCollection|\yii\db\BaseActiveRecord[] models collection instance. 92 | */ 93 | public function collect($collectionClass = null) 94 | { 95 | if ($collectionClass === null) { 96 | $collectionClass = $this->collectionClass; 97 | } 98 | 99 | return Yii::createObject( 100 | [ 101 | 'class' => $collectionClass, 102 | 'query' => $this->owner, 103 | ], 104 | [ 105 | null 106 | ] 107 | ); 108 | } 109 | 110 | /** 111 | * Returns query result as a batch collection object. 112 | * @param string|null $collectionClass collection class, if not set - [[batchCollectionClass]] will be used. 113 | * @param int $batchSize the number of records to be fetched in each batch. 114 | * @return GeneratorCollection|\yii\db\BaseActiveRecord[] models collection instance. 115 | */ 116 | public function batchCollect($collectionClass = null, $batchSize = 100) 117 | { 118 | if ($collectionClass === null) { 119 | $collectionClass = $this->batchCollectionClass; 120 | } 121 | 122 | return Yii::createObject( 123 | [ 124 | 'class' => $collectionClass, 125 | 'query' => $this->owner, 126 | ], 127 | [ 128 | $batchSize, 129 | ] 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/GeneratorCollection.php: -------------------------------------------------------------------------------- 1 | 27 | * @since 1.0 28 | */ 29 | class GeneratorCollection extends Component implements \Iterator 30 | { 31 | /** 32 | * @var ActiveQuery 33 | */ 34 | public $query; 35 | 36 | /** 37 | * @var int 38 | */ 39 | public $batchSize; 40 | 41 | /** 42 | * @var BatchQueryResult 43 | */ 44 | private $_batch; 45 | 46 | public function __construct($batchSize, $config = []) 47 | { 48 | $this->batchSize = $batchSize; 49 | parent::__construct($config); 50 | } 51 | 52 | /** 53 | * @return BatchQueryResult|BaseActiveRecord[] 54 | */ 55 | private function queryEach() 56 | { 57 | if ($this->query === null) { 58 | throw new InvalidCallException('This collection was not created from a query.'); 59 | } 60 | return $this->query->each($this->batchSize); // TODO inject DB 61 | } 62 | 63 | private function ensureBatch() 64 | { 65 | if ($this->_batch === null) { 66 | $this->_batch = $this->queryEach(); 67 | } 68 | } 69 | 70 | // basic collection operations 71 | 72 | 73 | public function map($callable) 74 | { 75 | foreach ($this->queryEach() as $key => $value) { 76 | yield $key => $callable($value, $key); 77 | } 78 | } 79 | 80 | public function filter($callable) 81 | { 82 | foreach ($this->queryEach() as $key => $value) { 83 | if ($callable($value, $key)) { 84 | yield $key => $value; 85 | } 86 | } 87 | } 88 | 89 | public function flatMap($callable) 90 | { 91 | foreach ($this->queryEach() as $key => $value) { 92 | foreach ($callable($value, $key) as $k => $v) { 93 | yield $k => $v; 94 | } 95 | } 96 | } 97 | 98 | public function indexBy($index) 99 | { 100 | foreach ($this->queryEach() as $key => $model) { 101 | yield ArrayHelper::getValue($model, $index) => $model; 102 | } 103 | } 104 | 105 | public function reduce($callable, $initialValue = null) 106 | { 107 | $result = $initialValue; 108 | foreach ($this->queryEach() as $key => $value) { 109 | $result = $callable($result, $value); 110 | } 111 | return $result; 112 | } 113 | 114 | public function values() 115 | { 116 | foreach ($this->queryEach() as $key => $value) { 117 | yield $value; 118 | } 119 | } 120 | 121 | public function keys() 122 | { 123 | foreach ($this->queryEach() as $key => $value) { 124 | yield $key; 125 | } 126 | } 127 | 128 | public function sum($field) 129 | { 130 | return $this->reduce(function ($carry, $model) use ($field) { 131 | return $carry + ArrayHelper::getValue($model, $field, 0); 132 | }, 0); 133 | } 134 | 135 | public function max($field) 136 | { 137 | return $this->reduce(function ($carry, $model) use ($field) { 138 | $value = ArrayHelper::getValue($model, $field, 0); 139 | if ($carry === null) { 140 | return $value; 141 | } 142 | return $value > $carry ? $value : $carry; 143 | }); 144 | } 145 | 146 | public function min($field) 147 | { 148 | return $this->reduce(function ($carry, $model) use ($field) { 149 | $value = ArrayHelper::getValue($model, $field, 0); 150 | if ($carry === null) { 151 | return $value; 152 | } 153 | return $value < $carry ? $value : $carry; 154 | }); 155 | } 156 | 157 | public function count() 158 | { 159 | return $this->reduce(function ($carry) { 160 | return $carry + 1; 161 | }, 0); 162 | } 163 | 164 | // AR specific stuff 165 | 166 | /** 167 | * https://github.com/yiisoft/yii2/issues/13921 168 | * 169 | * TODO add transaction support 170 | */ 171 | public function deleteAll() 172 | { 173 | foreach ($this->queryEach() as $model) { 174 | $model->delete(); 175 | } 176 | } 177 | 178 | /** 179 | * https://github.com/yiisoft/yii2/issues/13921 180 | * 181 | * TODO add transaction support 182 | */ 183 | public function updateAll($attributes, $safeOnly = true, $runValidation = true) 184 | { 185 | foreach ($this->queryEach() as $model) { 186 | $model->setAttributes($attributes, $safeOnly); 187 | $model->update($runValidation, \array_keys($attributes)); 188 | } 189 | return $this; 190 | } 191 | 192 | /** 193 | * @param array $fields 194 | * @param array $expand 195 | * @param bool $recursive 196 | * @return Collection|static 197 | */ 198 | public function toArray(array $fields = [], array $expand = [], $recursive = true) 199 | { 200 | return $this->map(function ($model) use ($fields, $expand, $recursive) { 201 | /** @var $model Arrayable */ 202 | return $model->toArray($fields, $expand, $recursive); 203 | }); 204 | } 205 | 206 | // Iterator methods 207 | 208 | /** 209 | * Return the current element 210 | * @link https://php.net/manual/en/iterator.current.php 211 | * @return mixed Can return any type. 212 | */ 213 | public function current() 214 | { 215 | $this->ensureBatch(); 216 | return $this->_batch->current(); 217 | } 218 | 219 | /** 220 | * Move forward to next element 221 | * @link https://php.net/manual/en/iterator.next.php 222 | * @return void Any returned value is ignored. 223 | */ 224 | public function next() 225 | { 226 | $this->ensureBatch(); 227 | $this->_batch->next(); 228 | } 229 | 230 | /** 231 | * Return the key of the current element 232 | * @link https://php.net/manual/en/iterator.key.php 233 | * @return mixed scalar on success, or null on failure. 234 | */ 235 | public function key() 236 | { 237 | $this->ensureBatch(); 238 | return $this->_batch->key(); 239 | } 240 | 241 | /** 242 | * Checks if current position is valid 243 | * @link https://php.net/manual/en/iterator.valid.php 244 | * @return bool The return value will be casted to boolean and then evaluated. 245 | * Returns true on success or false on failure. 246 | */ 247 | public function valid() 248 | { 249 | $this->ensureBatch(); 250 | return $this->_batch->valid(); 251 | } 252 | 253 | /** 254 | * Rewind the Iterator to the first element 255 | * @link https://php.net/manual/en/iterator.rewind.php 256 | * @return void Any returned value is ignored. 257 | */ 258 | public function rewind() 259 | { 260 | $this->ensureBatch(); 261 | $this->_batch->rewind(); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/ModelCollection.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 1.0 22 | */ 23 | class ModelCollection extends Collection 24 | { 25 | /** 26 | * @var ActiveQuery|null the query that returned this collection. 27 | * May be`null` if the collection has not been created by a query. 28 | */ 29 | public $query; 30 | 31 | /** 32 | * @var array|BaseActiveRecord[] 33 | */ 34 | private $_models; 35 | 36 | 37 | /** 38 | * Collection constructor. 39 | * @param array $models 40 | * @param array $config 41 | */ 42 | public function __construct($models = [], $config = []) 43 | { 44 | $this->_models = $models; 45 | parent::__construct([], $config); 46 | } 47 | 48 | /** 49 | * Lazy evaluation of models, if this collection has been created from a query. 50 | */ 51 | public function getData() 52 | { 53 | $this->ensureModels(); 54 | return (new Collection($this->_models))->getData(); 55 | } 56 | 57 | private function queryAll() 58 | { 59 | if ($this->query === null) { 60 | throw new InvalidCallException('This collection was not created from a query.'); 61 | } 62 | return $this->query->all(); 63 | } 64 | 65 | private function ensureModels() 66 | { 67 | if ($this->_models === null) { 68 | $this->_models = $this->queryAll(); 69 | } 70 | } 71 | 72 | /** 73 | * @return array|BaseActiveRecord[]|ActiveRecordInterface[]|Arrayable[] models contained in this collection. 74 | */ 75 | public function getModels() 76 | { 77 | return $this->getData(); 78 | } 79 | 80 | // TODO relational operations like link() and unlink() sync() 81 | // https://github.com/yiisoft/yii2/pull/12304#issuecomment-242339800 82 | // https://github.com/yiisoft/yii2/issues/10806#issuecomment-242346294 83 | 84 | // TODO addToRelation() by checking if query is a relation 85 | // https://github.com/yiisoft/yii2/issues/10806#issuecomment-241505294 86 | 87 | 88 | // https://github.com/yiisoft/yii2/issues/12743 89 | public function findWith($with) 90 | { 91 | if (!$this->query) { 92 | throw new InvalidCallException('This collection was not created from a query, so findWith() is not possible.'); 93 | } 94 | $this->ensureModels(); 95 | $this->query->findWith($with, $this->_models); 96 | return $this; 97 | } 98 | 99 | 100 | // AR specific stuff 101 | 102 | /** 103 | * https://github.com/yiisoft/yii2/issues/13921 104 | * 105 | * TODO add transaction support 106 | */ 107 | public function deleteAll() 108 | { 109 | $this->ensureModels(); 110 | foreach ($this->_models as $model) { 111 | $model->delete(); 112 | } 113 | } 114 | 115 | public function scenario($scenario) 116 | { 117 | $this->ensureModels(); 118 | foreach ($this->_models as $model) { 119 | $model->scenario = $scenario; 120 | } 121 | return $this; 122 | } 123 | 124 | /** 125 | * https://github.com/yiisoft/yii2/issues/13921 126 | * 127 | * TODO add transaction support 128 | * @param array $attributes 129 | * @param bool $safeOnly 130 | * @param bool $runValidation 131 | * @return ModelCollection 132 | * @throws \yii\db\Exception 133 | * @throws \yii\db\StaleObjectException 134 | */ 135 | public function updateAll($attributes, $safeOnly = true, $runValidation = true) 136 | { 137 | $this->ensureModels(); 138 | foreach ($this->_models as $model) { 139 | $model->setAttributes($attributes, $safeOnly); 140 | $model->update($runValidation, array_keys($attributes)); 141 | } 142 | return $this; 143 | } 144 | 145 | /** 146 | * @param array $attributes 147 | * @param bool $safeOnly 148 | * @param bool $runValidation 149 | * @return $this 150 | */ 151 | public function insertAll($attributes, $safeOnly = true, $runValidation = true) 152 | { 153 | $this->ensureModels(); 154 | foreach ($this->_models as $model) { 155 | $model->setAttributes($attributes, $safeOnly); 156 | $model->insert($runValidation, array_keys($attributes)); 157 | } 158 | // TODO could be a batch insert 159 | return $this; 160 | } 161 | 162 | public function fillAll($attributes, $safeOnly = true) 163 | { 164 | $this->ensureModels(); 165 | foreach ($this->_models as $model) { 166 | $model->setAttributes($attributes, $safeOnly); 167 | } 168 | return $this; 169 | } 170 | 171 | public function saveAll($runValidation = true, $attributeNames = null) 172 | { 173 | $this->ensureModels(); 174 | foreach ($this->_models as $model) { 175 | $model->save($runValidation, $attributeNames); 176 | } 177 | return $this; 178 | } 179 | 180 | /** 181 | * https://github.com/yiisoft/yii2/issues/10806#issuecomment-242119472 182 | * 183 | * @return bool 184 | */ 185 | public function validateAll() 186 | { 187 | $this->ensureModels(); 188 | $success = true; 189 | foreach ($this->_models as $model) { 190 | if (!$model->validate()) { 191 | $success = false; 192 | } 193 | } 194 | return $success; 195 | } 196 | 197 | /** 198 | * @param array $fields 199 | * @param array $expand 200 | * @param bool $recursive 201 | * @return Collection|static 202 | */ 203 | public function toArray(array $fields = [], array $expand = [], $recursive = true) 204 | { 205 | return $this->map(function ($model) use ($fields, $expand, $recursive) { 206 | /** @var $model Arrayable */ 207 | return $model->toArray($fields, $expand, $recursive); 208 | }); 209 | } 210 | 211 | /** 212 | * Encodes the collected models into a JSON string. 213 | * @param int $options the encoding options. For more details please refer to 214 | * . Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`. 215 | * @return string the encoding result. 216 | */ 217 | public function toJson($options = 320) 218 | { 219 | return Json::encode($this->toArray()->_models, $options); 220 | } 221 | } 222 | --------------------------------------------------------------------------------