Skip to content

Commit 0e573c8

Browse files
author
janvt
authored
Merge pull request #25 from Bl00D4NGEL/add-features-to-collection
Add functions to Collection
2 parents f5b449a + 069a600 commit 0e573c8

File tree

3 files changed

+490
-13
lines changed

3 files changed

+490
-13
lines changed

CHANGELOG.md

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Changelog
22

3+
## [1.5.0](https://github.com/geekcell/php-ddd/compare/v1.4.0...v1.5.0) (2024-01-05)
4+
5+
### Features
6+
7+
Added a couple of functions to the [Collection](./src/Domain/Collection.php) class to enable a smoother developer experience when trying use it in a more functional style
8+
9+
The following functions were added:
10+
* `fromIterable` - enables users of the collection to construction the collection from an iterable value like iterators, generators, etc.
11+
* `every` - Returns true if given callback returns truthy values for all items
12+
* `none` - Returns true if given callback returns falsy values for all items
13+
* `some` - Returns true if given callback returns truthy values on some items
14+
* `first` - Get the first element of the collection that matches a callback, if given. Throws exception if collection is empty or predicate is never satisfied
15+
* `firstOr` - Same as first but returns $fallbackValue if collection is empty or predicate is never satisfied
16+
* `last` - Get the last element of the collection that matches a callback, if given. Throws exception if collection is empty or predicate is never satisfied
17+
* `lastOr` - Same as last but returns $fallbackValue if collection is empty or predicate is never satisfied
18+
* `isEmpty` - Returns whether the collection is empty
19+
* `hasItems` - Returns whether the collection has items
20+
321
## [1.4.0](https://github.com/geekcell/php-ddd/compare/v1.3.1...v1.4.0) (2023-12-19)
422

523

src/Domain/Collection.php

+234-13
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,28 @@
44

55
namespace GeekCell\Ddd\Domain;
66

7+
use ArrayAccess;
8+
use ArrayIterator;
79
use Assert;
10+
use Countable;
11+
use InvalidArgumentException;
12+
use IteratorAggregate;
13+
use Traversable;
14+
use function array_filter;
15+
use function array_map;
16+
use function array_reduce;
17+
use function array_values;
18+
use function count;
19+
use function get_class;
20+
use function is_int;
21+
use function reset;
822

923
/**
1024
* @template T of object
11-
* @implements \IteratorAggregate<T>
12-
* @implements \ArrayAccess<mixed, T>
25+
* @implements IteratorAggregate<T>
26+
* @implements ArrayAccess<mixed, T>
1327
*/
14-
class Collection implements \ArrayAccess, \Countable, \IteratorAggregate
28+
class Collection implements ArrayAccess, Countable, IteratorAggregate
1529
{
1630
/**
1731
* @param T[] $items
@@ -26,11 +40,218 @@ final public function __construct(
2640
}
2741
}
2842

43+
/**
44+
* Creates a collection from a given iterable of items.
45+
* This function is useful when trying to create a collection from a generator or an iterator.
46+
*
47+
* @param iterable<T> $items
48+
* @param class-string<T>|null $itemType
49+
* @return self<T>
50+
* @throws Assert\AssertionFailedException
51+
*/
52+
public static function fromIterable(iterable $items, ?string $itemType = null): static
53+
{
54+
if (is_array($items)) {
55+
return new static($items, $itemType);
56+
}
57+
58+
if (!$items instanceof Traversable) {
59+
$items = [...$items];
60+
}
61+
62+
return new static(iterator_to_array($items), $itemType);
63+
}
64+
65+
/**
66+
* Returns true if every value in the collection passes the callback truthy test. Opposite of self::none().
67+
* Callback arguments will be element, index, collection.
68+
* Function short-circuits on first falsy return value.
69+
*
70+
* @param ?callable(T, int, static): bool $callback
71+
* @return bool
72+
*/
73+
public function every(callable $callback = null): bool
74+
{
75+
if ($callback === null) {
76+
$callback = static fn ($item, $index, $self) => $item;
77+
}
78+
79+
foreach ($this->items as $index => $item) {
80+
if (!$callback($item, $index, $this)) {
81+
return false;
82+
}
83+
}
84+
85+
return true;
86+
}
87+
88+
/**
89+
* Returns true if every value in the collection passes the callback falsy test. Opposite of self::every().
90+
* Callback arguments will be element, index, collection.
91+
* Function short-circuits on first truthy return value.
92+
*
93+
* @param ?callable(T, int, static): bool $callback
94+
* @return bool
95+
*/
96+
public function none(callable $callback = null): bool
97+
{
98+
if ($callback === null) {
99+
$callback = static fn ($item, $index, $self) => $item;
100+
}
101+
102+
foreach ($this->items as $index => $item) {
103+
if ($callback($item, $index, $this)) {
104+
return false;
105+
}
106+
}
107+
108+
return true;
109+
}
110+
111+
/**
112+
* Returns true if at least one value in the collection passes the callback truthy test.
113+
* Callback arguments will be element, index, collection.
114+
* Function short-circuits on first truthy return value.
115+
*
116+
* @param ?callable(T, int, static): bool $callback
117+
* @return bool
118+
*/
119+
public function some(callable $callback = null): bool
120+
{
121+
if ($callback === null) {
122+
$callback = static fn ($item, $index, $self) => $item;
123+
}
124+
125+
foreach ($this->items as $index => $item) {
126+
if ($callback($item, $index, $this)) {
127+
return true;
128+
}
129+
}
130+
131+
return false;
132+
}
133+
134+
/**
135+
* Returns the first element of the collection that matches the given callback.
136+
* If no callback is given the first element in the collection is returned.
137+
* Throws exception if collection is empty or the given callback was never satisfied.
138+
*
139+
* @param ?callable(T, int, static): bool $callback
140+
* @return T
141+
* @throws InvalidArgumentException
142+
*/
143+
public function first(callable $callback = null)
144+
{
145+
if ($this->items === []) {
146+
throw new InvalidArgumentException('No items in collection');
147+
}
148+
149+
foreach ($this->items as $index => $item) {
150+
if ($callback === null || $callback($item, $index, $this)) {
151+
return $item;
152+
}
153+
}
154+
155+
throw new InvalidArgumentException('No item found in collection that satisfies first callback');
156+
}
157+
158+
/**
159+
* Returns the first element of the collection that matches the given callback.
160+
* If no callback is given the first element in the collection is returned.
161+
* If the collection is empty the given fallback value is returned instead.
162+
*
163+
* @template U of T|mixed
164+
* @param ?callable(T, int, static): bool $callback
165+
* @param U $fallbackValue
166+
* @return U
167+
* @throws InvalidArgumentException
168+
*/
169+
public function firstOr(callable $callback = null, mixed $fallbackValue = null)
170+
{
171+
if ($this->items === []) {
172+
return $fallbackValue;
173+
}
174+
175+
foreach ($this->items as $index => $item) {
176+
if ($callback === null || $callback($item, $index, $this)) {
177+
return $item;
178+
}
179+
}
180+
181+
return $fallbackValue;
182+
}
183+
184+
/**
185+
* Returns the last element of the collection that matches the given callback.
186+
* If no callback is given the last element in the collection is returned.
187+
* Throws exception if collection is empty or the given callback was never satisfied.
188+
*
189+
* @param ?callable(T, int, static): bool $callback
190+
* @return T
191+
* @throws InvalidArgumentException
192+
*/
193+
public function last(callable $callback = null)
194+
{
195+
if ($this->items === []) {
196+
throw new InvalidArgumentException('No items in collection');
197+
}
198+
199+
foreach (array_reverse($this->items) as $index => $item) {
200+
if ($callback === null || $callback($item, $index, $this)) {
201+
return $item;
202+
}
203+
}
204+
205+
throw new InvalidArgumentException('No item found in collection that satisfies last callback');
206+
}
207+
208+
/**
209+
* Returns the last element of the collection that matches the given callback.
210+
* If no callback is given the last element in the collection is returned.
211+
* If the collection is empty the given fallback value is returned instead.
212+
*
213+
* @template U of T|mixed
214+
* @param ?callable(T, int, static): bool $callback
215+
* @param U $fallbackValue
216+
* @return U
217+
* @throws InvalidArgumentException
218+
*/
219+
public function lastOr(callable $callback = null, mixed $fallbackValue = null)
220+
{
221+
if ($this->items === []) {
222+
return $fallbackValue;
223+
}
224+
225+
foreach (array_reverse($this->items) as $index => $item) {
226+
if ($callback === null || $callback($item, $index, $this)) {
227+
return $item;
228+
}
229+
}
230+
231+
return $fallbackValue;
232+
}
233+
234+
/**
235+
* Returns whether the collection is empty (has no items)
236+
*/
237+
public function isEmpty(): bool
238+
{
239+
return $this->items === [];
240+
}
241+
242+
/**
243+
* Returns whether the collection has items
244+
*/
245+
public function hasItems(): bool
246+
{
247+
return $this->items !== [];
248+
}
249+
29250
/**
30251
* Add one or more items to the collection. It **does not** modify the
31252
* current collection, but returns a new one.
32253
*
33-
* @param mixed $item One or more items to add to the collection.
254+
* @param T|iterable<T> $item One or more items to add to the collection.
34255
* @return static
35256
*/
36257
public function add(mixed $item): static
@@ -56,7 +277,7 @@ public function add(mixed $item): static
56277
public function filter(callable $callback): static
57278
{
58279
return new static(
59-
\array_values(\array_filter($this->items, $callback)),
280+
array_values(array_filter($this->items, $callback)),
60281
$this->itemType,
61282
);
62283
}
@@ -74,15 +295,15 @@ public function filter(callable $callback): static
74295
*/
75296
public function map(callable $callback, bool $inferTypes = true): static
76297
{
77-
$mapResult = \array_map($callback, $this->items);
78-
$firstItem = \reset($mapResult);
298+
$mapResult = array_map($callback, $this->items);
299+
$firstItem = reset($mapResult);
79300

80301
if ($firstItem === false || !is_object($firstItem)) {
81302
return new static($mapResult);
82303
}
83304

84305
if ($inferTypes && $this->itemType !== null) {
85-
return new static($mapResult, \get_class($firstItem));
306+
return new static($mapResult, get_class($firstItem));
86307
}
87308

88309
return new static($mapResult);
@@ -98,15 +319,15 @@ public function map(callable $callback, bool $inferTypes = true): static
98319
*/
99320
public function reduce(callable $callback, mixed $initial = null): mixed
100321
{
101-
return \array_reduce($this->items, $callback, $initial);
322+
return array_reduce($this->items, $callback, $initial);
102323
}
103324

104325
/**
105326
* @inheritDoc
106327
*/
107328
public function offsetExists(mixed $offset): bool
108329
{
109-
if (!\is_int($offset)) {
330+
if (!is_int($offset)) {
110331
return false;
111332
}
112333

@@ -152,14 +373,14 @@ public function offsetUnset(mixed $offset): void
152373
*/
153374
public function count(): int
154375
{
155-
return \count($this->items);
376+
return count($this->items);
156377
}
157378

158379
/**
159380
* @inheritDoc
160381
*/
161-
public function getIterator(): \Traversable
382+
public function getIterator(): Traversable
162383
{
163-
return new \ArrayIterator($this->items);
384+
return new ArrayIterator($this->items);
164385
}
165386
}

0 commit comments

Comments
 (0)