1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Tdn\PhpTypes\Type; |
6
|
|
|
|
7
|
|
|
use ArrayIterator; |
8
|
|
|
use Closure; |
9
|
|
|
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor; |
10
|
|
|
use Doctrine\Common\Collections\Collection as CollectionInterface; |
11
|
|
|
use Tdn\PhpTypes\Exception\InvalidTransformationException; |
12
|
|
|
use Tdn\PhpTypes\Exception\InvalidTypeCastException; |
13
|
|
|
use Tdn\PhpTypes\Type\Traits\Boxable; |
14
|
|
|
use Tdn\PhpTypes\Type\Traits\Transmutable; |
15
|
|
|
use Doctrine\Common\Collections\Criteria; |
16
|
|
|
use Doctrine\Common\Collections\Selectable; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Class Collection. |
20
|
|
|
* |
21
|
|
|
* A Collection is a TypeInterface implementation that wraps around a regular PHP array. |
22
|
|
|
* This object implements Doctrine's Collection and is analogous to Doctrine's |
23
|
|
|
* ArrayCollection, with extra functionality. |
24
|
|
|
* |
25
|
|
|
* This object can be extended to create type specific collections. (either primitive or compound) |
26
|
|
|
*/ |
27
|
|
|
class Collection implements TransmutableTypeInterface, CollectionInterface, Selectable |
28
|
|
|
{ |
29
|
|
|
use Transmutable; |
30
|
|
|
use Boxable; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var null|string |
34
|
|
|
*/ |
35
|
|
|
private $type; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var array |
39
|
|
|
*/ |
40
|
|
|
private $elements; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @param array $elements |
44
|
|
|
* @param null|string $type |
45
|
|
|
*/ |
46
|
70 |
|
public function __construct(array $elements = array(), string $type = null) |
47
|
|
|
{ |
48
|
70 |
|
$this->type = $type; |
49
|
|
|
$this->elements = array_map(function ($element) { |
50
|
65 |
|
return $this->getRealValue($element); |
51
|
70 |
|
}, $elements); |
52
|
69 |
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @param $element |
56
|
|
|
* |
57
|
|
|
* @return bool |
58
|
|
|
*/ |
59
|
1 |
|
public function unshift($element) |
60
|
|
|
{ |
61
|
1 |
|
array_unshift($this->elements, $this->getRealValue($element)); |
62
|
|
|
|
63
|
1 |
|
return true; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* {@inheritdoc} |
68
|
|
|
*/ |
69
|
3 |
|
public function first() |
70
|
|
|
{ |
71
|
3 |
|
return reset($this->elements); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* {@inheritdoc} |
76
|
|
|
*/ |
77
|
3 |
|
public function last() |
78
|
|
|
{ |
79
|
3 |
|
return end($this->elements); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* {@inheritdoc} |
84
|
|
|
*/ |
85
|
6 |
|
public function key() |
86
|
|
|
{ |
87
|
6 |
|
return key($this->elements); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* {@inheritdoc} |
92
|
|
|
*/ |
93
|
9 |
|
public function next() |
94
|
|
|
{ |
95
|
9 |
|
return next($this->elements); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* {@inheritdoc} |
100
|
|
|
*/ |
101
|
6 |
|
public function current() |
102
|
|
|
{ |
103
|
6 |
|
return current($this->elements); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* {@inheritdoc} |
108
|
|
|
*/ |
109
|
1 |
|
public function remove($key) |
110
|
|
|
{ |
111
|
1 |
|
if (!isset($this->elements[$key]) && !array_key_exists($key, $this->elements)) { |
112
|
1 |
|
return null; |
113
|
|
|
} |
114
|
|
|
|
115
|
1 |
|
$removed = $this->elements[$key]; |
116
|
1 |
|
unset($this->elements[$key]); |
117
|
|
|
|
118
|
1 |
|
return $removed; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* {@inheritdoc} |
123
|
|
|
*/ |
124
|
1 |
|
public function removeElement($element) |
125
|
|
|
{ |
126
|
1 |
|
$key = array_search($element, $this->elements, true); |
127
|
|
|
|
128
|
1 |
|
if ($key === false) { |
129
|
1 |
|
return false; |
130
|
|
|
} |
131
|
|
|
|
132
|
1 |
|
unset($this->elements[$key]); |
133
|
|
|
|
134
|
1 |
|
return true; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Required by interface ArrayAccess. |
139
|
|
|
* |
140
|
|
|
* {@inheritdoc} |
141
|
|
|
*/ |
142
|
1 |
|
public function offsetExists($offset) |
143
|
|
|
{ |
144
|
1 |
|
return $this->containsKey($offset); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Required by interface ArrayAccess. |
149
|
|
|
* |
150
|
|
|
* {@inheritdoc} |
151
|
|
|
*/ |
152
|
1 |
|
public function offsetGet($offset) |
153
|
|
|
{ |
154
|
1 |
|
return $this->get($offset); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Required by interface ArrayAccess. |
159
|
|
|
* |
160
|
|
|
* {@inheritdoc} |
161
|
|
|
*/ |
162
|
|
|
public function offsetSet($offset, $value) |
163
|
|
|
{ |
164
|
|
|
if (!isset($offset)) { |
165
|
|
|
return $this->add($value); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
$this->set($offset, $value); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Required by interface ArrayAccess. |
173
|
|
|
* |
174
|
|
|
* {@inheritdoc} |
175
|
|
|
*/ |
176
|
1 |
|
public function offsetUnset($offset) |
177
|
|
|
{ |
178
|
1 |
|
return $this->remove($offset); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* {@inheritdoc} |
183
|
|
|
*/ |
184
|
1 |
|
public function containsKey($key) |
185
|
|
|
{ |
186
|
1 |
|
return isset($this->elements[$key]) || array_key_exists($key, $this->elements); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* {@inheritdoc} |
191
|
|
|
*/ |
192
|
1 |
|
public function contains($element) |
193
|
|
|
{ |
194
|
1 |
|
return in_array($element, $this->elements, true); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* {@inheritdoc} |
199
|
|
|
*/ |
200
|
1 |
|
public function exists(Closure $p) |
|
|
|
|
201
|
|
|
{ |
202
|
1 |
|
foreach ($this->elements as $key => $element) { |
203
|
1 |
|
if ($p($key, $element)) { |
204
|
1 |
|
return true; |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
208
|
1 |
|
return false; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* {@inheritdoc} |
213
|
|
|
*/ |
214
|
1 |
|
public function indexOf($element) |
215
|
|
|
{ |
216
|
1 |
|
return array_search($element, $this->elements, true); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* {@inheritdoc} |
221
|
|
|
*/ |
222
|
1 |
|
public function get($key) |
223
|
|
|
{ |
224
|
1 |
|
return isset($this->elements[$key]) ? $this->elements[$key] : null; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* {@inheritdoc} |
229
|
|
|
*/ |
230
|
3 |
|
public function getKeys() |
231
|
|
|
{ |
232
|
3 |
|
return array_keys($this->elements); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* {@inheritdoc} |
237
|
|
|
*/ |
238
|
3 |
|
public function getValues() |
239
|
|
|
{ |
240
|
3 |
|
return array_values($this->elements); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* {@inheritdoc} |
245
|
|
|
*/ |
246
|
5 |
|
public function count() |
247
|
|
|
{ |
248
|
5 |
|
return count($this->elements); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* {@inheritdoc} |
253
|
|
|
*/ |
254
|
1 |
|
public function isEmpty() |
255
|
|
|
{ |
256
|
1 |
|
return empty($this->elements); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Required by interface IteratorAggregate. |
261
|
|
|
* |
262
|
|
|
* {@inheritdoc} |
263
|
|
|
*/ |
264
|
10 |
|
public function getIterator() |
265
|
|
|
{ |
266
|
10 |
|
return new ArrayIterator($this->elements); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* {@inheritdoc} |
271
|
|
|
* |
272
|
|
|
* @return Collection |
273
|
|
|
*/ |
274
|
|
|
public function map(Closure $func) |
275
|
|
|
{ |
276
|
|
|
return new static(array_map($func, $this->elements), $this->type); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* {@inheritdoc} |
281
|
|
|
* |
282
|
|
|
* @return Collection |
283
|
|
|
*/ |
284
|
|
|
public function filter(Closure $p) |
285
|
|
|
{ |
286
|
|
|
return new static(array_filter($this->elements, $p), $this->type); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* {@inheritdoc} |
291
|
|
|
*/ |
292
|
2 |
|
public function forAll(Closure $p) |
|
|
|
|
293
|
|
|
{ |
294
|
2 |
|
foreach ($this->elements as $key => $element) { |
295
|
2 |
|
if (!$p($key, $element)) { |
296
|
2 |
|
return false; |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
|
300
|
1 |
|
return true; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* {@inheritdoc} |
305
|
|
|
* |
306
|
|
|
* @return Collection |
|
|
|
|
307
|
|
|
*/ |
308
|
|
|
public function partition(Closure $p) |
309
|
|
|
{ |
310
|
|
|
$matches = $noMatches = array(); |
311
|
|
|
|
312
|
|
|
foreach ($this->elements as $key => $element) { |
313
|
|
|
if ($p($key, $element)) { |
314
|
|
|
$matches[$key] = $element; |
315
|
|
|
} else { |
316
|
|
|
$noMatches[$key] = $element; |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
return array(new static($matches, $this->type), new static($noMatches, $this->type)); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* {@inheritdoc} |
325
|
|
|
*/ |
326
|
|
|
public function clear() |
327
|
|
|
{ |
328
|
|
|
$this->elements = array(); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* {@inheritdoc} |
333
|
|
|
*/ |
334
|
|
|
public function slice($offset, $length = null) |
335
|
|
|
{ |
336
|
|
|
return array_slice($this->elements, $offset, $length, true); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* {@inheritdoc} |
341
|
|
|
* |
342
|
|
|
* @return Collection |
343
|
|
|
*/ |
344
|
1 |
|
public function matching(Criteria $criteria) |
345
|
|
|
{ |
346
|
1 |
|
$expr = $criteria->getWhereExpression(); |
347
|
1 |
|
$filtered = $this->elements; |
348
|
|
|
|
349
|
1 |
|
if ($expr) { |
350
|
|
|
$visitor = new ClosureExpressionVisitor(); |
351
|
|
|
$filter = $visitor->dispatch($expr); |
352
|
|
|
$filtered = array_filter($filtered, $filter); |
353
|
|
|
} |
354
|
|
|
|
355
|
1 |
|
if ($orderings = $criteria->getOrderings()) { |
356
|
1 |
|
$next = null; |
357
|
1 |
|
foreach (array_reverse($orderings) as $field => $ordering) { |
358
|
1 |
|
$next = ClosureExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1); |
359
|
|
|
} |
360
|
|
|
|
361
|
1 |
|
if (null !== $next) { |
362
|
1 |
|
uasort($filtered, $next); |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
366
|
1 |
|
$offset = $criteria->getFirstResult(); |
367
|
1 |
|
$length = $criteria->getMaxResults(); |
368
|
|
|
|
369
|
1 |
|
if (null !== $offset || null !== $length) { |
370
|
|
|
$filtered = array_slice($filtered, (int) $offset, $length); |
371
|
|
|
} |
372
|
|
|
|
373
|
1 |
|
return new static($filtered, $this->type); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* @throws \LogicException when not collection is untyped or collection type does not contain __toString method |
378
|
|
|
* |
379
|
|
|
* @return Collection |
380
|
|
|
*/ |
381
|
|
|
public function unique() |
382
|
|
|
{ |
383
|
2 |
|
$closure = function ($key, $value) { |
384
|
2 |
|
return is_string($value); |
385
|
2 |
|
}; |
386
|
|
|
|
387
|
2 |
|
if ((null !== $this->type && method_exists($this->type, '__toString')) || $this->forAll($closure)) { |
388
|
1 |
|
$result = new static(array_unique($this->elements, SORT_STRING), $this->type); |
389
|
|
|
|
390
|
1 |
|
return $result; |
391
|
|
|
} |
392
|
|
|
|
393
|
1 |
|
throw new \LogicException('Collection instance is not typed, or type has no string support.'); |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* @param mixed $value |
398
|
|
|
*/ |
399
|
2 |
|
public function add($value) |
400
|
|
|
{ |
401
|
2 |
|
$this->elements[] = $this->getRealValue($value); |
402
|
|
|
|
403
|
2 |
|
return true; |
404
|
|
|
} |
405
|
|
|
|
406
|
1 |
|
public function set($key, $value) |
407
|
|
|
{ |
408
|
1 |
|
$this->elements[$key] = $this->getRealValue($value); |
409
|
1 |
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* {@inheritdoc} |
413
|
|
|
* |
414
|
|
|
* @return string|array|int |
415
|
|
|
*/ |
416
|
21 |
|
public function __invoke(int $toType = Type::ARRAY) |
417
|
|
|
{ |
418
|
21 |
|
$e = null; |
419
|
|
|
switch ($toType) { |
420
|
21 |
|
case Type::INT: |
421
|
1 |
|
return $this->count(); |
422
|
21 |
|
case Type::ARRAY: |
423
|
19 |
|
return $this->elements; |
424
|
4 |
|
case Type::STRING: |
425
|
|
|
try { |
426
|
2 |
|
return (StringType::valueOf($this))(Type::STRING); |
427
|
1 |
|
} catch (\Throwable $e) { |
428
|
1 |
|
$e = new \ErrorException($e->getMessage()); |
429
|
|
|
} |
430
|
|
|
// Intentionally throwing exception below. |
431
|
|
|
default: |
432
|
3 |
|
throw new InvalidTypeCastException(static::class, $this->getTranslatedType($toType), null, 0, $e); |
433
|
|
|
} |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* @param CollectionInterface $collection |
438
|
|
|
* @param bool $keepDupes |
439
|
|
|
* |
440
|
|
|
* @return Collection |
441
|
|
|
*/ |
442
|
2 |
|
public function merge(CollectionInterface $collection, $keepDupes = false): Collection |
443
|
|
|
{ |
444
|
2 |
|
if ($keepDupes) { |
445
|
1 |
|
return new self(array_merge($this->toArray(), $collection->toArray())); |
446
|
|
|
} |
447
|
|
|
|
448
|
1 |
|
return new self($this->toArray() + $collection->toArray()); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* {@inheritdoc} |
453
|
|
|
* |
454
|
|
|
* @return Collection |
455
|
|
|
*/ |
456
|
4 |
|
public static function valueOf($mixed): Collection |
457
|
|
|
{ |
458
|
4 |
|
return new static(self::asArray($mixed)); |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* @param string $delimeter |
463
|
|
|
* |
464
|
|
|
* @return StringType |
465
|
|
|
*/ |
466
|
1 |
|
public function implode(string $delimeter): StringType |
467
|
|
|
{ |
468
|
1 |
|
return StringType::create(implode($delimeter, $this->toArray())); |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* Returns a mixed variable as an array. |
473
|
|
|
* |
474
|
|
|
* @param $mixed |
475
|
|
|
* |
476
|
|
|
* @return array |
477
|
|
|
*/ |
478
|
4 |
|
private static function asArray($mixed): array |
479
|
|
|
{ |
480
|
4 |
|
if ($mixed instanceof CollectionInterface) { |
481
|
2 |
|
return $mixed->toArray(); |
482
|
|
|
} |
483
|
|
|
|
484
|
3 |
|
if ($mixed instanceof TypeInterface) { |
485
|
2 |
|
return [$mixed()]; |
486
|
|
|
} |
487
|
|
|
|
488
|
2 |
|
$type = strtolower(gettype($mixed)); |
489
|
|
|
switch ($type) { |
490
|
2 |
|
case 'integer': |
491
|
2 |
|
case 'double': |
492
|
2 |
|
case 'float': |
493
|
2 |
|
case 'string': |
494
|
2 |
|
case 'object': |
495
|
2 |
|
case 'resource': |
496
|
2 |
|
case 'boolean': |
497
|
1 |
|
return [$mixed]; |
498
|
2 |
|
case 'array': |
499
|
1 |
|
return $mixed; |
500
|
|
|
default: |
501
|
1 |
|
throw new InvalidTransformationException($type, static::class); |
502
|
|
|
} |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
/** |
506
|
|
|
* @param $value |
507
|
|
|
* |
508
|
|
|
* @return mixed |
509
|
|
|
*/ |
510
|
66 |
|
private function getRealValue($value) |
511
|
|
|
{ |
512
|
66 |
|
if (null !== $this->type && class_exists($this->type) && !$value instanceof $this->type) { |
513
|
10 |
|
return new $this->type($value); |
514
|
|
|
} |
515
|
|
|
|
516
|
56 |
|
return $value; |
517
|
|
|
} |
518
|
|
|
} |
519
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.