feat: ✨ create Tag entity and associated VOs

This commit is contained in:
Corentin Huet 2026-09-23 12:48:19 +02:00
parent b6975d913a
commit b5155c8ade
6 changed files with 255 additions and 0 deletions

View file

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Domain\Models\Entities;
use Domain\Models\ValueObjects\Tag\TagId;
use Domain\Models\ValueObjects\Tag\TagName;
use Shared\DDD\Models\AggregateRoot;
use Shared\DDD\Models\HasTimestampIdentifier;
/**
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Tag\TagId>
*/
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,
);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\Tag;
use Ramsey\Uuid\Uuid;
use Shared\DDD\Models\Uuidv7EntityId;
final readonly class TagId extends Uuidv7EntityId
{
public static function generate(): self
{
return new self(Uuid::uuid7());
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\Tag;
final readonly class TagName
{
public function __construct(
public string $value,
) {
$trimmed = trim($value);
if ($trimmed === '') {
throw new \DomainException('Tag name cannot be empty.');
}
}
}

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\Entities;
use Carbon\CarbonImmutable;
use Domain\Models\ValueObjects\Tag\TagId;
use Domain\Models\ValueObjects\Tag\TagName;
use Domain\Models\Entities\Tag;
use Ramsey\Uuid\Uuid;
covers(Tag::class);
describe('tag', function (): void {
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
it('creates a tag', function (): void {
// GIVEN
$id = TagId::generate();
$value = new TagName('Name');
// WHEN
$tag = Tag::hydrate(
$id,
$value,
);
// THEN
expect($tag->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'));
});
});

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Tag;
use Domain\Models\ValueObjects\Tag\TagId;
covers(TagId::class);
describe('brand id', function (): void {
it('generates a unique tag id', function (): void {
// WHEN
$id = TagId::generate();
// THEN
expect($id)->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);
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Tag;
use Domain\Models\ValueObjects\Tag\TagName;
covers(TagName::class);
describe('tag name', function (): void {
it('creates a valid tag name', function (): void {
// GIVEN
$value = 'Name';
// WHEN
$name = new TagName($value);
// THEN
expect($name->value)->toBe($value);
});
it('rejects invalid tag name', function (): void {
// GIVEN
$value = '';
// GIVEN / THEN
expect(fn (): TagName => new TagName($value))->toThrow(\DomainException::class);
});
});