• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NexusPHP / framework / 14369703574

29 Mar 2025 05:15PM UTC coverage: 100.0%. Remained the same
14369703574

push

github

paulbalandan
Fix phpstan error

958 of 958 relevant lines covered (100.0%)

7.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/Nexus/Collection/Collection.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of the Nexus framework.
7
 *
8
 * (c) John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Nexus\Collection;
15

16
use Nexus\Collection\Iterator\ClosureIteratorAggregate;
17
use Nexus\Collection\Iterator\RewindableIterator;
18

19
/**
20
 * @template TKey
21
 * @template T
22
 *
23
 * @implements CollectionInterface<TKey, T>
24
 *
25
 * @immutable
26
 */
27
final class Collection implements CollectionInterface
28
{
29
    /**
30
     * @var ClosureIteratorAggregate<TKey, T>
31
     */
32
    private ClosureIteratorAggregate $innerIterator;
33

34
    /**
35
     * @template S
36
     *
37
     * @param (\Closure(S): \Iterator<TKey, T>) $callable
38
     * @param iterable<int, S>                  $parameter
39
     */
40
    public function __construct(\Closure $callable, iterable $parameter = [])
41
    {
42
        $this->innerIterator = ClosureIteratorAggregate::from($callable, ...$parameter);
76✔
43
    }
44

45
    /**
46
     * @template WrapKey
47
     * @template Wrap
48
     *
49
     * @return self<WrapKey, Wrap>
50
     */
51
    public static function wrap(\Closure|iterable $items): self
52
    {
53
        if ($items instanceof \Closure) {
76✔
54
            return new self(static fn(): iterable => yield from $items());
18✔
55
        }
56

57
        return new self(static fn(): iterable => yield from $items);
64✔
58
    }
59

60
    /**
61
     * @return ($preserveKeys is false ? list<T> : array<array-key, T>)
62
     */
63
    public function all(bool $preserveKeys = false): array
64
    {
65
        return iterator_to_array($this, $preserveKeys);
56✔
66
    }
67

68
    public function any(\Closure $predicate): bool
69
    {
70
        foreach ($this as $key => $item) {
2✔
71
            if ($predicate($item, $key)) {
2✔
72
                return true;
2✔
73
            }
74
        }
75

76
        return false;
2✔
77
    }
78

79
    /**
80
     * @template U
81
     *
82
     * @param U ...$items
83
     *
84
     * @return self<int|TKey, T|U>
85
     */
86
    public function append(mixed ...$items): self
87
    {
88
        return new self(
6✔
89
            static function (iterable $collection) use ($items): iterable {
6✔
90
                $iterator = new \AppendIterator();
6✔
91

92
                foreach ([$collection, $items] as $iterable) {
6✔
93
                    $iterator->append(
6✔
94
                        new \NoRewindIterator(
6✔
95
                            (static fn(): \Generator => yield from $iterable)(),
6✔
96
                        ),
6✔
97
                    );
6✔
98
                }
99

100
                yield from $iterator;
6✔
101
            },
6✔
102
            [$this],
6✔
103
        );
6✔
104
    }
105

106
    /**
107
     * @template U
108
     *
109
     * @return self<T, U>
110
     */
111
    public function associate(iterable $values): self
112
    {
113
        $valuesIterator = (static fn(): \Generator => yield from $values)();
6✔
114

115
        return new self(
6✔
116
            static function (iterable $collection) use ($valuesIterator): iterable {
6✔
117
                foreach ($collection->values() as $key) {
6✔
118
                    if (! $valuesIterator->valid()) {
6✔
119
                        throw new \InvalidArgumentException('The number of values is lesser than the keys.');
2✔
120
                    }
121

122
                    yield $key => $valuesIterator->current();
6✔
123

124
                    $valuesIterator->next();
6✔
125
                }
126

127
                if ($valuesIterator->valid()) {
4✔
128
                    throw new \InvalidArgumentException('The number of values is greater than the keys.');
2✔
129
                }
130
            },
6✔
131
            [$this],
6✔
132
        );
6✔
133
    }
134

135
    /**
136
     * @return self<int, non-empty-array<TKey, T>>
137
     */
138
    public function chunk(int $size): self
139
    {
140
        return new self(
2✔
141
            static function (iterable $collection) use ($size): \Generator {
2✔
142
                $chunk = [];
2✔
143
                $count = 0;
2✔
144

145
                foreach ($collection as $key => $item) {
2✔
146
                    $chunk[$key] = $item;
2✔
147
                    ++$count;
2✔
148

149
                    if ($count === $size) {
2✔
150
                        yield $chunk;
2✔
151

152
                        $chunk = [];
2✔
153
                        $count = 0;
2✔
154
                    }
155
                }
156

157
                if ([] !== $chunk) {
2✔
158
                    yield $chunk;
2✔
159
                }
160
            },
2✔
161
            [$this],
2✔
162
        );
2✔
163
    }
164

165
    public function count(): int
166
    {
167
        return iterator_count($this);
6✔
168
    }
169

170
    /**
171
     * @return self<TKey, T>
172
     */
173
    public function cycle(): self
174
    {
175
        return new self(
4✔
176
            static fn(iterable $collection): iterable => new \InfiniteIterator(
4✔
177
                new RewindableIterator(
4✔
178
                    static fn(): \Generator => yield from $collection,
4✔
179
                ),
4✔
180
            ),
4✔
181
            [$this],
4✔
182
        );
4✔
183
    }
184

185
    /**
186
     * @return self<TKey, T>
187
     */
188
    public function diff(iterable ...$others): self
189
    {
190
        return new self(
2✔
191
            static function (iterable $collection) use ($others): iterable {
2✔
192
                $hashTable = self::generateDiffHashTable($others);
2✔
193

194
                foreach ($collection as $key => $value) {
2✔
195
                    if (! \array_key_exists(self::toArrayKey($value), $hashTable)) {
2✔
196
                        yield $key => $value;
2✔
197
                    }
198
                }
199
            },
2✔
200
            [$this],
2✔
201
        );
2✔
202
    }
203

204
    /**
205
     * @return self<TKey, T>
206
     */
207
    public function diffKey(iterable ...$others): self
208
    {
209
        return new self(
2✔
210
            static function (iterable $collection) use ($others): iterable {
2✔
211
                $hashTable = self::generateDiffHashTable($others);
2✔
212

213
                foreach ($collection as $key => $value) {
2✔
214
                    if (! \array_key_exists(self::toArrayKey($key), $hashTable)) {
2✔
215
                        yield $key => $value;
2✔
216
                    }
217
                }
218
            },
2✔
219
            [$this],
2✔
220
        );
2✔
221
    }
222

223
    /**
224
     * @return self<TKey, T>
225
     */
226
    public function drop(int $length): self
227
    {
228
        return $this->slice($length);
2✔
229
    }
230

231
    public function every(\Closure $predicate): bool
232
    {
233
        foreach ($this as $key => $item) {
2✔
234
            if (! $predicate($item, $key)) {
2✔
235
                return false;
2✔
236
            }
237
        }
238

239
        return true;
2✔
240
    }
241

242
    /**
243
     * @return self<TKey, T>
244
     */
245
    public function filter(?\Closure $predicate = null): self
246
    {
247
        $predicate ??= static fn(mixed $item): bool => (bool) $item;
2✔
248

249
        return new self(
2✔
250
            static function (iterable $collection) use ($predicate): iterable {
2✔
251
                foreach ($collection as $key => $item) {
2✔
252
                    if ($predicate($item)) {
2✔
253
                        yield $key => $item;
2✔
254
                    }
255
                }
256
            },
2✔
257
            [$this],
2✔
258
        );
2✔
259
    }
260

261
    /**
262
     * @return self<TKey, T>
263
     */
264
    public function filterKeys(?\Closure $predicate = null): self
265
    {
266
        $predicate ??= static fn(mixed $key): bool => (bool) $key;
8✔
267

268
        return new self(
8✔
269
            static function (iterable $collection) use ($predicate): iterable {
8✔
270
                foreach ($collection as $key => $item) {
8✔
271
                    if ($predicate($key)) {
8✔
272
                        yield $key => $item;
8✔
273
                    }
274
                }
275
            },
8✔
276
            [$this],
8✔
277
        );
8✔
278
    }
279

280
    /**
281
     * @return self<TKey, T>
282
     */
283
    public function filterWithKey(?\Closure $predicate = null): self
284
    {
285
        $predicate ??= static fn(mixed $item, mixed $key): bool => (bool) $item && (bool) $key;
6✔
286

287
        return new self(
6✔
288
            static function (iterable $collection) use ($predicate): iterable {
6✔
289
                foreach ($collection as $key => $item) {
6✔
290
                    if ($predicate($item, $key)) {
6✔
291
                        yield $key => $item;
6✔
292
                    }
293
                }
294
            },
6✔
295
            [$this],
6✔
296
        );
6✔
297
    }
298

299
    public function first(\Closure $predicate, mixed $default = null): mixed
300
    {
301
        return $this
2✔
302
            ->filterWithKey($predicate)
2✔
303
            ->append($default)
2✔
304
            ->limit(1)
2✔
305
            ->getIterator()
2✔
306
            ->current()
2✔
307
        ;
2✔
308
    }
309

310
    /**
311
     * @return self<T, TKey>
312
     */
313
    public function flip(): self
314
    {
315
        return new self(
2✔
316
            static function (iterable $collection): iterable {
2✔
317
                foreach ($collection as $key => $item) {
2✔
318
                    yield $item => $key;
2✔
319
                }
320
            },
2✔
321
            [$this],
2✔
322
        );
2✔
323
    }
324

325
    /**
326
     * @return self<TKey, T>
327
     */
328
    public function forget(mixed ...$keys): self
329
    {
330
        return $this->filterKeys(
2✔
331
            static fn(mixed $key): bool => ! \in_array($key, $keys, true),
2✔
332
        );
2✔
333
    }
334

335
    public function get(mixed $key, mixed $default = null): mixed
336
    {
337
        return $this
2✔
338
            ->filterKeys(static fn(mixed $k): bool => $key === $k)
2✔
339
            ->append($default)
2✔
340
            ->limit(1)
2✔
341
            ->getIterator()
2✔
342
            ->current()
2✔
343
        ;
2✔
344
    }
345

346
    /**
347
     * @return \Generator<TKey, T, mixed, void>
348
     */
349
    public function getIterator(): \Traversable
350
    {
351
        yield from $this->innerIterator->getIterator();
74✔
352
    }
353

354
    public function has(mixed $key): bool
355
    {
356
        return $this
2✔
357
            ->filterKeys(static fn(mixed $k): bool => $key === $k)
2✔
358
            ->limit(1)
2✔
359
            ->getIterator()
2✔
360
            ->valid()
2✔
361
        ;
2✔
362
    }
363

364
    /**
365
     * @return self<TKey, T>
366
     */
367
    public function intersect(iterable ...$others): self
368
    {
369
        return new self(
2✔
370
            static function (iterable $collection) use ($others): iterable {
2✔
371
                $hashTable = self::generateIntersectHashTable($others);
2✔
372
                $count = \count($others);
2✔
373

374
                foreach ($collection as $key => $value) {
2✔
375
                    $encodedValue = self::toArrayKey($value);
2✔
376

377
                    if (
378
                        \array_key_exists($encodedValue, $hashTable)
2✔
379
                        && $hashTable[$encodedValue] === $count
2✔
380
                    ) {
381
                        yield $key => $value;
2✔
382
                    }
383
                }
384
            },
2✔
385
            [$this],
2✔
386
        );
2✔
387
    }
388

389
    /**
390
     * @return self<TKey, T>
391
     */
392
    public function intersectKey(iterable ...$others): self
393
    {
394
        return new self(
2✔
395
            static function (iterable $collection) use ($others): iterable {
2✔
396
                $hashTable = self::generateIntersectHashTable($others);
2✔
397
                $count = \count($others);
2✔
398

399
                foreach ($collection as $key => $value) {
2✔
400
                    $encodedKey = self::toArrayKey($key);
2✔
401

402
                    if (
403
                        \array_key_exists($encodedKey, $hashTable)
2✔
404
                        && $hashTable[$encodedKey] === $count
2✔
405
                    ) {
406
                        yield $key => $value;
2✔
407
                    }
408
                }
409
            },
2✔
410
            [$this],
2✔
411
        );
2✔
412
    }
413

414
    /**
415
     * @return self<int, TKey>
416
     */
417
    public function keys(): self
418
    {
419
        return new self(
2✔
420
            static function (iterable $collection): iterable {
2✔
421
                foreach ($collection as $key => $_) {
2✔
422
                    yield $key;
2✔
423
                }
424
            },
2✔
425
            [$this],
2✔
426
        );
2✔
427
    }
428

429
    /**
430
     * @return self<TKey, T>
431
     */
432
    public function limit(int $limit = -1, int $offset = 0): self
433
    {
434
        return new self(
12✔
435
            static fn(iterable $collection): iterable => yield from new \LimitIterator(
12✔
436
                (static fn(): iterable => yield from $collection)(),
12✔
437
                $offset,
12✔
438
                $limit,
12✔
439
            ),
12✔
440
            [$this],
12✔
441
        );
12✔
442
    }
443

444
    /**
445
     * @template U
446
     *
447
     * @return self<TKey, U>
448
     */
449
    public function map(\Closure $predicate): self
450
    {
451
        return new self(
2✔
452
            static function (iterable $collection) use ($predicate): iterable {
2✔
453
                foreach ($collection as $key => $item) {
2✔
454
                    yield $key => $predicate($item);
2✔
455
                }
456
            },
2✔
457
            [$this],
2✔
458
        );
2✔
459
    }
460

461
    /**
462
     * @template UKey
463
     *
464
     * @return self<UKey, T>
465
     */
466
    public function mapKeys(\Closure $predicate): self
467
    {
468
        return new self(
2✔
469
            static function (iterable $collection) use ($predicate): iterable {
2✔
470
                foreach ($collection as $key => $item) {
2✔
471
                    yield $predicate($key) => $item;
2✔
472
                }
473
            },
2✔
474
            [$this],
2✔
475
        );
2✔
476
    }
477

478
    /**
479
     * @template U
480
     *
481
     * @return self<TKey, U>
482
     */
483
    public function mapWithKey(\Closure $predicate): self
484
    {
485
        return new self(
2✔
486
            static function (iterable $collection) use ($predicate): iterable {
2✔
487
                foreach ($collection as $key => $item) {
2✔
488
                    yield $key => $predicate($item, $key);
2✔
489
                }
490
            },
2✔
491
            [$this],
2✔
492
        );
2✔
493
    }
494

495
    /**
496
     * @return self<int, CollectionInterface<TKey, T>>
497
     */
498
    public function partition(\Closure $predicate): self
499
    {
500
        return new self(
2✔
501
            static function (iterable $collection) use ($predicate): iterable {
2✔
502
                yield $collection->filterWithKey($predicate);
2✔
503

504
                yield $collection->reject($predicate);
2✔
505
            },
2✔
506
            [$this],
2✔
507
        );
2✔
508
    }
509

510
    public function reduce(\Closure $predicate, mixed $initial = null): mixed
511
    {
512
        $accumulator = $initial;
2✔
513

514
        foreach ($this as $key => $item) {
2✔
515
            $accumulator = $predicate($accumulator, $item, $key);
2✔
516
        }
517

518
        return $accumulator;
2✔
519
    }
520

521
    /**
522
     * @template TAcc
523
     *
524
     * @return self<int, TAcc>
525
     */
526
    public function reductions(\Closure $predicate, mixed $initial = null): self
527
    {
528
        return new self(
2✔
529
            static function (iterable $collection) use ($predicate, $initial): iterable {
2✔
530
                $accumulator = $initial;
2✔
531

532
                foreach ($collection as $key => $item) {
2✔
533
                    $accumulator = $predicate($accumulator, $item, $key);
2✔
534

535
                    yield $accumulator;
2✔
536
                }
537
            },
2✔
538
            [$this],
2✔
539
        );
2✔
540
    }
541

542
    /**
543
     * @return self<TKey, T>
544
     */
545
    public function reject(?\Closure $predicate = null): self
546
    {
547
        $predicate ??= static fn(mixed $item, mixed $key): bool => (bool) $item && (bool) $key;
4✔
548

549
        return new self(
4✔
550
            static function (iterable $collection) use ($predicate): iterable {
4✔
551
                foreach ($collection as $key => $item) {
4✔
552
                    if (! $predicate($item, $key)) {
4✔
553
                        yield $key => $item;
4✔
554
                    }
555
                }
556
            },
4✔
557
            [$this],
4✔
558
        );
4✔
559
    }
560

561
    /**
562
     * @return self<TKey, T>
563
     */
564
    public function slice(int $start, ?int $length = null): self
565
    {
566
        return new self(
6✔
567
            static function (iterable $collection) use ($start, $length): iterable {
6✔
568
                if (0 === $length) {
6✔
569
                    yield from $collection;
2✔
570

571
                    return;
2✔
572
                }
573

574
                $i = 0;
6✔
575

576
                foreach ($collection as $key => $item) {
6✔
577
                    if ($i++ < $start) {
6✔
578
                        continue;
4✔
579
                    }
580

581
                    yield $key => $item;
6✔
582

583
                    if (null !== $length && $i >= $start + $length) {
6✔
584
                        break;
4✔
585
                    }
586
                }
587
            },
6✔
588
            [$this],
6✔
589
        );
6✔
590
    }
591

592
    /**
593
     * @return self<TKey, T>
594
     */
595
    public function take(int $length): self
596
    {
597
        return $this->slice(0, $length);
2✔
598
    }
599

600
    /**
601
     * @return self<TKey, T>
602
     */
603
    public function tap(\Closure ...$callbacks): self
604
    {
605
        return new self(
2✔
606
            static function (iterable $collection) use ($callbacks): iterable {
2✔
607
                foreach ($collection as $key => $item) {
2✔
608
                    foreach ($callbacks as $callback) {
2✔
609
                        $callback($item, $key);
2✔
610
                    }
611
                }
612

613
                yield from $collection;
2✔
614
            },
2✔
615
            [$this],
2✔
616
        );
2✔
617
    }
618

619
    /**
620
     * @return self<int, T>
621
     */
622
    public function values(): self
623
    {
624
        return new self(
8✔
625
            static function (iterable $collection): iterable {
8✔
626
                foreach ($collection as $item) {
8✔
627
                    yield $item;
8✔
628
                }
629
            },
8✔
630
            [$this],
8✔
631
        );
8✔
632
    }
633

634
    /**
635
     * Generates a hash table for lookup by `diff` and `diffKey`.
636
     *
637
     * @param array<array-key, iterable<mixed, mixed>> $iterables
638
     *
639
     * @return array<string, true>
640
     */
641
    private static function generateDiffHashTable(array $iterables): array
642
    {
643
        $hashTable = [];
4✔
644

645
        foreach ($iterables as $iterable) {
4✔
646
            foreach ($iterable as $value) {
4✔
647
                $hashTable[self::toArrayKey($value)] = true;
4✔
648
            }
649
        }
650

651
        return $hashTable;
4✔
652
    }
653

654
    /**
655
     * Generates a hash table for lookup by `intersect` and `intersectKey`.
656
     *
657
     * @param array<array-key, iterable<mixed, mixed>> $iterables
658
     *
659
     * @return array<string, int<1, max>>
660
     */
661
    private static function generateIntersectHashTable(array $iterables): array
662
    {
663
        $hashTable = [];
4✔
664

665
        foreach ($iterables as $iterable) {
4✔
666
            foreach ($iterable as $value) {
4✔
667
                $encodedValue = self::toArrayKey($value);
4✔
668

669
                if (! \array_key_exists($encodedValue, $hashTable)) {
4✔
670
                    $hashTable[$encodedValue] = 1;
4✔
671
                } else {
672
                    ++$hashTable[$encodedValue];
4✔
673
                }
674
            }
675
        }
676

677
        return $hashTable;
4✔
678
    }
679

680
    private static function toArrayKey(mixed $input): string
681
    {
682
        if (\is_string($input)) {
8✔
683
            return $input;
6✔
684
        }
685

686
        return (string) json_encode($input);
6✔
687
    }
688
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc