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: | |
19: | |
20: | |
21: | |
22: | |
23: | |
24: | |
25: | |
26: | |
27: | |
28: | |
29: | trait ReadableCollectionTrait |
30: | { |
31: | |
32: | protected $Items = []; |
33: | |
34: | |
35: | |
36: | |
37: | public function __construct($items = []) |
38: | { |
39: | $this->Items = $this->getItems($items); |
40: | } |
41: | |
42: | |
43: | |
44: | |
45: | public static function empty() |
46: | { |
47: | return new static(); |
48: | } |
49: | |
50: | |
51: | |
52: | |
53: | public function copy() |
54: | { |
55: | return clone $this; |
56: | } |
57: | |
58: | |
59: | |
60: | |
61: | public function has($key): bool |
62: | { |
63: | return array_key_exists($key, $this->Items); |
64: | } |
65: | |
66: | |
67: | |
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: | |
79: | |
80: | |
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: | |
92: | |
93: | $callback($item, $next, $prev); |
94: | $prev = $item; |
95: | } |
96: | $item = $next; |
97: | } |
98: | if ($i) { |
99: | |
100: | $callback($item, null, $prev); |
101: | } |
102: | |
103: | return $this; |
104: | } |
105: | |
106: | |
107: | |
108: | |
109: | |
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: | |
123: | |
124: | if ($callback($item, $next, $prev)) { |
125: | |
126: | |
127: | return $this->getReturnValue($mode, $key, $value); |
128: | } |
129: | $prev = $item; |
130: | } |
131: | $item = $next; |
132: | $key = $nextKey; |
133: | $value = $nextValue; |
134: | } |
135: | |
136: | if ($i && $callback($item, null, $prev)) { |
137: | |
138: | |
139: | return $this->getReturnValue($mode, $key, $value); |
140: | } |
141: | |
142: | return null; |
143: | } |
144: | |
145: | |
146: | |
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: | |
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: | |
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: | |
199: | |
200: | public function all(): array |
201: | { |
202: | return $this->Items; |
203: | } |
204: | |
205: | |
206: | |
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: | |
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: | |
239: | |
240: | public function toJson(int $flags = 0): string |
241: | { |
242: | return Json::stringify($this->jsonSerialize(), $flags); |
243: | } |
244: | |
245: | |
246: | |
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: | |
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: | |
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: | |
291: | |
292: | |
293: | |
294: | |
295: | public function getIterator(): Traversable |
296: | { |
297: | return new ArrayIterator($this->Items); |
298: | } |
299: | |
300: | |
301: | |
302: | |
303: | |
304: | |
305: | public function offsetExists($offset): bool |
306: | { |
307: | return array_key_exists($offset, $this->Items); |
308: | } |
309: | |
310: | |
311: | |
312: | |
313: | |
314: | #[ReturnTypeWillChange] |
315: | public function offsetGet($offset) |
316: | { |
317: | return $this->Items[$offset]; |
318: | } |
319: | |
320: | |
321: | |
322: | public function count(): int |
323: | { |
324: | return count($this->Items); |
325: | } |
326: | |
327: | |
328: | |
329: | |
330: | |
331: | |
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: | |
348: | |
349: | |
350: | |
351: | |
352: | protected function filterItems(array $items): array |
353: | { |
354: | return $items; |
355: | } |
356: | |
357: | |
358: | |
359: | |
360: | |
361: | |
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: | |
381: | |
382: | |
383: | |
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: | |
399: | |
400: | |
401: | |
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: | |