Skip to content
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. This projec

## Unreleased

### Added

- New methods on the `ListOfErrors` interface:
- `find()` to find the first matching error.
- `sole()` to get the first error, but only if exactly one error exists.
- `any()` to determine if any of the errors match.
- `every()` to determine if every error matches.
- `filter()` to get a new list containing only matching errors.

### Deprecated

- The `ListOfErrors::contains()` method is deprecated and will be removed in 6.0. Use the new `any()` method instead.
- Calling the `ListOfErrors::first()` method with arguments is deprecated and will be removed in 6.0. Use the new
`find()` method instead.

## [5.0.0-rc.1] - 2025-10-09

### Added
Expand Down
4 changes: 4 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ deptrac:
value: ^Deprecated$
ruleset:
Toolkit:
- Attributes
Domain:
- Toolkit
- Attributes
Application:
- Toolkit
- Domain
- PSR Log
- Attributes
Infrastructure:
- Toolkit
- Domain
- Application
- PSR Log
- Attributes
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
cacheDirectory=".phpunit.cache"
backupStaticProperties="false"
failOnWarning="true"
failOnDeprecation="true"
failOnDeprecation="false"
failOnNotice="true"
>
<coverage/>
Expand Down
38 changes: 36 additions & 2 deletions src/Contracts/Toolkit/Result/ListOfErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,51 @@ interface ListOfErrors extends ListIterator
/**
* Get the first error in the list, or the first matching error.
*
* @param Closure(Error): bool|UnitEnum|null $matcher
* @param (Closure(Error): bool)|UnitEnum|null $matcher
*/
public function first(Closure|UnitEnum|null $matcher = null): ?Error;

/**
* Find the first matching error in the list.
*
* @param (Closure(Error): bool)|UnitEnum $matcher
*/
public function find(Closure|UnitEnum $matcher): ?Error;

/**
* Get the first error in the list, but only if exactly one error exists. Otherwise, throw an exception.
*
* @param (Closure(Error): bool)|UnitEnum|null $matcher
*/
public function sole(Closure|UnitEnum|null $matcher = null): Error;

/**
* Does the list contain a matching error?
*
* @param Closure(Error): bool|UnitEnum $matcher
* @param (Closure(Error): bool)|UnitEnum $matcher
* @deprecated 6.0 use any() instead.
*/
public function contains(Closure|UnitEnum $matcher): bool;

/**
* Does the list contain at least one matching error?
*
* @param (Closure(Error): bool)|UnitEnum $matcher
*/
public function any(Closure|UnitEnum $matcher): bool;

/**
* Do all errors in the list match the provided matcher?
*
* @param (Closure(Error): bool)|UnitEnum $matcher
*/
public function every(Closure|UnitEnum $matcher): bool;

/**
* @param (Closure(Error): bool)|UnitEnum $matcher
*/
public function filter(Closure|UnitEnum $matcher): self;

/**
* Get all the unique error codes in the list.
*
Expand Down
6 changes: 6 additions & 0 deletions src/Contracts/Toolkit/Result/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@
*/
interface Result
{
/**
* Is the result a success?
*/
public function didSucceed(): bool;

/**
* Is the result a failure?
*/
public function didFail(): bool;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Testing/FakeDomainEventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function count(): int
}

/**
* Expect a single event to be dispatched and return it.
* Get the first event in the list, but only if exactly one event exists. Otherwise, throw an exception.
*/
public function sole(): DomainEvent
{
Expand Down
2 changes: 1 addition & 1 deletion src/Toolkit/Contracts.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class Contracts
/**
* Assert that the provided precondition is true.
*
* @param Closure(): string|string $message
* @param (Closure(): string)|string $message
* @phpstan-assert true $precondition
*/
public static function assert(bool $precondition, Closure|string $message = ''): void
Expand Down
80 changes: 63 additions & 17 deletions src/Toolkit/Result/ListOfErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
use CloudCreativity\Modules\Contracts\Toolkit\Result\Error as IError;
use CloudCreativity\Modules\Contracts\Toolkit\Result\ListOfErrors as IListOfErrors;
use CloudCreativity\Modules\Toolkit\Iterables\IsList;
use Deprecated;
use LogicException;
use UnitEnum;

use function CloudCreativity\Modules\Toolkit\enum_string;

final class ListOfErrors implements IListOfErrors
{
/** @use IsList<IError> */
Expand Down Expand Up @@ -48,34 +52,63 @@ public function first(Closure|UnitEnum|null $matcher = null): ?IError
return $this->stack[0] ?? null;
}

if ($matcher instanceof UnitEnum) {
$matcher = static fn (IError $error): bool => $error->is($matcher);
}
trigger_error(
'Calling first() with a matcher is deprecated and will be removed in 6.0; use find() instead.',
E_USER_DEPRECATED,
);

foreach ($this->stack as $error) {
if ($matcher($error)) {
return $error;
}
return $this->find($matcher);
}

public function find(Closure|UnitEnum $matcher): ?IError
{
return array_find($this->stack, $this->where($matcher));
}


public function sole(Closure|UnitEnum|null $matcher = null): IError
{
$errors = $matcher ? $this->filter($matcher) : $this;

if (count($errors->stack) === 1) {
return $errors->stack[0];
}

return null;
throw new LogicException(sprintf(
'Expected exactly one %s but there are %d.',
match (true) {
$matcher instanceof UnitEnum => sprintf('error with code "%s"', enum_string($matcher)),
$matcher instanceof Closure => 'error matching the criteria',
default => 'error',
},
count($errors->stack),
));
}

#[Deprecated(message: 'use any() instead', since: '5.0.0-rc.2')]
public function contains(Closure|UnitEnum $matcher): bool
{
if ($matcher instanceof UnitEnum) {
$matcher = static fn (IError $error): bool => $error->is($matcher);
}
return $this->any($matcher);
}

foreach ($this->stack as $error) {
if ($matcher($error)) {
return true;
}
}
public function any(Closure|UnitEnum $matcher): bool
{
return array_any($this->stack, $this->where($matcher));
}

return false;
public function every(Closure|UnitEnum $matcher): bool
{
return array_all($this->stack, $this->where($matcher));
}

public function filter(Closure|UnitEnum $matcher): self
{
return new self(...array_values(
array_filter($this->stack, $this->where($matcher)),
));
}


public function codes(): array
{
$codes = [];
Expand Down Expand Up @@ -153,4 +186,17 @@ public function toKeyedSet(): KeyedSetOfErrors
{
return new KeyedSetOfErrors(...$this->stack);
}

/**
* @param (Closure(IError): bool)|UnitEnum $matcher
* @return Closure(IError): bool
*/
private function where(Closure|UnitEnum $matcher): Closure
{
if ($matcher instanceof UnitEnum) {
return static fn (IError $error): bool => $error->is($matcher);
}

return $matcher;
}
}
Loading