Files
Coltura/doc/architecture-modulaire-malio.md

365 lines
14 KiB
Markdown

# Architecture Modulaire Monolith — MALIO
## Contexte
Projet monolith Symfony API Platform (back) + Nuxt (front dans un dossier `frontend/`).
L'objectif est une architecture **modular monolith DDD** permettant de vendre des modules indépendamment à chaque client (ex : un client achète GestionRH + Formation, un autre GestionRH + Paie + Pointage).
---
## Principes fondamentaux
1. **Chaque module est un bounded context autonome** — son propre Domain, Application, Infrastructure.
2. **Communication inter-modules uniquement par events ou contrats** — jamais d'import direct d'une entité d'un autre module.
3. **Les modules sont activables/désactivables** par tenant sans casser le reste.
4. **Le module `Core` est obligatoire** — il gère users, tenants, auth.
5. **Le dossier `Shared/`** contient le noyau technique commun (interfaces, value objects de base, bus).
6. **API Platform** : les `#[ApiResource]` sont sur des classes Resource dédiées dans `Infrastructure/ApiPlatform/Resource/`, jamais directement sur les entités du Domain.
7. **CQRS** : Command/Query handlers dans la couche Application, DTO d'entrée/sortie découplés des entités.
8. **Multi-tenant natif** : chaque entité porte un `tenantId`.
---
## Structure cible — Backend (`src/`)
```
src/
├── Kernel.php
├── Shared/
│ ├── Domain/
│ │ ├── ValueObject/
│ │ │ ├── AggregateId.php
│ │ │ └── Email.php
│ │ ├── Event/
│ │ │ └── DomainEventInterface.php
│ │ └── Contract/ ← Interfaces inter-modules
│ │ ├── UserResolverInterface.php
│ │ └── TenantAwareInterface.php
│ ├── Application/
│ │ └── Bus/
│ │ ├── CommandBusInterface.php
│ │ └── QueryBusInterface.php
│ └── Infrastructure/
│ ├── Doctrine/
│ ├── Messenger/
│ └── ApiPlatform/
│ └── OpenApi/
├── Module/
│ ├── Core/ ← Module obligatoire
│ │ ├── Domain/
│ │ │ ├── Entity/
│ │ │ │ ├── User.php
│ │ │ │ └── Tenant.php
│ │ │ ├── Repository/
│ │ │ │ └── UserRepositoryInterface.php
│ │ │ └── Event/
│ │ │ └── UserCreated.php
│ │ ├── Application/
│ │ │ ├── Command/
│ │ │ │ ├── CreateUser.php
│ │ │ │ └── CreateUserHandler.php
│ │ │ ├── Query/
│ │ │ │ ├── GetUserById.php
│ │ │ │ └── GetUserByIdHandler.php
│ │ │ └── DTO/
│ │ │ └── UserOutput.php
│ │ ├── Infrastructure/
│ │ │ ├── Doctrine/
│ │ │ │ ├── DoctrineUserRepository.php
│ │ │ │ └── mapping/
│ │ │ └── ApiPlatform/
│ │ │ ├── Resource/
│ │ │ │ └── UserResource.php
│ │ │ ├── State/
│ │ │ │ ├── Provider/
│ │ │ │ └── Processor/
│ │ │ └── Filter/
│ │ └── CoreModule.php ← Déclaration : config, routes, dépendances
│ │
│ ├── GestionRH/ ← Module vendable
│ │ ├── Domain/
│ │ │ ├── Entity/
│ │ │ │ ├── Employee.php
│ │ │ │ ├── Contract.php
│ │ │ │ └── Leave.php
│ │ │ ├── ValueObject/
│ │ │ ├── Repository/
│ │ │ │ └── EmployeeRepositoryInterface.php
│ │ │ ├── Event/
│ │ │ │ └── EmployeeHired.php
│ │ │ ├── Exception/
│ │ │ └── Service/
│ │ ├── Application/
│ │ │ ├── Command/
│ │ │ ├── Query/
│ │ │ ├── DTO/
│ │ │ └── Listener/ ← Réagit aux events d'autres modules
│ │ ├── Infrastructure/
│ │ │ ├── Doctrine/
│ │ │ │ ├── DoctrineEmployeeRepository.php
│ │ │ │ ├── mapping/
│ │ │ │ └── migrations/ ← Migrations propres au module
│ │ │ └── ApiPlatform/
│ │ │ ├── Resource/
│ │ │ │ └── EmployeeResource.php
│ │ │ └── State/
│ │ │ ├── Provider/
│ │ │ └── Processor/
│ │ └── GestionRHModule.php
│ │
│ ├── Formation/ ← Module vendable
│ │ ├── Domain/
│ │ ├── Application/
│ │ ├── Infrastructure/
│ │ └── FormationModule.php
│ │
│ ├── Pointage/ ← Module vendable
│ │ ├── Domain/
│ │ ├── Application/
│ │ ├── Infrastructure/
│ │ └── PointageModule.php
│ │
│ └── Paie/ ← Module vendable
│ ├── Domain/
│ ├── Application/
│ ├── Infrastructure/
│ └── PaieModule.php
└── config/
└── modules.php ← Liste des modules activés
```
---
## Structure cible — Frontend (`frontend/`)
```
frontend/
├── app/
│ ├── layouts/
│ │ └── default.vue ← Menu dynamique selon modules activés
│ ├── middleware/
│ │ └── modules.global.ts ← Bloque les routes de modules désactivés
│ └── app.vue
├── shared/
│ ├── composables/
│ │ ├── useAuth.ts
│ │ ├── useApi.ts
│ │ ├── useTenant.ts
│ │ └── useModules.ts ← Expose les modules activés (via API)
│ ├── components/
│ │ ├── ui/ ← Design system (boutons, tables, modals…)
│ │ ├── AppSidebar.vue
│ │ └── AppHeader.vue
│ ├── types/
│ │ └── index.ts
│ ├── utils/
│ └── stores/
│ ├── auth.ts
│ └── tenant.ts
├── modules/
│ ├── core/
│ │ ├── pages/
│ │ │ ├── login.vue
│ │ │ ├── dashboard.vue
│ │ │ └── users/
│ │ │ ├── index.vue
│ │ │ └── [id].vue
│ │ ├── components/
│ │ ├── composables/
│ │ ├── stores/
│ │ │ └── users.ts
│ │ ├── types/
│ │ └── core.module.ts
│ │
│ ├── gestion-rh/
│ │ ├── pages/
│ │ │ ├── employees/
│ │ │ │ ├── index.vue
│ │ │ │ └── [id].vue
│ │ │ ├── contracts/
│ │ │ └── leaves/
│ │ ├── components/
│ │ │ ├── EmployeeCard.vue
│ │ │ └── LeaveCalendar.vue
│ │ ├── composables/
│ │ │ └── useEmployees.ts
│ │ ├── stores/
│ │ │ └── employees.ts
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── gestion-rh.module.ts
│ │
│ ├── formation/
│ │ ├── pages/
│ │ ├── components/
│ │ ├── composables/
│ │ ├── stores/
│ │ ├── types/
│ │ └── formation.module.ts
│ │
│ ├── pointage/
│ │ ├── pages/
│ │ ├── components/
│ │ ├── composables/
│ │ ├── stores/
│ │ ├── types/
│ │ └── pointage.module.ts
│ │
│ └── paie/
│ ├── pages/
│ ├── components/
│ ├── composables/
│ ├── stores/
│ ├── types/
│ └── paie.module.ts
├── plugins/
│ └── modules-loader.ts ← Charge dynamiquement les modules activés
├── nuxt.config.ts
└── package.json
```
---
## Règles d'implémentation
### Backend
#### 1. Communication inter-modules
- **INTERDIT** : `use Module\GestionRH\Domain\Entity\Employee` depuis le module Paie.
- **AUTORISÉ** : passer par `Shared\Domain\Contract\EmployeeResolverInterface` ou écouter un domain event comme `EmployeeHired`.
- Les contrats (interfaces) partagés vivent dans `Shared/Domain/Contract/`.
#### 2. Couche Domain (aucune dépendance Symfony)
- Entités avec logique métier encapsulée (pas d'anemic model).
- Value Objects pour la validation (Email, Money, OrderStatus…).
- Repository = interface uniquement.
- Domain Events pour notifier les autres modules.
- Aucun `use Symfony\...` dans ce dossier.
#### 3. Couche Application
- CQRS : Command (écriture) + Query (lecture) avec leurs Handlers.
- Les Handlers orchestrent : ils appellent le Domain et les interfaces Repository.
- DTO Input/Output pour le contrat d'entrée/sortie, découplés des entités.
- Listeners pour réagir aux events d'autres modules.
#### 4. Couche Infrastructure
- Implémentations Doctrine des repositories.
- Mapping et migrations **propres à chaque module** (pas de migration centralisée).
- API Platform :
- `Resource/` : classes avec `#[ApiResource]`, jamais posé sur les entités Domain.
- `State/Provider/` : fournisseurs de données (GET).
- `State/Processor/` : traitement des mutations (POST/PUT/PATCH/DELETE), délègue au bus ou aux handlers.
- `Filter/` : filtres API Platform spécifiques au module.
- Les endpoints n'apparaissent dans l'OpenAPI que si le module est activé.
#### 5. Module declaration (`*Module.php`)
Chaque module déclare :
- Son identifiant unique.
- Ses dépendances (ex : GestionRH dépend de Core).
- Sa configuration de services.
- Ses routes.
#### 6. Activation/désactivation
- Fichier `config/modules.php` ou variable d'environnement listant les modules actifs.
- Le Kernel ne charge que les services/routes/migrations des modules activés.
- Endpoint API : `GET /api/tenant/modules` retourne la liste des modules activés pour le tenant courant.
#### 7. Multi-tenant
- Chaque entité porte un `tenantId`.
- Filtrage automatique Doctrine par tenant (Doctrine Filter ou listeners).
### Frontend
#### 1. Déclaration de module (`*.module.ts`)
```typescript
export default defineAppModule({
id: 'gestion-rh',
label: 'Gestion RH',
icon: 'i-lucide-users',
requiredModules: ['core'],
navigation: [
{ label: 'Employés', to: '/employees', icon: 'i-lucide-user' },
{ label: 'Contrats', to: '/contracts', icon: 'i-lucide-file-text' },
{ label: 'Congés', to: '/leaves', icon: 'i-lucide-calendar' },
],
permissions: ['employee.read', 'employee.write', 'leave.manage'],
})
```
#### 2. Chargement dynamique (`useModules`)
```typescript
export const useModules = () => {
const enabledModules = useState<string[]>('modules', () => [])
const isEnabled = (moduleId: string) =>
enabledModules.value.includes(moduleId)
return { enabledModules, isEnabled }
}
```
Au boot de l'app, appel `GET /api/tenant/modules` pour récupérer les modules activés.
#### 3. Middleware de protection des routes
```typescript
export default defineNuxtRouteMiddleware((to) => {
const { isEnabled } = useModules()
const moduleId = resolveModuleFromRoute(to.path)
if (moduleId && !isEnabled(moduleId)) {
return navigateTo('/dashboard')
}
})
```
#### 4. Sidebar dynamique
Le layout `default.vue` itère sur les modules activés et affiche leurs entrées `navigation`.
#### 5. Isolation
- Chaque module front a ses propres pages, components, composables, stores, types.
- Les composants partagés (design system) sont dans `shared/components/ui/`.
- Un module front ne doit jamais importer depuis un autre module front. Si besoin de données croisées, passer par l'API ou par un composable partagé dans `shared/`.
---
## Résumé des conventions de nommage
| Élément | Convention | Exemple |
|---------|-----------|---------|
| Module back | PascalCase | `Module/GestionRH/` |
| Module front | kebab-case | `modules/gestion-rh/` |
| Entity | PascalCase singulier | `Employee.php` |
| Repository interface | `*RepositoryInterface` | `EmployeeRepositoryInterface.php` |
| Repository impl | `Doctrine*Repository` | `DoctrineEmployeeRepository.php` |
| Command | Verbe + Nom | `CreateEmployee.php` |
| Command Handler | `*Handler` | `CreateEmployeeHandler.php` |
| DTO | `*Input` / `*Output` | `EmployeeOutput.php` |
| API Resource | `*Resource` | `EmployeeResource.php` |
| Provider | `*Provider` | `EmployeeProvider.php` |
| Processor | `*Processor` | `CreateEmployeeProcessor.php` |
| Module declaration back | `*Module.php` | `GestionRHModule.php` |
| Module declaration front | `*.module.ts` | `gestion-rh.module.ts` |
| Composable | `use*` | `useEmployees.ts` |
| Store | nom du domaine | `employees.ts` |
---
## Checklist de migration
Si le projet existe déjà avec une structure plate, voici l'ordre de migration recommandé :
1. Créer `Shared/` et y déplacer les interfaces/VO de base.
2. Créer `Module/Core/` et y migrer users, auth, tenants.
3. Pour chaque futur module vendable, créer le dossier `Module/<Nom>/` avec les 3 couches (Domain, Application, Infrastructure).
4. Déplacer les entités, repositories, services dans le bon module.
5. Remplacer les imports directs inter-modules par des contrats (`Shared/Domain/Contract/`) ou des events.
6. Isoler les migrations Doctrine par module.
7. Adapter les resources API Platform (les sortir des entités, créer les Providers/Processors).
8. Côté front, créer `shared/` et `modules/`, migrer les pages/composants dans le bon module.
9. Implémenter `useModules` + middleware de routes + sidebar dynamique.
10. Tester l'activation/désactivation d'un module de bout en bout.