- 11 tests couvrant le login (3) et la visibilite sidebar par RBAC (8) - 6 personas seedes via la commande app:seed-e2e, miroir cote front dans frontend/tests/e2e/_fixtures/personas.ts - Page Objects (LoginPage, SidebarComponent) avec selecteurs stables par href + loginAs programmatique via cookie BEARER - Targets Makefile : seed-e2e, test-e2e, test-e2e-ui, install-e2e-deps - CLAUDE.md + README.md : workflow E2E + regle d'or "un E2E par bug prod uniquement" pour garder la suite maintenable dans la duree Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
318 lines
18 KiB
Markdown
318 lines
18 KiB
Markdown
# 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 nuxt-test # Vitest (tests unitaires frontend)
|
|
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
|
|
```
|
|
|
|
### Tests E2E (Playwright)
|
|
|
|
La suite E2E vit dans `frontend/tests/e2e/` (personas, Page Objects, specs). Elle tourne sur l'host (Playwright a besoin d'un navigateur reel, pas dans un container PHP).
|
|
|
|
**Bootstrap one-time par poste de dev :**
|
|
```bash
|
|
make install-e2e-deps # Telecharge Chromium + installe les libs systeme (sudo)
|
|
```
|
|
A relancer uniquement si `@playwright/test` upgrade de version majeure.
|
|
|
|
**Workflow nominal :**
|
|
```bash
|
|
# Terminal 1
|
|
make start # Containers up
|
|
make seed-e2e # Cree les 6 personas e2e.* (idempotent)
|
|
make dev-nuxt # Dev server sur :3004 (garder ouvert)
|
|
|
|
# Terminal 2
|
|
make test-e2e # Run la suite
|
|
# ou
|
|
make test-e2e-ui # UI interactive pour debug
|
|
```
|
|
|
|
**Regle d'or E2E** (a respecter pour garder la suite maintenable sur la duree) : un nouveau test E2E ne s'ajoute QUE quand un bug critique est passe en prod. Sinon, la bonne place est un test unitaire Vitest (plus rapide, plus stable). Cf. `frontend/tests/e2e/_fixtures/personas.ts` pour etendre la matrice RBAC via un persona existant plutot que d'ajouter un test.
|
|
|
|
**Etendre la matrice RBAC** : pour ajouter une permission testable, toucher les 3 endroits (sinon drift garanti) :
|
|
1. `config/sidebar.php` — attacher `permission` au bon item
|
|
2. `frontend/tests/e2e/_fixtures/personas.ts` — ajuster `permissions` + `expectedAdminLinks` d'un persona existant (ne pas creer de nouveau persona par reflexe)
|
|
3. `src/Module/Core/Infrastructure/Console/SeedE2ECommand.php` — miroir back du meme persona
|
|
|
|
## 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` (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<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` (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 `<input>` / `<select>` bruts.
|
|
- **Exceptions autorisees** (a commenter en TODO lors du premier usage, pour pouvoir migrer quand la lib evoluera) :
|
|
1. Type de champ non couvert par la lib : `datetime-local`, `date`, `color picker`, `file drag & drop`, etc.
|
|
2. Bug connu bloquant du composant : ex. `MalioSelect` avec options a valeur string (cf. note dans le Lesstime CLAUDE.md). Documenter le bug avec un commentaire pointant la limitation.
|
|
- Toute autre exception doit etre validee par l'equipe avant merge.
|
|
- **Tableaux de donnees : utiliser `MalioDataTable`.** Tout affichage LISTE tabulaire (donnees metier paginees, CRUD admin) doit passer par `MalioDataTable` (pagination + slots `#header-*` pour filtres + `#cell-*` pour rendu custom). Pas de `<table>` brut avec pagination custom. **Exception** : les tableaux purement presentationnels non-paginables (diff field/old/new, grille de comparaison, matrice RBAC, etc.) peuvent rester en `<table>` HTML brut — MalioDataTable n'est pas adapte a ces cas.
|
|
- **Audit ManyToMany** : le listener trace les modifications de collections to-many (`permissions`, etc.) sous forme `{fieldName: {added: [ids], removed: [ids]}}`. Toute nouvelle entite `#[Auditable]` portant des ManyToMany a auditer beneficie automatiquement de cette couverture — aucune action supplementaire requise.
|
|
|
|
### 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)
|
|
|
|
## Delegation Codex
|
|
|
|
Pour les taches mecaniques (tests, boilerplate, renommages, refacto repetitif), delegue a Codex via le plugin `codex`. Garde Claude pour la reflexion, l'architecture et la verification.
|
|
|
|
- **Codex** = junior dev rapide et pas cher (executions mecaniques)
|
|
- **Claude** = senior dev qui verifie et reflechit (design, review, decisions)
|
|
|
|
C'est le meilleur ratio qualite/credits.
|