feat(shared): implement entity identity abstractions and base value objects

This commit is contained in:
Jérémy Munsch 2026-09-20 17:16:35 +02:00
parent ba51a5c312
commit c28ce7c488
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
10 changed files with 200 additions and 9 deletions

View file

@ -4,6 +4,6 @@ declare(strict_types=1);
namespace Shared\DDD\Exceptions;
class NotFoundException extends \DomainException
abstract class NotFoundException extends \DomainException
{
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Shared\DDD\Models;
use Shared\Helpers\UuidHelper;
use Ramsey\Uuid\UuidInterface;
abstract readonly class Uuidv7EntityId extends EntityId
{
public function __construct(
public UuidInterface $identifier
) {
parent::__construct($identifier->toString());
try {
UuidHelper::validateUuid($identifier);
} catch (\InvalidArgumentException $e) {
throw new \DomainException(
"Invalid value '{$identifier->toString()}' is not a valid UUID",
0,
$e
);
}
if (! UuidHelper::isV7($identifier)) {
throw new \DomainException(
"Invalid value '{$identifier}' is not a valid V7 UUID",
);
}
}
public function toDateTime(): \DateTimeInterface
{
return UuidHelper::toDateTime($this->identifier);
}
abstract public static function generate(): static;
}

View file

@ -2,12 +2,14 @@
declare(strict_types=1);
namespace Shared\Models\ValueObjects;
namespace Shared\ValueObjects;
use Shared\DDD\Models\Equatable;
/**
* Represents a valid email.
*/
final readonly class Email
final readonly class Email implements \Stringable, Equatable
{
public string $value;
@ -34,4 +36,15 @@ final readonly class Email
$this->value = mb_strtolower($trimmed, 'UTF-8');
}
#[\Override]
public function equals(mixed $other): bool
{
return $other instanceof self && $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
}

View file

@ -2,13 +2,15 @@
declare(strict_types=1);
namespace Shared\Models\ValueObjects;
namespace Shared\ValueObjects;
use Shared\DDD\Models\Equatable;
/**
* Represents a valid Rcs.
* See: https://entreprendre.service-public.gouv.fr/vosdroits/F31190.
*/
final readonly class Rcs
final readonly class Rcs implements \Stringable, Equatable
{
private const string REGEX = '/^RCS\s+[A-ZÀ-ÖØ-Ý][A-ZÀ-ÖØ-Ý\s-]*\s+\d{3}\s\d{3}\s\d{3}$/u';
@ -29,4 +31,15 @@ final readonly class Rcs
throw new \DomainException('Rcs is not a valid Rcs text.');
}
}
#[\Override]
public function equals(mixed $other): bool
{
return $other instanceof self && $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Shared\DDD\Models;
use Shared\DDD\Models\EntityId;
final readonly class DummyEntityId extends EntityId
{
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Shared\DDD\Models;
use Shared\DDD\Models\Uuidv7EntityId;
final readonly class DummyUuidv7EntityId extends Uuidv7EntityId
{
#[\Override]
public static function generate(): static
{
throw new \UnexpectedValueException('Not implemented');
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Shared\DDD\Models;
use Shared\DDD\Models\EntityId;
covers(EntityId::class);
describe('entity id', function (): void {
it('should create an entity id', function (): void {
// GIVEN / WHEN
$id = new DummyEntityId('123');
// THEN
expect($id->value)->toBe('123');
});
it('can be casted to string', function (): void {
// GIVEN / WHEN
$id = new DummyEntityId('123');
// THEN
expect((string) $id)->toBe('123');
});
it('can be compared for equality', function (): void {
// GIVEN / WHEN
$id1 = new DummyEntityId('123');
$id2 = new DummyEntityId('123');
// THEN
expect($id1->equals($id2))->toBeTrue();
});
});

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Shared\DDD\Models;
use Ramsey\Uuid\Uuid;
use Shared\DDD\Models\Uuidv7EntityId;
covers(Uuidv7EntityId::class);
describe('uuidv7 entity id', function (): void {
it('should create a uuidv7 entity id', function (): void {
// GIVEN / WHEN
$uuid = Uuid::uuid7();
$id = new DummyUuidv7EntityId($uuid);
// THEN
expect($id->value)->toBe((string) $uuid);
});
it('can be casted to string', function (): void {
// GIVEN / WHEN
$uuid = Uuid::uuid7();
$id = new DummyUuidv7EntityId($uuid);
// THEN
expect((string) $id)->toBe((string) $uuid);
});
it('can be compared for equality', function (): void {
// GIVEN / WHEN
$uuid = Uuid::uuid7();
$id1 = new DummyUuidv7EntityId($uuid);
$id2 = new DummyUuidv7EntityId($uuid);
// THEN
expect($id1->equals($id2))->toBeTrue();
});
it('can be converted to a datetime', function (): void {
// GIVEN / WHEN
$uuid = Uuid::uuid7();
$id = new DummyUuidv7EntityId($uuid);
// THEN
expect($id->toDateTime())->toBeInstanceOf(\DateTimeImmutable::class);
});
});
describe('uuidv7 entity id validation rules', function (): void {
it('should throw an exception when non v7 UUID is used', function (): void {
// GIVEN
$uuid = Uuid::uuid4();
// THEN
$this->expectException(\DomainException::class);
// WHEN
new DummyUuidv7EntityId($uuid);
});
});

View file

@ -2,9 +2,9 @@
declare(strict_types=1);
namespace Tests\Unit\Shared\Models\ValueObjects;
namespace Tests\Unit\Shared\ValueObjects;
use Shared\Models\ValueObjects\Email;
use Shared\ValueObjects\Email;
covers(Email::class);

View file

@ -2,9 +2,9 @@
declare(strict_types=1);
namespace Tests\Unit\Shared\Models\ValueObjects;
namespace Tests\Unit\Shared\ValueObjects;
use Shared\Models\ValueObjects\Rcs;
use Shared\ValueObjects\Rcs;
covers(Rcs::class);