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

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

  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)

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é :

  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.