<?phpnamespace App\Repository\Cvs;use App\Entity\Cvs\Jobs;use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;use Doctrine\Persistence\ManagerRegistry;/** * @extends ServiceEntityRepository<Jobs> * * @method Jobs|null find($id, $lockMode = null, $lockVersion = null) * @method Jobs|null findOneBy(array $criteria, array $orderBy = null) * @method Jobs[] findAll() * @method Jobs[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */class JobsRepository extends ServiceEntityRepository{ public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Jobs::class); } /** * Recherche des offres par mots-clés, filtrées par locale, avec pagination. * Si $cityKeyword est fourni, contraint les résultats à matcher la ville (en plus du query). * * @param string $query Mots-clés de recherche * @param string $locale Langue (fr, en) * @param int $offset Décalage pour la pagination * @param int $limit Nombre de résultats * @param string|null $cityKeyword Si fourni, le résultat doit aussi matcher j.city LIKE %cityKeyword% * @return Jobs[] */ public function searchJobs(string $query = '', string $locale = 'fr', int $offset = 0, int $limit = 10, ?string $cityKeyword = null): array { $qb = $this->createQueryBuilder('j') ->where('j.online = :online') ->andWhere('j.locale = :locale') ->setParameter('online', true) ->setParameter('locale', $locale) ->orderBy('j.createdAt', 'DESC') ->setFirstResult($offset) ->setMaxResults($limit); if (!empty(trim($query))) { $terms = array_filter(explode(' ', trim($query)), function($t) { return strlen($t) >= 2; }); foreach ($terms as $i => $term) { $param = 'term' . $i; $qb->andWhere( '(j.jobTitle LIKE :' . $param . ' OR j.companyName LIKE :' . $param . ' OR j.category LIKE :' . $param . ' OR j.city LIKE :' . $param . ' OR j.requiredSkills LIKE :' . $param . ')' ) ->setParameter($param, '%' . $term . '%'); } } // Contrainte ville : doit matcher la ville indépendamment du query if ($cityKeyword !== null && trim($cityKeyword) !== '') { $qb->andWhere('j.city LIKE :cityKw') ->setParameter('cityKw', '%' . trim($cityKeyword) . '%'); } return $qb->getQuery()->getResult(); } /** * Compte le total d'offres correspondant à la recherche (pour pagination). * Si $cityKeyword est fourni, contraint le compte à la ville. * * @param string $query Mots-clés de recherche * @param string $locale Langue (fr, en) * @param string|null $cityKeyword * @return int */ public function countSearchJobs(string $query = '', string $locale = 'fr', ?string $cityKeyword = null): int { $qb = $this->createQueryBuilder('j') ->select('COUNT(j.id)') ->where('j.online = :online') ->andWhere('j.locale = :locale') ->setParameter('online', true) ->setParameter('locale', $locale); if (!empty(trim($query))) { $terms = array_filter(explode(' ', trim($query)), function($t) { return strlen($t) >= 2; }); foreach ($terms as $i => $term) { $param = 'term' . $i; $qb->andWhere( '(j.jobTitle LIKE :' . $param . ' OR j.companyName LIKE :' . $param . ' OR j.category LIKE :' . $param . ' OR j.city LIKE :' . $param . ' OR j.requiredSkills LIKE :' . $param . ')' ) ->setParameter($param, '%' . $term . '%'); } } if ($cityKeyword !== null && trim($cityKeyword) !== '') { $qb->andWhere('j.city LIKE :cityKw') ->setParameter('cityKw', '%' . trim($cityKeyword) . '%'); } return (int) $qb->getQuery()->getSingleScalarResult(); } /** * Recherche des offres par keyword exact dans le champ keywords uniquement * * @param string $keyword Le mot-clé exact à chercher * @param string $locale Langue (fr, en) * @param int $limit Nombre max de résultats * @return Jobs[] */ public function findByKeyword(string $keyword, string $locale = 'fr', int $limit = 4): array { $keyword = trim($keyword); if (empty($keyword)) { return []; } return $this->createQueryBuilder('j') ->where('j.online = :online') ->andWhere('j.locale = :locale') ->andWhere('j.keywords LIKE :kw') ->setParameter('online', true) ->setParameter('locale', $locale) ->setParameter('kw', '%' . $keyword . '%') ->orderBy('j.createdAt', 'DESC') ->setMaxResults($limit) ->getQuery() ->getResult(); } /** * Recommande des offres similaires aux offres likées par le candidat * (même catégorie ou mêmes compétences requises) * * @param array $likedJobIds IDs des jobs déjà likés * @param int $limit Nombre max de résultats * @return Jobs[] */ public function findRecommendedByLikedJobs(array $likedJobIds, int $limit = 10): array { if (empty($likedJobIds)) { return []; } // Récupérer les catégories et compétences des jobs likés $likedJobs = $this->createQueryBuilder('lj') ->select('lj.category, lj.requiredSkills') ->where('lj.id IN (:ids)') ->setParameter('ids', $likedJobIds) ->getQuery() ->getResult(); $categories = []; $skillTerms = []; foreach ($likedJobs as $lj) { if (!empty($lj['category'])) { $categories[] = $lj['category']; } if (!empty($lj['requiredSkills'])) { // Extraire quelques mots-clés des compétences $words = array_filter(explode(',', $lj['requiredSkills']), function($w) { return strlen(trim($w)) >= 3; }); foreach (array_slice($words, 0, 3) as $word) { $skillTerms[] = trim($word); } } } $categories = array_unique($categories); $skillTerms = array_unique(array_slice($skillTerms, 0, 5)); $qb = $this->createQueryBuilder('j') ->where('j.online = :online') ->andWhere('j.id NOT IN (:excludeIds)') ->setParameter('online', true) ->setParameter('excludeIds', $likedJobIds); // Construire les conditions OR pour catégories et skills $orConditions = []; $i = 0; foreach ($categories as $cat) { $param = 'cat' . $i; $orConditions[] = 'j.category = :' . $param; $qb->setParameter($param, $cat); $i++; } foreach ($skillTerms as $skill) { $param = 'skill' . $i; $orConditions[] = 'j.requiredSkills LIKE :' . $param; $qb->setParameter($param, '%' . $skill . '%'); $i++; } if (empty($orConditions)) { return []; } $qb->andWhere('(' . implode(' OR ', $orConditions) . ')') ->orderBy('j.createdAt', 'DESC') ->setMaxResults($limit); return $qb->getQuery()->getResult(); } /** * Recommande des offres provenant des entreprises likées par le candidat * * @param array $likedEnterpriseIds IDs des entreprises likées * @param array $excludeJobIds IDs des jobs à exclure (déjà likés) * @param int $limit Nombre max de résultats * @return Jobs[] */ public function findRecommendedByLikedEnterprises(array $likedEnterpriseIds, array $excludeJobIds = [], int $limit = 10): array { if (empty($likedEnterpriseIds)) { return []; } $qb = $this->createQueryBuilder('j') ->where('j.online = :online') ->andWhere('j.enterprise IN (:enterpriseIds)') ->setParameter('online', true) ->setParameter('enterpriseIds', $likedEnterpriseIds); if (!empty($excludeJobIds)) { $qb->andWhere('j.id NOT IN (:excludeIds)') ->setParameter('excludeIds', $excludeJobIds); } $qb->orderBy('j.createdAt', 'DESC') ->setMaxResults($limit); return $qb->getQuery()->getResult(); } /** * Trouve des offres similaires à une offre donnée * (même catégorie, mêmes compétences, même ville, ou même entreprise) * * @param Jobs $job L'offre de référence * @param int $limit Nombre max de résultats * @return Jobs[] */ public function findSimilarJobs(Jobs $job, int $limit = 5): array { $qb = $this->createQueryBuilder('j') ->where('j.online = :online') ->andWhere('j.locale = :locale') ->andWhere('j.id != :currentId') ->setParameter('online', true) ->setParameter('locale', $job->getLocale()) ->setParameter('currentId', $job->getId()); $orConditions = []; $i = 0; // Même catégorie if (!empty($job->getCategory())) { $param = 'cat' . $i; $orConditions[] = 'j.category = :' . $param; $qb->setParameter($param, $job->getCategory()); $i++; } // Même ville if (!empty($job->getCity())) { $param = 'city' . $i; $orConditions[] = 'j.city = :' . $param; $qb->setParameter($param, $job->getCity()); $i++; } // Compétences similaires (mots-clés extraits de requiredSkills) if (!empty($job->getRequiredSkills())) { $skills = array_filter(explode(',', $job->getRequiredSkills()), function($w) { return strlen(trim($w)) >= 3; }); foreach (array_slice($skills, 0, 3) as $skill) { $param = 'skill' . $i; $orConditions[] = 'j.requiredSkills LIKE :' . $param; $qb->setParameter($param, '%' . trim($skill) . '%'); $i++; } } // Mots-clés du titre similaires if (!empty($job->getJobTitle())) { $titleWords = array_filter(explode(' ', $job->getJobTitle()), function($w) { return strlen(trim($w)) >= 4; }); foreach (array_slice($titleWords, 0, 2) as $word) { $param = 'title' . $i; $orConditions[] = 'j.jobTitle LIKE :' . $param; $qb->setParameter($param, '%' . trim($word) . '%'); $i++; } } if (empty($orConditions)) { // Fallback : retourner les offres les plus récentes $qb->orderBy('j.createdAt', 'DESC') ->setMaxResults($limit); return $qb->getQuery()->getResult(); } $qb->andWhere('(' . implode(' OR ', $orConditions) . ')') ->orderBy('j.createdAt', 'DESC') ->setMaxResults($limit); $results = $qb->getQuery()->getResult(); // Si pas assez de résultats, compléter avec des offres récentes if (count($results) < $limit) { $excludeIds = array_map(function($j) { return $j->getId(); }, $results); $excludeIds[] = $job->getId(); $complement = $this->createQueryBuilder('j') ->where('j.online = :online') ->andWhere('j.locale = :locale') ->andWhere('j.id NOT IN (:excludeIds)') ->setParameter('online', true) ->setParameter('locale', $job->getLocale()) ->setParameter('excludeIds', $excludeIds) ->orderBy('j.createdAt', 'DESC') ->setMaxResults($limit - count($results)) ->getQuery() ->getResult(); $results = array_merge($results, $complement); } return $results; } /** * Compte le nombre total d'offres en ligne sur tout le site (toutes locales confondues) * * @return int */ public function countAllOnlineJobs(): int { return (int) $this->createQueryBuilder('j') ->select('COUNT(j.id)') ->where('j.online = :online') ->setParameter('online', true) ->getQuery() ->getSingleScalarResult(); }}