1: <?php declare(strict_types=1);
2:
3: namespace Salient\Collection;
4:
5: use Salient\Contract\Collection\CollectionInterface;
6: use Salient\Contract\Core\Arrayable;
7: use Salient\Contract\Core\Comparable;
8: use Salient\Contract\Core\Jsonable;
9: use Salient\Utility\Arr;
10: use Salient\Utility\Json;
11: use ArrayIterator;
12: use InvalidArgumentException;
13: use IteratorAggregate;
14: use JsonSerializable;
15: use OutOfRangeException;
16: use ReturnTypeWillChange;
17: use Traversable;
18:
19: /**
20: * @api
21: *
22: * @template TKey of array-key
23: * @template TValue
24: *
25: * @phpstan-require-implements CollectionInterface
26: * @phpstan-require-implements IteratorAggregate
27: */
28: trait ReadOnlyCollectionTrait
29: {
30: /** @var array<TKey,TValue> */
31: protected array $Items;
32:
33: /**
34: * @inheritDoc
35: */
36: public function __construct($items = [])
37: {
38: $this->Items = $this->getItemsArray($items);
39: }
40:
41: /**
42: * @inheritDoc
43: */
44: public function isEmpty(): bool
45: {
46: return !$this->Items;
47: }
48:
49: /**
50: * @inheritDoc
51: */
52: public function has($key): bool
53: {
54: return array_key_exists($key, $this->Items);
55: }
56:
57: /**
58: * @inheritDoc
59: */
60: public function get($key)
61: {
62: if (!array_key_exists($key, $this->Items)) {
63: throw new OutOfRangeException(sprintf('Item not found: %s', $key));
64: }
65: return $this->Items[$key];
66: }
67:
68: /**
69: * @inheritDoc
70: */
71: public function forEach(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE)
72: {
73: $prev = null;
74: $item = null;
75:
76: foreach ($this->Items as $nextKey => $nextValue) {
77: $next = $this->getCallbackValue($mode, $nextKey, $nextValue);
78: if ($item !== null) {
79: $callback($item, $next, $prev);
80: }
81: $prev = $item;
82: $item = $next;
83: }
84: if ($item !== null) {
85: $callback($item, null, $prev);
86: }
87:
88: return $this;
89: }
90:
91: /**
92: * @inheritDoc
93: */
94: public function find(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE | CollectionInterface::FIND_VALUE)
95: {
96: $prev = null;
97: $item = null;
98: $key = null;
99: $value = null;
100:
101: foreach ($this->Items as $nextKey => $nextValue) {
102: $next = $this->getCallbackValue($mode, $nextKey, $nextValue);
103: if ($item !== null && $callback($item, $next, $prev)) {
104: /** @var TKey $key */
105: /** @var TValue $value */
106: // @phpstan-ignore return.type
107: return $this->getReturnValue($mode, $key, $value);
108: }
109: $prev = $item;
110: $item = $next;
111: $key = $nextKey;
112: $value = $nextValue;
113: }
114: if ($item !== null && $callback($item, null, $prev)) {
115: /** @var TKey $key */
116: /** @var TValue $value */
117: // @phpstan-ignore return.type
118: return $this->getReturnValue($mode, $key, $value);
119: }
120:
121: return null;
122: }
123:
124: /**
125: * @inheritDoc
126: */
127: public function hasValue($value, bool $strict = false): bool
128: {
129: if ($strict) {
130: return in_array($value, $this->Items, true);
131: }
132:
133: foreach ($this->Items as $item) {
134: if (!$this->compareItems($value, $item)) {
135: return true;
136: }
137: }
138: return false;
139: }
140:
141: /**
142: * @inheritDoc
143: */
144: public function keyOf($value, bool $strict = false)
145: {
146: if ($strict) {
147: return Arr::search($this->Items, $value, true);
148: }
149:
150: foreach ($this->Items as $key => $item) {
151: if (!$this->compareItems($value, $item)) {
152: return $key;
153: }
154: }
155: return null;
156: }
157:
158: /**
159: * @inheritDoc
160: */
161: public function firstOf($value)
162: {
163: foreach ($this->Items as $item) {
164: if (!$this->compareItems($value, $item)) {
165: return $item;
166: }
167: }
168: return null;
169: }
170:
171: /**
172: * @inheritDoc
173: */
174: public function all(): array
175: {
176: return $this->Items;
177: }
178:
179: /**
180: * @inheritDoc
181: */
182: public function toArray(bool $preserveKeys = true): array
183: {
184: foreach ($this->Items as $key => $value) {
185: if ($value instanceof Arrayable) {
186: $value = $value->toArray($preserveKeys);
187: }
188: if ($preserveKeys) {
189: $array[$key] = $value;
190: } else {
191: $array[] = $value;
192: }
193: }
194: return $array ?? [];
195: }
196:
197: /**
198: * @return array<TKey,mixed>
199: */
200: public function jsonSerialize(): array
201: {
202: foreach ($this->Items as $key => $value) {
203: if ($value instanceof JsonSerializable) {
204: $array[$key] = $value->jsonSerialize();
205: } elseif ($value instanceof Jsonable) {
206: $array[$key] = Json::objectAsArray($value->toJson());
207: } elseif ($value instanceof Arrayable) {
208: $array[$key] = $value->toArray();
209: } else {
210: $array[$key] = $value;
211: }
212: }
213: return $array ?? [];
214: }
215:
216: /**
217: * @inheritDoc
218: */
219: public function toJson(int $flags = 0): string
220: {
221: return Json::encode($this->jsonSerialize(), $flags);
222: }
223:
224: /**
225: * @inheritDoc
226: */
227: public function first()
228: {
229: return $this->Items ? reset($this->Items) : null;
230: }
231:
232: /**
233: * @inheritDoc
234: */
235: public function last()
236: {
237: return $this->Items ? end($this->Items) : null;
238: }
239:
240: /**
241: * @inheritDoc
242: */
243: public function nth(int $n)
244: {
245: if ($n === 0) {
246: throw new InvalidArgumentException('Argument #1 ($n) is 1-based, 0 given');
247: }
248:
249: $keys = array_keys($this->Items);
250: if ($n < 0) {
251: $keys = array_reverse($keys);
252: $n = -$n;
253: }
254: $key = $keys[$n - 1] ?? null;
255: return $key === null
256: ? null
257: : $this->Items[$key];
258: }
259:
260: /**
261: * @return Traversable<TKey,TValue>
262: */
263: public function getIterator(): Traversable
264: {
265: return new ArrayIterator($this->Items);
266: }
267:
268: /**
269: * @param TKey $offset
270: */
271: public function offsetExists($offset): bool
272: {
273: return array_key_exists($offset, $this->Items);
274: }
275:
276: /**
277: * @param TKey $offset
278: * @return TValue
279: */
280: #[ReturnTypeWillChange]
281: public function offsetGet($offset)
282: {
283: return $this->Items[$offset];
284: }
285:
286: public function count(): int
287: {
288: return count($this->Items);
289: }
290:
291: /**
292: * @param Arrayable<TKey,TValue>|iterable<TKey,TValue> $items
293: * @return array<TKey,TValue>
294: */
295: protected function getItemsArray($items): array
296: {
297: $items = $this->getItems($items);
298: return is_array($items)
299: ? $items
300: : iterator_to_array($items);
301: }
302:
303: /**
304: * @param Arrayable<TKey,TValue>|iterable<TKey,TValue> $items
305: * @return iterable<TKey,TValue>
306: */
307: protected function getItems($items): iterable
308: {
309: if ($items instanceof self) {
310: $items = $items->Items;
311: } elseif ($items instanceof Arrayable) {
312: $items = $items->toArray();
313: }
314: // @phpstan-ignore argument.type
315: return $this->filterItems($items);
316: }
317:
318: /**
319: * Override to normalise items applied to the collection
320: *
321: * @param iterable<TKey,TValue> $items
322: * @return iterable<TKey,TValue>
323: */
324: protected function filterItems(iterable $items): iterable
325: {
326: return $items;
327: }
328:
329: /**
330: * Compare items using Comparable::compare() if implemented
331: *
332: * @param TValue $a
333: * @param TValue $b
334: */
335: protected function compareItems($a, $b): int
336: {
337: if (
338: $a instanceof Comparable
339: && $b instanceof Comparable
340: ) {
341: if ($b instanceof $a) {
342: return $a->compare($a, $b);
343: }
344: if ($a instanceof $b) {
345: return $b->compare($a, $b);
346: }
347: }
348: return $a <=> $b;
349: }
350:
351: /**
352: * @param int-mask-of<CollectionInterface::*> $mode
353: * @param TKey $key
354: * @param TValue $value
355: * @return ($mode is 3|11|19 ? array{TKey,TValue} : ($mode is 2|10|18 ? TKey : TValue))
356: */
357: protected function getCallbackValue(int $mode, $key, $value)
358: {
359: $mode &= CollectionInterface::CALLBACK_USE_BOTH;
360: return $mode === CollectionInterface::CALLBACK_USE_KEY
361: ? $key
362: : ($mode === CollectionInterface::CALLBACK_USE_BOTH
363: ? [$key, $value]
364: : $value);
365: }
366:
367: /**
368: * @param int-mask-of<CollectionInterface::*> $mode
369: * @param TKey $key
370: * @param TValue $value
371: * @return ($mode is 16|17|18|19 ? TKey : TValue)
372: */
373: protected function getReturnValue(int $mode, $key, $value)
374: {
375: return $mode & CollectionInterface::FIND_KEY
376: && !($mode & CollectionInterface::FIND_VALUE)
377: ? $key
378: : $value;
379: }
380: }
381: