refactor(core) : isole Core du module Sites via SiteProviderInterface + resolve_target_entities
Supprime les imports directs de App\Module\Sites\* depuis le module Core : - SiteProviderInterface (Shared/Contract) : contrat minimal findByName(), etendu par SiteRepositoryInterface pour reutilisation DI. - AppFixtures : injecte SiteProviderInterface au lieu de SiteRepositoryInterface. - User.php : targetEntity des mappings ORM pointe desormais sur SiteInterface, resolu a la classe concrete via doctrine.orm.resolve_target_entities (pattern officiel Doctrine pour les bounded contexts DDD). - JoinColumn/InverseJoinColumn explicites sur la ManyToMany user_site pour forcer les noms de colonnes (sinon Doctrine derive site_interface_id). Respecte la regle CLAUDE.md "jamais d'import direct entre modules" — il reste l'exception SitesFixtures::class dans getDependencies() (contrainte d'API DependentFixtureInterface, meme registre que resolve_target_entities). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,13 @@ doctrine:
|
|||||||
identity_generation_preferences:
|
identity_generation_preferences:
|
||||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||||
auto_mapping: true
|
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:
|
mappings:
|
||||||
Core:
|
Core:
|
||||||
type: attribute
|
type: attribute
|
||||||
|
|||||||
@@ -28,5 +28,8 @@ services:
|
|||||||
App\Module\Sites\Domain\Repository\SiteRepositoryInterface:
|
App\Module\Sites\Domain\Repository\SiteRepositoryInterface:
|
||||||
alias: App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository
|
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:
|
App\Module\Sites\Application\Service\CurrentSiteProviderInterface:
|
||||||
alias: App\Module\Sites\Application\Service\CurrentSiteProvider
|
alias: App\Module\Sites\Application\Service\CurrentSiteProvider
|
||||||
|
|||||||
@@ -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\Processor\UserRbacProcessor;
|
||||||
use App\Module\Core\Infrastructure\ApiPlatform\State\Provider\MeProvider;
|
use App\Module\Core\Infrastructure\ApiPlatform\State\Provider\MeProvider;
|
||||||
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
|
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
|
||||||
// Note architecture : User.php utilise SiteInterface (Shared) pour les
|
// Note architecture : User.php n'importe plus rien depuis le module Sites.
|
||||||
// type-hints afin de ne pas coupler le module Core au module Sites.
|
// Les type-hints utilisent SiteInterface (Shared/Contract) et le mapping ORM
|
||||||
// La seule reference concrete a Site subsiste dans les metadonnees ORM
|
// pointe vers la meme interface, resolue vers la classe concrete Site au boot
|
||||||
// (targetEntity) via FQCN string, ce qui est obligatoire pour Doctrine.
|
// via `doctrine.orm.resolve_target_entities` (cf. config/packages/doctrine.yaml).
|
||||||
// SiteNotAuthorizedException est importee depuis Shared (sa location canonique).
|
// C'est le pattern officiel Doctrine pour les bounded contexts DDD.
|
||||||
use App\Module\Sites\Domain\Entity\Site;
|
|
||||||
use App\Shared\Domain\Attribute\Auditable;
|
use App\Shared\Domain\Attribute\Auditable;
|
||||||
use App\Shared\Domain\Attribute\AuditIgnore;
|
use App\Shared\Domain\Attribute\AuditIgnore;
|
||||||
use App\Shared\Domain\Contract\SiteInterface;
|
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
|
* Le groupe `user:list` a ete retire deliberement (securite : evite
|
||||||
* de fuiter la liste des sites de chaque user via GET /api/users).
|
* de fuiter la liste des sites de chaque user via GET /api/users).
|
||||||
*
|
*
|
||||||
* @var Collection<int, Site>
|
* @var Collection<int, SiteInterface>
|
||||||
*/
|
*/
|
||||||
#[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\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'])]
|
#[Groups(['me:read', 'user:rbac:read', 'user:rbac:write'])]
|
||||||
private Collection $sites;
|
private Collection $sites;
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
* le prechargement pour /api/me. Le groupe `user:list` a ete retire
|
* le prechargement pour /api/me. Le groupe `user:list` a ete retire
|
||||||
* deliberement (securite : evite de fuiter le site actif via /api/users).
|
* 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')]
|
#[ORM\JoinColumn(name: 'current_site_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||||
#[Groups(['me:read'])]
|
#[Groups(['me:read'])]
|
||||||
private ?SiteInterface $currentSite = null;
|
private ?SiteInterface $currentSite = null;
|
||||||
@@ -378,7 +379,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<int, Site>
|
* @return Collection<int, SiteInterface>
|
||||||
*/
|
*/
|
||||||
public function getSites(): Collection
|
public function getSites(): Collection
|
||||||
{
|
{
|
||||||
@@ -392,7 +393,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
* session Doctrine (cf. ticket 2 review point #1).
|
* session Doctrine (cf. ticket 2 review point #1).
|
||||||
*
|
*
|
||||||
* Le parametre est type SiteInterface pour eviter le couplage Core → Sites.
|
* 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
|
public function addSite(SiteInterface $site): static
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use App\Module\Core\Domain\Entity\Role;
|
|||||||
use App\Module\Core\Domain\Entity\User;
|
use App\Module\Core\Domain\Entity\User;
|
||||||
use App\Module\Core\Domain\Repository\RoleRepositoryInterface;
|
use App\Module\Core\Domain\Repository\RoleRepositoryInterface;
|
||||||
use App\Module\Core\Domain\Security\SystemRoles;
|
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\Module\Sites\Infrastructure\DataFixtures\SitesFixtures;
|
||||||
|
use App\Shared\Domain\Contract\SiteInterface;
|
||||||
|
use App\Shared\Domain\Contract\SiteProviderInterface;
|
||||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||||
use Doctrine\Persistence\ObjectManager;
|
use Doctrine\Persistence\ObjectManager;
|
||||||
@@ -39,7 +39,7 @@ class AppFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
private readonly RoleRepositoryInterface $roleRepository,
|
private readonly RoleRepositoryInterface $roleRepository,
|
||||||
private readonly SiteRepositoryInterface $siteRepository,
|
private readonly SiteProviderInterface $siteProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,9 +135,9 @@ class AppFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
return $role;
|
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) {
|
if (null === $site) {
|
||||||
throw new RuntimeException(sprintf(
|
throw new RuntimeException(sprintf(
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace App\Module\Sites\Domain\Repository;
|
namespace App\Module\Sites\Domain\Repository;
|
||||||
|
|
||||||
use App\Module\Sites\Domain\Entity\Site;
|
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;
|
public function findById(int $id): ?Site;
|
||||||
|
|
||||||
|
|||||||
21
src/Shared/Domain/Contract/SiteProviderInterface.php
Normal file
21
src/Shared/Domain/Contract/SiteProviderInterface.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Shared\Domain\Contract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrat minimal pour acceder a un site depuis un module qui n'est pas Sites.
|
||||||
|
*
|
||||||
|
* Permet a du code Core/Shared (commandes de seed, fixtures, etc.) de
|
||||||
|
* recuperer un Site par son nom sans importer directement depuis le module
|
||||||
|
* Sites — ce qui violerait la regle "jamais d'import direct entre modules"
|
||||||
|
* (cf. CLAUDE.md section "Regles d'architecture").
|
||||||
|
*
|
||||||
|
* Implementation concrete : App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository
|
||||||
|
* (via SiteRepositoryInterface qui etend ce contrat).
|
||||||
|
*/
|
||||||
|
interface SiteProviderInterface
|
||||||
|
{
|
||||||
|
public function findByName(string $name): ?SiteInterface;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user