# 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'app - `GET /api/modules` (public) — liste des IDs de modules actifs - `GET /api/sidebar` (public) — sections de la sidebar + `disabledRoutes` - Filtre automatiquement les items dont le `module` owner n'est pas actif - Les sections vides apres filtrage sont supprimees - `disabledRoutes` = `to` des items filtres (utilise par le middleware front) - `GET /api/me` (auth) — user courant ### Flux d'activation/desactivation d'un module Pour activer/desactiver un module, tu touches **uniquement** `config/modules.php` : ```php return [ \App\Module\Core\CoreModule::class, // \App\Module\Commercial\CommercialModule::class, // commente = desactive ]; ``` Cascade automatique : 1. `GET /api/modules` ne retourne plus `commercial` 2. `GET /api/sidebar` filtre les items `module: 'commercial'` → section "Commercial" disparait, ses routes passent dans `disabledRoutes` 3. Frontend : sidebar se met a jour, middleware `modules.global.ts` redirige toute navigation vers `/commercial` ou `/commercial/*` 4. 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` : ```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.php` avec `ID`, `LABEL`, `REQUIRED` - `config/modules.php` = seule source de verite pour l'activation - `config/sidebar.php` = seule source de verite pour l'organisation de la sidebar (chaque item reference son module owner via la cle `module`) - Migrations par module dans `src/Module/{Module}/Infrastructure/Doctrine/Migrations/` - **Exception connue** : avec plusieurs `migrations_paths` configures, Doctrine Migrations 3.x trie les migrations par FQCN alphabetique et non par version timestamp → ordre d'execution incorrect entre namespaces sur une base vide. Tant que ce n'est pas resolu (via un `MigrationsComparator` custom ou un upgrade), les migrations d'initialisation critiques (setup user, RBAC, etc.) vivent au namespace racine `DoctrineMigrations` dans `migrations/`. Le namespace modulaire reste configure pour les futures migrations applicatives (qui dependent d'un schema deja cree). **Frontend :** - Chaque module est un layer Nuxt auto-detecte (`modules/*/nuxt.config.ts` minimal) - Un module front ne doit pas importer depuis un autre module — utiliser `shared/` - `useSidebar()` fetch `/api/sidebar` et expose `sections`, `disabledRoutes`, `isRouteDisabled()` - Le layout `default.vue` itere sur les sections retournees par l'API, applique `t()` sur les labels - Middleware `auth.global.ts` charge la sidebar apres authentification - Middleware `modules.global.ts` redirige si la route demandee est dans `disabledRoutes` - 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 de `extends` dans `nuxt.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`, cookie `BEARER` - **Docker** : PHP-FPM + Node 24, Nginx (port 8083), PostgreSQL (port 5437) ## Commandes ```bash 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/` : ```bash 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 : `() : ` (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` (parametre `app.version`) - A chaque creation de tag, **toujours** mettre a jour `config/version.yaml` avec la meme version - Faire un commit separe de bump : `chore : bump version to v` - Puis creer le tag et pusher : `git tag v && 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..*` | `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` (via `config/routes/api_platform.yaml`) - Le login (`/login_check`) est hors prefix `/api`, nginx reecrit `REQUEST_URI` vers `/login_check` - PHP CS Fixer : regles Symfony + PSR-12 + strict types - Roles : `ROLE_ADMIN`, `ROLE_USER` — hierarchie dans `security.yaml` - **Permissions RBAC** : format obligatoire `module.resource[.subresource].action` en snake_case, ex : `core.users.view`, `commercial.clients.contacts.edit`. Declarees via la methode statique `permissions()` des `*Module.php`, synchronisees par la commande `app:sync-permissions`. Verification via `is_granted('module.resource.action')` cote API Platform et `usePermissions()` cote front. - PostgreSQL : noms de colonnes toujours en **minuscules** dans le SQL brut - Controllers custom sous `/api/` : ajouter `priority: 1` sur `#[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()` (pas `getClientMimeType()`) pour valider cote serveur - **Audit obligatoire** : toute entite (nouvelle ou existante) doit porter `#[Auditable]` (dans `Shared/Domain/Attribute/`). Les champs sensibles (password, token, secret) doivent etre annotes `#[AuditIgnore]`. Spec complete : `doc/audit-log.md` ### 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.ts` protege les routes + charge la sidebar apres login - Middleware global `modules.global.ts` redirige les routes des modules desactives - Traductions dans `frontend/i18n/locales/` avec le namespace `sidebar.*` pour la nav - 4 espaces d'indentation - Les labels de sidebar sont des cles i18n, jamais du texte brut (le layout applique `t()` dessus) - **Tableaux : pas de persistance URL.** Aucun etat de tableau (filtres, pagination, tri, tri par colonne, selection, ligne active...) ne doit etre persiste dans la query string ou reinjecte depuis `route.query` au montage. L'etat vit uniquement dans le composant (reactive locale). Seuls les deep links "de navigation metier" (ex: ouvrir un detail precis `/users/42`) sont dans l'URL, jamais l'etat UI du tableau. Exceptions autorisees sur demande explicite de l'utilisateur. - **Composants formulaires : utiliser `@malio/layer-ui`.** Tout champ de formulaire / filtre doit utiliser les composants `Malio*` (MalioInputText, MalioSelect, MalioSelectCheckbox, MalioCheckbox, MalioRadioButton, MalioInputNumber, MalioInputAmount, MalioInputPassword, MalioInputTextArea, MalioInputUpload, MalioTime, MalioButton, MalioButtonIcon) plutot que des `` / `