diff --git a/src/Domain/Models/Entities/Tag.php b/src/Domain/Models/Entities/Tag.php new file mode 100644 index 0000000..449c8df --- /dev/null +++ b/src/Domain/Models/Entities/Tag.php @@ -0,0 +1,60 @@ + + */ +final readonly class Tag implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public TagId $id, + public TagName $value, + public ?\DateTimeInterface $updatedAt = null, + ) { + } + + public function id(): TagId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public static function create( + TagName $value, + ?\DateTimeInterface $updatedAt = null, + ): Tag { + return new self( + id: TagId::generate(), + value: $value, + updatedAt: $updatedAt, + ); + } + + public static function hydrate( + TagId $id, + TagName $value, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + value: $value, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/ValueObjects/Tag/TagId.php b/src/Domain/Models/ValueObjects/Tag/TagId.php new file mode 100644 index 0000000..9ae28e2 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Tag/TagId.php @@ -0,0 +1,16 @@ +id())->toBe($id) + ->and($tag->value)->toBe($value) + ->and($tag->updatedAt)->toBeNull() + ->and($tag->updatedAt())->toBeNull(); + }); + + it('creates a tag with an explicit updatedAt date', function (): void { + // GIVEN + $id = TagId::generate(); + $name = new TagName('Name'); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); + + // WHEN + $tag = Tag::hydrate( + $id, + $name, + $updatedAt + ); + + // THEN + expect($tag->updatedAt)->toBe($updatedAt) + ->and($tag->updatedAt())->toBe($updatedAt); + }); + + it('creates a new tag with a freshly generated id', function (): void { + // GIVEN + $value = new TagName('Name'); + + // WHEN + $tag = Tag::create( + $value, + ); + + // THEN + expect($tag->id())->toBeInstanceOf(TagId::class) + ->and($tag->value)->toBe($value) + ->and($tag->updatedAt)->toBeNull() + ->and($tag->updatedAt())->toBeNull(); + }); + + it('generates a different id for each created tag', function (): void { + // GIVEN + $value = new TagName('Name'); + + // WHEN + $firstTag = Tag::create( + $value + ); + $secondTag = Tag::create( + $value + ); + + // THEN + expect($firstTag->id()->equals($secondTag->id()))->toBeFalse(); + }); + + it('returns the creation date extracted from the id', function (): void { + // GIVEN + $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); + $id = new TagId(Uuid::uuid7($expected)); + $tag = Tag::hydrate( + $id, + new TagName('Name'), + ); + + // WHEN + $result = $tag->createdAt(); + + // THEN + expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v')); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php b/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php new file mode 100644 index 0000000..298d233 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(TagId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = TagId::generate(); + $second = TagId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php new file mode 100644 index 0000000..be3f6b4 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid tag name', function (): void { + // GIVEN + $value = ''; + + // GIVEN / THEN + expect(fn (): TagName => new TagName($value))->toThrow(\DomainException::class); + }); +});