From bf03fb9fa1b674a20c0504770cd92591636edd49 Mon Sep 17 00:00:00 2001 From: Unay Santisteban Date: Sat, 31 Dec 2022 19:59:31 +0100 Subject: [PATCH] Feature/PHP 8.1 Upgrade --- .gitattributes | 1 + .github/workflows/documentation.yml | 10 +- .github/workflows/test.yml | 14 +- composer.json | 23 +- src/Exceptions/ImmutableException.php | 2 +- src/Exceptions/InstantiationException.php | 18 ++ src/Traits/HasAttributes.php | 6 +- src/Traits/HasEquality.php | 2 +- src/Traits/HasInvariants.php | 6 +- src/Traits/HasTypeCheck.php | 6 +- src/Traits/IsAggregate.php | 2 +- src/Traits/IsEntity.php | 2 +- src/Traits/IsModel.php | 9 +- src/TypedCollection.php | 14 +- src/ValueObjects/ArrayValue.php | 38 ++-- src/ValueObjects/EnumValue.php | 6 +- src/ValueObjects/FloatValue.php | 2 +- src/ValueObjects/StringValue.php | 18 +- src/ValueObjects/UUIDValue.php | 2 +- src/ValueObjects/Value.php | 2 +- tests/AggregatesTest.php | 48 +++++ tests/Pest.php | 11 - tests/Sample/Events/OrderCreated.php | 65 ++++++ tests/Sample/Models/Order.php | 82 ++++++++ tests/Sample/Models/OrderLine.php | 37 ++++ tests/Sample/Models/OrderLines.php | 20 ++ tests/Sample/Models/Price.php | 12 ++ tests/Sample/Models/Reference.php | 54 +++++ tests/Sample/Models/SampleList.php | 16 ++ tests/ValueObjects/StringValueTest.php | 63 ------ tests/ValueObjectsTest.php | 244 ++++++++++++++++++++++ wiki/Domain-Modeling-Aggregates.md | 13 ++ wiki/Domain-Modeling-Entities.md | 1 + wiki/Domain-Modeling-Value-Objects.md | 10 + 34 files changed, 717 insertions(+), 142 deletions(-) create mode 100644 src/Exceptions/InstantiationException.php create mode 100644 tests/AggregatesTest.php create mode 100644 tests/Sample/Events/OrderCreated.php create mode 100644 tests/Sample/Models/Order.php create mode 100644 tests/Sample/Models/OrderLine.php create mode 100644 tests/Sample/Models/OrderLines.php create mode 100644 tests/Sample/Models/Price.php create mode 100644 tests/Sample/Models/Reference.php create mode 100644 tests/Sample/Models/SampleList.php delete mode 100644 tests/ValueObjects/StringValueTest.php create mode 100644 tests/ValueObjectsTest.php diff --git a/.gitattributes b/.gitattributes index 964352c..ef126f9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,5 @@ /.gitattributes export-ignore /phpunit.xml export-ignore /tests export-ignore +/wiki export-ignore /README.md \ No newline at end of file diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index af5f935..4fb3dc4 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,18 +1,16 @@ name: Documentation on: - pull_request: - types: [ closed ] + push: + branches: [ 'main' ] jobs: publish: - if: github.event.pull_request.merged == true runs-on: ubuntu-latest strategy: matrix: - php-version: - - "7.4" + php-version: [ '8.1' ] steps: - name: Checkout source code @@ -39,7 +37,7 @@ jobs: - name: Install Dependencies run: composer install --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - - name: Pusblish documentation to Wiki + - name: Publish documentation to Wiki uses: SwiftDocOrg/github-wiki-publish-action@v1 with: path: "wiki/" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae3fa7a..2556fb2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,10 +2,9 @@ name: Test on: push: - branches: - - main + branches: [ 'main' ] pull_request: - types: [ opened, synchronize, reopened ] + types: [ 'opened', 'synchronize', 'reopened' ] jobs: test: @@ -13,11 +12,13 @@ jobs: strategy: matrix: - php-version: [ "8.0", "8.1" ] + php-version: [ '8.1' ] steps: - name: Checkout source code uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Set up PHP ${{ matrix.php-version }} uses: shivammathur/setup-php@v2 @@ -36,8 +37,9 @@ jobs: - name: Validate composer.json and composer.lock run: composer validate - - name: Install Dependencies - run: composer install --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest - name: Execute Static Code analysis run: composer analyse diff --git a/composer.json b/composer.json index 50c59fe..623a801 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "minimum-stability": "stable", "require": { - "php": "^8.0", + "php": "^8.1.0", "ext-json": "*", "ramsey/uuid": "^4.1", "nesbot/carbon": "^2.40", @@ -21,20 +21,29 @@ "complex-heart/contracts": "^1.0.0" }, "require-dev": { - "mockery/mockery": "^1.4", - "phpunit/phpunit": "^9.3", - "fakerphp/faker": "^1.9.1", - "phpstan/phpstan": "^1.6.8", - "pestphp/pest": "^1.4" + "pestphp/pest": "^1.22.3", + "pestphp/pest-plugin-mock": "^1.0.0", + "pestphp/pest-plugin-faker": "^1.0.0", + "phpstan/phpstan": "^1.9.0" }, "autoload": { "psr-4": { "ComplexHeart\\Domain\\Model\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "ComplexHeart\\Domain\\Model\\Test\\": "tests/" + } + }, "scripts": { "test": "vendor/bin/pest --configuration=phpunit.xml --coverage-clover=coverage.xml --log-junit=test.xml", - "analyse": "vendor/bin/phpstan analyse src --no-progress --level=5" + "test-cov": "vendor/bin/pest --configuration=phpunit.xml --coverage-html=coverage", + "analyse": "vendor/bin/phpstan analyse src --no-progress --level=5", + "check": [ + "@analyse", + "@test" + ] }, "config": { "allow-plugins": { diff --git a/src/Exceptions/ImmutableException.php b/src/Exceptions/ImmutableException.php index 2f2b7ed..f057474 100644 --- a/src/Exceptions/ImmutableException.php +++ b/src/Exceptions/ImmutableException.php @@ -15,4 +15,4 @@ class ImmutableException extends RuntimeException { -} \ No newline at end of file +} diff --git a/src/Exceptions/InstantiationException.php b/src/Exceptions/InstantiationException.php new file mode 100644 index 0000000..5caa276 --- /dev/null +++ b/src/Exceptions/InstantiationException.php @@ -0,0 +1,18 @@ + + * @package ComplexHeart\Domain\Model\Exceptions + */ +class InstantiationException extends RuntimeException +{ + +} diff --git a/src/Traits/HasAttributes.php b/src/Traits/HasAttributes.php index 669be72..6bc1002 100644 --- a/src/Traits/HasAttributes.php +++ b/src/Traits/HasAttributes.php @@ -24,7 +24,7 @@ final public static function attributes(): array { return array_filter( array_keys(get_class_vars(static::class)), - fn(string $item): bool => strpos($item, '_') !== 0 + fn(string $item): bool => !str_starts_with($item, '_') ); } @@ -63,7 +63,7 @@ final protected function hydrate(iterable $source): void * * @return mixed|null */ - final protected function get(string $attribute) + final protected function get(string $attribute): mixed { if (in_array($attribute, static::attributes())) { $method = $this->getStringKey($attribute, 'get', 'Value'); @@ -82,7 +82,7 @@ final protected function get(string $attribute) * @param string $attribute * @param mixed $value */ - final protected function set(string $attribute, $value): void + final protected function set(string $attribute, mixed $value): void { if (in_array($attribute, $this->attributes())) { $method = $this->getStringKey($attribute, 'set', 'Value'); diff --git a/src/Traits/HasEquality.php b/src/Traits/HasEquality.php index ebf6c0d..760e9e1 100644 --- a/src/Traits/HasEquality.php +++ b/src/Traits/HasEquality.php @@ -46,5 +46,5 @@ protected function hash(): string * * @return string */ - abstract function __toString(); + abstract function __toString(): string; } diff --git a/src/Traits/HasInvariants.php b/src/Traits/HasInvariants.php index a0f6bd2..f45aae1 100644 --- a/src/Traits/HasInvariants.php +++ b/src/Traits/HasInvariants.php @@ -24,7 +24,7 @@ final public static function invariants(): array { $invariants = []; foreach (get_class_methods(static::class) as $invariant) { - if (strpos($invariant, 'invariant') === 0 && $invariant !== 'invariants') { + if (str_starts_with($invariant, 'invariant') && $invariant !== 'invariants') { $invariants[$invariant] = str_replace( 'invariant ', '', @@ -52,7 +52,7 @@ final public static function invariants(): array * * If exception is thrown the error message will be the exception message. * - * $onFail function must have following signature: + * $onFail function must have the following signature: * fn(array) => void * * @param callable|null $onFail @@ -74,7 +74,7 @@ private function check(callable $onFail = null): void } } - if (count($violations) > 0) { + if (!empty($violations)) { if (is_null($onFail)) { $customizedHandler = function (array $violations) use ($handler): void { call_user_func_array([$this, $handler], [$violations]); diff --git a/src/Traits/HasTypeCheck.php b/src/Traits/HasTypeCheck.php index f2c20bc..2e965e2 100644 --- a/src/Traits/HasTypeCheck.php +++ b/src/Traits/HasTypeCheck.php @@ -20,7 +20,7 @@ trait HasTypeCheck * * @return bool */ - protected function isValueTypeValid($value, string $validType): bool + protected function isValueTypeValid(mixed $value, string $validType): bool { if ($validType === 'mixed') { return true; @@ -42,8 +42,8 @@ protected function isValueTypeValid($value, string $validType): bool * * @return bool */ - protected function isValueTypeNotValid($value, string $validType): bool + protected function isValueTypeNotValid(mixed $value, string $validType): bool { return !$this->isValueTypeValid($value, $validType); } -} \ No newline at end of file +} diff --git a/src/Traits/IsAggregate.php b/src/Traits/IsAggregate.php index 847e0d8..69fa06b 100644 --- a/src/Traits/IsAggregate.php +++ b/src/Traits/IsAggregate.php @@ -31,7 +31,7 @@ trait IsAggregate * * @return static */ - protected function withOverrides(array $overrides) + protected function withOverrides(array $overrides): static { $new = $this->overrideEntity($overrides); diff --git a/src/Traits/IsEntity.php b/src/Traits/IsEntity.php index 73d8919..8f01e9b 100644 --- a/src/Traits/IsEntity.php +++ b/src/Traits/IsEntity.php @@ -36,7 +36,7 @@ trait IsEntity * * @return static */ - protected function withOverrides(array $overrides) + protected function withOverrides(array $overrides): static { return $this->overrideAttributes( filter(fn($value, string $key): bool => $key !== 'id', $overrides) diff --git a/src/Traits/IsModel.php b/src/Traits/IsModel.php index edc59a3..6cb86c6 100644 --- a/src/Traits/IsModel.php +++ b/src/Traits/IsModel.php @@ -4,6 +4,7 @@ namespace ComplexHeart\Domain\Model\Traits; +use ComplexHeart\Domain\Model\Exceptions\InstantiationException; use RuntimeException; use Doctrine\Instantiator\Instantiator; use Doctrine\Instantiator\Exception\ExceptionInterface; @@ -27,7 +28,7 @@ trait IsModel * * @return static */ - protected function initialize(array $source, callable $onFail = null) + protected function initialize(array $source, callable $onFail = null): static { $this->hydrate($this->mapSource($source)); $this->check($onFail); @@ -42,14 +43,14 @@ protected function initialize(array $source, callable $onFail = null) * * @throws RuntimeException */ - final public static function make() + final public static function make(): static { try { return (new Instantiator()) ->instantiate(static::class) ->initialize(func_get_args()); } catch (ExceptionInterface $e) { - throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + throw new InstantiationException($e->getMessage(), $e->getCode(), $e); } } @@ -60,7 +61,7 @@ final public static function make() * * @return static */ - protected function withOverrides(array $overrides) + protected function withOverrides(array $overrides): static { return self::make(...array_values(array_merge($this->values(), $overrides))); } diff --git a/src/TypedCollection.php b/src/TypedCollection.php index a9020c9..db6028e 100644 --- a/src/TypedCollection.php +++ b/src/TypedCollection.php @@ -52,7 +52,7 @@ public function __construct(array $items = []) * * @throws InvariantViolation */ - protected function checkKeyType($key): void + protected function checkKeyType(mixed $key): void { $supported = ['string', 'integer']; if (!in_array($this->keyType, $supported)) { @@ -73,7 +73,7 @@ protected function checkKeyType($key): void * * @throws InvariantViolation */ - protected function checkValueType($item): void + protected function checkValueType(mixed $item): void { if ($this->isValueTypeNotValid($item, $this->valueType)) { throw new InvariantViolation("All items in the collection must be type of $this->valueType"); @@ -121,7 +121,7 @@ protected function invariantKeysAndValuesMustMatchTheRequiredType(): bool * @return static * @throws InvariantViolation */ - public function push(...$values) + public function push(...$values): static { foreach ($values as $value) { $this->checkValueType($value); @@ -138,7 +138,7 @@ public function push(...$values) * * @throws InvariantViolation */ - public function offsetSet($key, $value): void + public function offsetSet(mixed $key, mixed $value): void { if ($this->keyType !== 'mixed') { $this->checkKeyType($key); @@ -158,7 +158,7 @@ public function offsetSet($key, $value): void * @return static * @throws InvariantViolation */ - public function prepend($value, $key = null) + public function prepend($value, $key = null): static { if ($this->keyType !== 'mixed') { $this->checkKeyType($key); @@ -177,7 +177,7 @@ public function prepend($value, $key = null) * @return static * @throws InvariantViolation */ - public function add($item) + public function add(mixed $item): static { $this->checkValueType($item); @@ -192,7 +192,7 @@ public function add($item) * * @return Collection */ - public function pluck($value, $key = null) + public function pluck($value, $key = null): Collection { return $this->toBase()->pluck($value, $key); } diff --git a/src/ValueObjects/ArrayValue.php b/src/ValueObjects/ArrayValue.php index 9128dcd..0cf4315 100644 --- a/src/ValueObjects/ArrayValue.php +++ b/src/ValueObjects/ArrayValue.php @@ -43,11 +43,11 @@ abstract class ArrayValue extends Value implements IteratorAggregate, ArrayAcces protected int $_maxItems = 0; /** - * The type of each items in the array. + * The type of each item in the array. * * @var string */ - protected string $valueType = 'mixed'; + protected string $_valueType = 'mixed'; /** * ArrayValue constructor. @@ -88,15 +88,15 @@ protected function invariantMustHaveMinimumNumberOfElements(): bool */ protected function invariantItemsMustMatchTheRequiredType(): bool { - if ($this->valueType !== 'mixed') { + if ($this->_valueType !== 'mixed') { $primitives = ['integer', 'boolean', 'float', 'string', 'array', 'object', 'callable']; - $check = in_array($this->valueType, $primitives) - ? fn($value): bool => gettype($value) !== $this->valueType - : fn($value): bool => !($value instanceof $this->valueType); + $check = in_array($this->_valueType, $primitives) + ? fn($value): bool => gettype($value) !== $this->_valueType + : fn($value): bool => !($value instanceof $this->_valueType); foreach ($this->value as $item) { if ($check($item)) { - throw new InvariantViolation("All items must be type of $this->valueType"); + throw new InvariantViolation("All items must be type of $this->_valueType"); } } } @@ -133,12 +133,12 @@ public function getIterator(): Traversable } /** - * Whether a offset exists. + * Whether offset exists. * * @param mixed $offset * @return bool */ - public function offsetExists($offset): bool + public function offsetExists(mixed $offset): bool { return isset($this->value[$offset]); } @@ -149,7 +149,7 @@ public function offsetExists($offset): bool * @param mixed $offset * @return mixed|null */ - public function offsetGet($offset): mixed + public function offsetGet(mixed $offset): mixed { return $this->offsetExists($offset) ? $this->value[$offset] @@ -162,7 +162,7 @@ public function offsetGet($offset): mixed * @param mixed $offset * @param mixed $value */ - public function offsetSet($offset, $value): void + public function offsetSet(mixed $offset, mixed $value): void { throw new ImmutableException("Illegal attempt of change immutable value on offset $offset"); } @@ -172,7 +172,7 @@ public function offsetSet($offset, $value): void * * @param mixed $offset */ - public function offsetUnset($offset): void + public function offsetUnset(mixed $offset): void { throw new ImmutableException("Illegal attempt of unset immutable value on offset $offset"); } @@ -187,16 +187,26 @@ public function serialize(): ?string return serialize($this->value); } + public function __serialize(): array + { + return $this->values(); + } + /** * Constructs the object. * * @param string $data */ - public function unserialize($data): void + public function unserialize(string $data): void { $this->value = unserialize($data); } + public function __unserialize(array $data): void + { + $this->initialize($data); + } + /** * Count elements of an object. * @@ -216,4 +226,4 @@ public function __toString(): string { return json_encode($this->value); } -} \ No newline at end of file +} diff --git a/src/ValueObjects/EnumValue.php b/src/ValueObjects/EnumValue.php index dc65c1c..2cb92be 100644 --- a/src/ValueObjects/EnumValue.php +++ b/src/ValueObjects/EnumValue.php @@ -21,7 +21,7 @@ abstract class EnumValue extends Value * * @var mixed */ - protected $value; + protected mixed $value; /** * Internal cache. @@ -35,7 +35,7 @@ abstract class EnumValue extends Value * * @param mixed $value */ - public function __construct($value) + public function __construct(mixed $value) { $this->initialize(['value' => $value]); } @@ -72,7 +72,7 @@ protected function invariantValueMustBeOneOfAllowed(): bool * * @return bool */ - public static function isValid($value): bool + public static function isValid(mixed $value): bool { return in_array($value, static::getValues(), true); } diff --git a/src/ValueObjects/FloatValue.php b/src/ValueObjects/FloatValue.php index 5ccfe47..d4cba17 100644 --- a/src/ValueObjects/FloatValue.php +++ b/src/ValueObjects/FloatValue.php @@ -40,4 +40,4 @@ public function __toString(): string { return (string)$this->value(); } -} \ No newline at end of file +} diff --git a/src/ValueObjects/StringValue.php b/src/ValueObjects/StringValue.php index 33218c6..25e3ba8 100644 --- a/src/ValueObjects/StringValue.php +++ b/src/ValueObjects/StringValue.php @@ -10,8 +10,6 @@ /** * Class StringValue * - * @method string value() - * * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ @@ -66,7 +64,7 @@ protected function invariantValueMinLengthMustBeValid(): bool $length = strlen($this->value); if ($this->_minLength > 0 && $length < $this->_minLength) { throw new InvariantViolation( - "Min length {$this->_minLength} is required, given {$length}" + "Min length $this->_minLength is required, given {$length}" ); } @@ -84,7 +82,7 @@ protected function invariantValueMaxLengthMustBeValid(): bool $length = strlen($this->value); if ($this->_maxLength > 0 && $length > $this->_maxLength) { throw new InvariantViolation( - "Max length {$this->_maxLength} exceeded, given {$length}" + "Max length $this->_maxLength exceeded, given {$length}" ); } @@ -102,13 +100,23 @@ protected function invariantValueMustMatchRegexPattern(): bool && preg_match($this->_pattern, $this->value) !== 1 ) { throw new InvalidArgumentException( - "Invalid value, does not match pattern {$this->_pattern}" + "Invalid value, $this->value does not match pattern {$this->_pattern}" ); } return true; } + /** + * Returns the actual string value. + * + * @return string + */ + public function value(): string + { + return $this->value; + } + /** * To string method... string is a string... * diff --git a/src/ValueObjects/UUIDValue.php b/src/ValueObjects/UUIDValue.php index 2a2380c..cd2633a 100644 --- a/src/ValueObjects/UUIDValue.php +++ b/src/ValueObjects/UUIDValue.php @@ -69,7 +69,7 @@ public static function random(): self } /** - * Check if the given identifier is the same than the current one. + * Check if the given identifier is the same as the current one. * * @param Identifier $other * diff --git a/src/ValueObjects/Value.php b/src/ValueObjects/Value.php index 600426f..5e34aa7 100644 --- a/src/ValueObjects/Value.php +++ b/src/ValueObjects/Value.php @@ -16,4 +16,4 @@ abstract class Value implements ValueObject { use IsValueObject; -} \ No newline at end of file +} diff --git a/tests/AggregatesTest.php b/tests/AggregatesTest.php new file mode 100644 index 0000000..29d3b7d --- /dev/null +++ b/tests/AggregatesTest.php @@ -0,0 +1,48 @@ +publishDomainEvents(mock(EventBus::class)->expect( + publish: function (Event ...$events) { + expect($events)->toHaveCount(1); + foreach ($events as $event) { + expect($event)->toBeInstanceOf(Event::class); + } + } + )); +}) + ->group('Unit'); + +test('Aggregate should has identity based in identifier.', function () { + $order1 = Order::create(1, 'Vincent Vega'); + $order2 = Order::create(1, 'Vincent Vega'); + $order3 = Order::create(2, 'Vincent Vega'); + + expect($order1->equals($order2))->toBeTrue(); + expect($order1->equals($order3))->toBeFalse(); + expect($order1->equals(new stdClass()))->toBeFalse(); +}) + ->group('Unit'); + +test('Aggregate should create new instance with new values.', function () { + expect(Order::create(1, 'Vincent Vega')->withName('Jules')->name) + ->toEqual('Jules'); +}) + ->group('Unity'); + +test('Aggregate should return correct debug information.', function () { + $order = Order::create(1, 'Vincent Vega'); + + expect($order->__debugInfo())->toHaveKeys([ + 'reference', + 'name', + 'lines', + 'created', + 'domainEvents' + ]); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 5949c61..545e8f5 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,8 +11,6 @@ | */ -// uses(Tests\TestCase::class)->in('Feature'); - /* |-------------------------------------------------------------------------- | Expectations @@ -24,10 +22,6 @@ | */ -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - /* |-------------------------------------------------------------------------- | Functions @@ -38,8 +32,3 @@ | global functions to help you to reduce the number of lines of code in your test files. | */ - -function something() -{ - // .. -} diff --git a/tests/Sample/Events/OrderCreated.php b/tests/Sample/Events/OrderCreated.php new file mode 100644 index 0000000..8c3b245 --- /dev/null +++ b/tests/Sample/Events/OrderCreated.php @@ -0,0 +1,65 @@ + + * @package ComplexHeart\Domain\Model\Test\Sample\Events + */ +class OrderCreated implements Event +{ + use HasAttributes; + + private readonly ID $id; // @phpstan-ignore-line + + private readonly string $name; // @phpstan-ignore-line + + private readonly Timestamp $timestamp; // @phpstan-ignore-line + + private readonly Aggregate $payload; // @phpstan-ignore-line + + public function __construct(Aggregate $aggregate, ?ID $id = null, ?Timestamp $timestamp = null) + { + $this->hydrate([ + 'id' => is_null($id) ? ID::random() : $id, + 'name' => 'order.created', + 'payload' => $aggregate, + 'timestamp' => is_null($timestamp) ? new Timestamp() : $timestamp, + ]); + } + + public function eventId(): string + { + return $this->id->value(); + } + + public function eventName(): string + { + return $this->name; + } + + public function occurredOn(): string + { + return (string) (((float) ($this->timestamp->getTimestamp().'.'.$this->timestamp->format('u'))) * 1000); + } + + public function payload(): array + { + return $this->payload->values(); + } + + public function aggregateId(): string + { + return $this->payload->id()->value(); + } +} \ No newline at end of file diff --git a/tests/Sample/Models/Order.php b/tests/Sample/Models/Order.php new file mode 100644 index 0000000..6c6d882 --- /dev/null +++ b/tests/Sample/Models/Order.php @@ -0,0 +1,82 @@ + + * @package ComplexHeart\Domain\Model\Test\Sample\Models + */ +final class Order implements Aggregate +{ + use IsAggregate; + + public readonly Reference $reference; // @phpstan-ignore-line + + public readonly string $name; // @phpstan-ignore-line + + public readonly OrderLines $lines; // @phpstan-ignore-line + + public readonly Timestamp $created; // @phpstan-ignore-line + + public function __construct(Reference $reference, string $name, OrderLines $lines, Timestamp $created) + { + $this->initialize([ + 'reference' => $reference, + 'name' => $name, + 'lines' => $lines, + 'created' => $created + ]); + + $this->registerDomainEvent(new OrderCreated($this)); + } + + public static function create(int $number, string $name): Order + { + $created = Timestamp::now(); + + return new Order( + reference: Reference::fromTimestamp($created, $number), + name: $name, + lines: OrderLines::empty(), + created: $created + ); + } + + public function id(): Identifier + { + return $this->reference; + } + + /** + * Adds a new OrderLine to the Order. + * + * @throws InvariantViolation + */ + public function addOrderLine(OrderLine $line): Order + { + $this->lines->add($line); + + return $this; + } + + public function withName(string $name): self + { + return $this->withOverrides(['name' => $name]); + } + + public function __toString(): string + { + return $this->reference->value(); + } +} \ No newline at end of file diff --git a/tests/Sample/Models/OrderLine.php b/tests/Sample/Models/OrderLine.php new file mode 100644 index 0000000..099aaff --- /dev/null +++ b/tests/Sample/Models/OrderLine.php @@ -0,0 +1,37 @@ + + * @package ComplexHeart\Domain\Model\Test\Sample\Models + */ +final class OrderLine implements ValueObject +{ + use IsValueObject; + + public readonly string $concept; // @phpstan-ignore-line + + public readonly int $quantity; // @phpstan-ignore-line + + public function __construct(string $concept, int $quantity) + { + $this->initialize([ + 'concept' => $concept, + 'quantity' => $quantity + ]); + } + + public function __toString(): string + { + return "$this->concept x$this->quantity"; + } +} \ No newline at end of file diff --git a/tests/Sample/Models/OrderLines.php b/tests/Sample/Models/OrderLines.php new file mode 100644 index 0000000..e6c728f --- /dev/null +++ b/tests/Sample/Models/OrderLines.php @@ -0,0 +1,20 @@ + + * @package ComplexHeart\Domain\Model\Test\Sample\Models + */ +final class OrderLines extends TypedCollection +{ + protected string $keyType = 'integer'; + + protected string $valueType = OrderLine::class; +} \ No newline at end of file diff --git a/tests/Sample/Models/Price.php b/tests/Sample/Models/Price.php new file mode 100644 index 0000000..05b9b48 --- /dev/null +++ b/tests/Sample/Models/Price.php @@ -0,0 +1,12 @@ + + * @package ComplexHeart\Domain\Model\Test\Sample\Models + */ +final class Reference extends StringValue implements Identifier +{ + protected int $_maxLength = 17; + + protected int $_minLength = 3; + + protected string $_pattern = '/F[0-9]{4}(\.[0-9]{2}){2}\-[0-9]{1,6}/'; + + /** + * @return bool + * @throws InvariantViolation + */ + protected function invariantMustStartWithFCharacter(): bool + { + if (!str_starts_with($this->value(), 'F')) { + throw new InvariantViolation('Reference must start with F character.'); + } + + return true; + } + + /** + * Create a new Reference instance from the timestamp. + * + * @param Timestamp $timestamp + * @param int $number + * @return Reference + */ + public static function fromTimestamp(Timestamp $timestamp, int $number): Reference + { + return new Reference(strtr('F{timestamp}-{number}', [ + '{timestamp}' => $timestamp->format('Y.m.d'), + '{number}' => $number, + ])); + } + +} diff --git a/tests/Sample/Models/SampleList.php b/tests/Sample/Models/SampleList.php new file mode 100644 index 0000000..8859cdb --- /dev/null +++ b/tests/Sample/Models/SampleList.php @@ -0,0 +1,16 @@ +value(), 'PR') !== 0) { - throw new InvariantViolation('Product Name must start with PR chars'); - } - - return true; - } -} - -test('Should create a valid Value Object.', function () { - $vo = new ProductName('PR sample'); - expect($vo)->toEqual('PR sample'); - expect((string)$vo)->toEqual('PR sample'); -})->group('Unit'); - -test('Should return true on equal ValueObjects.', function () { - $vo = new ProductName('PR sample'); - expect($vo->equals(new ProductName('PR sample')))->toBeTrue(); -})->group('Unit'); - -test('Should return false on not equal ValueObjects.', function () { - $vo = new ProductName('PR sample'); - expect($vo->equals(new ProductName('PR diff')))->toBeFalse(); -})->group('Unit'); - -test('Should throw exception on min length invariant violation.', function () { - new class('a') extends StringValue { - protected int $_minLength = 5; - }; - -}) - ->throws(InvariantViolation::class) - ->group('Unit'); - -test('Should throw exception on max length invariant violation.', function () { - new class('this is long') extends StringValue { - protected int $_maxLength = 5; - }; -}) - ->throws(InvariantViolation::class) - ->group('Unit'); - -test('Should throw exception on regex invariant violation.', function () { - new class('INVALID') extends StringValue { - protected string $_pattern = '[a-z]'; - }; -}) - ->throws(InvariantViolation::class) - ->group('Unit'); \ No newline at end of file diff --git a/tests/ValueObjectsTest.php b/tests/ValueObjectsTest.php new file mode 100644 index 0000000..3df048b --- /dev/null +++ b/tests/ValueObjectsTest.php @@ -0,0 +1,244 @@ +toEqual('F2022.12.01-00001'); + expect((string) $vo)->toEqual('F2022.12.01-00001'); +}) + ->group('Unit'); + +test('StringValue should return true on equal StringValue Objects.', function () { + $vo = new Reference('F2022.12.01-00001'); + expect($vo->equals(new Reference('F2022.12.01-00001')))->toBeTrue(); +}) + ->group('Unit'); + +test('StringValue should return false on not equal StringValue Objects.', function () { + $vo = new Reference('F2022.12.01-00001'); + expect($vo->equals(new Reference('F2022.12.01-00002')))->toBeFalse(); +}) + ->group('Unit'); + +test('StringValue should throw exception on min length invariant violation.', function () { + new class('a') extends StringValue { + protected int $_minLength = 5; + }; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('StringValue should throw exception on max length invariant violation.', function () { + new class('this is long') extends StringValue { + protected int $_maxLength = 5; + }; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('StringValue should throw exception on regex invariant violation.', function () { + new class('INVALID') extends StringValue { + protected string $_pattern = '[a-z]'; + }; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('BooleanValue should create a valid BooleanValue Object.', function () { + $vo = new class(true) extends BooleanValue { + protected array $_strings = [ + 'true' => 'Yes', + 'false' => 'No', + ]; + }; + expect((string) $vo)->toEqual('Yes'); +}) + ->group('Unit'); + +test('IntegerValue should create a valid IntegerValue Object.', function () { + $vo = new class(1) extends IntegerValue { + protected int $_maxValue = 100; + + protected int $_minValue = 1; + }; + expect((string) $vo)->toEqual('1'); +}) + ->group('Unit'); + +test('IntegerValue should throw exception on min value invariant violation.', function () { + new class(0) extends IntegerValue { + protected int $_maxValue = 100; + + protected int $_minValue = 1; + }; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('IntegerValue should throw exception on mix value invariant violation.', function () { + new class(101) extends IntegerValue { + protected int $_maxValue = 100; + + protected int $_minValue = 1; + }; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('FloatValue should create a valid FloatValue Object.', function () { + $vo = new class(3.14) extends FloatValue { + }; + expect((string) $vo)->toEqual('3.14'); +}) + ->group('Unit'); + +test('ArrayValue should create a valid ArrayValue Object.', function () { + $vo = new class([1]) extends ArrayValue { + protected int $_minItems = 1; + + protected int $_maxItems = 10; + + protected string $valueType = 'integer'; + }; + expect($vo)->toHaveCount(1); +}) + ->group('Unit'); + +test('ArrayValue should throw exception on invalid item type.', function () { + new SampleList([0]); +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('ArrayValue should throw exception on invalid minimum number of items.', function () { + new class([]) extends ArrayValue { + protected int $_minItems = 2; + + protected int $_maxItems = 0; + + protected string $valueType = 'integer'; + }; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('ArrayValue should throw exception on invalid maximum number of items.', function () { + new class([1, 2]) extends ArrayValue { + protected int $_minItems = 0; + + protected int $_maxItems = 1; + + protected string $valueType = 'integer'; + }; +}) + ->throws(InvariantViolation::class) + ->group('Unit'); + +test('ArrayValue should implement correctly ArrayAccess interface.', function () { + $vo = new class(['one', 'two']) extends ArrayValue { + protected int $_minItems = 1; + + protected int $_maxItems = 10; + + protected string $valueType = 'string'; + }; + + expect($vo)->toHaveCount(2); + expect($vo)->toBeIterable(); + expect($vo->getIterator())->toBeInstanceOf(ArrayIterator::class); + expect($vo[0])->toEqual('one'); +}); + +test('ArrayValue should throw exception on deleting a value.', function () { + $vo = new class(['one', 'two']) extends ArrayValue { + protected int $_minItems = 1; + + protected int $_maxItems = 10; + + protected string $valueType = 'string'; + }; + unset($vo[1]); +}) + ->group('Unit') + ->throws(ImmutableException::class); + +test('ArrayValue should throw exception on changing a value.', function () { + $vo = new class(['one', 'two']) extends ArrayValue { + protected int $_minItems = 1; + + protected int $_maxItems = 10; + + protected string $valueType = 'string'; + }; + $vo[1] = 'NewOne'; +}) + ->group('Unit') + ->throws(ImmutableException::class); + +test('ArrayValue should be converted to string correctly.', function () { + $vo = new class(['one', 'two']) extends ArrayValue { + protected int $_minItems = 1; + + protected int $_maxItems = 10; + + protected string $valueType = 'string'; + }; + + expect((string) $vo) + ->toBeString() + ->toEqual('["one","two"]'); +}); + +test('ArrayValue should implement correctly Serializable interface.', function () { + $vo = new SampleList(['one', 'two']); + $vo->unserialize($vo->serialize()); + + expect($vo->values())->toEqual(['value' => ['one', 'two']]); +}); + +test('ArrayValue should implement successfully serialize and unserialize methods.', function () { + $vo = new SampleList(['one', 'two']); + + expect($vo)->toEqual(unserialize(serialize($vo))); +}); + +test('UUIDValue should create a valid UUIDValue Object.', function () { + $vo = UUIDValue::random(); + + expect($vo->is($vo))->toBeTrue(); + expect((string) $vo)->toEqual($vo->__toString()); +}); + +test('DateTimeValue should create a valid DateTimeValue Object.', function () { + $vo = new DateTimeValue('2023-01-01T22:25:00+01:00'); + + expect($vo->values())->toBe(['value' => '2023-01-01T22:25:00+01:00']); +}); + +test('EnumValue should create a valid EnumValue Object.', function () { + $vo = new class ('one') extends EnumValue { + const ONE = 'one'; + 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'); +}); + diff --git a/wiki/Domain-Modeling-Aggregates.md b/wiki/Domain-Modeling-Aggregates.md index e69de29..717595f 100644 --- a/wiki/Domain-Modeling-Aggregates.md +++ b/wiki/Domain-Modeling-Aggregates.md @@ -0,0 +1,13 @@ +# Aggregates + +> Aggregate is a cluster of domain objects that can be treated as a single unit.\ +> -- Martin Fowler + +Creating a Value Object is quite easy you only need to use the Trait `IsAggregate` this will +add the `HasAttributes`, `HasInvariants`, `HasDomainEvents`, `HasIdentity` and `HasEquality` Traits. +In addition, you could use the `Aggregate` interface to expose the `publishDomainEvents` method. + +The following example illustrates the implementation of these components. + + + diff --git a/wiki/Domain-Modeling-Entities.md b/wiki/Domain-Modeling-Entities.md index e69de29..cc78d0e 100644 --- a/wiki/Domain-Modeling-Entities.md +++ b/wiki/Domain-Modeling-Entities.md @@ -0,0 +1 @@ +# Entities \ No newline at end of file diff --git a/wiki/Domain-Modeling-Value-Objects.md b/wiki/Domain-Modeling-Value-Objects.md index f545500..8b45f86 100644 --- a/wiki/Domain-Modeling-Value-Objects.md +++ b/wiki/Domain-Modeling-Value-Objects.md @@ -1,3 +1,13 @@ +# Value Objects + +> A small simple object, like money or a date range, whose equality isn't based on identity.\ +> -- Martin Fowler + +Creating a Value Object is quite easy you only need to use the Trait `IsValueObject` this will +add the `HasAttributes`, `HasInvariants` and the `HasEquality` Traits. In addition, you could +use the `ValueObject` interface to expose the `values` and `equals` methods. + +The following example illustrates the implementation of these components. ```php use ComplexHeart\Contracts\Domain\Model\ValueObject;