Skip to content

Add functions to Collection #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## [1.5.0](https://github.com/geekcell/php-ddd/compare/v1.4.0...v1.5.0) (2024-01-05)

### Features

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

The following functions were added:
* `fromIterable` - enables users of the collection to construction the collection from an iterable value like iterators, generators, etc.
* `every` - Returns true if given callback returns truthy values for all items
* `none` - Returns true if given callback returns falsy values for all items
* `some` - Returns true if given callback returns truthy values on some items
* `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
* `firstOr` - Same as first but returns $fallbackValue if collection is empty or predicate is never satisfied
* `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
* `lastOr` - Same as last but returns $fallbackValue if collection is empty or predicate is never satisfied
* `isEmpty` - Returns whether the collection is empty
* `hasItems` - Returns whether the collection has items

## [1.4.0](https://github.com/geekcell/php-ddd/compare/v1.3.1...v1.4.0) (2023-12-19)


Expand Down
247 changes: 234 additions & 13 deletions src/Domain/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@

namespace GeekCell\Ddd\Domain;

use ArrayAccess;
use ArrayIterator;
use Assert;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;
use function array_filter;
use function array_map;
use function array_reduce;
use function array_values;
use function count;
use function get_class;
use function is_int;
use function reset;

/**
* @template T of object
* @implements \IteratorAggregate<T>
* @implements \ArrayAccess<mixed, T>
* @implements IteratorAggregate<T>
* @implements ArrayAccess<mixed, T>
*/
class Collection implements \ArrayAccess, \Countable, \IteratorAggregate
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
/**
* @param T[] $items
Expand All @@ -26,11 +40,218 @@ final public function __construct(
}
}

/**
* Creates a collection from a given iterable of items.
* This function is useful when trying to create a collection from a generator or an iterator.
*
* @param iterable<T> $items
* @param class-string<T>|null $itemType
* @return self<T>
* @throws Assert\AssertionFailedException
*/
public static function fromIterable(iterable $items, ?string $itemType = null): static
{
if (is_array($items)) {
return new static($items, $itemType);
}

if (!$items instanceof Traversable) {
$items = [...$items];
}

return new static(iterator_to_array($items), $itemType);
}

/**
* Returns true if every value in the collection passes the callback truthy test. Opposite of self::none().
* Callback arguments will be element, index, collection.
* Function short-circuits on first falsy return value.
*
* @param ?callable(T, int, static): bool $callback
* @return bool
*/
public function every(callable $callback = null): bool
{
if ($callback === null) {
$callback = static fn ($item, $index, $self) => $item;
}

foreach ($this->items as $index => $item) {
if (!$callback($item, $index, $this)) {
return false;
}
}

return true;
}

/**
* Returns true if every value in the collection passes the callback falsy test. Opposite of self::every().
* Callback arguments will be element, index, collection.
* Function short-circuits on first truthy return value.
*
* @param ?callable(T, int, static): bool $callback
* @return bool
*/
public function none(callable $callback = null): bool
{
if ($callback === null) {
$callback = static fn ($item, $index, $self) => $item;
}

foreach ($this->items as $index => $item) {
if ($callback($item, $index, $this)) {
return false;
}
}

return true;
}

/**
* Returns true if at least one value in the collection passes the callback truthy test.
* Callback arguments will be element, index, collection.
* Function short-circuits on first truthy return value.
*
* @param ?callable(T, int, static): bool $callback
* @return bool
*/
public function some(callable $callback = null): bool
{
if ($callback === null) {
$callback = static fn ($item, $index, $self) => $item;
}

foreach ($this->items as $index => $item) {
if ($callback($item, $index, $this)) {
return true;
}
}

return false;
}

/**
* Returns the first element of the collection that matches the given callback.
* If no callback is given the first element in the collection is returned.
* Throws exception if collection is empty or the given callback was never satisfied.
*
* @param ?callable(T, int, static): bool $callback
* @return T
* @throws InvalidArgumentException
*/
public function first(callable $callback = null)
{
if ($this->items === []) {
throw new InvalidArgumentException('No items in collection');
}

foreach ($this->items as $index => $item) {
if ($callback === null || $callback($item, $index, $this)) {
return $item;
}
}

throw new InvalidArgumentException('No item found in collection that satisfies first callback');
}

/**
* Returns the first element of the collection that matches the given callback.
* If no callback is given the first element in the collection is returned.
* If the collection is empty the given fallback value is returned instead.
*
* @template U of T|mixed
* @param ?callable(T, int, static): bool $callback
* @param U $fallbackValue
* @return U
* @throws InvalidArgumentException
*/
public function firstOr(callable $callback = null, mixed $fallbackValue = null)
{
if ($this->items === []) {
return $fallbackValue;
}

foreach ($this->items as $index => $item) {
if ($callback === null || $callback($item, $index, $this)) {
return $item;
}
}

return $fallbackValue;
}

/**
* Returns the last element of the collection that matches the given callback.
* If no callback is given the last element in the collection is returned.
* Throws exception if collection is empty or the given callback was never satisfied.
*
* @param ?callable(T, int, static): bool $callback
* @return T
* @throws InvalidArgumentException
*/
public function last(callable $callback = null)
{
if ($this->items === []) {
throw new InvalidArgumentException('No items in collection');
}

foreach (array_reverse($this->items) as $index => $item) {
if ($callback === null || $callback($item, $index, $this)) {
return $item;
}
}

throw new InvalidArgumentException('No item found in collection that satisfies last callback');
}

/**
* Returns the last element of the collection that matches the given callback.
* If no callback is given the last element in the collection is returned.
* If the collection is empty the given fallback value is returned instead.
*
* @template U of T|mixed
* @param ?callable(T, int, static): bool $callback
* @param U $fallbackValue
* @return U
* @throws InvalidArgumentException
*/
public function lastOr(callable $callback = null, mixed $fallbackValue = null)
{
if ($this->items === []) {
return $fallbackValue;
}

foreach (array_reverse($this->items) as $index => $item) {
if ($callback === null || $callback($item, $index, $this)) {
return $item;
}
}

return $fallbackValue;
}

/**
* Returns whether the collection is empty (has no items)
*/
public function isEmpty(): bool
{
return $this->items === [];
}

/**
* Returns whether the collection has items
*/
public function hasItems(): bool
{
return $this->items !== [];
}

/**
* Add one or more items to the collection. It **does not** modify the
* current collection, but returns a new one.
*
* @param mixed $item One or more items to add to the collection.
* @param T|iterable<T> $item One or more items to add to the collection.
* @return static
*/
public function add(mixed $item): static
Expand All @@ -56,7 +277,7 @@ public function add(mixed $item): static
public function filter(callable $callback): static
{
return new static(
\array_values(\array_filter($this->items, $callback)),
array_values(array_filter($this->items, $callback)),
$this->itemType,
);
}
Expand All @@ -74,15 +295,15 @@ public function filter(callable $callback): static
*/
public function map(callable $callback, bool $inferTypes = true): static
{
$mapResult = \array_map($callback, $this->items);
$firstItem = \reset($mapResult);
$mapResult = array_map($callback, $this->items);
$firstItem = reset($mapResult);

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

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

return new static($mapResult);
Expand All @@ -98,15 +319,15 @@ public function map(callable $callback, bool $inferTypes = true): static
*/
public function reduce(callable $callback, mixed $initial = null): mixed
{
return \array_reduce($this->items, $callback, $initial);
return array_reduce($this->items, $callback, $initial);
}

/**
* @inheritDoc
*/
public function offsetExists(mixed $offset): bool
{
if (!\is_int($offset)) {
if (!is_int($offset)) {
return false;
}

Expand Down Expand Up @@ -152,14 +373,14 @@ public function offsetUnset(mixed $offset): void
*/
public function count(): int
{
return \count($this->items);
return count($this->items);
}

/**
* @inheritDoc
*/
public function getIterator(): \Traversable
public function getIterator(): Traversable
{
return new \ArrayIterator($this->items);
return new ArrayIterator($this->items);
}
}
Loading