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:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<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\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<int, Site>
|
||||
* @return Collection<int, SiteInterface>
|
||||
*/
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
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