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