- Spec detaillee des fondations RBAC backend (entites Role/Permission, sync command, migration, fixtures, tests) dans docs/rbac/ticket-343-spec.md - Ajout CLAUDE.md des regles projet : commentaires francais (PHP + TS/Vue) et convention de nommage des permissions module.resource[.sub].action Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
Coltura
CRM/ERP. Monorepo Symfony 8 (API Platform 4) + Nuxt 4. Architecture Modular Monolith DDD.
Architecture Modulaire
Le projet suit une architecture modular monolith pilotee par le backend : chaque module metier est un bounded context autonome, activable/desactivable par tenant. Le module Core est obligatoire.
Principe fondamental : le backend est la source de verite unique.
- Le backend dicte quels modules sont actifs (
config/modules.php). - Le backend dicte l'organisation de la sidebar (
config/sidebar.php), decouplee des modules eux-memes. - Le frontend ne connait rien : il scanne automatiquement les modules comme layers Nuxt et demande la sidebar au backend.
Backend — Organisation par module
src/
Kernel.php
Shared/ # Noyau technique partage
Domain/
ValueObject/ # VO de base (Email...)
Event/ # DomainEventInterface
Contract/ # Interfaces inter-modules (UserResolverInterface, TenantAwareInterface)
Application/
Bus/ # CommandBusInterface, QueryBusInterface (interfaces seules)
Infrastructure/
ApiPlatform/
Resource/ # AppVersion, ModulesResource, SidebarResource
State/ # AppVersionProvider, ModulesProvider, SidebarProvider
Module/
Core/ # Module obligatoire (auth, users)
CoreModule.php # Declaration (ID, LABEL, REQUIRED)
Domain/
Entity/ # Entites Doctrine + API Platform (User)
Repository/ # Interfaces repositories (UserRepositoryInterface)
Event/ # Domain events (UserCreated)
Application/
DTO/ # UserOutput
Infrastructure/
Doctrine/ # DoctrineUserRepository, Migrations/
ApiPlatform/
State/
Provider/ # MeProvider
Processor/ # UserPasswordHasherProcessor
Console/ # CreateUserCommand
DataFixtures/ # AppFixtures
Commercial/ # Autre module (exemple)
CommercialModule.php
config/
modules.php # Liste des modules actifs (source de verite activation)
sidebar.php # Structure de la sidebar (source de verite navigation)
version.yaml
jwt/ # Cles JWT
packages/ # Config Symfony
migrations/ # Anciennes migrations Doctrine
infra/dev/ # Docker dev
infra/prod/ # Docker prod (multi-stage)
Frontend — Organisation modulaire (auto-detectee)
frontend/
app/ # Shell applicatif
layouts/ # default.vue, auth.vue
middleware/ # auth.global.ts, modules.global.ts
shared/ # Code partage (hors modules)
composables/ # useApi, useAppVersion, useSidebar
components/ui/ # AppTopNav, ...
stores/ # auth, ui
services/ # auth
types/ # SidebarSection, SidebarItem, UserData
utils/ # api (Hydra)
modules/ # Modules auto-detectes comme layers Nuxt
core/
nuxt.config.ts # Marqueur layer (vide)
pages/ # index.vue, login.vue
commercial/
nuxt.config.ts
pages/ # commercial.vue
app.vue # Composant racine
nuxt.config.ts # Scanne modules/*/ automatiquement
i18n/locales/ # Traductions (cles sidebar.*, etc.)
assets/ # CSS, images
public/ # Fichiers statiques
Endpoints API cles
GET /api/version(public) — version de l'appGET /api/modules(public) — liste des IDs de modules actifsGET /api/sidebar(public) — sections de la sidebar +disabledRoutes- Filtre automatiquement les items dont le
moduleowner n'est pas actif - Les sections vides apres filtrage sont supprimees
disabledRoutes=todes items filtres (utilise par le middleware front)
- Filtre automatiquement les items dont le
GET /api/me(auth) — user courant
Flux d'activation/desactivation d'un module
Pour activer/desactiver un module, tu touches uniquement config/modules.php :
return [
\App\Module\Core\CoreModule::class,
// \App\Module\Commercial\CommercialModule::class, // commente = desactive
];
Cascade automatique :
GET /api/modulesne retourne pluscommercialGET /api/sidebarfiltre les itemsmodule: 'commercial'→ section "Commercial" disparait, ses routes passent dansdisabledRoutes- Frontend : sidebar se met a jour, middleware
modules.global.tsredirige toute navigation vers/commercialou/commercial/* - Le code du module reste dans le bundle Nuxt (layer auto-detecte) → reactivation instantanee sans rebuild
Reorganiser la sidebar sans toucher aux modules
Pour deplacer un item (ex: "Commandes fournisseurs") d'une section a une autre, tu edites juste config/sidebar.php :
// Avant : sous Commercial
['label' => 'sidebar.commercial.suppliers', 'to' => '/commercial/suppliers', 'module' => 'commercial'],
// Apres : sous Production (l'item reste "owned" par Commercial, seule sa place change)
[
'label' => 'sidebar.production.section',
'items' => [
['label' => 'sidebar.commercial.suppliers', 'to' => '/commercial/suppliers', 'module' => 'commercial'],
],
],
Le code du module Commercial n'est pas touche.
Regles d'architecture
Backend :
- Le domaine (
Domain/) peut garder les attributs ORM (approche pragmatique) mais les repositories sont des interfaces - Communication inter-modules par
Shared/Domain/Contract/ou domain events — jamais d'import direct entre modules - Chaque module declare un
*Module.phpavecID,LABEL,REQUIRED config/modules.php= seule source de verite pour l'activationconfig/sidebar.php= seule source de verite pour l'organisation de la sidebar (chaque item reference son module owner via la clemodule)- Migrations par module dans
src/Module/{Module}/Infrastructure/Doctrine/Migrations/
Frontend :
- Chaque module est un layer Nuxt auto-detecte (
modules/*/nuxt.config.tsminimal) - Un module front ne doit pas importer depuis un autre module — utiliser
shared/ useSidebar()fetch/api/sidebaret exposesections,disabledRoutes,isRouteDisabled()- Le layout
default.vueitere sur les sections retournees par l'API, appliquet()sur les labels - Middleware
auth.global.tscharge la sidebar apres authentification - Middleware
modules.global.tsredirige si la route demandee est dansdisabledRoutes - Les composables avec state singleton (refs module-level) doivent exposer une fonction
reset*()et etre reinitialises au logout (ex:useSidebar().resetSidebar()) - Interdit :
.module.ts,modules-loader.ts, hardcode de la sidebar, edition manuelle deextendsdansnuxt.config.ts
Stack
- Backend : PHP 8.4, Symfony 8.0, API Platform 4, Doctrine ORM, PostgreSQL 16
- Frontend : Nuxt 4 (SSR off / SPA), Vue 3, Pinia, Tailwind CSS, @malio/layer-ui, nuxt-toast, @nuxtjs/i18n, @nuxt/icon
- Auth : JWT HTTP-only cookie (lexik/jwt-authentication-bundle), login a
/login_check, cookieBEARER - Docker : PHP-FPM + Node 24, Nginx (port 8083), PostgreSQL (port 5437)
Commandes
make start # Demarrer les containers
make stop # Arreter les containers
make restart # Redemarrer les containers
make install # Install complet (composer, migrations, fixtures, build Nuxt)
make reset # Tout supprimer et reinstaller (supprime la BDD)
make dev-nuxt # Dev server Nuxt (hot reload, port 3004)
make shell # Shell dans le container PHP
make shell-root # Shell root dans le container PHP
make cache-clear # Vider le cache Symfony
make migration-migrate # Lancer les migrations
make fixtures # Charger les fixtures
make db-reset # Reset BDD + migrations + fixtures
make test # PHPUnit
make php-cs-fixer-allow-risky # Fix code style PHP
make logs-dev # Tail logs Symfony
Si make cache-clear echoue pour cause de permissions sur var/ :
docker exec -t -u root php-coltura-fpm chown -R www-data:www-data /var/www/html/var
docker exec -t -u www-data php-coltura-fpm php bin/console cache:clear
Conventions
Commits
Format : <type>(<scope optionnel>) : <message> (espace avant et apres :)
Types autorises (minuscules) : build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test
Exemples : feat : add login page, fix(auth) : prevent null token crash
Tags & Versioning
- La version de l'app est dans
config/version.yaml(parametreapp.version) - A chaque creation de tag, toujours mettre a jour
config/version.yamlavec la meme version - Faire un commit separe de bump :
chore : bump version to v<X.Y.Z> - Puis creer le tag et pusher :
git tag v<X.Y.Z> && git push origin develop --tags
Nommage
| Element | Convention | Exemple |
|---|---|---|
| Module back | PascalCase | Module/Commercial/ |
| Module front | kebab-case | modules/commercial/ |
| Module ID | snake_case | commercial, gestion_rh |
| Entity | PascalCase singulier | User.php |
| Repository interface | *RepositoryInterface |
UserRepositoryInterface.php |
| Repository impl | Doctrine*Repository |
DoctrineUserRepository.php |
| DTO | *Output / *Input |
UserOutput.php |
| API Resource | classe dans Infrastructure/ApiPlatform/Resource/ |
UserResource.php |
| Provider | *Provider |
MeProvider.php |
| Processor | *Processor |
UserPasswordHasherProcessor.php |
| Module declaration back | *Module.php |
CommercialModule.php |
| Composable front | use* |
useSidebar.ts |
| Cles i18n sidebar | sidebar.<module>.* |
sidebar.commercial.overview |
Backend
- Toujours
declare(strict_types=1)en haut des fichiers PHP - Commentaires en francais : tout commentaire PHP (docblock, inline, bloc) doit etre redige en francais. Le code (noms de classes, methodes, variables) reste en anglais. Objectif : faciliter la relecture par l'equipe FR sans polluer l'API publique du code.
- API Platform : utiliser ApiResource, Providers, Processors — pas de controllers
- Routes API prefixees
/api(viaconfig/routes/api_platform.yaml) - Le login (
/login_check) est hors prefix/api, nginx reecritREQUEST_URIvers/login_check - PHP CS Fixer : regles Symfony + PSR-12 + strict types
- Roles :
ROLE_ADMIN,ROLE_USER— hierarchie danssecurity.yaml - Permissions RBAC : format obligatoire
module.resource[.subresource].actionen snake_case, ex :core.users.view,commercial.clients.contacts.edit. Declarees via la methode statiquepermissions()des*Module.php, synchronisees par la commandeapp:sync-permissions. Verification viais_granted('module.resource.action')cote API Platform etusePermissions()cote front. - PostgreSQL : noms de colonnes toujours en minuscules dans le SQL brut
- Controllers custom sous
/api/: ajouterpriority: 1sur#[Route]pour eviter le conflit avec API Platform{id} - Serialization : pour embarquer une relation (pas IRI), ajouter le groupe du parent aux proprietes de l'entite cible
- Upload fichiers : utiliser
$file->getMimeType()(pasgetClientMimeType()) pour valider cote serveur
Frontend
- TypeScript strict
- Commentaires en francais : tout commentaire TS/Vue (JSDoc, inline, bloc) doit etre redige en francais. Le code reste en anglais. Meme regle que cote backend.
- Composable
useApi()pour tous les appels API (gere cookies, erreurs, toasts, i18n) - Stores Pinia :
useAuthStore(auth),useUiStore(ui) - Middleware global
auth.global.tsprotege les routes + charge la sidebar apres login - Middleware global
modules.global.tsredirige les routes des modules desactives - Traductions dans
frontend/i18n/locales/avec le namespacesidebar.*pour la nav - 4 espaces d'indentation
- Les labels de sidebar sont des cles i18n, jamais du texte brut (le layout applique
t()dessus)
Nginx
/api/*-> Symfony (via try_files + index.php)/api/login_check-> location exact match, fastcgi direct avec REQUEST_URI reecrit en/login_check/-> SPA frontend (frontend/dist/)
Docker
- Container PHP :
php-coltura-fpm - Container Nginx :
nginx-coltura - Container DB : PostgreSQL sur port 5437 (interne et externe)
- Config Docker dev :
infra/dev/.env.docker(override local :infra/dev/.env.docker.local) - Config Docker prod :
infra/prod/(Dockerfile multi-stage, docker-compose.prod.yml) - Apres modif nginx :
docker restart nginx-coltura
Fixtures
- User admin :
admin/admin(ROLE_ADMIN) - Users internes :
alice/alice,bob/bob(ROLE_USER)