├── 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 | [](https://packagist.org/packages/yiisoft/yii2-collection)
18 | [](https://packagist.org/packages/yiisoft/yii2-collection)
19 | [](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 |
--------------------------------------------------------------------------------