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