| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Co-authored-by: Matthieu <mtholot19@gmail.com> Reviewed-on: #8 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
27 KiB
Ticket #01 — 1/4 — Brique fondatrice du module Sites (Backend)
1. Objectif
Ce ticket livre la couche de donnees du module optionnel Sites. Il cree le bounded context, declare le module a Symfony, enregistre ses permissions RBAC, installe la table site en base et seed trois etablissements de demonstration utilises par les tickets suivants.
Le resultat attendu est un socle de persistance activable par tenant via config/modules.php, sans UI, sans API publique, sans couplage au module Core, et sur lequel les tickets 2/3/4 pourront greffer : rattachement utilisateurs, selecteur de site dans la navbar, administration CRUD.
2. Périmètre
IN
- Creer le module
/home/m-tristan/workspace/Coltura/src/Module/Sites/SitesModule.phpavecID = 'sites',LABEL = 'Sites',REQUIRED = false, et une methode statiquepermissions()declarant les deux codes RBACsites.viewetsites.manage. - Creer l'entite Doctrine
Siteavecid,name(unique),city,postalCode,color,fullAddress,createdAt,updatedAtet les contraintes de validation applicatives associees (NotBlank, Length, Regex hex#RRGGBB, Regex CP FR^\d{5}$, UniqueEntity). - Creer l'interface
SiteRepositoryInterfaceet son implementation DoctrineDoctrineSiteRepository, avec un contrat CRUD complet (findById,findByName,findAllOrderedByName,save,remove) en anticipation du ticket 2. - Creer une migration Doctrine creant la table
siteavec son index uniqueuniq_site_name. La migration est placee dans/home/m-tristan/workspace/Coltura/migrations/au namespace racineDoctrineMigrationsconformement a l'exception documentee dansCLAUDE.md(bug de tri alphabetique des migrations multi-namespaces dans Doctrine Migrations 3.x). - Creer
SitesFixturescreant trois sites de demonstration :Chatellerault(#056CF2),Saint-Jean(#10B981),Pommevic(#F59E0B). Fixtures idempotentes via lookup par nom lorsque le purger Doctrine est desactive. - Enregistrer
SitesModule::classdans/home/m-tristan/workspace/Coltura/config/modules.phppour l'activer par defaut. - Declarer le mapping Doctrine du module dans
/home/m-tristan/workspace/Coltura/config/packages/doctrine.yaml(inconditionnel, le mapping reste charge meme si le module est retire demodules.php). - Enregistrer l'alias service
SiteRepositoryInterface → DoctrineSiteRepositorydans/home/m-tristan/workspace/Coltura/config/services.yaml. - Ajouter deux suites de tests PHPUnit :
SiteTest(pureTestCase) pour le comportement de l'entite (constructeur, getters/setters, lifecyclePreUpdate).SiteValidationTest(KernelTestCase) pour la validation complete : regex hex, regex CP FR, NotBlank, Length, UniqueEntity via Doctrine.
OUT
- Ticket
#02: relationUser ↔ Site(FK ou ManyToMany selon decision UX), expose les sites de l'utilisateur courant via/api/meet propage l'autorisation au niveau des ressources decoupees par site. - Ticket
#03: integration dans la navbar Coltura (selecteur de site actif, persistance du choix cote front, consommation du flux issu du ticket 2). - Ticket
#04: ecran d'administration CRUD des sites (page admin/sites, DataTable, drawer creation/edition, modale suppression, API PlatformSiteresource avec voters RBAC). - Gestion des soft-deletes sur
Site: non introduite dans ce ticket. - Rattachement historique ou audit trail des modifications : hors scope.
3. Fichiers à créer
Domaine — Entité
/home/m-tristan/workspace/Coltura/src/Module/Sites/Domain/Entity/Site.php: entite Doctrine porteuse des attributs metier (nom unique, ville, code postal FR, couleur hex, adresse complete multi-ligne) et des timestamps auto-maintenus via lifecycle callbacks.
Domaine — Repository
/home/m-tristan/workspace/Coltura/src/Module/Sites/Domain/Repository/SiteRepositoryInterface.php: contrat d'acces domaine a l'entite Site (CRUD applicatif ; l'acces API Platform du ticket 4 utilisera le provider Doctrine par defaut).
Infrastructure — Doctrine
/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/Doctrine/DoctrineSiteRepository.php: implementation Doctrine deSiteRepositoryInterfacebasee surServiceEntityRepository.
Infrastructure — Migration
/home/m-tristan/workspace/Coltura/migrations/Version<timestamp>.php: migration racine (namespaceDoctrineMigrations) qui cree la tablesiteet son index unique. Emplacement racine et non modulaire, cf. exception documentee dansCLAUDE.md(bug Doctrine 3.x sur le tri alphabetique des migrations multi-namespaces).
Infrastructure — DataFixtures
/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/DataFixtures/SitesFixtures.php: fixture Doctrine seedant les 3 sites de demonstration. Ne declare pas deDependentFixtureInterface(aucune dependance a AppFixtures dans ce ticket).
Module — Declaration
/home/m-tristan/workspace/Coltura/src/Module/Sites/SitesModule.php: marker class du module avecID,LABEL,REQUIREDetpermissions(). Meme pattern queCoreModule.
Tests
/home/m-tristan/workspace/Coltura/tests/Module/Sites/Domain/Entity/SiteTest.php: tests unitaires purs (TestCase) couvrant constructeur, getters, setters et lifecyclePreUpdate./home/m-tristan/workspace/Coltura/tests/Module/Sites/Domain/Entity/SiteValidationTest.php: tests de validation (KernelTestCase) couvrant regex hex, regex CP FR, NotBlank, Length sur tous les champs, etUniqueEntityvia la DB de test.
4. Fichiers à modifier
/home/m-tristan/workspace/Coltura/config/modules.php: ajouterApp\Module\Sites\SitesModule::classdans le tableau de retour. Le module est actif par defaut. Le commenter suffit a le desactiver sans autre intervention (les permissions deviendront orphelines a la prochaine sync mais la table reste)./home/m-tristan/workspace/Coltura/config/packages/doctrine.yaml: ajouter une mappingSites:alignee sur le pattern du moduleCore:. Le mapping est inconditionnel : il reste declare meme siSitesModule::classest retire demodules.php. Le commentaire doit etre explicite sur cette decoupe (activation fonctionnelle viamodules.php, structure DB via la mapping Doctrine)./home/m-tristan/workspace/Coltura/config/services.yaml: ajouter l'aliasApp\Module\Sites\Domain\Repository\SiteRepositoryInterface→App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository. Pattern aligne sur les trois aliases Core existants.
5. Schéma cible — mapping Doctrine
Comme pour le ticket RBAC (ticket-343), le schema est decrit par les attributs Doctrine plutot que par le SQL brut. Le fichier de migration contient le SQL final (section 6).
Conventions respectées
declare(strict_types=1)en tete de tous les fichiers PHP.- Identifiants de classe et proprietes en anglais, commentaires en francais (cf.
CLAUDE.md). - PostgreSQL : noms de colonnes en snake_case minuscules, Doctrine les deduit des proprietes camelCase (
postalCode→postal_code,fullAddress→full_address,createdAt→created_at,updatedAt→updated_at). - Le nom de table
siten'est pas un mot reserve PostgreSQL : pas de backtick necessaire.
Entité Site
#[ORM\Entity(repositoryClass: DoctrineSiteRepository::class)]
#[ORM\Table(name: 'site')]
#[ORM\UniqueConstraint(name: 'uniq_site_name', columns: ['name'])]
#[ORM\HasLifecycleCallbacks]
#[UniqueEntity(fields: ['name'], message: 'Un site avec ce nom existe deja.')]
class Site
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100)]
#[Assert\NotBlank(message: 'Le nom du site est requis.')]
#[Assert\Length(max: 100, ...)]
private string $name;
#[ORM\Column(length: 100)]
#[Assert\NotBlank(message: 'La ville du site est requise.')]
#[Assert\Length(max: 100, ...)]
private string $city;
#[ORM\Column(name: 'postal_code', length: 10)]
#[Assert\NotBlank(message: 'Le code postal est requis.')]
#[Assert\Length(max: 10, ...)]
#[Assert\Regex(pattern: '/^\d{5}$/', message: '...')]
private string $postalCode;
#[ORM\Column(length: 7)]
#[Assert\NotBlank(message: 'La couleur est requise.')]
#[Assert\Regex(pattern: '/^#[0-9A-Fa-f]{6}$/', message: '...')]
private string $color;
#[ORM\Column(name: 'full_address', type: Types::TEXT)]
#[Assert\NotBlank(message: 'L\'adresse complete est requise.')]
#[Assert\Length(max: 500, ...)]
private string $fullAddress;
#[ORM\Column(name: 'created_at', type: Types::DATETIME_IMMUTABLE)]
private DateTimeImmutable $createdAt;
#[ORM\Column(name: 'updated_at', type: Types::DATETIME_IMMUTABLE)]
private DateTimeImmutable $updatedAt;
}
Contraintes fonctionnelles :
nameest unique en base (uniq_site_name) et porte egalement la contrainte applicativeUniqueEntitypour que le validator remonte une violation lisible avant d'atteindre la violation DB.colorest contraint par regex a un code hex strict de 7 caracteres#RRGGBB, majuscules ou minuscules. La colonneVARCHAR(7)est dimensionnee au plus juste car la regex est exhaustive.postalCodeest contraint a 5 chiffres exacts via regex (format FR). La colonneVARCHAR(10)est volontairement plus large que la regex pour laisser marge si le projet etend plus tard la regex a d'autres formats (UK, PT, ...). Choix assume : evite une migration DDL au ticket suivant, cout DB negligeable sur un champ court.fullAddressest de typeTEXT(PostgreSQL) pour permettre une adresse multi-ligne, mais borne cote applicatif a 500 caracteres viaAssert\Length(max: 500)comme garde DoS basique (une adresse FR complete tient largement dans cette enveloppe).createdAtest seede dans le constructeur et ne change plus jamais apres persistance.updatedAtest seede dans le constructeur a la meme valeur quecreatedAt, puis refresh a chaque update via le callback#[ORM\PreUpdate].
Mapping Doctrine — doctrine.yaml
# Mapping inconditionnelle du module Sites : la structure DB existe meme
# si SitesModule::class est retire de config/modules.php. L'activation
# fonctionnelle (ex: exposition des permissions, futurs endpoints API)
# passe exclusivement par config/modules.php.
Sites:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Module/Sites/Domain/Entity'
prefix: 'App\Module\Sites\Domain\Entity'
alias: Sites
6. Plan de migration Doctrine
La migration est placee dans /home/m-tristan/workspace/Coltura/migrations/Version<timestamp>.php au namespace racine DoctrineMigrations, conformement a l'exception documentee dans CLAUDE.md. Tant que le bug de tri alphabetique des MigrationsComparator multi-namespaces n'est pas resolu (via un comparator custom ou un upgrade Doctrine), toute migration d'initialisation (creation de table sur base vide) reste au namespace racine.
up() — ordre des instructions
- Creer la table
siteavec toutes les colonnes NOT NULL :id INT GENERATED BY DEFAULT AS IDENTITY NOT NULLname VARCHAR(100) NOT NULLcity VARCHAR(100) NOT NULLpostal_code VARCHAR(10) NOT NULLcolor VARCHAR(7) NOT NULLfull_address TEXT NOT NULLcreated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULLupdated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULLPRIMARY KEY (id)
- Creer l'index unique
uniq_site_namesursite(name)pour garantir l'invariant metier "un site porte un nom unique" au niveau DB. Le validator applicatifUniqueEntitys'appuie dessus en lecture avant qu'une tentative d'insertion concurrente ne remonte la violation DB.
down() — rollback
DROP TABLE site. Aucune FK n'existe depuis ou vers cette table dans ce ticket ; le rollback est donc trivial et safe.
Precision timestamp
PostgreSQL TIMESTAMP(0) WITHOUT TIME ZONE stocke a la seconde pres. Les DateTimeImmutable PHP portent une precision microseconde mais perdent cette precision au round-trip DB. Les tests unitaires de lifecycle doivent en tenir compte (cf. section 10 — usage de reflection plutot qu'un sleep).
7. Intégration avec sync-permissions
Le ticket ne modifie pas SyncPermissionsCommand. Il exploite l'algorithme existant (cf. ticket-343 section 7) en declarant SitesModule::permissions() dans un format strictement conforme au contrat attendu par la commande :
public static function permissions(): array
{
return [
['code' => 'sites.view', 'label' => 'Voir les sites'],
['code' => 'sites.manage', 'label' => 'Gerer les sites (creer, editer, supprimer)'],
];
}
Regles de validation appliquees par SyncPermissionsCommand :
- Chaque entree doit contenir exactement les cles
codeetlabel. - Le prefixe du code doit correspondre a
SitesModule::ID . '.', soitsites.. - Ni
codenilabelne peuvent etre une chaine vide.
Comportement a attendre :
- Apres
php bin/console app:sync-permissions, les deux lignessites.viewetsites.managesont presentes dans la tablepermissionavecmodule = 'sites'etorphan = false. - Si
SitesModule::classest retire deconfig/modules.phpet la commande relancee, les deux lignes sont marqueesorphan = true(non supprimees, pour preserver les assignations). Reactiver le module les remet aorphan = false. - La cle
modulen'est pas presente dans le payload : elle est auto-injectee par la commande depuisSitesModule::ID.
Granularité des permissions
sites.manage est une permission composite couvrant creation, edition et suppression. Ce choix reste simple pour un ticket fondateur, mais le ticket 4 (administration CRUD) devra arbitrer si une granularite plus fine (sites.create, sites.edit, sites.delete) est necessaire pour les besoins UX. Si oui, la migration de permissions se fera naturellement via la commande de sync : ajouter les trois codes dans permissions(), retirer sites.manage → la sync marque l'ancien orphelin sans casser les roles deja existants.
8. Méthodes clés détaillées
Site::__construct
Le constructeur prend les cinq champs metier obligatoires et positionne les deux timestamps a la meme valeur :
public function __construct(
string $name,
string $city,
string $postalCode,
string $color,
string $fullAddress,
) {
$this->name = $name;
$this->city = $city;
$this->postalCode = $postalCode;
$this->color = $color;
$this->fullAddress = $fullAddress;
$now = new DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
}
Justification :
- Tous les champs sont passes au constructeur pour forcer l'invariant "un Site instancie est toujours complet". L'alternative (setters post-new) autoriserait des etats transitoires invalides.
createdAtetupdatedAtpartagent la meme valeur a l'instanciation, ce qui garantitupdated_at >= created_atau niveau base. Le premier appel aonPreUpdate()fera avancer uniquementupdatedAt.
Site::onPreUpdate
#[ORM\PreUpdate]
public function onPreUpdate(): void
{
$this->updatedAt = new DateTimeImmutable();
}
Justification :
- Callback Doctrine declenche uniquement quand Doctrine detecte au moins un changement sur l'entite en session de persistance. Pas de risque de tick silencieux sur un find pur.
createdAtn'est volontairement jamais touche ici : il est immuable apres persistance.- Pas de
#[ORM\PrePersist]: le constructeur gere deja l'initialisation, inutile de dupliquer la logique dans un callback qui pourrait etre appele a vide.
SitesFixtures::ensureSite
private function ensureSite(
ObjectManager $manager,
string $name,
string $city,
string $postalCode,
string $color,
string $fullAddress,
): Site {
$site = $this->siteRepository->findByName($name);
if (null === $site) {
$site = new Site($name, $city, $postalCode, $color, $fullAddress);
$manager->persist($site);
return $site;
}
$site->setCity($city);
$site->setPostalCode($postalCode);
$site->setColor($color);
$site->setFullAddress($fullAddress);
return $site;
}
Contrat honnete sur l'idempotence (cf. docblock en tete de fixture) :
- Supporte : lookup par nom avec purger Doctrine actif (cas nominal de
doctrine:fixtures:load). - Supporte : lookup par nom hors purger si la fixture est rejouee telle quelle sur une base deja seedee → les autres champs sont re-alignes sur les valeurs de reference.
- Non supporte : chargement cumulatif apres qu'une autre fixture ait
persist(sansflush) des Site dans la meme session →findByNameviafindOneByn'inspecte pas l'unit-of-work et peut creer un doublon. - Non supporte : renommage d'un site dans la fixture → le lookup par
namerate, un nouveau site est cree, l'ancien reste en base si le purger est desactive.
9. Fixtures Sites
Trois sites de demonstration, avec des couleurs distinctes suffisamment contrastees pour un futur affichage visuel (ticket 3 — navbar) :
| Nom | Ville | CP | Couleur | Commentaire |
|---|---|---|---|---|
| Chatellerault | Chatellerault | 86100 | #056CF2 |
Couleur imposee par le ticket (bleu Coltura). |
| Saint-Jean | Saint-Jean-de-Sauves | 86330 | #10B981 |
Vert emeraude (contraste avec le bleu). |
| Pommevic | Pommevic | 82400 | #F59E0B |
Ambre (troisieme teinte nettement distincte). |
Les adresses completes sont des chaines multi-lignes (voie + CP/ville), cas nominal d'exploitation du type TEXT sur full_address.
Ordre d'execution global des fixtures
SitesFixtures est une Fixture sans dependance : elle peut s'executer dans n'importe quel ordre relatif aux autres fixtures Core (AppFixtures). Aucune FK inter-modules dans ce ticket.
Le ticket 2 introduira probablement une relation User ↔ Site ; SitesFixtures devra alors etre declare comme dependance de AppFixtures (ou inversement, selon la direction de la FK) via DependentFixtureInterface::getDependencies().
10. Plan de tests PHPUnit
Deux suites separees, motivation :
SiteTestreste enTestCasepur (pas de kernel) pour tester le comportement mecanique de l'entite — rapide, zero dependance DB.SiteValidationTestutiliseKernelTestCasepour avoir acces au validator applicatif, indispensable pour testerUniqueEntitydont le validator est backed par Doctrine et necessite donc unManagerRegistryreel.
SiteTest — tests unitaires purs
testConstructorInitialState: verifie que le constructeur positionne correctement les 5 champs metier et les deux timestamps (DateTimeImmutable).testCreatedAtAndUpdatedAtAreInitiallyEqual: verifie l'invariant "a l'instanciation,createdAt == updatedAt".testOnPreUpdateAdvancesUpdatedAtOnly: utiliseReflectionpour forcerupdatedAta une valeur anterieure (-1 hour), appelleonPreUpdate(), et verifie queupdatedAtavance strictement mais quecreatedAtreste immuable.- Justification reflection : eviter un
sleep/usleepflaky en CI et lent.
- Justification reflection : eviter un
testSettersMutateFields: verifie que les setters publics modifient correctement les champs metier.
SiteValidationTest — tests d'integration validator
Bootstrap : self::bootKernel() dans setUp(), recuperation de ValidatorInterface et EntityManagerInterface depuis le container.
Tests de validation scalaire (via DataProvider PHPUnit 12+, attribut #[DataProvider]) :
testValidSitePassesValidation: un Site correct passe sans violation.testColorMustBeHexRrggbb/testValidColorsAreAccepted: jeu de donnees invalide (red,#FFF,FFFFFF,rgb(...),#1234567,#12345G,"") vs valide (#ABCDEF,#abcdef,#0a1B2c,#000000,#FFFFFF).testPostalCodeMustMatchFrFormat/testValidPostalCodesAreAccepted: jeu de donnees invalide (1234,123456,8610A,86-100,"",86 100) vs valide (86100,75001,97100,20000).testBlankNameIsRejected,testBlankCityIsRejected,testBlankFullAddressIsRejected:NotBlanksur chaque champ obligatoire.testNameLongerThan100CharsIsRejected,testCityLongerThan100CharsIsRejected:Length(max: 100).
Test d'unicite :
6. testDuplicateNameIsRejected : auto-suffisant — persiste lui-meme un site porteur d'un nom unique (Test-Duplicate-<uniqid>), flush, tente de valider un second Site avec le meme nom, verifie qu'au moins une violation porte UniqueEntity::NOT_UNIQUE_ERROR sur la property name, puis supprime le site en finally.
- Justification : pas de dependance aux fixtures (robustesse, pas de couplage sur
Chatellerault). Assertion precise sur lecodede violation +propertyPath, pas sur le message (resistant aux traductions).
Pattern finally pour cleanup
try {
$duplicate = new Site($name, ...);
$violations = $this->validator->validate($duplicate);
// assertions...
} finally {
$this->em->remove($original);
$this->em->flush();
}
Garantit le cleanup meme si une assertion rate, sans dependre d'une transaction globale de test.
11. Risques et points d'attention
Risque 1 — Mapping Doctrine inconditionnel
Le mapping Sites: est declare dans doctrine.yaml sans dependance a config/modules.php. Consequence : retirer SitesModule::class de modules.php ne desactive pas le mapping Doctrine ni la table site.
Decision assumee et alignee avec le traitement du module Core :
- La structure DB est "toujours la" (migrations jouees inconditionnellement).
- L'activation fonctionnelle (exposition des permissions, futurs endpoints) passe exclusivement par
modules.php.
Cela doit etre explicite dans doctrine.yaml via un commentaire en tete du bloc Sites: pour eviter qu'un futur reviewer n'interprete le mapping comme un oubli.
Risque 2 — Migration racine vs migration modulaire
La migration est placee dans migrations/ et non dans src/Module/Sites/Infrastructure/Doctrine/Migrations/. C'est une exception documentee dans CLAUDE.md et dans le docblock de la migration elle-meme, motivee par un bug de tri alphabetique des MigrationsComparator en Doctrine Migrations 3.x lorsque plusieurs migrations_paths sont declares.
Consequence pour les tickets futurs :
- Tant que le bug n'est pas resolu, toute nouvelle migration d'initialisation (creation de table sur base vide) continuera d'aller au namespace racine.
- Les migrations applicatives (ajout de colonne, backfill) qui supposent un schema deja en place peuvent vivre dans le namespace modulaire, comme prevu.
- Une fois le bug resolu (comparator custom ou upgrade Doctrine), migrer les fichiers vers le namespace modulaire sera un simple
git mv+ ajustement du namespace PHP.
Risque 3 — Idempotence des fixtures non cumulative
Le docblock de SitesFixtures declare explicitement les cas d'idempotence supportes et non supportes (cf. section 8). Ne pas promettre une robustesse que le pattern ne tient pas : si un futur ticket introduit une fixture persistant des Site avant SitesFixtures sans flush intermediaire, un doublon peut apparaitre. Le contrat ecrit permet au reviewer de ce futur ticket de reagir.
Risque 4 — Regex couleur non normalisee
La regex /^#[0-9A-Fa-f]{6}$/ accepte majuscules et minuscules. Les fixtures utilisent des majuscules ; si l'UI du ticket 4 permet de saisir en minuscules, deux couleurs "visuellement identiques" pourront coexister en base avec casse differente, cassant toute comparaison naive ($a->color === $b->color). A decider au ticket 4 : normaliser en uppercase a la persistance, ou assumer le stockage tel quel et normaliser uniquement a la comparaison.
Risque 5 — Precision timestamp PostgreSQL TIMESTAMP(0)
PostgreSQL TIMESTAMP(0) ecrete a la seconde pres. Deux updates espaces de moins d'une seconde produisent le meme updated_at en base. Pas un probleme pour les cas d'usage metier de ce ticket (edition manuelle), mais a garder en tete si un ticket futur introduit un updatedAt comme cle de tri ou de detection de version optimiste.
12. Ordre d'exécution recommandé
- Exploration — Lire le module Core (
CoreModule.php,User.php,Role.php) pour aligner le style. - Module declaration — Creer
SitesModule.phpavecpermissions(). - Entite — Creer
Site.phpavec tous les attributs Doctrine et contraintes de validation. - Repository — Creer
SiteRepositoryInterface.phppuisDoctrineSiteRepository.php. - Configuration — Enregistrer le mapping dans
doctrine.yaml, l'alias dansservices.yaml, le module dansmodules.php. - Migration — Generer le fichier de migration (manuellement ou via
doctrine:migrations:diffpuis ajuster), jouermake migration-migrate. - Fixtures — Creer
SitesFixtures.php, jouermake fixturespuismake sync-permissions. - Tests unitaires — Ecrire
SiteTest.php(TestCase pur). - Tests validation — Ecrire
SiteValidationTest.php(KernelTestCase). - Validation DoD —
make test-db-setup && make test(doit passer 148/148), verifier que designer SitesModule ne casse rien. - CS fixer —
make php-cs-fixer-allow-risky FILES="src/Module/Sites tests/Module/Sites migrations/Version<timestamp>.php config/...".
13. Critères d'acceptation (DoD)
SitesModule.phpexiste et declare exactement 2 permissions (sites.view,sites.manage) prefixeessites.conformement au format attendu parSyncPermissionsCommand.SitesModule::classest enregistre dansconfig/modules.phpet active par defaut.- Entite
Sitecreee avec tous les champs, contraintes de validation (NotBlank,Length,Regex hex,Regex CP FR,UniqueEntity) et timestamps auto. SiteRepositoryInterfaceexpose au minimumfindById,findByName,findAllOrderedByName,save,remove;DoctrineSiteRepositoryl'implemente.- La migration existe dans
migrations/(namespaceDoctrineMigrations), cree la tablesiteet l'index uniqueuniq_site_name, est jouable viamake migration-migrate. SitesFixturescree les 3 sites avec couleurs distinctes et docblock honnete sur son idempotence.make fixturescharge les 3 sites sans erreur et est rejouable apres purge.- Apres
app:sync-permissions, la tablepermissioncontientsites.viewetsites.manageavecmodule = 'sites'etorphan = false. - Le mapping
Sites:est declare dansdoctrine.yamlavec un commentaire explicite sur son caractere inconditionnel. - L'alias
SiteRepositoryInterface → DoctrineSiteRepositoryest declare dansservices.yaml. make testpasse 148/148 tests avecSitesModule::classactive.make testpasse 148/148 tests avecSitesModule::classcommente dansconfig/modules.php.make php-cs-fixer-allow-riskyne signale aucune correction sur les fichiers du ticket.- Aucun import direct depuis
src/Module/Core/...verssrc/Module/Sites/...ni l'inverse (independance des bounded contexts).