Skip to content

Commit 44006d7

Browse files
authored
Feature/Invariant system now throws user defined exceptions. (#19)
1 parent 578dba6 commit 44006d7

File tree

3 files changed

+107
-40
lines changed

3 files changed

+107
-40
lines changed

src/Traits/HasInvariants.php

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace ComplexHeart\Domain\Model\Traits;
66

77
use ComplexHeart\Domain\Model\Exceptions\InvariantViolation;
8-
use Exception;
8+
use Throwable;
9+
10+
use function Lambdish\Phunctional\map;
911

1012
/**
1113
* Trait HasInvariants
@@ -15,26 +17,38 @@
1517
*/
1618
trait HasInvariants
1719
{
20+
/**
21+
* Static property to keep cached invariants list to optimize performance.
22+
*
23+
* @var array<string, string[]>
24+
*/
25+
protected static $_invariantsCache = [];
26+
1827
/**
1928
* Retrieve the object invariants.
2029
*
2130
* @return string[]
2231
*/
2332
final public static function invariants(): array
2433
{
25-
$invariants = [];
26-
foreach (get_class_methods(static::class) as $invariant) {
27-
if (str_starts_with($invariant, 'invariant') && !in_array($invariant, ['invariants', 'invariantHandler'])) {
28-
$invariantRuleName = preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant);
29-
if (is_null($invariantRuleName)) {
30-
continue;
31-
}
34+
if (array_key_exists(static::class, static::$_invariantsCache) === false) {
35+
$invariants = [];
36+
foreach (get_class_methods(static::class) as $invariant) {
37+
if (str_starts_with($invariant, 'invariant') && !in_array($invariant,
38+
['invariants', 'invariantHandler'])) {
39+
$invariantRuleName = preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant);
40+
if (is_null($invariantRuleName)) {
41+
continue;
42+
}
3243

33-
$invariants[$invariant] = str_replace('invariant ', '', strtolower($invariantRuleName));
44+
$invariants[$invariant] = str_replace('invariant ', '', strtolower($invariantRuleName));
45+
}
3446
}
47+
48+
static::$_invariantsCache[static::class] = $invariants;
3549
}
3650

37-
return $invariants;
51+
return static::$_invariantsCache[static::class];
3852
}
3953

4054
/**
@@ -52,57 +66,68 @@ final public static function invariants(): array
5266
* If exception is thrown the error message will be the exception message.
5367
*
5468
* $onFail function must have the following signature:
55-
* fn(array<string, string>) => void
69+
* fn(array<string, Throwable>) => void
5670
*
5771
* @param string|callable $onFail
72+
* @param string $exception
5873
*
5974
* @return void
6075
*/
61-
private function check(string|callable $onFail = 'invariantHandler'): void
62-
{
63-
$violations = $this->computeInvariantViolations();
76+
private function check(
77+
string|callable $onFail = 'invariantHandler',
78+
string $exception = InvariantViolation::class
79+
): void {
80+
$violations = $this->computeInvariantViolations($exception);
6481
if (!empty($violations)) {
65-
call_user_func_array($this->computeInvariantHandler($onFail), [$violations]);
82+
call_user_func_array($this->computeInvariantHandler($onFail, $exception), [$violations]);
6683
}
6784
}
6885

6986
/**
7087
* Computes the list of invariant violations.
7188
*
72-
* @return array<string, string>
89+
* @param string $exception
90+
*
91+
* @return array<string, Throwable>
7392
*/
74-
private function computeInvariantViolations(): array
93+
private function computeInvariantViolations(string $exception): array
7594
{
7695
$violations = [];
7796
foreach (static::invariants() as $invariant => $rule) {
7897
try {
7998
if (!$this->{$invariant}()) {
80-
$violations[$invariant] = $rule;
99+
/** @var array<string, Throwable> $violations */
100+
$violations[$invariant] = new $exception($rule);
81101
}
82-
} catch (Exception $e) {
83-
$violations[$invariant] = $e->getMessage();
102+
} catch (Throwable $e) {
103+
/** @var array<string, Throwable> $violations */
104+
$violations[$invariant] = $e;
84105
}
85106
}
86107

87108
return $violations;
88109
}
89110

90-
private function computeInvariantHandler(string|callable $handlerFn): callable
111+
private function computeInvariantHandler(string|callable $handlerFn, string $exception): callable
91112
{
92113
if (!is_string($handlerFn)) {
93114
return $handlerFn;
94115
}
95116

96117
return method_exists($this, $handlerFn)
97-
? function (array $violations) use ($handlerFn): void {
98-
$this->{$handlerFn}($violations);
118+
? function (array $violations) use ($handlerFn, $exception): void {
119+
$this->{$handlerFn}($violations, $exception);
99120
}
100-
: function (array $violations): void {
101-
throw new InvariantViolation(
121+
: function (array $violations) use ($exception): void {
122+
if (count($violations) === 1) {
123+
throw array_shift($violations);
124+
}
125+
126+
throw new $exception( // @phpstan-ignore-line
102127
sprintf(
103-
"Unable to create %s due %s",
128+
"Unable to create %s due: %s",
104129
basename(str_replace('\\', '/', static::class)),
105-
implode(",", $violations),
130+
implode(", ", map(fn(Throwable $e): string => $e->getMessage(), $violations)),
106131

107132
)
108133
);

tests/TraitsTest.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
declare(strict_types=1);
44

55
use ComplexHeart\Domain\Model\Errors\ImmutabilityError;
6+
use ComplexHeart\Domain\Model\Exceptions\InvariantViolation;
67
use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Errors\InvalidPriceError;
78
use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Price;
9+
use ComplexHeart\Domain\Model\Traits\HasInvariants;
810

911
test('Object with HasImmutability should throw ImmutabilityError for any update properties attempts.', function () {
1012
$price = new Price(100.0, 'EUR');
@@ -33,4 +35,45 @@
3335
new Price(-10.0, 'EURO');
3436
})
3537
->group('Unit')
36-
->throws(InvalidPriceError::class);
38+
->throws(InvalidPriceError::class);
39+
40+
test('Object with HasInvariants should execute custom invariant handler as closure.', function () {
41+
new class () {
42+
use HasInvariants;
43+
44+
public function __construct()
45+
{
46+
$this->check(fn(array $violations) => throw new ValueError('From custom Handler'));
47+
}
48+
49+
protected function invariantAlwaysFail(): bool
50+
{
51+
return false;
52+
}
53+
};
54+
})
55+
->group('Unit')
56+
->throws(ValueError::class);
57+
58+
test('Object with HasInvariants should throw exception with list of exceptions', function () {
59+
new class () {
60+
use HasInvariants;
61+
62+
public function __construct()
63+
{
64+
$this->check();
65+
}
66+
67+
protected function invariantAlwaysFailOne(): bool
68+
{
69+
return false;
70+
}
71+
72+
protected function invariantAlwaysFailTwo(): bool
73+
{
74+
return false;
75+
}
76+
};
77+
})
78+
->group('Unit')
79+
->throws(InvariantViolation::class, 'always fail one, always fail two');

tests/ValueObjectsTest.php

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
protected string $_pattern = '[a-z]';
5656
};
5757
})
58-
->throws(InvariantViolation::class)
58+
->throws(InvalidArgumentException::class)
5959
->group('Unit');
6060

6161
test('BooleanValue should create a valid BooleanValue Object.', function () {
@@ -157,10 +157,10 @@
157157
protected string $valueType = 'string';
158158
};
159159

160-
expect($vo)->toHaveCount(2);
161-
expect($vo)->toBeIterable();
162-
expect($vo->getIterator())->toBeInstanceOf(ArrayIterator::class);
163-
expect($vo[0])->toEqual('one');
160+
expect($vo)->toHaveCount(2)
161+
->and($vo)->toBeIterable()
162+
->and($vo->getIterator())->toBeInstanceOf(ArrayIterator::class)
163+
->and($vo[0])->toEqual('one');
164164
})
165165
->group('Unit');
166166

@@ -223,8 +223,8 @@
223223
test('UUIDValue should create a valid UUIDValue Object.', function () {
224224
$vo = UUIDValue::random();
225225

226-
expect($vo->is($vo))->toBeTrue();
227-
expect((string) $vo)->toEqual($vo->__toString());
226+
expect($vo->is($vo))->toBeTrue()
227+
->and((string) $vo)->toEqual($vo->__toString());
228228
})
229229
->group('Unit');
230230

@@ -241,10 +241,9 @@
241241
const TWO = 'two';
242242
};
243243

244-
expect($vo->value())->toBe('one');
245-
expect($vo->value())->toBe((string) $vo);
246-
247-
expect($vo::getLabels()[0])->toBe('ONE');
248-
expect($vo::getLabels()[1])->toBe('TWO');
244+
expect($vo->value())->toBe('one')
245+
->and($vo->value())->toBe((string) $vo)
246+
->and($vo::getLabels()[0])->toBe('ONE')
247+
->and($vo::getLabels()[1])->toBe('TWO');
249248
})
250249
->group('Unit');

0 commit comments

Comments
 (0)