Skip to content

Commit fa0e4ad

Browse files
author
Bl00D4NGEL
committed
feat: add first, firstOr, last, lastOr functions to collection
1 parent a18d46f commit fa0e4ad

File tree

2 files changed

+199
-4
lines changed

2 files changed

+199
-4
lines changed

src/Domain/Collection.php

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ final public function __construct(
3131

3232
/**
3333
* Creates a collection from a given iterable of items.
34-
* This function is useful when trying to create a collection from a generator or an iterator
34+
* This function is useful when trying to create a collection from a generator or an iterator.
3535
*
3636
* @param iterable<T> $items
3737
* @param class-string<T>|null $itemType
@@ -54,7 +54,7 @@ public static function fromIterable(iterable $items, ?string $itemType = null):
5454
/**
5555
* Returns true if every value in the collection passes the callback truthy test. Opposite of self::none().
5656
* Callback arguments will be element, index, collection.
57-
* Function short-circuits on first falsy return value
57+
* Function short-circuits on first falsy return value.
5858
*
5959
* @param ?callable(T, int, static): bool $callback
6060
* @return bool
@@ -77,7 +77,7 @@ public function every(callable $callback = null): bool
7777
/**
7878
* Returns true if every value in the collection passes the callback falsy test. Opposite of self::every().
7979
* Callback arguments will be element, index, collection.
80-
* Function short-circuits on first truthy return value
80+
* Function short-circuits on first truthy return value.
8181
*
8282
* @param ?callable(T, int, static): bool $callback
8383
* @return bool
@@ -100,7 +100,7 @@ public function none(callable $callback = null): bool
100100
/**
101101
* Returns true if at least one value in the collection passes the callback truthy test.
102102
* Callback arguments will be element, index, collection.
103-
* Function short-circuits on first truthy return value
103+
* Function short-circuits on first truthy return value.
104104
*
105105
* @param ?callable(T, int, static): bool $callback
106106
* @return bool
@@ -120,6 +120,106 @@ public function some(callable $callback = null): bool
120120
return false;
121121
}
122122

123+
/**
124+
* Returns the first element of the collection that matches the given callback.
125+
* If no callback is given the first element in the collection is returned.
126+
* Throws exception if collection is empty or the given callback was never satisfied.
127+
*
128+
* @param ?callable(T, int, static): bool $callback
129+
* @return T
130+
* @throws InvalidArgumentException
131+
*/
132+
public function first(callable $callback = null)
133+
{
134+
if ($this->items === []) {
135+
throw new InvalidArgumentException('No items in collection');
136+
}
137+
138+
foreach ($this->items as $index => $item) {
139+
if ($callback === null || $callback($item, $index, $this)) {
140+
return $item;
141+
}
142+
}
143+
144+
throw new InvalidArgumentException('No item found in collection that satisfies first callback');
145+
}
146+
147+
/**
148+
* Returns the first element of the collection that matches the given callback.
149+
* If no callback is given the first element in the collection is returned.
150+
* If the collection is empty the given fallback value is returned instead.
151+
*
152+
* @template U of T|mixed
153+
* @param ?callable(T, int, static): bool $callback
154+
* @param U $fallbackValue
155+
* @return U
156+
* @throws InvalidArgumentException
157+
*/
158+
public function firstOr(callable $callback = null, mixed $fallbackValue = null)
159+
{
160+
if ($this->items === []) {
161+
return $fallbackValue;
162+
}
163+
164+
foreach ($this->items as $index => $item) {
165+
if ($callback === null || $callback($item, $index, $this)) {
166+
return $item;
167+
}
168+
}
169+
170+
return $fallbackValue;
171+
}
172+
173+
/**
174+
* Returns the last element of the collection that matches the given callback.
175+
* If no callback is given the last element in the collection is returned.
176+
* Throws exception if collection is empty or the given callback was never satisfied.
177+
*
178+
* @param ?callable(T, int, static): bool $callback
179+
* @return T
180+
* @throws InvalidArgumentException
181+
*/
182+
public function last(callable $callback = null)
183+
{
184+
if ($this->items === []) {
185+
throw new InvalidArgumentException('No items in collection');
186+
}
187+
188+
foreach (array_reverse($this->items) as $index => $item) {
189+
if ($callback === null || $callback($item, $index, $this)) {
190+
return $item;
191+
}
192+
}
193+
194+
throw new InvalidArgumentException('No item found in collection that satisfies last callback');
195+
}
196+
197+
/**
198+
* Returns the last element of the collection that matches the given callback.
199+
* If no callback is given the last element in the collection is returned.
200+
* If the collection is empty the given fallback value is returned instead.
201+
*
202+
* @template U of T|mixed
203+
* @param ?callable(T, int, static): bool $callback
204+
* @param U $fallbackValue
205+
* @return U
206+
* @throws InvalidArgumentException
207+
*/
208+
public function lastOr(callable $callback = null, mixed $fallbackValue = null)
209+
{
210+
if ($this->items === []) {
211+
return $fallbackValue;
212+
}
213+
214+
foreach (array_reverse($this->items) as $index => $item) {
215+
if ($callback === null || $callback($item, $index, $this)) {
216+
return $item;
217+
}
218+
}
219+
220+
return $fallbackValue;
221+
}
222+
123223
/**
124224
* Add one or more items to the collection. It **does not** modify the
125225
* current collection, but returns a new one.

tests/Domain/CollectionTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use ArrayIterator;
88
use Assert;
99
use GeekCell\Ddd\Domain\Collection;
10+
use InvalidArgumentException;
1011
use PHPUnit\Framework\TestCase;
1112

1213
/**
@@ -431,4 +432,98 @@ public function testSomeShortCircuitsOnFirstFalsyValue(): void
431432
return true;
432433
});
433434
}
435+
436+
public function testFirst(): void
437+
{
438+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
439+
$collection = new Collection($items);
440+
441+
$this->assertSame(1, $collection->first());
442+
}
443+
444+
public function testFirstThrowsExceptionOnEmptyCollection(): void
445+
{
446+
$collection = new Collection([]);
447+
448+
$this->expectException(InvalidArgumentException::class);
449+
$this->expectExceptionMessage('No items in collection');
450+
$collection->first();
451+
}
452+
453+
public function testFirstThrowsExceptionIfCallbackIsNeverSatisfied(): void
454+
{
455+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
456+
$collection = new Collection($items);
457+
$this->expectException(InvalidArgumentException::class);
458+
$this->expectExceptionMessage('No item found in collection that satisfies first callback');
459+
$collection->first(static fn () => false);
460+
}
461+
462+
public function testFirstOrReturnsFirstValueInCollectionIfNoCallbackIsGiven(): void
463+
{
464+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
465+
$collection = new Collection($items);
466+
$this->assertSame(1, $collection->firstOr());
467+
}
468+
469+
public function testFirstOrReturnsFirstValueThatSatisfiesCallback(): void
470+
{
471+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
472+
$collection = new Collection($items);
473+
$this->assertSame(6, $collection->firstOr(static fn ($item) => $item > 5));
474+
}
475+
476+
public function testFirstOrReturnsFallbackValueIfCallbackIsNeverSatisfied(): void
477+
{
478+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
479+
$collection = new Collection($items);
480+
$this->assertSame(-1, $collection->firstOr(static fn ($item) => $item > 10, -1));
481+
}
482+
483+
public function testLast(): void
484+
{
485+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
486+
$collection = new Collection($items);
487+
488+
$this->assertSame(10, $collection->last());
489+
}
490+
491+
public function testLastThrowsExceptionOnEmptyCollection(): void
492+
{
493+
$collection = new Collection([]);
494+
495+
$this->expectException(InvalidArgumentException::class);
496+
$this->expectExceptionMessage('No items in collection');
497+
$collection->last();
498+
}
499+
500+
public function testLastThrowsExceptionIfCallbackIsNeverSatisfied(): void
501+
{
502+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
503+
$collection = new Collection($items);
504+
$this->expectException(InvalidArgumentException::class);
505+
$this->expectExceptionMessage('No item found in collection that satisfies last callback');
506+
$collection->last(static fn () => false);
507+
}
508+
509+
public function testLastOrReturnsLastValueInCollectionIfNoCallbackIsGiven(): void
510+
{
511+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
512+
$collection = new Collection($items);
513+
$this->assertSame(10, $collection->lastOr());
514+
}
515+
516+
public function testLastOrReturnsLastValueThatSatisfiesCallback(): void
517+
{
518+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
519+
$collection = new Collection($items);
520+
$this->assertSame(10, $collection->lastOr(static fn ($item) => $item > 5));
521+
}
522+
523+
public function testLastOrReturnsFallbackValueIfCallbackIsNeverSatisfied(): void
524+
{
525+
$items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
526+
$collection = new Collection($items);
527+
$this->assertSame(-1, $collection->lastOr(static fn ($item) => $item > 10, -1));
528+
}
434529
}

0 commit comments

Comments
 (0)