Bounded contexts & frontières de service
Kohinos n'est pas un monolithe homogène : c'est une fédération de domaines
(Wallet/eMLc, SEPA, SSA, Adhésion, Paiement externe, BOffice) qui partagent une base de données
et un kernel Symfony, mais qui doivent rester isolés logiquement. Cette
séparation n'est pas un luxe : c'est une condition de l'auto-hébergement (chaque MLCC
n'utilise pas tous les contexts), du dev solo (un humain ne tient pas tout en tête)
et surtout de l'intégrité financière (un bug dans HelloAsso ne doit pas
pouvoir corrompre la chaîne d'Operation).
Carte des contexts
flowchart TB
subgraph Core["Core monétaire (eMLc)"]
WALLET["Wallet / Comptes<br/>Account*, Flux, Operation"]
end
subgraph Subs["Adhésion & Cotisation"]
ADH["Adherent / Prestataire<br/>MlcSubscriptionService"]
end
subgraph Recur["Récurrence & SEPA"]
SEPA["SepaProcessingService<br/>SepaMandate, Recurring*Request"]
end
subgraph SSA["SSA / Solidoume"]
SSAFUND["SsaOrchestrator<br/>SolidoumeParameter, Strategy"]
end
subgraph Pay["Paiement externe"]
HA["HelloAssoPaymentService<br/>PayZen (legacy)<br/>PendingPaymentData"]
end
subgraph BO["BOffice / SAV"]
BOFF["BofficeApiClient<br/>TicketFix*"]
end
subgraph Notif["Notifications (transverse)"]
MAIL["FluxNotificationService<br/>SepaNotificationService"]
end
ADH -- "création Account*" --> WALLET
HA -- "crédit eMLc<br/>(AchatMonnaie)" --> WALLET
SEPA -- "Achat récurrent / Cotisation" --> WALLET
SEPA -- "déclenche subscriptions" --> ADH
SSAFUND -- "TransactionAdherent*Ssa<br/>prélèvement + redistribution" --> WALLET
SSAFUND -- "cotisation participant" --> SEPA
BOFF -. "lecture seule (API)" .-> WALLET
WALLET -- "FluxEvent::CREATED" --> MAIL
SEPA -- "MLCEvents::SEPA_*" --> MAIL
SSAFUND-- "SsaExecutionResult" --> MAIL
HA -- "MLCEvents::PAYMENT_*" --> MAIL
Six contexts métier + un context transverse (notifications).
Les arêtes pleines représentent des appels de service ; les arêtes pointillées, un accès
en lecture via API. Les notifications transitent uniquement par events.
Tableau récapitulatif
Pour chaque context : où vit son code, ce qu'il possède en propre, ce qu'il publie en événements et de qui il dépend / qui dépend de lui.
| Context | Namespaces | Entités possédées | Events publiés | Dépendances entrantes | Dépendances sortantes |
|---|---|---|---|---|---|
| Wallet / eMLc | App\Entity\(Account*|Flux*|Operation*|Transaction*)App\Repository\(Account*|Flux*|Operation*)Repository |
AccountAdherent, AccountPrestataire,
AccountComptoir, AccountSiege,
AccountGroupe, Flux, Operation*,
AchatMonnaie, Retrait, Change, Don,
Transaction* |
FluxEvent::CREATED,MLCEvents::FLUX_PERSISTED |
SEPA, SSA, Adhésion, Paiement externe, BOffice | Notifications (event) |
| Adhésion / Cotisation | App\Service\Subscription\*App\Entity\(Adherent|Prestataire|Cotisation*) |
Adherent, Prestataire, Cotisation*,
CotisationInfos, RecurringMlcSubscriptionRequest |
UserEvent::REGISTERED,MLCEvents::COTISATION_PAID |
SSA (cotisation participant), SEPA (subscription récurrente) | Wallet (création Account*), Notifications |
| SEPA / Récurrence | App\Service\Sepa\*App\Service\Purchase\RecurringPurchaseServiceApp\Entity\(SepaMandate|Recurring*Request|IbanChangeRequest) |
SepaMandate, RecurringPurchaseRequest,
RecurringMlcSubscriptionRequest, RecurringSsaSubscriptionRequest,
IbanChangeRequest |
MLCEvents::SEPA_MANDATE_VALIDATED,MLCEvents::SEPA_PROCESSED,MLCEvents::SEPA_REVOKED |
SSA (cotisation participant), Adhésion (cotisation annuelle) | Wallet (création AchatMonnaie / Cotisation),
Notifications |
| SSA / SSA | App\Service\Ssa\*App\Entity\(SolidoumeParameter|SolidoumeItem|PrestataireSolidoumeParameter|CotisationSsaAdherent|TransactionAdherent*Ssa) |
SolidoumeParameter, SolidoumeItem,
PrestataireSolidoumeParameter, CotisationSsaAdherent,
TransactionAdherentAdherentSsa, TransactionAdherentEmlctoSsa |
MLCEvents::SSA_EXECUTED,MLCEvents::SSA_PARTICIPANT_DROPPED |
Aucune (point d'entrée : commande kohinos:ssa:execute) |
Wallet (Transaction*), SEPA (cotisation participant), Notifications |
| Paiement externe | App\Service\HelloAssoPaymentServiceApp\Service\HelloAssoWebhookAuthenticatorApp\Service\PaymentDataManagerApp\Entity\(HelloAsso|Payment|PaymentToken|PendingPaymentData) |
HelloAsso, Payment, PaymentToken,
PendingPaymentData |
MLCEvents::PAYMENT_RECEIVED,MLCEvents::PAYMENT_FAILED |
Adhésion (paiement cotisation), Wallet (achat eMLc) | Wallet (création AchatMonnaie), Notifications |
| BOffice / SAV | App\Service\BofficeApiClientApp\Entity\(TicketFix|TicketFixDestroy|TicketFixPrint) |
TicketFix, TicketFixDestroy, TicketFixPrint |
— | Wallet (lecture seule via API REST) | Notifications |
| Notifications (transverse) | App\Service\(FluxNotificationService|SepaNotificationService|CotisationNotificationService|RegistrationNotificationService|UserNotificationPreferenceService)App\EventListener\* |
— | — | Tous via MLCEvents, FluxEvent,
UserEvent |
symfony/mailer (SMTP self-host) |
Règles de communication entre contexts
Une fois la carte tracée, les règles deviennent simples (et appliquées par revue de PR / PHPStan quand possible) :
- Effets de bord = event dispatcher. Un context qui veut signaler un événement
(cotisation payée, mandat validé, prélèvement exécuté) publie un event de
App\Event\MLCEventsouApp\Event\FluxEvent. Il n'appelle jamais directement un service d'un autre context pour notifier. - Appels directs autorisés seulement pour les opérations financières atomiques.
SEPA → Wallet pour créer un
AchatMonnaiereste un appel synchrone : la consistance financière exige la même transaction Doctrine, donc le même thread d'exécution. - Pas de dépendance circulaire entre contexts. SSA peut appeler Wallet, mais
Wallet ne connaît pas SSA. La rupture de cycle se vérifie sur les
usede chaqueApp\Service\*. - Un repository ne sort jamais de son context.
SolidoumeItemRepositoryn'est injecté que dansApp\Service\Ssa\*. Si SEPA a besoin d'une info SSA, il passe par un service du context SSA (Application Service), jamais par le repo directement. - Les entités possédées par un context ne sont pas modifiées ailleurs.
Personne en dehors de SEPA ne fait
$mandate->setStatus(...). Personne en dehors du Wallet ne fait$account->applyOperation(...). - Le BOffice est en lecture seule sur le Wallet. L'accès se fait
via
BofficeApiClient(HTTP), pas via injection de repos. Cela garantit qu'un futur déploiement BOffice séparé reste possible (souveraineté, sobriété).
Aucun $em->createQuery(...) ou QueryBuilder hors d'un repository du
context propriétaire. Un Controller, un Admin Sonata, un Service applicatif ou une
Command qui a besoin d'une requête métier appelle une méthode find* nommée du
repo concerné. Cette règle est non négociable : elle conditionne la possibilité de
refactorer un context sans casser les autres.
Anti-patterns observés (à corriger en priorité)
- God service. Un seul service qui touche Wallet + SEPA + SSA + Notifications. Signe : plus de 6 dépendances injectées, plus de 300 lignes, méthodes qui font deux choses. À découper en services applicatifs par context.
- Accès cross-context via repo direct. Exemple anti-pattern :
un
SsaSubscriberqui injecteSepaMandateRepositoryet écrit$mandate->setStatus(...). Doit passer parSepaMandateService::revoke($mandate, $reason). - Logique métier dans un Admin Sonata. Calcul de cotisation, génération
d'UMR, création d'
OperationdansprePersist()d'unAbstractAdmin. La logique appartient au service applicatif du context ; l'Admin n'orchestre que l'I/O et délègue. - EventSubscriber qui flush. Un subscriber Doctrine lifecycle
(
postPersist,postUpdate) qui appelle$em->flush()casse l'atomicité. Voir conventions §atomicité financière. - Notification synchrone codée en dur.
$this->mailer->send(...)directement dans un service métier (SEPA, SSA…). À remplacer par un event écouté côtéApp\EventListener\EmailListenerouFluxNotificationService. - Mélange Adhésion / Wallet dans un Form. Un
FormTypequi crée à la fois l'Adherentet l'AccountAdherent. Le compte se crée tout seul via lepostPersistdeAccountableObject: voir domaine §comptes.
Où ranger un nouveau bout de code ?
Arbre de décision rapide :
1. Le code touche un solde, une Operation, un Flux ?
→ context Wallet. Service dans src/Service/, méthode find* dans Repository/.
2. Le code parle de mandat, d'IBAN, de prélèvement récurrent ?
→ context SEPA. src/Service/Sepa/*.
3. Le code parle de fonds Solidoume, cotisation SSA, redistribution ?
→ context SSA. src/Service/Ssa/* (et Strategy/ pour la stratégie).
4. Le code parle d'inscription, cotisation annuelle MLC, statut adhérent/prestataire ?
→ context Adhésion. src/Service/Subscription/*.
5. Le code parle de HelloAsso / PayZen / webhook / token de paiement ?
→ context Paiement externe. src/Service/HelloAsso*.php, PaymentDataManager.
6. Le code parle de ticket SAV ?
→ context BOffice. src/Service/BofficeApiClient.
7. Le code envoie un email ?
→ context Notifications. Écouter un event, ne pas appeler un context métier.
Évolutions visées
- Namespaces par context. Aujourd'hui
App\Entity\est plat : cibleApp\Wallet\Entity\,App\Sepa\Entity\,App\Ssa\Entity\… Migration progressive, mapping Doctrine inchangé. - Application Services exposés explicitement. Chaque context exposera une
interface
*ServiceInterface(déjà fait pour SEPA / MLC / SSA Former Participant) ; les autres contexts ne dépendent que de l'interface. - Events typés. Migrer
MLCEvents(string constants) vers des classes d'events PHP 8.1+ avec payloads typés (cohérent avec les enums du domaine). - Bus de message Symfony Messenger pour les opérations asynchrones (envoi d'email, génération de PDF). Pas pour les opérations financières : elles restent synchrones et transactionnelles.