97301dcd6c
Auto Tag Develop / tag (push) Successful in 7s
## 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 <malio@yuno.malio.fr> Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #50 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
68 lines
2.9 KiB
PHP
68 lines
2.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Commercial\Domain\Repository;
|
|
|
|
use App\Module\Commercial\Domain\Entity\Client;
|
|
use Doctrine\ORM\QueryBuilder;
|
|
|
|
interface ClientRepositoryInterface
|
|
{
|
|
public function findById(int $id): ?Client;
|
|
|
|
public function save(Client $client): void;
|
|
|
|
/**
|
|
* Construit un QueryBuilder de liste pour le repertoire clients.
|
|
* - Exclut toujours les clients soft-deletes (deleted_at IS NOT NULL, RG-1.24).
|
|
* - Archivage (RG-1.25) :
|
|
* - $archivedOnly = true -> uniquement les archives (is_archived = true) ;
|
|
* - sinon $includeArchived = true -> actifs + archives (echappatoire) ;
|
|
* - sinon (defaut) -> uniquement les actifs (is_archived = false).
|
|
* $archivedOnly a la priorite sur $includeArchived.
|
|
* - Tri par defaut : companyName ASC (RG-1.26).
|
|
* - $search : recherche fuzzy insensible a la casse sur companyName +
|
|
* lastName + email (metacaracteres LIKE echappes). Ignore si null/vide.
|
|
* - $categoryCodes : restreint aux clients possedant au moins une categorie
|
|
* dont le code est dans la liste (OR — ERP-78). Liste vide = pas de filtre.
|
|
* - $siteIds : restreint aux clients ayant au moins une adresse rattachee a
|
|
* l'un des sites donnes (OR — RG-1.10). Liste vide = pas de filtre.
|
|
*
|
|
* Filtrage centralise ICI (et non dans les providers/controllers) pour que
|
|
* 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<string> $categoryCodes
|
|
* @param list<int> $siteIds
|
|
*/
|
|
public function createListQueryBuilder(
|
|
bool $includeArchived = false,
|
|
?string $search = null,
|
|
array $categoryCodes = [],
|
|
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<Client> $clients
|
|
*/
|
|
public function hydrateListCollections(array $clients): void;
|
|
}
|