Skip to content
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

Feature/Invariant system now throws user defined exceptions. #19

Merged
merged 1 commit into from
Jul 17, 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
79 changes: 52 additions & 27 deletions src/Traits/HasInvariants.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
namespace ComplexHeart\Domain\Model\Traits;

use ComplexHeart\Domain\Model\Exceptions\InvariantViolation;
use Exception;
use Throwable;

use function Lambdish\Phunctional\map;

/**
* Trait HasInvariants
Expand All @@ -15,26 +17,38 @@
*/
trait HasInvariants
{
/**
* Static property to keep cached invariants list to optimize performance.
*
* @var array<string, string[]>
*/
protected static $_invariantsCache = [];

/**
* Retrieve the object invariants.
*
* @return string[]
*/
final public static function invariants(): array
{
$invariants = [];
foreach (get_class_methods(static::class) as $invariant) {
if (str_starts_with($invariant, 'invariant') && !in_array($invariant, ['invariants', 'invariantHandler'])) {
$invariantRuleName = preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant);
if (is_null($invariantRuleName)) {
continue;
}
if (array_key_exists(static::class, static::$_invariantsCache) === false) {
$invariants = [];
foreach (get_class_methods(static::class) as $invariant) {
if (str_starts_with($invariant, 'invariant') && !in_array($invariant,
['invariants', 'invariantHandler'])) {
$invariantRuleName = preg_replace('/[A-Z]([A-Z](?![a-z]))*/', ' $0', $invariant);
if (is_null($invariantRuleName)) {
continue;
}

$invariants[$invariant] = str_replace('invariant ', '', strtolower($invariantRuleName));
$invariants[$invariant] = str_replace('invariant ', '', strtolower($invariantRuleName));
}
}

static::$_invariantsCache[static::class] = $invariants;
}

return $invariants;
return static::$_invariantsCache[static::class];
}

/**
Expand All @@ -52,57 +66,68 @@ final public static function invariants(): array
* If exception is thrown the error message will be the exception message.
*
* $onFail function must have the following signature:
* fn(array<string, string>) => void
* fn(array<string, Throwable>) => void
*
* @param string|callable $onFail
* @param string $exception
*
* @return void
*/
private function check(string|callable $onFail = 'invariantHandler'): void
{
$violations = $this->computeInvariantViolations();
private function check(
string|callable $onFail = 'invariantHandler',
string $exception = InvariantViolation::class
): void {
$violations = $this->computeInvariantViolations($exception);
if (!empty($violations)) {
call_user_func_array($this->computeInvariantHandler($onFail), [$violations]);
call_user_func_array($this->computeInvariantHandler($onFail, $exception), [$violations]);
}
}

/**
* Computes the list of invariant violations.
*
* @return array<string, string>
* @param string $exception
*
* @return array<string, Throwable>
*/
private function computeInvariantViolations(): array
private function computeInvariantViolations(string $exception): array
{
$violations = [];
foreach (static::invariants() as $invariant => $rule) {
try {
if (!$this->{$invariant}()) {
$violations[$invariant] = $rule;
/** @var array<string, Throwable> $violations */
$violations[$invariant] = new $exception($rule);
}
} catch (Exception $e) {
$violations[$invariant] = $e->getMessage();
} catch (Throwable $e) {
/** @var array<string, Throwable> $violations */
$violations[$invariant] = $e;
}
}

return $violations;
}

private function computeInvariantHandler(string|callable $handlerFn): callable
private function computeInvariantHandler(string|callable $handlerFn, string $exception): callable
{
if (!is_string($handlerFn)) {
return $handlerFn;
}

return method_exists($this, $handlerFn)
? function (array $violations) use ($handlerFn): void {
$this->{$handlerFn}($violations);
? function (array $violations) use ($handlerFn, $exception): void {
$this->{$handlerFn}($violations, $exception);
}
: function (array $violations): void {
throw new InvariantViolation(
: function (array $violations) use ($exception): void {
if (count($violations) === 1) {
throw array_shift($violations);
}

throw new $exception( // @phpstan-ignore-line
sprintf(
"Unable to create %s due %s",
"Unable to create %s due: %s",
basename(str_replace('\\', '/', static::class)),
implode(",", $violations),
implode(", ", map(fn(Throwable $e): string => $e->getMessage(), $violations)),

)
);
Expand Down
45 changes: 44 additions & 1 deletion tests/TraitsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
declare(strict_types=1);

use ComplexHeart\Domain\Model\Errors\ImmutabilityError;
use ComplexHeart\Domain\Model\Exceptions\InvariantViolation;
use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Errors\InvalidPriceError;
use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Price;
use ComplexHeart\Domain\Model\Traits\HasInvariants;

test('Object with HasImmutability should throw ImmutabilityError for any update properties attempts.', function () {
$price = new Price(100.0, 'EUR');
Expand Down Expand Up @@ -33,4 +35,45 @@
new Price(-10.0, 'EURO');
})
->group('Unit')
->throws(InvalidPriceError::class);
->throws(InvalidPriceError::class);

test('Object with HasInvariants should execute custom invariant handler as closure.', function () {
new class () {
use HasInvariants;

public function __construct()
{
$this->check(fn(array $violations) => throw new ValueError('From custom Handler'));
}

protected function invariantAlwaysFail(): bool
{
return false;
}
};
})
->group('Unit')
->throws(ValueError::class);

test('Object with HasInvariants should throw exception with list of exceptions', function () {
new class () {
use HasInvariants;

public function __construct()
{
$this->check();
}

protected function invariantAlwaysFailOne(): bool
{
return false;
}

protected function invariantAlwaysFailTwo(): bool
{
return false;
}
};
})
->group('Unit')
->throws(InvariantViolation::class, 'always fail one, always fail two');
23 changes: 11 additions & 12 deletions tests/ValueObjectsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
protected string $_pattern = '[a-z]';
};
})
->throws(InvariantViolation::class)
->throws(InvalidArgumentException::class)
->group('Unit');

test('BooleanValue should create a valid BooleanValue Object.', function () {
Expand Down Expand Up @@ -157,10 +157,10 @@
protected string $valueType = 'string';
};

expect($vo)->toHaveCount(2);
expect($vo)->toBeIterable();
expect($vo->getIterator())->toBeInstanceOf(ArrayIterator::class);
expect($vo[0])->toEqual('one');
expect($vo)->toHaveCount(2)
->and($vo)->toBeIterable()
->and($vo->getIterator())->toBeInstanceOf(ArrayIterator::class)
->and($vo[0])->toEqual('one');
})
->group('Unit');

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

expect($vo->is($vo))->toBeTrue();
expect((string) $vo)->toEqual($vo->__toString());
expect($vo->is($vo))->toBeTrue()
->and((string) $vo)->toEqual($vo->__toString());
})
->group('Unit');

Expand All @@ -241,10 +241,9 @@
const TWO = 'two';
};

expect($vo->value())->toBe('one');
expect($vo->value())->toBe((string) $vo);

expect($vo::getLabels()[0])->toBe('ONE');
expect($vo::getLabels()[1])->toBe('TWO');
expect($vo->value())->toBe('one')
->and($vo->value())->toBe((string) $vo)
->and($vo::getLabels()[0])->toBe('ONE')
->and($vo::getLabels()[1])->toBe('TWO');
})
->group('Unit');
Loading