From 97301dcd6c3db0e880f88259d91490a171fe2587 Mon Sep 17 00:00:00 2001 From: THOLOT DECHENE Matthieu Date: Wed, 3 Jun 2026 09:44:31 +0000 Subject: [PATCH] =?UTF-8?q?refactor(commercial)=20:=20d=C3=A9coupler=20l'h?= =?UTF-8?q?ydratation=20des=20collections=20de=20la=20s=C3=A9lection=20cli?= =?UTF-8?q?ents=20(ERP-100)=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Contexte Issu de la review ERP-62 (#44). `DoctrineClientRepository::createListQueryBuilder()` portait 3 `leftJoin+addSelect` to-many imbriqués (`categories × addresses × addresses.sites`) **partagés** entre : - la **liste paginée** (`ClientProvider`) — bornée, OK ; - l'**export XLSX** et **`?pagination=false`** — `getResult()` sans pagination → hydratation du **produit cartésien sur tout le référentiel** (1 client à 5 cat × 4 adr × 3 sites = 60 lignes SQL, × N clients). Défaut d'altitude : un « QueryBuilder de liste » (contrat = filtres) imposait une stratégie d'hydratation à tout appelant. ## Changements - **`createListQueryBuilder()`** redevient **filtres + tri seuls** — conforme au contrat de l'interface. - Nouvelle méthode **`hydrateListCollections(array $clients)`** : recharge les collections en **2 requêtes `WHERE id IN(...)` séparées** (catégories d'un côté, adresses+sites de l'autre) via l'identity map Doctrine. Casse le triple cartésien en `cat + (addr × site)`. - **3 appelants** branchés sur cette stratégie unique : - liste paginée : `fetchJoinCollection: false` (COUNT simple) + hydratation de la page ; - `?pagination=false` : hydratation après `getResult()` ; - export XLSX : hydratation après `getResult()`. ## Tests - `make test` : **465 OK**. - Nouveau test `ClientExportControllerTest::testExportPopulatesCategoryAndSiteColumns` : garde-fou sur les valeurs Catégories/Sites de l'export (qu'un oubli d'hydratation rendrait silencieusement vides). - `php-cs-fixer` : 0 correction. ## Notes - Benchmark « 1000+ clients » non exécuté (pas de jeu de données à cette échelle en dev) ; le cartésien est supprimé structurellement. - `addr × site` reste un join imbriqué (inévitable pour agréger les sites par adresse), désormais non multiplié par les catégories. Closes ERP-100. --------- Co-authored-by: admin malio Co-authored-by: Matthieu Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Starseed/pulls/50 Co-authored-by: THOLOT DECHENE Matthieu Co-committed-by: THOLOT DECHENE Matthieu --- .../Repository/ClientRepositoryInterface.php | 21 ++++++++ .../State/Provider/ClientProvider.php | 19 +++++-- .../Controller/ClientExportController.php | 5 ++ .../Doctrine/DoctrineClientRepository.php | 54 +++++++++++++++---- .../Api/ClientExportControllerTest.php | 35 ++++++++++++ 5 files changed, 120 insertions(+), 14 deletions(-) diff --git a/src/Module/Commercial/Domain/Repository/ClientRepositoryInterface.php b/src/Module/Commercial/Domain/Repository/ClientRepositoryInterface.php index 2bc081b..8bdafc7 100644 --- a/src/Module/Commercial/Domain/Repository/ClientRepositoryInterface.php +++ b/src/Module/Commercial/Domain/Repository/ClientRepositoryInterface.php @@ -33,6 +33,12 @@ interface ClientRepositoryInterface * la liste paginee (ClientProvider) et l'export (ClientExportController) * partagent strictement la meme logique de selection. * + * Contrat = SELECTION uniquement (filtres + tri). Aucun fetch-join to-many : + * l'hydratation des collections affichees est une decision de l'appelant + * (cf. {@see self::hydrateListCollections()}), pour ne pas imposer le cout + * d'un produit cartesien a un consommateur qui ne filtrerait/compterait que + * (ERP-100). + * * @param list $categoryCodes * @param list $siteIds */ @@ -43,4 +49,19 @@ interface ClientRepositoryInterface array $siteIds = [], bool $archivedOnly = false, ): QueryBuilder; + + /** + * Hydrate en lot les collections affichees par le repertoire (categories, + * adresses et leurs sites) sur un jeu de clients DEJA charges, via l'identity + * map Doctrine (memes instances). A appeler apres une selection bornee (page + * courante ou jeu d'export) pour eviter le N+1 a la serialisation, sans + * imposer de fetch-join au QueryBuilder de selection (ERP-100). + * + * Charge les categories et les adresses/sites en DEUX requetes distinctes + * (et non un triple fetch-join) pour ne pas multiplier categories x adresses + * x sites en un seul produit cartesien. + * + * @param list $clients + */ + public function hydrateListCollections(array $clients): void; } diff --git a/src/Module/Commercial/Infrastructure/ApiPlatform/State/Provider/ClientProvider.php b/src/Module/Commercial/Infrastructure/ApiPlatform/State/Provider/ClientProvider.php index 7c47760..1d90cef 100644 --- a/src/Module/Commercial/Infrastructure/ApiPlatform/State/Provider/ClientProvider.php +++ b/src/Module/Commercial/Infrastructure/ApiPlatform/State/Provider/ClientProvider.php @@ -83,8 +83,13 @@ final class ClientProvider implements ProviderInterface // Echappatoire ?pagination=false : collection complete sans Paginator // (cf. convention ERP-72 — utile pour un