*/ class DoctrineClientRepository extends ServiceEntityRepository implements ClientRepositoryInterface { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Client::class); } public function findById(int $id): ?Client { return $this->find($id); } public function save(Client $client): void { $this->getEntityManager()->persist($client); $this->getEntityManager()->flush(); } public function createListQueryBuilder( bool $includeArchived = false, ?string $search = null, ?string $categoryCode = null, ): QueryBuilder { $qb = $this->createQueryBuilder('c') // Jointures + addSelect pour hydrater en une seule requete les // collections affichees par le Repertoire (colonnes Catégories / // Site(s)) : sans cela, la serialisation declenche un N+1 (une // requete par client, puis par adresse). Le Paginator ORM // (fetchJoinCollection: true, cf. ClientProvider) gere le COUNT // malgre ces jointures to-many. ->leftJoin('c.categories', 'cat')->addSelect('cat') ->leftJoin('c.addresses', 'addr')->addSelect('addr') ->leftJoin('addr.sites', 'site')->addSelect('site') ->andWhere('c.deletedAt IS NULL') ->orderBy('c.companyName', 'ASC') ; if (!$includeArchived) { $qb->andWhere('c.isArchived = false'); } $this->applySearch($qb, $search); $this->applyCategoryCode($qb, $categoryCode); return $qb; } /** * Recherche fuzzy insensible a la casse sur companyName + lastName + email. * Les metacaracteres LIKE (%, _, \) saisis sont echappes pour rester * litteraux. */ private function applySearch(QueryBuilder $qb, ?string $search): void { if (null === $search || '' === trim($search)) { return; } $escaped = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], trim($search)); $pattern = '%'.mb_strtolower($escaped, 'UTF-8').'%'; $qb->andWhere( 'LOWER(c.companyName) LIKE :search ' .'OR LOWER(c.lastName) LIKE :search ' .'OR LOWER(c.email) LIKE :search', )->setParameter('search', $pattern); } /** * Restreint aux clients possedant au moins une categorie du code donne * (ERP-78 : filtrage par code de Category, plus par type). Alimente notamment * les selects « distributeur » (categoryCode=DISTRIBUTEUR) et « courtier » * (COURTIER) cote front (RG-1.03). Sous-requete IN (plutot qu'un JOIN sur la * collection M2M) pour ne pas perturber le DISTINCT / ORDER BY principal. */ private function applyCategoryCode(QueryBuilder $qb, ?string $categoryCode): void { if (null === $categoryCode || '' === trim($categoryCode)) { return; } $sub = $this->getEntityManager()->createQueryBuilder() ->select('c2.id') ->from(Client::class, 'c2') ->join('c2.categories', 'cat2') ->where('cat2.code = :categoryCode') ; $qb->andWhere($qb->expr()->in('c.id', $sub->getDQL())) ->setParameter('categoryCode', trim($categoryCode)) ; } }