Skip to content

Commit dafe4bc

Browse files
authored
Feature/Update event contracts and dependencies (#25)
* Update event contracts and dependencies for improved compatibility * Add error aggregation support with new interfaces and traits
1 parent 4f81c8a commit dafe4bc

File tree

12 files changed

+204
-125
lines changed

12 files changed

+204
-125
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
php-version: [ '8.2', '8.3', '8.4']
16+
php-version: [ '8.2', '8.3', '8.4' ]
1717

1818
steps:
1919
- name: Checkout source code
@@ -40,7 +40,10 @@ jobs:
4040

4141
- name: Install Dependencies
4242
if: steps.composer-cache.outputs.cache-hit != 'true'
43-
run: composer install --prefer-dist --no-progress --no-suggest
43+
run: composer install --prefer-dist --no-progress
44+
45+
- name: Check Code Style
46+
run: composer pint-test
4447

4548
- name: Execute Static Code analysis
4649
run: composer analyse

composer.json

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@
1111
],
1212
"minimum-stability": "stable",
1313
"require": {
14-
"php": "^8.1.0",
14+
"php": "^8.2",
1515
"ext-json": "*",
1616
"ramsey/uuid": "^4.1.0",
1717
"nesbot/carbon": "^3.2.0",
1818
"illuminate/collections": "^11.0.0",
1919
"lambdish/phunctional": "^2.1.0",
2020
"doctrine/instantiator": "^2.0.0",
21-
"complex-heart/contracts": "^2.0.0"
21+
"complex-heart/contracts": "^3.0.0"
2222
},
2323
"require-dev": {
2424
"mockery/mockery": "^1.6.0",
25-
"pestphp/pest": "^2.0",
26-
"pestphp/pest-plugin-faker": "^2.0",
25+
"pestphp/pest": "^3.8.4",
26+
"pestphp/pest-plugin-faker": "^3.0.0",
2727
"phpstan/phpstan": "^1.0",
2828
"phpstan/extension-installer": "^1.3",
2929
"phpstan/phpstan-mockery": "^1.1",
@@ -41,13 +41,9 @@
4141
},
4242
"scripts": {
4343
"test": "vendor/bin/pest --configuration=phpunit.xml --coverage --coverage-clover=coverage.xml --log-junit=test.xml",
44-
"test-cov": "vendor/bin/pest --configuration=phpunit.xml --coverage --coverage-html=coverage",
45-
"analyse": "vendor/bin/phpstan analyse src --no-progress --memory-limit=4G --level=8",
46-
"check": [
47-
"@analyse",
48-
"@test"
49-
],
50-
"pint": "vendor/bin/pint --preset psr12"
44+
"analyse": "vendor/bin/phpstan analyse --no-progress --memory-limit=4G",
45+
"pint-test": "vendor/bin/pint --preset=psr12 --test",
46+
"pint": "vendor/bin/pint --preset=psr12"
5147
},
5248
"config": {
5349
"allow-plugins": {

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
parameters:
2+
paths:
3+
- src/
4+
level: 8

src/Contracts/Aggregatable.php renamed to src/Exceptions/Contracts/Aggregatable.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
declare(strict_types=1);
44

5-
namespace ComplexHeart\Domain\Model\Contracts;
5+
namespace ComplexHeart\Domain\Model\Exceptions\Contracts;
66

77
/**
88
* Interface Aggregatable
99
*
10-
* Marker interface for exceptions that can be aggregated during invariant validation.
10+
* Marker interface for exceptions/errors that can be aggregated during invariant validation.
1111
*
12-
* Exceptions implementing this interface will be collected and aggregated when
12+
* Exceptions/Errors implementing this interface will be collected and aggregated when
1313
* multiple invariants fail. Exceptions NOT implementing this interface will be
1414
* thrown immediately, stopping invariant checking.
1515
*
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ComplexHeart\Domain\Model\Exceptions\Contracts;
6+
7+
use Exception;
8+
use Throwable;
9+
10+
/**
11+
* Interface AggregatesErrors
12+
*
13+
* Marks exceptions/errors that can hold and aggregate multiple error messages.
14+
*
15+
* Exceptions/Errors implementing this interface must provide a static factory method
16+
* to create instances from an array of error messages. This allows the
17+
* invariant system to aggregate multiple errors into a single exception.
18+
*
19+
* @author Unay Santisteban <usantisteban@othercode.io>
20+
*/
21+
interface AggregatesErrors
22+
{
23+
/**
24+
* Create an exception instance from one or more error messages.
25+
*
26+
* @param array<int, Throwable&Aggregatable> $errors
27+
* @param int $code
28+
* @param Exception|null $previous
29+
* @return static
30+
*/
31+
public static function fromErrors(array $errors, int $code = 0, ?Exception $previous = null): static;
32+
}

src/Exceptions/InvariantViolation.php

Lines changed: 5 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,77 +4,17 @@
44

55
namespace ComplexHeart\Domain\Model\Exceptions;
66

7-
use ComplexHeart\Domain\Model\Contracts\Aggregatable;
7+
use ComplexHeart\Domain\Model\Exceptions\Contracts\Aggregatable;
8+
use ComplexHeart\Domain\Model\Exceptions\Contracts\AggregatesErrors;
9+
use ComplexHeart\Domain\Model\Exceptions\Traits\CanAggregateErrors;
810
use Exception;
911

1012
/**
1113
* Class InvariantViolation
1214
*
1315
* @author Unay Santisteban <usantisteban@othercode.io>
14-
* @package ComplexHeart\Domain\Model\Exceptions
1516
*/
16-
class InvariantViolation extends Exception implements Aggregatable
17+
class InvariantViolation extends Exception implements Aggregatable, AggregatesErrors
1718
{
18-
/**
19-
* @var array<int, string> List of all violation messages
20-
*/
21-
private array $violations = [];
22-
23-
/**
24-
* Create an invariant violation exception from one or more violations.
25-
*
26-
* @param array<int, string> $violations
27-
* @param int $code
28-
* @param Exception|null $previous
29-
* @return self
30-
*/
31-
public static function fromViolations(array $violations, int $code = 0, ?Exception $previous = null): self
32-
{
33-
$count = count($violations);
34-
35-
// Format message based on count
36-
if ($count === 1) {
37-
$message = $violations[0];
38-
} else {
39-
$message = sprintf(
40-
"Multiple invariant violations (%d):\n- %s",
41-
$count,
42-
implode("\n- ", $violations)
43-
);
44-
}
45-
46-
$exception = new self($message, $code, $previous);
47-
$exception->violations = $violations;
48-
return $exception;
49-
}
50-
51-
/**
52-
* Check if this exception has multiple violations.
53-
*
54-
* @return bool
55-
*/
56-
public function hasMultipleViolations(): bool
57-
{
58-
return count($this->violations) > 1;
59-
}
60-
61-
/**
62-
* Get all violation messages.
63-
*
64-
* @return array<int, string>
65-
*/
66-
public function getViolations(): array
67-
{
68-
return $this->violations;
69-
}
70-
71-
/**
72-
* Get the count of violations.
73-
*
74-
* @return int
75-
*/
76-
public function getViolationCount(): int
77-
{
78-
return count($this->violations);
79-
}
19+
use CanAggregateErrors;
8020
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ComplexHeart\Domain\Model\Exceptions\Traits;
6+
7+
use ComplexHeart\Domain\Model\Exceptions\Contracts\Aggregatable;
8+
use Exception;
9+
use Throwable;
10+
11+
use function Lambdish\Phunctional\map;
12+
13+
/**
14+
* Trait CanAggregateErrors
15+
*
16+
* Provides the implementation for exceptions that can aggregate multiple errors.
17+
*
18+
* This trait implements the complete logic for:
19+
* - Creating exceptions from multiple error messages
20+
* - Storing error messages
21+
* - Formatting aggregated messages
22+
* - Providing access to individual errors
23+
*
24+
* Use this trait along with AggregatesErrors interface to create
25+
* exceptions that can hold multiple error messages.
26+
*
27+
* @author Unay Santisteban <usantisteban@othercode.io>
28+
*/
29+
trait CanAggregateErrors
30+
{
31+
/**
32+
* @var array<int, Throwable&Aggregatable>
33+
*/
34+
private array $errors = [];
35+
36+
/**
37+
* Create an exception from one or more error messages.
38+
*
39+
* @param array<int, Throwable&Aggregatable> $errors
40+
* @param int $code
41+
* @param Exception|null $previous
42+
* @return static
43+
*/
44+
public static function fromErrors(array $errors, int $code = 0, ?Exception $previous = null): static
45+
{
46+
$messages = map(fn (Throwable $e): string => $e->getMessage(), $errors);
47+
48+
$count = count($messages);
49+
50+
// Format message based on count
51+
if ($count === 1) {
52+
$message = $messages[0];
53+
} else {
54+
$message = sprintf("Multiple errors (%d):\n- %s", $count, implode("\n- ", $messages));
55+
}
56+
57+
// @phpstan-ignore-next-line (Safe usage - trait designed for Exception classes with standard constructor)
58+
$exception = new static($message, $code, $previous);
59+
$exception->errors = $errors;
60+
return $exception;
61+
}
62+
63+
/**
64+
* Check if this exception has multiple errors.
65+
*
66+
* @return bool
67+
*/
68+
public function hasMultipleErrors(): bool
69+
{
70+
return count($this->errors) > 1;
71+
}
72+
73+
/**
74+
* Get all error messages.
75+
*
76+
* @return array<int, Throwable&Aggregatable>
77+
*/
78+
public function getErrors(): array
79+
{
80+
return $this->errors;
81+
}
82+
83+
/**
84+
* Get the count of errors.
85+
*
86+
* @return int
87+
*/
88+
public function getErrorCount(): int
89+
{
90+
return count($this->errors);
91+
}
92+
}

src/Traits/HasDomainEvents.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
namespace ComplexHeart\Domain\Model\Traits;
66

7-
use ComplexHeart\Domain\Contracts\ServiceBus\Event;
8-
use ComplexHeart\Domain\Contracts\ServiceBus\EventBus;
7+
use ComplexHeart\Domain\Contracts\Events\Event;
8+
use ComplexHeart\Domain\Contracts\Events\EventBus;
99

1010
/**
1111
* Trait HasDomainEvents

src/Traits/HasInvariants.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44

55
namespace ComplexHeart\Domain\Model\Traits;
66

7-
use ComplexHeart\Domain\Model\Contracts\Aggregatable;
7+
use ComplexHeart\Domain\Model\Exceptions\Contracts\Aggregatable;
8+
use ComplexHeart\Domain\Model\Exceptions\Contracts\AggregatesErrors;
89
use ComplexHeart\Domain\Model\Exceptions\InvariantViolation;
910
use Throwable;
1011

11-
use function Lambdish\Phunctional\map;
12-
1312
/**
1413
* Trait HasInvariants
1514
*
@@ -139,8 +138,9 @@ private function computeInvariantHandler(string|callable $handlerFn, string $exc
139138
* Throw invariant violations (single or aggregated).
140139
*
141140
* Responsible for all exception throwing logic:
142-
* - Non-aggregatable exceptions: throw the first one immediately
143-
* - Aggregatable exceptions: aggregate and throw as InvariantViolation
141+
* - If $exception class doesn't support aggregation: throw first violation immediately
142+
* - If individual violations are non-aggregatable: throw the first one immediately
143+
* - If all conditions pass: aggregate violations using $exception class
144144
*
145145
* @param array<string, Throwable> $violations
146146
* @param string $exception
@@ -149,6 +149,12 @@ private function computeInvariantHandler(string|callable $handlerFn, string $exc
149149
*/
150150
private function throwInvariantViolations(array $violations, string $exception): void
151151
{
152+
// Early check: Does the exception class support aggregation?
153+
if (!is_subclass_of($exception, AggregatesErrors::class)) {
154+
// @phpstan-ignore-next-line (array_shift always returns Throwable from non-empty violations array)
155+
throw array_shift($violations);
156+
}
157+
152158
// Separate aggregatable from non-aggregatable violations
153159
$aggregatable = [];
154160
$nonAggregatable = [];
@@ -163,13 +169,14 @@ private function throwInvariantViolations(array $violations, string $exception):
163169

164170
// If there are non-aggregatable exceptions, throw the first one immediately
165171
if (!empty($nonAggregatable)) {
172+
// @phpstan-ignore-next-line (array_shift always returns Throwable from non-empty array)
166173
throw array_shift($nonAggregatable);
167174
}
168175

169-
// All violations are aggregatable - aggregate them
176+
// All violations are aggregatable - aggregate them using the provided exception class
170177
if (!empty($aggregatable)) {
171-
$messages = map(fn (Throwable $e): string => $e->getMessage(), $aggregatable);
172-
throw InvariantViolation::fromViolations(array_values($messages));
178+
// @phpstan-ignore-next-line (fromErrors returns Throwable instance implementing AggregatesErrors)
179+
throw $exception::fromErrors(array_values($aggregatable));
173180
}
174181
}
175182
}

tests/AggregatesTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
declare(strict_types=1);
44

5-
use ComplexHeart\Domain\Contracts\ServiceBus\Event;
6-
use ComplexHeart\Domain\Contracts\ServiceBus\EventBus;
5+
6+
use ComplexHeart\Domain\Contracts\Events\Event;
7+
use ComplexHeart\Domain\Contracts\Events\EventBus;
78
use ComplexHeart\Domain\Model\Test\Fixtures\OrderManagement\Domain\Order;
89
use ComplexHeart\Domain\Model\ValueObjects\UUIDValue;
910

0 commit comments

Comments
 (0)