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