refactor(ddd): introduce AggregateRoot and ReadModel interfaces extending Entity

This commit is contained in:
Jérémy Munsch 2026-08-24 21:44:51 +02:00
parent e10bd7741d
commit cb97e6f591
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
13 changed files with 93 additions and 33 deletions

View file

@ -8,14 +8,14 @@ use Application\Security\Models\ValueObjects\ClientIdentifier;
use Application\Security\Models\ValueObjects\ClientName;
use Application\Security\Models\ValueObjects\ClientSecret;
use Application\Security\Models\ValueObjects\Entities\ClientId;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\AggregateRoot;
use Shared\DDD\Model\HasTimestampIdentifier;
use Shared\Time\Utc;
/**
* @implements Entity<ClientId>
* @implements AggregateRoot<ClientId>
*/
final class Client implements Entity, HasTimestampIdentifier
final class Client implements AggregateRoot, HasTimestampIdentifier
{
private function __construct(
public readonly ClientId $id,

View file

@ -7,13 +7,13 @@ namespace Application\Security\Models\Entities;
use Application\Security\Models\ValueObjects\Entities\SecurityAuditLogDetails;
use Application\Security\Models\ValueObjects\Entities\SecurityAuditLogEntryId;
use Ramsey\Uuid\UuidInterface;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\HasTimestampIdentifier;
use Shared\DDD\Model\ReadModel;
/**
* @implements Entity<SecurityAuditLogEntryId>
* @implements ReadModel<SecurityAuditLogEntryId>
*/
final readonly class SecurityAuditLogEntry implements Entity, HasTimestampIdentifier
final readonly class SecurityAuditLogEntry implements HasTimestampIdentifier, ReadModel
{
public function __construct(
public SecurityAuditLogEntryId $id,

View file

@ -9,21 +9,18 @@ use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Brand\BrandId;
use Domain\Models\ValueObjects\Brand\BrandName;
use Ramsey\Uuid\Uuid;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\AggregateRoot;
use Shared\DDD\Model\HasTimestampIdentifier;
/** @implements Entity<BrandId> */
final readonly class Brand implements Entity, HasTimestampIdentifier
/** @implements AggregateRoot<BrandId> */
final readonly class Brand implements AggregateRoot, HasTimestampIdentifier
{
public BrandId $id;
private function __construct(
BrandId $id,
public BrandId $id,
public BrandName $name,
public BrandCode $code,
public ArcadiaId $arcadiaId,
) {
$this->id = $id;
}
#[\Override]

View file

@ -8,10 +8,10 @@ use Domain\Models\ValueObjects\Employees\ArcadiaEmployeeId;
use Domain\Models\ValueObjects\EstablishmentCode;
use Domain\Models\ValueObjects\ServiceId;
use Domain\Models\ValueObjects\SiteId;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\ReadModel;
/** @implements Entity<ArcadiaEmployeeId> */
final readonly class Employee implements Entity
/** @implements ReadModel<ArcadiaEmployeeId> */
final readonly class Employee implements ReadModel
{
private function __construct(
public ArcadiaEmployeeId $publicEmployeeNumber,

View file

@ -21,10 +21,10 @@ use Domain\Models\ValueObjects\Inco\Composition\ReferenceMacronutrients;
use Domain\Models\ValueObjects\Inco\Composition\SpecialCompounds;
use Domain\Models\ValueObjects\Inco\Composition\Vitamins;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\ReadModel;
/** @implements Entity<IncoCompositionId> */
final readonly class IncoComposition implements Entity
/** @implements ReadModel<IncoCompositionId> */
final readonly class IncoComposition implements ReadModel
{
private function __construct(
public IncoCompositionId $id,

View file

@ -14,10 +14,10 @@ use Domain\Models\ValueObjects\Inco\Ingredients;
use Domain\Models\ValueObjects\Inco\Nutriscore;
use Domain\Models\ValueObjects\Inco\PortionAmount;
use Domain\Models\ValueObjects\Inco\StorageCondition;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\ReadModel;
/** @implements Entity<IncoId> */
final readonly class IncoSheet implements Entity
/** @implements ReadModel<IncoId> */
final readonly class IncoSheet implements ReadModel
{
/**
* @param array<\Domain\Models\ValueObjects\Inco\Allergen> $allergens

View file

@ -5,10 +5,10 @@ declare(strict_types=1);
namespace Domain\Models\Entities\Inco;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\ReadModel;
/** @implements Entity<IncoId> */
final readonly class IncoShortSheet implements Entity
/** @implements ReadModel<IncoId> */
final readonly class IncoShortSheet implements ReadModel
{
private function __construct(
public IncoId $id,

View file

@ -4,11 +4,20 @@ declare(strict_types=1);
namespace Domain\Models\ValueObjects\Brand;
final readonly class ArcadiaBrand
use Shared\DDD\Model\ReadModel;
/** @implements ReadModel<ArcadiaId> */
final readonly class ArcadiaBrand implements ReadModel
{
public function __construct(
public ArcadiaId $id,
public BrandName $name,
) {
}
#[\Override]
public function id(): ArcadiaId
{
return $this->id;
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Shared\DDD\Model;
/**
* @template T of EntityId
*
* @extends Entity<T>
*/
interface AggregateRoot extends Entity
{
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Shared\DDD\Model;
/**
* Interface pour les modèles de lecture et projections en lecture seule.
*
* Par définition, un modèle de lecture n'est pas modifiable.
* Toute implémentation doit être `final readonly class`.
*
* @template T of EntityId
*
* @extends Entity<T>
*/
interface ReadModel extends Entity
{
}

View file

@ -5,10 +5,10 @@ declare(strict_types=1);
namespace Shared\DDD\Port;
/**
* @template TId
* @template TEntity
* @template TId of \Shared\DDD\Model\EntityId
* @template TReadModel of \Shared\DDD\Model\ReadModel
*
* @method TEntity|null find(TId $id)
* @method TReadModel|null find(TId $id)
*/
interface Directory
{

View file

@ -6,11 +6,11 @@ namespace Shared\DDD\Port;
/**
* @template TId of \Shared\DDD\Model\EntityId
* @template TEntity of \Shared\DDD\Model\Entity
* @template TAggregate of \Shared\DDD\Model\AggregateRoot
*
* @method TEntity|null find(TId $id)
* @method void persist(TEntity $entity)
* @method void delete(TEntity $entity)
* @method TAggregate|null find(TId $id)
* @method void persist(TAggregate $aggregate)
* @method void delete(TAggregate $aggregate)
*
* @throws \Shared\DDD\Exception\NotFoundException
*/

View file

@ -4,7 +4,9 @@ declare(strict_types=1);
namespace Tests\Architecture;
use Shared\DDD\Model\AggregateRoot;
use Shared\DDD\Model\Entity;
use Shared\DDD\Model\ReadModel;
use Shared\DDD\Port\Directory;
use Shared\DDD\Port\Repository;
@ -93,3 +95,22 @@ describe('Domain Architecture Rules', function (): void {
'view',
]);
});
describe('AggregateRoot & ReadModel Architecture Rules', function (): void {
arch('AggregateRoot implementations are final')
->expect(AggregateRoot::class)
->classes()
->toBeFinal();
arch('Domain AggregateRoot implementations are readonly')
->expect('Domain\Models\Entities')
->classes()
->toBeReadonly();
arch('ReadModel implementations are final and readonly')
->expect(ReadModel::class)
->classes()
->toBeFinal()
->toBeReadonly()
->ignoring(ReadModel::class);
});