1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace Igni\Storage\Mapping\Collection; |
4
|
|
|
|
5
|
|
|
use Igni\Storage\Exception\CollectionException; |
6
|
|
|
use Traversable; |
7
|
|
|
use Igni\Exception\OutOfBoundsException; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* Immutable collection representation. |
11
|
|
|
*/ |
12
|
|
|
class Collection implements \Igni\Storage\Mapping\Collection |
13
|
|
|
{ |
14
|
|
|
protected $length = 0; |
15
|
|
|
protected $cursor = 0; |
16
|
|
|
protected $items = []; |
17
|
|
|
|
18
|
21 |
|
public function __construct(iterable $cursor = null) |
19
|
|
|
{ |
20
|
21 |
|
if ($cursor === null) { |
21
|
4 |
|
$this->items = []; |
22
|
4 |
|
$this->length = 0; |
23
|
4 |
|
return; |
24
|
|
|
} |
25
|
|
|
|
26
|
|
|
/** @TODO: Maybe keep cursor as iterator */ |
27
|
21 |
|
if ($cursor instanceof Traversable) { |
28
|
7 |
|
$this->items = iterator_to_array($cursor); |
29
|
|
|
} else { |
30
|
14 |
|
$this->items = $cursor; |
31
|
|
|
} |
32
|
|
|
|
33
|
21 |
|
$this->length = count($this->items); |
34
|
21 |
|
} |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Reduces a collection to a single value by iteratively combining elements of the collection |
38
|
|
|
* using the provided callable. |
39
|
|
|
* |
40
|
|
|
* @param callable $f($previousValue, $current) |
41
|
|
|
* @param mixed $initialValue |
42
|
|
|
* @return mixed |
43
|
|
|
*/ |
44
|
1 |
|
public function reduce(callable $f, $initialValue = null) |
45
|
|
|
{ |
46
|
1 |
|
foreach ($this as $element) { |
47
|
1 |
|
$initialValue = $f($initialValue, $element); |
48
|
|
|
} |
49
|
|
|
|
50
|
1 |
|
return $initialValue; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Returns new collection with items sorted by the order specified by the compare function. |
55
|
|
|
* |
56
|
|
|
* @param callable $compare |
57
|
|
|
* @return Collection |
58
|
|
|
*/ |
59
|
1 |
|
public function sort(callable $compare): self |
60
|
|
|
{ |
61
|
1 |
|
$collection = clone $this; |
62
|
1 |
|
usort($collection->items, $compare); |
|
|
|
|
63
|
|
|
|
64
|
1 |
|
return $collection; |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Returns new collection with elements that are created by calling f callable on each element. |
69
|
|
|
* |
70
|
|
|
* @param callable $f($element) |
71
|
|
|
* @return Collection |
72
|
|
|
*/ |
73
|
1 |
|
public function map(callable $f): self |
74
|
|
|
{ |
75
|
1 |
|
$collection = new self(); |
76
|
1 |
|
foreach ($this as $item) { |
77
|
1 |
|
$collection->items[] = $f($item); |
78
|
|
|
} |
79
|
1 |
|
$collection->length = $this->length; |
80
|
|
|
|
81
|
1 |
|
return $collection; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Returns new collection with inserted element(s) at position index. |
86
|
|
|
* |
87
|
|
|
* @param int $index |
88
|
|
|
* @param mixed ...$elements |
89
|
|
|
* @return Collection |
90
|
|
|
*/ |
91
|
3 |
|
public function insert(int $index, ...$elements): self |
92
|
|
|
{ |
93
|
3 |
|
if ($index < 0 || $index - 1 > $this->length) { |
94
|
1 |
|
throw CollectionException::forInvalidIndex($index); |
95
|
|
|
} |
96
|
|
|
|
97
|
2 |
|
$collection = clone $this; |
98
|
2 |
|
array_splice($collection->items, $index, 0, $elements); |
|
|
|
|
99
|
|
|
|
100
|
2 |
|
return $collection; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Returns a new collection with elements extracted as slice. |
105
|
|
|
* |
106
|
|
|
* @param int $start |
107
|
|
|
* @param int $length |
108
|
|
|
* @return Collection |
109
|
|
|
*/ |
110
|
1 |
|
public function slice(int $start, int $length): self |
111
|
|
|
{ |
112
|
1 |
|
$items = array_slice($this->items, $start, $length); |
|
|
|
|
113
|
1 |
|
$collection = new self(); |
114
|
1 |
|
$collection->items = $items; |
115
|
1 |
|
$collection->length = $length; |
116
|
|
|
|
117
|
1 |
|
return $collection; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Returns a new Collection with all elements that satisfy the predicate test. |
122
|
|
|
* @param callable $test($element) |
123
|
|
|
* @return Collection |
124
|
|
|
*/ |
125
|
1 |
|
public function where(callable $test): self |
126
|
|
|
{ |
127
|
1 |
|
$collection = new self(); |
128
|
1 |
|
$length = 0; |
129
|
1 |
|
foreach ($this as $item) { |
130
|
1 |
|
if ($test($item)) { |
131
|
1 |
|
$length++; |
132
|
1 |
|
$collection->items[] = $item; |
133
|
|
|
} |
134
|
|
|
} |
135
|
1 |
|
$collection->length = $length; |
136
|
|
|
|
137
|
1 |
|
return $collection; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Checks whether every element of this iterable satisfies test |
142
|
|
|
* @param callable $test($element) |
143
|
|
|
* @return bool |
144
|
|
|
*/ |
145
|
1 |
|
public function every(callable $test): bool |
146
|
|
|
{ |
147
|
1 |
|
foreach($this as $item) { |
148
|
1 |
|
if (!$test($item)) { |
149
|
1 |
|
return false; |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
1 |
|
return true; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Checks whether any element of this iterable satisfies test |
158
|
|
|
* @param callable $test($element) |
159
|
|
|
* @return bool |
160
|
|
|
*/ |
161
|
1 |
|
public function any(callable $test): bool |
162
|
|
|
{ |
163
|
1 |
|
foreach($this as $item) { |
164
|
1 |
|
if ($test($item)) { |
165
|
1 |
|
return true; |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
|
169
|
1 |
|
return false; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Returns instance of the collection with reversed items. |
174
|
|
|
* @return Collection |
175
|
|
|
*/ |
176
|
1 |
|
public function reverse(): self |
177
|
|
|
{ |
178
|
1 |
|
$collection = clone $this; |
179
|
1 |
|
$collection->items = array_reverse($collection->items); |
|
|
|
|
180
|
|
|
|
181
|
1 |
|
return $collection; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Removes all objects from this collection; the length of the collection becomes zero. |
186
|
|
|
*/ |
187
|
1 |
|
public function clear(): void |
188
|
|
|
{ |
189
|
1 |
|
$this->items = []; |
190
|
1 |
|
$this->length = 0; |
191
|
1 |
|
$this->cursor = 0; |
192
|
1 |
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Returns new collection with value added to the end of this list, extending the length by one. |
196
|
|
|
* |
197
|
|
|
* @param mixed $element |
198
|
|
|
* @return Collection |
199
|
|
|
*/ |
200
|
1 |
|
public function add($element): self |
201
|
|
|
{ |
202
|
1 |
|
$collection = clone $this; |
203
|
1 |
|
$collection->cursor = 0; |
204
|
1 |
|
$collection->items[] = $element; |
205
|
1 |
|
$collection->length++; |
206
|
|
|
|
207
|
1 |
|
return $collection; |
208
|
|
|
} |
209
|
|
|
|
210
|
1 |
|
public function addMany(...$elements): self |
211
|
|
|
{ |
212
|
1 |
|
$collection = clone $this; |
213
|
1 |
|
$collection->cursor = 0; |
214
|
1 |
|
$collection->items = array_merge($collection->items, $elements); |
|
|
|
|
215
|
1 |
|
$collection->length += count($elements); |
216
|
|
|
|
217
|
1 |
|
return $collection; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Returns new collection with removed value, decreasing the length by one. |
222
|
|
|
* |
223
|
|
|
* @param $element |
224
|
|
|
* @return Collection |
225
|
|
|
*/ |
226
|
1 |
|
public function remove($element): self |
227
|
|
|
{ |
228
|
1 |
|
$index = array_search($element, $this->items); |
|
|
|
|
229
|
1 |
|
if ($index === false) { |
230
|
|
|
return $this; |
231
|
|
|
} |
232
|
|
|
|
233
|
1 |
|
$collection = clone $this; |
234
|
1 |
|
array_splice($collection->items, $index, 1); |
|
|
|
|
235
|
1 |
|
$collection->length--; |
236
|
1 |
|
$collection->cursor = 0; |
237
|
|
|
|
238
|
1 |
|
return $collection; |
239
|
|
|
} |
240
|
|
|
|
241
|
1 |
|
public function removeMany(...$elements): self |
242
|
|
|
{ |
243
|
1 |
|
$collection = clone $this; |
244
|
1 |
|
$collection->items = array_values(array_diff($collection->items, $elements)); |
|
|
|
|
245
|
1 |
|
$collection->length -= count($elements); |
246
|
1 |
|
$collection->cursor = 0; |
247
|
|
|
|
248
|
1 |
|
return $collection; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Checks if collection contains given element. |
253
|
|
|
* @param $element |
254
|
|
|
* @return bool |
255
|
|
|
*/ |
256
|
1 |
|
public function contains($element): bool |
257
|
|
|
{ |
258
|
1 |
|
return in_array($element, $this->items); |
|
|
|
|
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Returns first element of the collection |
263
|
|
|
* @return mixed |
264
|
|
|
*/ |
265
|
1 |
|
public function first() |
266
|
|
|
{ |
267
|
1 |
|
$this->cursor = 0; |
268
|
|
|
|
269
|
1 |
|
return $this->at($this->cursor); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Returns last element of the collection |
274
|
|
|
* @return mixed |
275
|
|
|
*/ |
276
|
1 |
|
public function last() |
277
|
|
|
{ |
278
|
1 |
|
$this->cursor = $this->length - 1; |
279
|
|
|
|
280
|
1 |
|
return $this->current(); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Returns element at the index |
285
|
|
|
* @param int $offset |
286
|
|
|
* @return mixed |
287
|
|
|
*/ |
288
|
9 |
|
public function at(int $offset) |
289
|
|
|
{ |
290
|
9 |
|
if ($offset < $this->length) { |
291
|
9 |
|
return $this->items[$offset]; |
292
|
|
|
} |
293
|
|
|
|
294
|
1 |
|
throw new OutOfBoundsException("Invalid offset: ${offset}"); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Returns current element |
299
|
|
|
* @return mixed |
300
|
|
|
*/ |
301
|
|
|
public function current() |
302
|
|
|
{ |
303
|
9 |
|
return $this->at($this->cursor); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Moves cursor of the collection forward by one. |
308
|
|
|
*/ |
309
|
|
|
public function next(): void |
310
|
|
|
{ |
311
|
7 |
|
$this->cursor++; |
312
|
7 |
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* Moves cursor of the collection backward by one. |
316
|
|
|
*/ |
317
|
|
|
public function previous(): void |
318
|
|
|
{ |
319
|
1 |
|
if ($this->cursor > 0) { |
320
|
1 |
|
$this->cursor--; |
321
|
|
|
} |
322
|
1 |
|
} |
323
|
|
|
|
324
|
|
|
public function key(): int |
325
|
|
|
{ |
326
|
1 |
|
return $this->cursor; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
public function valid(): bool |
330
|
|
|
{ |
331
|
6 |
|
if ($this->cursor < $this->length) { |
332
|
6 |
|
return true; |
333
|
|
|
} |
334
|
|
|
|
335
|
6 |
|
return false; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
public function rewind(): void |
339
|
|
|
{ |
340
|
6 |
|
$this->cursor = 0; |
341
|
6 |
|
} |
342
|
|
|
|
343
|
|
|
public function count(): int |
344
|
|
|
{ |
345
|
3 |
|
return $this->length; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
public function toArray(): array |
349
|
|
|
{ |
350
|
11 |
|
return $this->items; |
|
|
|
|
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
public static function fromList(...$elements): Collection |
354
|
|
|
{ |
355
|
1 |
|
return new self($elements); |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
|