(clients ayant >= 1 categorie de ce type) ; * - pagination obligatoire (convention Starseed ERP-72) : Paginator ORM ; * echappatoire ?pagination=false pour alimenter un cote front). if (!$this->pagination->isEnabled($operation, $context)) { // @var list $result return $qb->getQuery()->getResult(); } $limit = $this->pagination->getLimit($operation, $context); $page = max(1, $this->pagination->getPage($context)); $offset = ($page - 1) * $limit; $qb->setFirstResult($offset)->setMaxResults($limit); // fetchJoinCollection: true pour un COUNT correct des que des JOINs // to-many seront ajoutes (sous-collections embarquees en detail). return new Paginator(new DoctrinePaginator($qb->getQuery(), fetchJoinCollection: true)); } /** * @param array $uriVariables */ private function provideItem(array $uriVariables): ?Client { $id = $uriVariables['id'] ?? null; if (!is_int($id) && !(is_string($id) && ctype_digit($id))) { return null; } $client = $this->repository->findById((int) $id); if (null === $client) { return null; } // Soft-delete : jamais expose au M1 (HP-M2-1) — 404 via retour null. // Les archives restent visibles en detail (consultation + restauration). if (null !== $client->getDeletedAt()) { return null; } return $client; } /** * Recherche fuzzy insensible a la casse sur companyName + lastName + email. * Les metacaracteres LIKE (%, _, \) saisis sont echappes pour rester * litteraux. */ private function applySearch(QueryBuilder $qb, mixed $search): void { if (!is_string($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 type donne. * Sous-requete IN (plutot qu'un JOIN sur la collection M2M) pour ne pas * perturber le DISTINCT / ORDER BY de la requete paginee principale. */ private function applyCategoryType(QueryBuilder $qb, mixed $categoryType): void { if (!is_string($categoryType) || '' === trim($categoryType)) { return; } // Sous-requete construite via l'EntityManager (et non // $repository->createQueryBuilder()) : createQueryBuilder() n'est pas // declaree sur ClientRepositoryInterface, l'appeler exposerait un detail // d'implementation Doctrine hors du contrat (fuite d'abstraction). $sub = $this->em->createQueryBuilder() ->select('c2.id') ->from(Client::class, 'c2') ->join('c2.categories', 'cat2') ->join('cat2.categoryType', 'ct2') ->where('ct2.code = :categoryType') ; $qb->andWhere($qb->expr()->in('c.id', $sub->getDQL())) ->setParameter('categoryType', trim($categoryType)) ; } /** * Lit un flag booleen issu des query params. Accepte true / "true" / "1". */ private function readBool(mixed $raw): bool { if (is_bool($raw)) { return $raw; } return is_string($raw) && in_array(strtolower($raw), ['true', '1'], true); } }