14 KiB
14 KiB
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
- Chaque module est un bounded context autonome — son propre Domain, Application, Infrastructure.
- Communication inter-modules uniquement par events ou contrats — jamais d'import direct d'une entité d'un autre module.
- Les modules sont activables/désactivables par tenant sans casser le reste.
- Le module
Coreest obligatoire — il gère users, tenants, auth. - Le dossier
Shared/contient le noyau technique commun (interfaces, value objects de base, bus). - API Platform : les
#[ApiResource]sont sur des classes Resource dédiées dansInfrastructure/ApiPlatform/Resource/, jamais directement sur les entités du Domain. - CQRS : Command/Query handlers dans la couche Application, DTO d'entrée/sortie découplés des entités.
- 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\Employeedepuis le module Paie. - AUTORISÉ : passer par
Shared\Domain\Contract\EmployeeResolverInterfaceou écouter un domain event commeEmployeeHired. - 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.phpou 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/modulesretourne 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)
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)
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
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é :
- Créer
Shared/et y déplacer les interfaces/VO de base. - Créer
Module/Core/et y migrer users, auth, tenants. - Pour chaque futur module vendable, créer le dossier
Module/<Nom>/avec les 3 couches (Domain, Application, Infrastructure). - Déplacer les entités, repositories, services dans le bon module.
- Remplacer les imports directs inter-modules par des contrats (
Shared/Domain/Contract/) ou des events. - Isoler les migrations Doctrine par module.
- Adapter les resources API Platform (les sortir des entités, créer les Providers/Processors).
- Côté front, créer
shared/etmodules/, migrer les pages/composants dans le bon module. - Implémenter
useModules+ middleware de routes + sidebar dynamique. - Tester l'activation/désactivation d'un module de bout en bout.