diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index ab7b09e..48eb309 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -26,6 +26,13 @@ doctrine: identity_generation_preferences: Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity auto_mapping: true + # Mapping contrat DDD → classe concrete. Permet au module Core de + # referencer `SiteInterface` dans ses ORM mappings (User) sans importer + # la classe concrete du module Sites. Pattern officiel Doctrine pour + # les bounded contexts, remplace l'ancien import direct + # `App\Module\Sites\Domain\Entity\Site` dans User.php. + resolve_target_entities: + App\Shared\Domain\Contract\SiteInterface: App\Module\Sites\Domain\Entity\Site mappings: Core: type: attribute diff --git a/config/services.yaml b/config/services.yaml index 0bb0ab0..fa6c942 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -28,5 +28,8 @@ services: App\Module\Sites\Domain\Repository\SiteRepositoryInterface: alias: App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository + App\Shared\Domain\Contract\SiteProviderInterface: + alias: App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository + App\Module\Sites\Application\Service\CurrentSiteProviderInterface: alias: App\Module\Sites\Application\Service\CurrentSiteProvider diff --git a/src/Module/Core/Domain/Entity/User.php b/src/Module/Core/Domain/Entity/User.php index 5733e21..00e6d50 100644 --- a/src/Module/Core/Domain/Entity/User.php +++ b/src/Module/Core/Domain/Entity/User.php @@ -15,12 +15,11 @@ use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\UserProcessor; use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\UserRbacProcessor; use App\Module\Core\Infrastructure\ApiPlatform\State\Provider\MeProvider; use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository; -// Note architecture : User.php utilise SiteInterface (Shared) pour les -// type-hints afin de ne pas coupler le module Core au module Sites. -// La seule reference concrete a Site subsiste dans les metadonnees ORM -// (targetEntity) via FQCN string, ce qui est obligatoire pour Doctrine. -// SiteNotAuthorizedException est importee depuis Shared (sa location canonique). -use App\Module\Sites\Domain\Entity\Site; +// Note architecture : User.php n'importe plus rien depuis le module Sites. +// Les type-hints utilisent SiteInterface (Shared/Contract) et le mapping ORM +// pointe vers la meme interface, resolue vers la classe concrete Site au boot +// via `doctrine.orm.resolve_target_entities` (cf. config/packages/doctrine.yaml). +// C'est le pattern officiel Doctrine pour les bounded contexts DDD. use App\Shared\Domain\Attribute\Auditable; use App\Shared\Domain\Attribute\AuditIgnore; use App\Shared\Domain\Contract\SiteInterface; @@ -139,10 +138,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface * Le groupe `user:list` a ete retire deliberement (securite : evite * de fuiter la liste des sites de chaque user via GET /api/users). * - * @var Collection + * @var Collection */ - #[ORM\ManyToMany(targetEntity: 'App\Module\Sites\Domain\Entity\Site', inversedBy: 'users', fetch: 'LAZY')] + #[ORM\ManyToMany(targetEntity: SiteInterface::class, inversedBy: 'users', fetch: 'LAZY')] #[ORM\JoinTable(name: 'user_site')] + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\InverseJoinColumn(name: 'site_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[Groups(['me:read', 'user:rbac:read', 'user:rbac:write'])] private Collection $sites; @@ -162,7 +163,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface * le prechargement pour /api/me. Le groupe `user:list` a ete retire * deliberement (securite : evite de fuiter le site actif via /api/users). */ - #[ORM\ManyToOne(targetEntity: 'App\Module\Sites\Domain\Entity\Site', fetch: 'LAZY')] + #[ORM\ManyToOne(targetEntity: SiteInterface::class, fetch: 'LAZY')] #[ORM\JoinColumn(name: 'current_site_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] #[Groups(['me:read'])] private ?SiteInterface $currentSite = null; @@ -378,7 +379,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface } /** - * @return Collection + * @return Collection */ public function getSites(): Collection { @@ -392,7 +393,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface * session Doctrine (cf. ticket 2 review point #1). * * Le parametre est type SiteInterface pour eviter le couplage Core → Sites. - * En pratique seule App\Module\Sites\Domain\Entity\Site est passee ici. + * La classe concrete injectee au runtime est resolue par Doctrine via + * `resolve_target_entities` (cf. note architecture en tete de fichier). */ public function addSite(SiteInterface $site): static { diff --git a/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php b/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php index 1c2be08..dbd31f5 100644 --- a/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php +++ b/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php @@ -8,9 +8,9 @@ use App\Module\Core\Domain\Entity\Role; use App\Module\Core\Domain\Entity\User; use App\Module\Core\Domain\Repository\RoleRepositoryInterface; use App\Module\Core\Domain\Security\SystemRoles; -use App\Module\Sites\Domain\Entity\Site; -use App\Module\Sites\Domain\Repository\SiteRepositoryInterface; use App\Module\Sites\Infrastructure\DataFixtures\SitesFixtures; +use App\Shared\Domain\Contract\SiteInterface; +use App\Shared\Domain\Contract\SiteProviderInterface; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\Persistence\ObjectManager; @@ -39,7 +39,7 @@ class AppFixtures extends Fixture implements DependentFixtureInterface public function __construct( private readonly UserPasswordHasherInterface $passwordHasher, private readonly RoleRepositoryInterface $roleRepository, - private readonly SiteRepositoryInterface $siteRepository, + private readonly SiteProviderInterface $siteProvider, ) {} /** @@ -135,9 +135,9 @@ class AppFixtures extends Fixture implements DependentFixtureInterface return $role; } - private function requireSite(string $name): Site + private function requireSite(string $name): SiteInterface { - $site = $this->siteRepository->findByName($name); + $site = $this->siteProvider->findByName($name); if (null === $site) { throw new RuntimeException(sprintf( diff --git a/src/Module/Sites/Domain/Repository/SiteRepositoryInterface.php b/src/Module/Sites/Domain/Repository/SiteRepositoryInterface.php index bdf6251..d44fd5f 100644 --- a/src/Module/Sites/Domain/Repository/SiteRepositoryInterface.php +++ b/src/Module/Sites/Domain/Repository/SiteRepositoryInterface.php @@ -5,8 +5,9 @@ declare(strict_types=1); namespace App\Module\Sites\Domain\Repository; use App\Module\Sites\Domain\Entity\Site; +use App\Shared\Domain\Contract\SiteProviderInterface; -interface SiteRepositoryInterface +interface SiteRepositoryInterface extends SiteProviderInterface { public function findById(int $id): ?Site; diff --git a/src/Shared/Domain/Contract/SiteProviderInterface.php b/src/Shared/Domain/Contract/SiteProviderInterface.php new file mode 100644 index 0000000..011498d --- /dev/null +++ b/src/Shared/Domain/Contract/SiteProviderInterface.php @@ -0,0 +1,21 @@ +