diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2556fb2..99e1a3b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - php-version: [ '8.1' ] + php-version: [ '8.1', '8.2', '8.3' ] steps: - name: Checkout source code diff --git a/README.md b/README.md index c47981a..5e18913 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@ the functionality you need without compromising the essence of your own domain. Let's see a very basic example: ```php -use ComplexHeart\Contracts\Domain\Model\ValueObject; -use ComplexHeart\Domain\Model\Traits\IsValueObject; +use ComplexHeart\Contracts\Domain\Model\ValueObject;use ComplexHeart\Domain\Model\IsValueObject; /** * Class Color diff --git a/composer.json b/composer.json index 623a801..46fa4b5 100644 --- a/composer.json +++ b/composer.json @@ -1,30 +1,30 @@ { "name": "complex-heart/domain-model", - "description": "Domain model toolset to properly build Value Objects, Entities, Aggregates and Services.", + "description": "Domain model toolset to properly build Value Objects, Entities and Aggregates.", "type": "library", "license": "Apache-2.0", "authors": [ { "name": "Unay Santisteban", - "email": "usantisteban@othercode.es" + "email": "usantisteban@othercode.io" } ], "minimum-stability": "stable", "require": { - "php": "^8.1.0", + "php": "^8.0", "ext-json": "*", "ramsey/uuid": "^4.1", "nesbot/carbon": "^2.40", - "illuminate/collections": "^9.0.0", + "illuminate/collections": "^9.0.0|^10.0", "lambdish/phunctional": "^2.1", - "doctrine/instantiator": "^1.4", - "complex-heart/contracts": "^1.0.0" + "doctrine/instantiator": "^1.0|^2.0", + "complex-heart/contracts": "^1.0" }, "require-dev": { - "pestphp/pest": "^1.22.3", - "pestphp/pest-plugin-mock": "^1.0.0", - "pestphp/pest-plugin-faker": "^1.0.0", - "phpstan/phpstan": "^1.9.0" + "pestphp/pest": "^1.0|^2.0", + "pestphp/pest-plugin-mock": "^1.0|^2.0", + "pestphp/pest-plugin-faker": "^1.0|^2.0", + "phpstan/phpstan": "^1.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index 2c39d02..e266abd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,12 +2,9 @@ - - - ./src - + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" + cacheDirectory=".phpunit.cache"> + @@ -18,4 +15,9 @@ + + + ./src + + diff --git a/src/Errors/ImmutabilityError.php b/src/Errors/ImmutabilityError.php new file mode 100644 index 0000000..36d02d7 --- /dev/null +++ b/src/Errors/ImmutabilityError.php @@ -0,0 +1,18 @@ + + * @package ComplexHeart\Domain\Model\Errors + */ +class ImmutabilityError extends Error +{ + +} diff --git a/src/Exceptions/ImmutableException.php b/src/Exceptions/ImmutableException.php deleted file mode 100644 index f057474..0000000 --- a/src/Exceptions/ImmutableException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @package ComplexHeart\Domain\Model\Exceptions - */ -class ImmutableException extends RuntimeException -{ - -} diff --git a/src/Exceptions/InstantiationException.php b/src/Exceptions/InstantiationException.php index 5caa276..11d50c4 100644 --- a/src/Exceptions/InstantiationException.php +++ b/src/Exceptions/InstantiationException.php @@ -9,7 +9,7 @@ /** * Class InstantiationException * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Exceptions */ class InstantiationException extends RuntimeException diff --git a/src/Exceptions/InvariantViolation.php b/src/Exceptions/InvariantViolation.php index 2acbbec..4636b62 100644 --- a/src/Exceptions/InvariantViolation.php +++ b/src/Exceptions/InvariantViolation.php @@ -9,7 +9,7 @@ /** * Class InvariantViolation * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Exceptions */ class InvariantViolation extends Exception diff --git a/src/Traits/IsAggregate.php b/src/IsAggregate.php similarity index 53% rename from src/Traits/IsAggregate.php rename to src/IsAggregate.php index 69fa06b..6fe5f81 100644 --- a/src/Traits/IsAggregate.php +++ b/src/IsAggregate.php @@ -2,7 +2,9 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Traits; +namespace ComplexHeart\Domain\Model; + +use ComplexHeart\Domain\Model\Traits\HasDomainEvents; /** * Trait IsAggregate @@ -13,34 +15,13 @@ * @see https://martinfowler.com/bliki/DDD_Aggregate.html * @see https://martinfowler.com/bliki/EvansClassification.html * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait IsAggregate { use HasDomainEvents; - use IsEntity { - withOverrides as private overrideEntity; - } - - /** - * Creates a new instance overriding the given attributes and - * propagating the domain events to the new instance. - * - * @param array $overrides - * - * @return static - */ - protected function withOverrides(array $overrides): static - { - $new = $this->overrideEntity($overrides); - - foreach ($this->_domainEvents as $event) { - $new->registerDomainEvent($event); - } - - return $new; - } + use IsEntity; /** * This method is called by var_dump() when dumping an object to diff --git a/src/IsEntity.php b/src/IsEntity.php new file mode 100644 index 0000000..f2abdac --- /dev/null +++ b/src/IsEntity.php @@ -0,0 +1,28 @@ + Objects that have a distinct identity that runs through time and different representations. + * > -- Martin Fowler + * + * @see https://martinfowler.com/bliki/EvansClassification.html + * + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model\Traits + */ +trait IsEntity +{ + use IsModel; + use HasIdentity; + use HasEquality { + HasIdentity::hash insteadof HasEquality; + } +} diff --git a/src/Traits/IsModel.php b/src/IsModel.php similarity index 66% rename from src/Traits/IsModel.php rename to src/IsModel.php index 6cb86c6..1746f47 100644 --- a/src/Traits/IsModel.php +++ b/src/IsModel.php @@ -2,17 +2,19 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Traits; +namespace ComplexHeart\Domain\Model; use ComplexHeart\Domain\Model\Exceptions\InstantiationException; -use RuntimeException; -use Doctrine\Instantiator\Instantiator; +use ComplexHeart\Domain\Model\Traits\HasAttributes; +use ComplexHeart\Domain\Model\Traits\HasInvariants; use Doctrine\Instantiator\Exception\ExceptionInterface; +use Doctrine\Instantiator\Instantiator; +use RuntimeException; /** * Trait IsModel * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait IsModel @@ -30,7 +32,17 @@ trait IsModel */ protected function initialize(array $source, callable $onFail = null): static { - $this->hydrate($this->mapSource($source)); + // check if the array is indexed or associative. + $isIndexed = fn($source): bool => ([] !== $source) && array_keys($source) === range(0, count($source) - 1); + + $source = $isIndexed($source) + // combine the attributes keys with the provided source values. + ? array_combine(array_slice(static::attributes(), 0, count($source)), $source) + // return the already mapped array source. + : $source; + + + $this->hydrate($source); $this->check($onFail); return $this; @@ -53,36 +65,4 @@ final public static function make(): static throw new InstantiationException($e->getMessage(), $e->getCode(), $e); } } - - /** - * Creates a new instance overriding the given values. - * - * @param array $overrides - * - * @return static - */ - protected function withOverrides(array $overrides): static - { - return self::make(...array_values(array_merge($this->values(), $overrides))); - } - - /** - * Map the given source with the actual attributes by position, if - * the provided array is already mapped (assoc) return it directly. - * - * @param array $source - * - * @return array - */ - final protected function mapSource(array $source): array - { - // check if the array is indexed or associative. - $isIndexed = fn($source): bool => ([] !== $source) && array_keys($source) === range(0, count($source) - 1); - - return $isIndexed($source) - // combine the attributes keys with the provided source values. - ? array_combine(array_slice(static::attributes(), 0, count($source)), $source) - // return the already mapped array source. - : $source; - } } diff --git a/src/Traits/IsValueObject.php b/src/IsValueObject.php similarity index 73% rename from src/Traits/IsValueObject.php rename to src/IsValueObject.php index 111eba6..cf17349 100644 --- a/src/Traits/IsValueObject.php +++ b/src/IsValueObject.php @@ -2,7 +2,10 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Traits; +namespace ComplexHeart\Domain\Model; + +use ComplexHeart\Domain\Model\Traits\HasEquality; +use ComplexHeart\Domain\Model\Traits\HasImmutability; /** * Trait IsValueObject @@ -14,13 +17,14 @@ * @see https://martinfowler.com/bliki/ValueObject.html * @see https://martinfowler.com/bliki/EvansClassification.html * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait IsValueObject { use IsModel; use HasEquality; + use HasImmutability; /** * Represents the object as String. diff --git a/src/Traits/HasAttributes.php b/src/Traits/HasAttributes.php index 6bc1002..0ef6185 100644 --- a/src/Traits/HasAttributes.php +++ b/src/Traits/HasAttributes.php @@ -9,7 +9,7 @@ /** * Trait HasAttributes * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait HasAttributes @@ -133,11 +133,11 @@ protected function canCall(string $method): bool * $user->name() will access the private attribute name. * * @param string $attribute - * @param array $values - * + * @param array $_ * @return mixed|null + * @deprecated will be removed in version 3.0 */ - public function __call(string $attribute, array $values) + public function __call(string $attribute, array $_): mixed { return $this->get($attribute); } diff --git a/src/Traits/HasDomainEvents.php b/src/Traits/HasDomainEvents.php index f8d783b..6ece37a 100644 --- a/src/Traits/HasDomainEvents.php +++ b/src/Traits/HasDomainEvents.php @@ -12,7 +12,7 @@ * * @see https://martinfowler.com/eaaDev/DomainEvent.html * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait HasDomainEvents diff --git a/src/Traits/HasEquality.php b/src/Traits/HasEquality.php index 760e9e1..5f03cc4 100644 --- a/src/Traits/HasEquality.php +++ b/src/Traits/HasEquality.php @@ -7,7 +7,7 @@ /** * Trait HasEquality * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait HasEquality diff --git a/src/Traits/HasIdentity.php b/src/Traits/HasIdentity.php index 4110de6..00a1c55 100644 --- a/src/Traits/HasIdentity.php +++ b/src/Traits/HasIdentity.php @@ -4,14 +4,12 @@ namespace ComplexHeart\Domain\Model\Traits; - - use ComplexHeart\Contracts\Domain\Model\Identifier; /** * Trait HasIdentity * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait HasIdentity diff --git a/src/Traits/HasImmutability.php b/src/Traits/HasImmutability.php new file mode 100644 index 0000000..8075769 --- /dev/null +++ b/src/Traits/HasImmutability.php @@ -0,0 +1,61 @@ + + * @package ComplexHeart\Domain\Model\Traits + * @deprecated Will be removed in future versions. For PHP >= 8 use readonly keyword. + */ +trait HasImmutability +{ + /** + * Enforces the immutability by blocking any attempts of update any property. + * + * @param string $name + * @param $_ + * @return void + */ + final public function __set(string $name, $_): void + { + $class = static::class; + throw new ImmutabilityError("Cannot modify property $name from immutable $class object."); + } + + /** + * Accessor for the protected or private properties. + * + * @param string $name + * @return mixed + */ + final public function __get(string $name): mixed + { + return method_exists($this, 'get') + ? $this->get($name) + : $this->{$name}; + } + + /** + * Creates a new instance overriding the given values. + * + * @param array $overrides + * + * @return static + */ + protected function withOverrides(array $overrides): static + { + return self::make(...array_values(array_merge($this->values(), $overrides))); + } +} diff --git a/src/Traits/HasInvariants.php b/src/Traits/HasInvariants.php index f45aae1..c3d9cb7 100644 --- a/src/Traits/HasInvariants.php +++ b/src/Traits/HasInvariants.php @@ -10,7 +10,7 @@ /** * Trait HasInvariants * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait HasInvariants @@ -24,7 +24,7 @@ final public static function invariants(): array { $invariants = []; foreach (get_class_methods(static::class) as $invariant) { - if (str_starts_with($invariant, 'invariant') && $invariant !== 'invariants') { + if (str_starts_with($invariant, 'invariant') && !in_array($invariant, ['invariants', 'invariantHandler'])) { $invariants[$invariant] = str_replace( 'invariant ', '', diff --git a/src/Traits/HasTypeCheck.php b/src/Traits/HasTypeCheck.php index 2e965e2..3586e75 100644 --- a/src/Traits/HasTypeCheck.php +++ b/src/Traits/HasTypeCheck.php @@ -7,7 +7,7 @@ /** * Trait HasTypeValidation * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Traits */ trait HasTypeCheck diff --git a/src/Traits/IsEntity.php b/src/Traits/IsEntity.php deleted file mode 100644 index 8f01e9b..0000000 --- a/src/Traits/IsEntity.php +++ /dev/null @@ -1,45 +0,0 @@ - Objects that have a distinct identity that runs through time and different representations. - * > -- Martin Fowler - * - * @see https://martinfowler.com/bliki/EvansClassification.html - * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Model\Traits - */ -trait IsEntity -{ - use IsModel { - withOverrides as private overrideAttributes; - } - use HasIdentity; - use HasEquality { - HasIdentity::hash insteadof HasEquality; - } - - /** - * Creates a new instance overriding the given attributes excluding the id, - * as the Entities are identified by the ID if it changes we will have a - * different entity. - * - * @param array $overrides - * - * @return static - */ - protected function withOverrides(array $overrides): static - { - return $this->overrideAttributes( - filter(fn($value, string $key): bool => $key !== 'id', $overrides) - ); - } -} diff --git a/src/TypedCollection.php b/src/TypedCollection.php index db6028e..7e738a2 100644 --- a/src/TypedCollection.php +++ b/src/TypedCollection.php @@ -12,7 +12,7 @@ /** * Class TypedCollection * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\Domain */ class TypedCollection extends Collection diff --git a/src/ValueObjects/ArrayValue.php b/src/ValueObjects/ArrayValue.php index 0cf4315..3a9b88c 100644 --- a/src/ValueObjects/ArrayValue.php +++ b/src/ValueObjects/ArrayValue.php @@ -4,8 +4,9 @@ namespace ComplexHeart\Domain\Model\ValueObjects; +use ComplexHeart\Domain\Model\Errors\ImmutabilityError; use Countable; -use ComplexHeart\Domain\Model\Exceptions\ImmutableException; + use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; use Serializable; use ArrayAccess; @@ -16,7 +17,7 @@ /** * Class ArrayValue * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ abstract class ArrayValue extends Value implements IteratorAggregate, ArrayAccess, Serializable, Countable @@ -164,7 +165,7 @@ public function offsetGet(mixed $offset): mixed */ public function offsetSet(mixed $offset, mixed $value): void { - throw new ImmutableException("Illegal attempt of change immutable value on offset $offset"); + throw new ImmutabilityError("Illegal attempt of change immutable value on offset $offset"); } /** @@ -174,7 +175,7 @@ public function offsetSet(mixed $offset, mixed $value): void */ public function offsetUnset(mixed $offset): void { - throw new ImmutableException("Illegal attempt of unset immutable value on offset $offset"); + throw new ImmutabilityError("Illegal attempt of unset immutable value on offset $offset"); } /** diff --git a/src/ValueObjects/BooleanValue.php b/src/ValueObjects/BooleanValue.php index 89636de..164a939 100644 --- a/src/ValueObjects/BooleanValue.php +++ b/src/ValueObjects/BooleanValue.php @@ -9,7 +9,7 @@ * * @method bool value() * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ abstract class BooleanValue extends Value diff --git a/src/ValueObjects/DateTimeValue.php b/src/ValueObjects/DateTimeValue.php index e730a5b..528c47e 100644 --- a/src/ValueObjects/DateTimeValue.php +++ b/src/ValueObjects/DateTimeValue.php @@ -13,7 +13,7 @@ /** * Class DateTimeValue * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ class DateTimeValue extends CarbonImmutable implements ValueObject diff --git a/src/ValueObjects/EnumValue.php b/src/ValueObjects/EnumValue.php index 2cb92be..d58b28d 100644 --- a/src/ValueObjects/EnumValue.php +++ b/src/ValueObjects/EnumValue.php @@ -11,7 +11,7 @@ * * @method mixed value() * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ abstract class EnumValue extends Value diff --git a/src/ValueObjects/FloatValue.php b/src/ValueObjects/FloatValue.php index d4cba17..29da549 100644 --- a/src/ValueObjects/FloatValue.php +++ b/src/ValueObjects/FloatValue.php @@ -9,7 +9,7 @@ * * @method float value() * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ abstract class FloatValue extends Value diff --git a/src/ValueObjects/IntegerValue.php b/src/ValueObjects/IntegerValue.php index d2423c9..6c7be3f 100644 --- a/src/ValueObjects/IntegerValue.php +++ b/src/ValueObjects/IntegerValue.php @@ -11,7 +11,7 @@ * * @method int value() * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ abstract class IntegerValue extends Value diff --git a/src/ValueObjects/StringValue.php b/src/ValueObjects/StringValue.php index 25e3ba8..12f41d7 100644 --- a/src/ValueObjects/StringValue.php +++ b/src/ValueObjects/StringValue.php @@ -10,7 +10,7 @@ /** * Class StringValue * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ abstract class StringValue extends Value diff --git a/src/ValueObjects/UUIDValue.php b/src/ValueObjects/UUIDValue.php index cd2633a..d833c37 100644 --- a/src/ValueObjects/UUIDValue.php +++ b/src/ValueObjects/UUIDValue.php @@ -11,7 +11,7 @@ /** * Class UUIDValue * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ class UUIDValue extends Value implements Identifier diff --git a/src/ValueObjects/Value.php b/src/ValueObjects/Value.php index 5e34aa7..15c752b 100644 --- a/src/ValueObjects/Value.php +++ b/src/ValueObjects/Value.php @@ -5,12 +5,12 @@ namespace ComplexHeart\Domain\Model\ValueObjects; use ComplexHeart\Contracts\Domain\Model\ValueObject; -use ComplexHeart\Domain\Model\Traits\IsValueObject; +use ComplexHeart\Domain\Model\IsValueObject; /** * Class Value * - * @author Unay Santisteban + * @author Unay Santisteban * @package ComplexHeart\Domain\Model\ValueObjects */ abstract class Value implements ValueObject diff --git a/tests/AggregatesTest.php b/tests/AggregatesTest.php index 29d3b7d..7d7be60 100644 --- a/tests/AggregatesTest.php +++ b/tests/AggregatesTest.php @@ -4,17 +4,17 @@ use ComplexHeart\Contracts\Domain\ServiceBus\Event; use ComplexHeart\Contracts\Domain\ServiceBus\EventBus; -use ComplexHeart\Domain\Model\Test\Sample\Models\Order; +use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Order; test('Aggregate should register domain event successfully.', function () { - Order::create(1, 'Vincent Vega')->publishDomainEvents(mock(EventBus::class)->expect( - publish: function (Event ...$events) { + $eventBus = mock(EventBus::class); + $eventBus->shouldReceive('publish') + ->withArgs(function (Event ...$events) { expect($events)->toHaveCount(1); - foreach ($events as $event) { - expect($event)->toBeInstanceOf(Event::class); - } - } - )); + return true; + }); + + Order::create(1, 'Vincent Vega')->publishDomainEvents($eventBus); }) ->group('Unit'); diff --git a/tests/OrderManagement/Domain/Errors/InvalidPriceError.php b/tests/OrderManagement/Domain/Errors/InvalidPriceError.php new file mode 100644 index 0000000..cda1d51 --- /dev/null +++ b/tests/OrderManagement/Domain/Errors/InvalidPriceError.php @@ -0,0 +1,12 @@ + + * @package ComplexHeart\Domain\Model\Test\OrderManagement\Events + */ +class OrderCreated implements Event +{ + private ID $id; + + private string $name = 'order.created'; + + private array $payload; + + private Timestamp $timestamp; + + /** + * @param Order $order + * @param ID|null $id + * @param Timestamp|null $timestamp + * @throws Exception + */ + public function __construct(Order $order, ?ID $id = null, ?Timestamp $timestamp = null) + { + $this->id = is_null($id) ? ID::random() : $id; + $this->payload = [ + 'id' => $order->id()->value(), + 'name' => $order->name, + 'orderLines' => $order->lines->map(fn(OrderLine $line) => $line->values()), + 'tags' => $order->tags->values()['value'], + 'created' => $order->created->toIso8601String(), + ]; + $this->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; + } + + public function aggregateId(): string + { + return $this->payload['id']; + } +} \ No newline at end of file diff --git a/tests/Sample/Models/Order.php b/tests/OrderManagement/Domain/Order.php similarity index 51% rename from tests/Sample/Models/Order.php rename to tests/OrderManagement/Domain/Order.php index 6c6d882..e814e51 100644 --- a/tests/Sample/Models/Order.php +++ b/tests/OrderManagement/Domain/Order.php @@ -2,55 +2,49 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Test\Sample\Models; +namespace ComplexHeart\Domain\Model\Test\OrderManagement\Domain; use ComplexHeart\Contracts\Domain\Model\Aggregate; use ComplexHeart\Contracts\Domain\Model\Identifier; use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; -use ComplexHeart\Domain\Model\Test\Sample\Events\OrderCreated; -use ComplexHeart\Domain\Model\Traits\IsAggregate; +use ComplexHeart\Domain\Model\IsAggregate; +use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Events\OrderCreated; use ComplexHeart\Domain\Model\ValueObjects\DateTimeValue as Timestamp; /** * Class Order * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Model\Test\Sample\Models + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model\Test\OrderManagement\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 function __construct( + public Reference $reference, + public string $name, + public OrderLines $lines, + public Tags $tags, + public Timestamp $created + ) { + $this->check(); } public static function create(int $number, string $name): Order { $created = Timestamp::now(); - - return new Order( + $order = new Order( reference: Reference::fromTimestamp($created, $number), name: $name, lines: OrderLines::empty(), + tags: new Tags(), created: $created ); + + $order->registerDomainEvent(new OrderCreated($order)); + + return $order; } public function id(): Identifier @@ -63,7 +57,7 @@ public function id(): Identifier * * @throws InvariantViolation */ - public function addOrderLine(OrderLine $line): Order + public function addOrderLine(OrderLine $line): self { $this->lines->add($line); @@ -72,7 +66,8 @@ public function addOrderLine(OrderLine $line): Order public function withName(string $name): self { - return $this->withOverrides(['name' => $name]); + $this->name = $name; + return $this; } public function __toString(): string diff --git a/tests/Sample/Models/OrderLine.php b/tests/OrderManagement/Domain/OrderLine.php similarity index 58% rename from tests/Sample/Models/OrderLine.php rename to tests/OrderManagement/Domain/OrderLine.php index 099aaff..b2f5488 100644 --- a/tests/Sample/Models/OrderLine.php +++ b/tests/OrderManagement/Domain/OrderLine.php @@ -2,25 +2,25 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Test\Sample\Models; +namespace ComplexHeart\Domain\Model\Test\OrderManagement\Domain; use ComplexHeart\Contracts\Domain\Model\ValueObject; -use ComplexHeart\Domain\Model\Traits\IsValueObject; +use ComplexHeart\Domain\Model\IsValueObject; /** * Class OrderLine * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Model\Test\Sample\Models + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model\Test\OrderManagement\Models */ final class OrderLine implements ValueObject { use IsValueObject; - public readonly string $concept; // @phpstan-ignore-line + public string $concept; - public readonly int $quantity; // @phpstan-ignore-line + public int $quantity; public function __construct(string $concept, int $quantity) { diff --git a/tests/Sample/Models/OrderLines.php b/tests/OrderManagement/Domain/OrderLines.php similarity index 57% rename from tests/Sample/Models/OrderLines.php rename to tests/OrderManagement/Domain/OrderLines.php index e6c728f..be3f107 100644 --- a/tests/Sample/Models/OrderLines.php +++ b/tests/OrderManagement/Domain/OrderLines.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Test\Sample\Models; +namespace ComplexHeart\Domain\Model\Test\OrderManagement\Domain; use ComplexHeart\Domain\Model\TypedCollection; /** * Class LineItems * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Model\Test\Sample\Models + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model\Test\OrderManagement\Models */ final class OrderLines extends TypedCollection { diff --git a/tests/OrderManagement/Domain/Price.php b/tests/OrderManagement/Domain/Price.php new file mode 100644 index 0000000..7def73c --- /dev/null +++ b/tests/OrderManagement/Domain/Price.php @@ -0,0 +1,57 @@ + + * @package ComplexHeart\Domain\Model\Test\OrderManagement\Models + */ +final class Price implements ValueObject +{ + use IsValueObject; + + public function __construct( + private float $amount, + private string $currency + ) { + $this->check(); + } + + protected function invariantHandler(array $violations): void + { + throw new InvalidPriceError("Invalid Price values: ".implode(",", $violations)); + } + + protected function invariantAmountMustBeGreaterThanZero(): bool + { + return $this->amount >= 0; + } + + protected function invariantCurrencyMustBeHaveThreeCharacters(): bool + { + return strlen($this->currency) == 3; + } + + public function applyDiscount(float $discount): self + { + return $this->withOverrides([ + 'amount' => $this->amount - ($this->amount * ($discount / 100)) + ]); + } + + public function __toString(): string + { + return "$this->amount $this->currency"; + } +} \ No newline at end of file diff --git a/tests/Sample/Models/Reference.php b/tests/OrderManagement/Domain/Reference.php similarity index 87% rename from tests/Sample/Models/Reference.php rename to tests/OrderManagement/Domain/Reference.php index 4e2f8ce..3b45305 100644 --- a/tests/Sample/Models/Reference.php +++ b/tests/OrderManagement/Domain/Reference.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Test\Sample\Models; +namespace ComplexHeart\Domain\Model\Test\OrderManagement\Domain; use ComplexHeart\Contracts\Domain\Model\Identifier; use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; -use ComplexHeart\Domain\Model\ValueObjects\StringValue; use ComplexHeart\Domain\Model\ValueObjects\DateTimeValue as Timestamp; +use ComplexHeart\Domain\Model\ValueObjects\StringValue; /** * Class Reference * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Model\Test\Sample\Models + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model\Test\OrderManagement\Models */ final class Reference extends StringValue implements Identifier { diff --git a/tests/Sample/Models/SampleList.php b/tests/OrderManagement/Domain/Tags.php similarity index 56% rename from tests/Sample/Models/SampleList.php rename to tests/OrderManagement/Domain/Tags.php index 8859cdb..319406f 100644 --- a/tests/Sample/Models/SampleList.php +++ b/tests/OrderManagement/Domain/Tags.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Model\Test\Sample\Models; +namespace ComplexHeart\Domain\Model\Test\OrderManagement\Domain; use ComplexHeart\Domain\Model\ValueObjects\ArrayValue; -final class SampleList extends ArrayValue +final class Tags extends ArrayValue { - protected int $_minItems = 1; + protected int $_minItems = 0; protected int $_maxItems = 10; diff --git a/tests/Sample/Events/OrderCreated.php b/tests/Sample/Events/OrderCreated.php deleted file mode 100644 index 8c3b245..0000000 --- a/tests/Sample/Events/OrderCreated.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @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/Price.php b/tests/Sample/Models/Price.php deleted file mode 100644 index 05b9b48..0000000 --- a/tests/Sample/Models/Price.php +++ /dev/null @@ -1,12 +0,0 @@ -amount = 0.0; +}) + ->group('Unit') + ->throws(ImmutabilityError::class); + +test('Object with HasImmutability should expose primitive values.', function () { + $price = new Price(100.0, 'EUR'); + expect($price->amount)->toBeFloat(); + expect($price->currency)->toBeString(); +}) + ->group('Unit'); + +test('Object with HasImmutability should return new instance with override values.', function () { + $price = new Price(100.0, 'EUR'); + $newPrice = $price->applyDiscount(10.0); + + expect($newPrice)->toBeInstanceOf(Price::class); + expect($newPrice->amount)->toBe(90.0); +}) + ->group('Unit'); + +test('Object with HasInvariants should support custom invariant handler.', function () { + new Price(-10.0, 'EURO'); +}) + ->group('Unit') + ->throws(InvalidPriceError::class); \ No newline at end of file diff --git a/tests/ValueObjectsTest.php b/tests/ValueObjectsTest.php index 3df048b..cdb823c 100644 --- a/tests/ValueObjectsTest.php +++ b/tests/ValueObjectsTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -use ComplexHeart\Domain\Model\Exceptions\ImmutableException; +use ComplexHeart\Domain\Model\Errors\ImmutabilityError; use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; -use ComplexHeart\Domain\Model\Test\Sample\Models\Reference; -use ComplexHeart\Domain\Model\Test\Sample\Models\SampleList; +use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Reference; +use ComplexHeart\Domain\Model\Test\OrderManagement\Domain\Tags; use ComplexHeart\Domain\Model\ValueObjects\ArrayValue; +use ComplexHeart\Domain\Model\ValueObjects\BooleanValue; use ComplexHeart\Domain\Model\ValueObjects\DateTimeValue; use ComplexHeart\Domain\Model\ValueObjects\EnumValue; use ComplexHeart\Domain\Model\ValueObjects\FloatValue; use ComplexHeart\Domain\Model\ValueObjects\IntegerValue; use ComplexHeart\Domain\Model\ValueObjects\StringValue; -use ComplexHeart\Domain\Model\ValueObjects\BooleanValue; use ComplexHeart\Domain\Model\ValueObjects\UUIDValue; test('StringValue should create a valid StringValue Object.', function () { @@ -119,7 +119,7 @@ ->group('Unit'); test('ArrayValue should throw exception on invalid item type.', function () { - new SampleList([0]); + new Tags([0]); }) ->throws(InvariantViolation::class) ->group('Unit'); @@ -161,7 +161,8 @@ expect($vo)->toBeIterable(); expect($vo->getIterator())->toBeInstanceOf(ArrayIterator::class); expect($vo[0])->toEqual('one'); -}); +}) + ->group('Unit'); test('ArrayValue should throw exception on deleting a value.', function () { $vo = new class(['one', 'two']) extends ArrayValue { @@ -174,7 +175,7 @@ unset($vo[1]); }) ->group('Unit') - ->throws(ImmutableException::class); + ->throws(ImmutabilityError::class); test('ArrayValue should throw exception on changing a value.', function () { $vo = new class(['one', 'two']) extends ArrayValue { @@ -187,7 +188,7 @@ $vo[1] = 'NewOne'; }) ->group('Unit') - ->throws(ImmutableException::class); + ->throws(ImmutabilityError::class); test('ArrayValue should be converted to string correctly.', function () { $vo = new class(['one', 'two']) extends ArrayValue { @@ -201,33 +202,38 @@ expect((string) $vo) ->toBeString() ->toEqual('["one","two"]'); -}); +}) + ->group('Unit'); test('ArrayValue should implement correctly Serializable interface.', function () { - $vo = new SampleList(['one', 'two']); + $vo = new Tags(['one', 'two']); $vo->unserialize($vo->serialize()); expect($vo->values())->toEqual(['value' => ['one', 'two']]); -}); +}) + ->group('Unit'); test('ArrayValue should implement successfully serialize and unserialize methods.', function () { - $vo = new SampleList(['one', 'two']); + $vo = new Tags(['one', 'two']); expect($vo)->toEqual(unserialize(serialize($vo))); -}); +}) + ->group('Unit'); test('UUIDValue should create a valid UUIDValue Object.', function () { $vo = UUIDValue::random(); expect($vo->is($vo))->toBeTrue(); expect((string) $vo)->toEqual($vo->__toString()); -}); +}) + ->group('Unit'); 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']); -}); +}) + ->group('Unit'); test('EnumValue should create a valid EnumValue Object.', function () { $vo = new class ('one') extends EnumValue { @@ -240,5 +246,5 @@ expect($vo::getLabels()[0])->toBe('ONE'); expect($vo::getLabels()[1])->toBe('TWO'); -}); - +}) + ->group('Unit'); diff --git a/wiki/Domain-Modeling-Value-Objects.md b/wiki/Domain-Modeling-Value-Objects.md index 8b45f86..81c54bb 100644 --- a/wiki/Domain-Modeling-Value-Objects.md +++ b/wiki/Domain-Modeling-Value-Objects.md @@ -10,8 +10,7 @@ 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; -use ComplexHeart\Domain\Model\Traits\IsValueObject; +use ComplexHeart\Contracts\Domain\Model\ValueObject;use ComplexHeart\Domain\Model\IsValueObject; /** * Class Color