> */ public static function comments(): array { return [ 'audit_log' => [ '_table' => "Journal d'audit append-only — trace toutes les modifications BDD sur entites annotees #[Auditable]. Lecture seule via API.", 'id' => "UUID v7 — identifiant de la ligne d'audit (genere en PHP, ordre temporel garanti).", 'entity_type' => "Type d'entite auditee au format module.Entity (ex: core.User, commercial.Client) — evite les collisions inter-modules.", 'entity_id' => "Identifiant de l'entite auditee (supporte INT et UUID — stocke en varchar pour rester generique).", 'action' => "Type d'operation auditee : 'create', 'update' ou 'delete'.", 'changes' => 'Snapshot complet pour create/delete, diff {champ: {old, new}} pour update. Cles sensibles filtrees (password, token, secret).', 'performed_by' => "Username de l'auteur de l'action (denormalise, survit a la suppression du user) — vaut 'system' en CLI.", 'performed_at' => "Horodatage UTC de l'action auditee.", 'ip_address' => "Adresse IP de l'auteur (IPv4/IPv6) — null hors contexte HTTP.", 'request_id' => "UUID v4 de la requete HTTP — regroupe les changements d'un meme flush, facilite la correlation logs.", ], 'category' => [ '_table' => 'Categories M0 — referentiel type par category_type, soft-delete via deleted_at, unicite (LOWER(name), category_type_id) parmi les actifs.', 'id' => 'Identifiant interne auto-incremente.', 'name' => 'Libelle de la categorie (≤ 120 caracteres) — unique par type parmi les actifs (RG-1.06).', 'category_type_id' => 'Reference au type de la categorie — FK -> category_type.id, ON DELETE RESTRICT (un type ne peut etre supprime tant qu il a des categories).', 'deleted_at' => 'Horodatage UTC du soft-delete (archivage logique) — null si la categorie est active.', ] + self::timestampableBlamableComments(), 'category_type' => [ '_table' => 'Referentiel statique des types de categories — code technique stable + libelle FR.', 'id' => 'Identifiant interne auto-incremente.', 'code' => 'Code technique stable du type (snake_case, ≤ 40 caracteres) — unique, utilise dans le code et les configurations.', 'label' => 'Libelle affichable du type (FR, ≤ 120 caracteres).', ], 'permission' => [ '_table' => 'Referentiel des permissions RBAC — codes au format module.resource[.subresource].action, synchronise par app:sync-permissions.', 'id' => 'Identifiant interne auto-incremente.', 'code' => 'Code RBAC au format module.resource[.subresource].action — unique, synchronise par app:sync-permissions.', 'label' => 'Libelle affichable de la permission (FR).', 'module' => 'Identifiant du module proprietaire de la permission (snake_case, ex: core, commercial).', 'orphan' => "Drapeau permission orpheline — vrai quand son module declarant a ete supprime, masquee de l'interface RBAC.", ], 'role' => [ '_table' => 'Referentiel des roles RBAC — agregent un ensemble de permissions, attribues aux utilisateurs.', 'id' => 'Identifiant interne auto-incremente.', 'code' => 'Code technique stable du role (snake_case) — utilise dans le code (ex: admin, user). Unique.', 'label' => 'Libelle affichable du role (FR).', 'description' => 'Description longue du role (optionnelle).', 'is_system' => "Drapeau role systeme — bloque la suppression et la modification du code via l'interface.", ], 'role_permission' => [ '_table' => 'Table de jointure roles <-> permissions (ManyToMany).', 'role_id' => 'FK -> role.id, ON DELETE CASCADE — role qui porte la permission.', 'permission_id' => 'FK -> permission.id, ON DELETE CASCADE — permission attribuee au role.', ], 'site' => [ '_table' => 'Sites geographiques — perimetre de scoping multi-site, attribues aux utilisateurs via user_site.', 'id' => 'Identifiant interne auto-incremente.', 'name' => 'Nom du site (≤ 100 caracteres).', 'city' => 'Ville du site (≤ 100 caracteres).', 'postal_code' => 'Code postal (chaine ≤ 20 caracteres) — VARCHAR pour gerer les zeros initiaux et les formats internationaux.', 'color' => "Code couleur hexadecimal (#RRGGBB) — differenciation visuelle dans l'UI.", 'street' => "Numero et voie de l'adresse (≤ 200 caracteres).", 'complement' => "Complement d'adresse (etage, batiment...) — optionnel.", 'created_at' => 'Horodatage UTC de creation de la ligne — rempli par TimestampableBlamableSubscriber au prePersist.', 'updated_at' => 'Horodatage UTC de derniere modification — rempli par TimestampableBlamableSubscriber au preUpdate.', ], 'user' => [ '_table' => 'Comptes utilisateurs Starseed — authentification JWT, RBAC via roles et permissions directes.', 'id' => 'Identifiant interne auto-incremente.', 'username' => 'Identifiant de connexion (≤ 100 caracteres) — unique.', 'password' => 'Hash du mot de passe (algorithme courant Symfony) — exclu de l audit via #[AuditIgnore].', 'created_at' => 'Horodatage UTC de creation du compte — rempli manuellement dans le constructeur (pas via TimestampableBlamableSubscriber).', 'is_admin' => 'Drapeau super-administrateur — bypass complet RBAC. Faux par defaut.', 'current_site_id' => "Site actuellement selectionne par l'utilisateur (contexte de session) — FK -> site.id, ON DELETE SET NULL.", ], 'user_permission' => [ '_table' => 'Table de jointure utilisateurs <-> permissions directes (hors role).', 'user_id' => 'FK -> user.id, ON DELETE CASCADE — utilisateur destinataire de la permission directe.', 'permission_id' => 'FK -> permission.id, ON DELETE CASCADE — permission accordee individuellement.', ], 'user_role' => [ '_table' => 'Table de jointure utilisateurs <-> roles (ManyToMany).', 'user_id' => 'FK -> user.id, ON DELETE CASCADE — utilisateur portant le role.', 'role_id' => 'FK -> role.id, ON DELETE CASCADE — role attribue a l utilisateur.', ], 'user_site' => [ '_table' => 'Table de jointure utilisateurs <-> sites accessibles — gere le scoping multi-site (un user ne voit que les donnees de ses sites).', 'user_id' => 'FK -> user.id, ON DELETE CASCADE — utilisateur ayant acces au site.', 'site_id' => 'FK -> site.id, ON DELETE CASCADE — site accessible par l utilisateur.', ], ]; } /** * Descriptions standardisees pour les 4 colonnes du pattern * Timestampable/Blamable (`TimestampableBlamableTrait`). * * @return array */ public static function timestampableBlamableComments(): array { return [ 'created_at' => 'Horodatage UTC de creation de la ligne — rempli par TimestampableBlamableSubscriber au prePersist.', 'updated_at' => 'Horodatage UTC de derniere modification — rempli par TimestampableBlamableSubscriber au preUpdate.', 'created_by' => "ID de l'utilisateur ayant cree la ligne — null hors HTTP (CLI, migration, fixture). FK -> \"user\".id, ON DELETE SET NULL.", 'updated_by' => "ID de l'utilisateur ayant modifie la ligne en dernier — null hors HTTP. FK -> \"user\".id, ON DELETE SET NULL.", ]; } /** * Construit la liste des requetes SQL `COMMENT ON TABLE/COLUMN` (en * dollar-quoting Postgres `$_$`) a partir du catalogue. * * @return list */ public static function toSqlStatements(): array { $statements = []; foreach (self::comments() as $table => $entries) { $quotedTable = self::quoteIdent($table); foreach ($entries as $column => $description) { if ('_table' === $column) { $statements[] = sprintf('COMMENT ON TABLE %s IS $_$%s$_$', $quotedTable, $description); continue; } $statements[] = sprintf( 'COMMENT ON COLUMN %s.%s IS $_$%s$_$', $quotedTable, self::quoteIdent($column), $description, ); } } return $statements; } /** * Quote un identifiant SQL avec des guillemets doubles. Necessaire pour * la table `user` (mot reserve PG) ; applique a tous par coherence. */ private static function quoteIdent(string $name): string { return '"'.str_replace('"', '""', $name).'"'; } }