1: <?php declare(strict_types=1);
2:
3: namespace Salient\Collection;
4:
5: use Salient\Contract\Collection\CollectionInterface;
6:
7: /**
8: * Implements CollectionInterface
9: *
10: * Unless otherwise noted, {@see CollectionTrait} methods operate on one
11: * instance of the class. Immutable collections should use
12: * {@see ImmutableCollectionTrait} instead.
13: *
14: * @see CollectionInterface
15: *
16: * @api
17: *
18: * @template TKey of array-key
19: * @template TValue
20: *
21: * @phpstan-require-implements CollectionInterface
22: */
23: trait CollectionTrait
24: {
25: /** @use ReadableCollectionTrait<TKey,TValue> */
26: use ReadableCollectionTrait;
27:
28: /**
29: * @inheritDoc
30: */
31: public function set($key, $value)
32: {
33: $items = $this->Items;
34: $items[$key] = $value;
35: return $this->maybeReplaceItems($items);
36: }
37:
38: /**
39: * @inheritDoc
40: */
41: public function unset($key)
42: {
43: if (!array_key_exists($key, $this->Items)) {
44: return $this;
45: }
46: $items = $this->Items;
47: unset($items[$key]);
48: return $this->replaceItems($items);
49: }
50:
51: /**
52: * @inheritDoc
53: */
54: public function pop(&$last = null)
55: {
56: if (!$this->Items) {
57: $last = null;
58: return $this;
59: }
60: $items = $this->Items;
61: $last = array_pop($items);
62: return $this->replaceItems($items);
63: }
64:
65: /**
66: * @return static A copy of the collection with items sorted by value.
67: */
68: public function sort()
69: {
70: $items = $this->Items;
71: uasort($items, fn($a, $b) => $this->compareItems($a, $b));
72: return $this->maybeReplaceItems($items, true);
73: }
74:
75: /**
76: * @return static A copy of the collection with items in reverse order.
77: */
78: public function reverse()
79: {
80: $items = array_reverse($this->Items, true);
81: return $this->maybeReplaceItems($items, true);
82: }
83:
84: /**
85: * @template T of TValue|TKey|array{TKey,TValue}
86: * @template TReturn
87: *
88: * @param callable(T, T|null $next, T|null $prev): TReturn $callback
89: */
90: public function map(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE)
91: {
92: $items = [];
93: $prev = null;
94: $item = null;
95: $key = null;
96: $i = 0;
97:
98: foreach ($this->Items as $nextKey => $nextValue) {
99: $next = $this->getCallbackValue($mode, $nextKey, $nextValue);
100: if ($i++) {
101: /** @var T $item */
102: /** @var T $next */
103: /** @var TKey $key */
104: $items[$key] = $callback($item, $next, $prev);
105: $prev = $item;
106: }
107: $item = $next;
108: $key = $nextKey;
109: }
110: if ($i) {
111: /** @var T $item */
112: /** @var TKey $key */
113: $items[$key] = $callback($item, null, $prev);
114: }
115:
116: // @phpstan-ignore argument.type, return.type
117: return $this->maybeReplaceItems($items, true);
118: }
119:
120: /**
121: * @template T of TValue|TKey|array{TKey,TValue}
122: *
123: * @param callable(T, T|null $next, T|null $prev): bool $callback
124: * @return static A copy of the collection with items that satisfy `$callback`.
125: */
126: public function filter(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE)
127: {
128: $items = [];
129: $prev = null;
130: $item = null;
131: $key = null;
132: $value = null;
133: $i = 0;
134:
135: foreach ($this->Items as $nextKey => $nextValue) {
136: $next = $this->getCallbackValue($mode, $nextKey, $nextValue);
137: if ($i++) {
138: /** @var T $item */
139: /** @var T $next */
140: if ($callback($item, $next, $prev)) {
141: /** @var TKey $key */
142: /** @var TValue $value */
143: $items[$key] = $value;
144: }
145: $prev = $item;
146: }
147: $item = $next;
148: $key = $nextKey;
149: $value = $nextValue;
150: }
151: /** @var T $item */
152: if ($i && $callback($item, null, $prev)) {
153: /** @var TKey $key */
154: /** @var TValue $value */
155: $items[$key] = $value;
156: }
157:
158: return $this->maybeReplaceItems($items, true);
159: }
160:
161: /**
162: * @return static A copy of the collection with items that have keys in
163: * `$keys`.
164: */
165: public function only(array $keys)
166: {
167: return $this->maybeReplaceItems(
168: array_intersect_key($this->Items, array_flip($keys)),
169: true
170: );
171: }
172:
173: /**
174: * @return static A copy of the collection with items that have keys in
175: * `$index`.
176: */
177: public function onlyIn(array $index)
178: {
179: return $this->maybeReplaceItems(
180: array_intersect_key($this->Items, $index),
181: true
182: );
183: }
184:
185: /**
186: * @return static A copy of the collection with items that have keys not in
187: * `$keys`.
188: */
189: public function except(array $keys)
190: {
191: return $this->maybeReplaceItems(
192: array_diff_key($this->Items, array_flip($keys)),
193: true
194: );
195: }
196:
197: /**
198: * @return static A copy of the collection with items that have keys not in
199: * `$index`.
200: */
201: public function exceptIn(array $index)
202: {
203: return $this->maybeReplaceItems(
204: array_diff_key($this->Items, $index),
205: true
206: );
207: }
208:
209: /**
210: * @return static A copy of the collection with items starting from
211: * `$offset`.
212: */
213: public function slice(int $offset, ?int $length = null)
214: {
215: $items = array_slice($this->Items, $offset, $length, true);
216: return $this->maybeReplaceItems($items, true);
217: }
218:
219: /**
220: * @inheritDoc
221: */
222: public function shift(&$first = null)
223: {
224: if (!$this->Items) {
225: $first = null;
226: return $this;
227: }
228: $items = $this->Items;
229: $first = array_shift($items);
230: return $this->replaceItems($items);
231: }
232:
233: /**
234: * @inheritDoc
235: */
236: public function merge($items)
237: {
238: $_items = $this->getItems($items);
239: if (!$_items) {
240: return $this;
241: }
242: // array_merge() can't be used here because it renumbers numeric keys
243: $items = $this->Items;
244: foreach ($_items as $key => $_item) {
245: if (is_int($key)) {
246: $items[] = $_item;
247: continue;
248: }
249: $items[$key] = $_item;
250: }
251: return $this->maybeReplaceItems($items);
252: }
253:
254: // Partial implementation of `ArrayAccess`:
255:
256: /**
257: * @param TKey|null $offset
258: * @param TValue $value
259: */
260: public function offsetSet($offset, $value): void
261: {
262: $items = $this->Items;
263: if ($offset === null) {
264: $items[] = $value;
265: } else {
266: $items[$offset] = $value;
267: }
268: $this->replaceItems($items, false);
269: }
270:
271: /**
272: * @param TKey $offset
273: */
274: public function offsetUnset($offset): void
275: {
276: $items = $this->Items;
277: unset($items[$offset]);
278: $this->maybeReplaceItems($items, false);
279: }
280:
281: // --
282:
283: /**
284: * @param array<TKey,TValue> $items
285: * @return static
286: */
287: protected function maybeReplaceItems(array $items, ?bool $getClone = null)
288: {
289: if ($items === $this->Items) {
290: return $this;
291: }
292: return $this->replaceItems($items, $getClone);
293: }
294:
295: /**
296: * @param array<TKey,TValue> $items
297: * @return static
298: */
299: protected function replaceItems(array $items, ?bool $getClone = null)
300: {
301: $clone = $getClone === false
302: ? $this
303: : ($getClone
304: ? clone $this
305: : $this->maybeClone());
306: $clone->Items = $items;
307: $clone->handleItemsReplaced();
308: return $clone;
309: }
310:
311: /**
312: * @return static
313: */
314: protected function maybeClone()
315: {
316: return $this;
317: }
318:
319: /**
320: * Called when items in the collection are replaced
321: */
322: protected function handleItemsReplaced(): void {}
323: }
324: