src/Repository/Cvs/JobsRepository.php line 398

Open in your IDE?
  1. <?php
  2. namespace App\Repository\Cvs;
  3. use App\Entity\Cvs\Jobs;
  4. use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
  5. use Doctrine\Persistence\ManagerRegistry;
  6. /**
  7.  * @extends ServiceEntityRepository<Jobs>
  8.  *
  9.  * @method Jobs|null find($id, $lockMode = null, $lockVersion = null)
  10.  * @method Jobs|null findOneBy(array $criteria, array $orderBy = null)
  11.  * @method Jobs[]    findAll()
  12.  * @method Jobs[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
  13.  */
  14. class JobsRepository extends ServiceEntityRepository
  15. {
  16.     public function __construct(ManagerRegistry $registry)
  17.     {
  18.         parent::__construct($registryJobs::class);
  19.     }
  20.     /**
  21.      * Recherche des offres par mots-clés, filtrées par locale, avec pagination.
  22.      * Si $cityKeyword est fourni, contraint les résultats à matcher la ville (en plus du query).
  23.      *
  24.      * @param string $query Mots-clés de recherche
  25.      * @param string $locale Langue (fr, en)
  26.      * @param int $offset Décalage pour la pagination
  27.      * @param int $limit Nombre de résultats
  28.      * @param string|null $cityKeyword Si fourni, le résultat doit aussi matcher j.city LIKE %cityKeyword%
  29.      * @return Jobs[]
  30.      */
  31.     public function searchJobs(string $query ''string $locale 'fr'int $offset 0int $limit 10, ?string $cityKeyword null): array
  32.     {
  33.         $qb $this->createQueryBuilder('j')
  34.             ->where('j.online = :online')
  35.             ->andWhere('j.locale = :locale')
  36.             ->setParameter('online'true)
  37.             ->setParameter('locale'$locale)
  38.             ->orderBy('j.createdAt''DESC')
  39.             ->setFirstResult($offset)
  40.             ->setMaxResults($limit);
  41.         if (!empty(trim($query))) {
  42.             $terms array_filter(explode(' 'trim($query)), function($t) { return strlen($t) >= 2; });
  43.             foreach ($terms as $i => $term) {
  44.                 $param 'term' $i;
  45.                 $qb->andWhere(
  46.                     '(j.jobTitle LIKE :' $param .
  47.                     ' OR j.companyName LIKE :' $param .
  48.                     ' OR j.category LIKE :' $param .
  49.                     ' OR j.city LIKE :' $param .
  50.                     ' OR j.requiredSkills LIKE :' $param ')'
  51.                 )
  52.                     ->setParameter($param'%' $term '%');
  53.             }
  54.         }
  55.         // Contrainte ville : doit matcher la ville indépendamment du query
  56.         if ($cityKeyword !== null && trim($cityKeyword) !== '') {
  57.             $qb->andWhere('j.city LIKE :cityKw')
  58.                 ->setParameter('cityKw''%' trim($cityKeyword) . '%');
  59.         }
  60.         return $qb->getQuery()->getResult();
  61.     }
  62.     /**
  63.      * Compte le total d'offres correspondant à la recherche (pour pagination).
  64.      * Si $cityKeyword est fourni, contraint le compte à la ville.
  65.      *
  66.      * @param string $query Mots-clés de recherche
  67.      * @param string $locale Langue (fr, en)
  68.      * @param string|null $cityKeyword
  69.      * @return int
  70.      */
  71.     public function countSearchJobs(string $query ''string $locale 'fr', ?string $cityKeyword null): int
  72.     {
  73.         $qb $this->createQueryBuilder('j')
  74.             ->select('COUNT(j.id)')
  75.             ->where('j.online = :online')
  76.             ->andWhere('j.locale = :locale')
  77.             ->setParameter('online'true)
  78.             ->setParameter('locale'$locale);
  79.         if (!empty(trim($query))) {
  80.             $terms array_filter(explode(' 'trim($query)), function($t) { return strlen($t) >= 2; });
  81.             foreach ($terms as $i => $term) {
  82.                 $param 'term' $i;
  83.                 $qb->andWhere(
  84.                     '(j.jobTitle LIKE :' $param .
  85.                     ' OR j.companyName LIKE :' $param .
  86.                     ' OR j.category LIKE :' $param .
  87.                     ' OR j.city LIKE :' $param .
  88.                     ' OR j.requiredSkills LIKE :' $param ')'
  89.                 )
  90.                     ->setParameter($param'%' $term '%');
  91.             }
  92.         }
  93.         if ($cityKeyword !== null && trim($cityKeyword) !== '') {
  94.             $qb->andWhere('j.city LIKE :cityKw')
  95.                 ->setParameter('cityKw''%' trim($cityKeyword) . '%');
  96.         }
  97.         return (int) $qb->getQuery()->getSingleScalarResult();
  98.     }
  99.     /**
  100.      * Retourne des offres aléatoires en ligne, filtrées par locale.
  101.      * Utilisé pour l'affichage par défaut sur la homepage (avant toute recherche).
  102.      *
  103.      * @param string|null $locale Langue de filtrage (fr, en). Si null, toutes locales confondues.
  104.      * @param int $offset Décalage pour la pagination
  105.      * @param int $limit Nombre max de résultats
  106.      * @param int|null $seed Seed pour rendre l'ordre aléatoire reproductible
  107.      *                       (utile pour que la pagination reste cohérente sur une même session
  108.      *                       sans réafficher les mêmes offres à chaque scroll)
  109.      * @return Jobs[]
  110.      */
  111.     public function findRandomJobs(?string $locale nullint $offset 0int $limit 10, ?int $seed null): array
  112.     {
  113.         // Récupère les IDs des offres en ligne (filtrées par locale si fournie)
  114.         $qb $this->createQueryBuilder('j')
  115.             ->select('j.id')
  116.             ->where('j.online = :online')
  117.             ->setParameter('online'true);
  118.         if ($locale !== null) {
  119.             $qb->andWhere('j.locale = :locale')
  120.                 ->setParameter('locale'$locale);
  121.         }
  122.         $ids $qb->getQuery()->getScalarResult();
  123.         if (empty($ids)) {
  124.             return [];
  125.         }
  126.         $ids array_map(function($row) { return (int) $row['id']; }, $ids);
  127.         // Mélange Fisher-Yates (correct, contrairement à usort + rand)
  128.         if ($seed !== null) {
  129.             mt_srand($seed);
  130.         }
  131.         $count count($ids);
  132.         for ($i $count 1$i 0$i--) {
  133.             $j mt_rand(0$i);
  134.             $tmp $ids[$i];
  135.             $ids[$i] = $ids[$j];
  136.             $ids[$j] = $tmp;
  137.         }
  138.         if ($seed !== null) {
  139.             mt_srand(); // reset à un seed aléatoire pour la suite
  140.         }
  141.         // Slice selon offset / limit
  142.         $pageIds array_slice($ids$offset$limit);
  143.         if (empty($pageIds)) {
  144.             return [];
  145.         }
  146.         // Récupère les entités
  147.         $jobs $this->createQueryBuilder('j')
  148.             ->where('j.id IN (:ids)')
  149.             ->setParameter('ids'$pageIds)
  150.             ->getQuery()
  151.             ->getResult();
  152.         // Réordonne les résultats dans l'ordre des IDs aléatoires
  153.         $indexed = [];
  154.         foreach ($jobs as $job) {
  155.             $indexed[$job->getId()] = $job;
  156.         }
  157.         $ordered = [];
  158.         foreach ($pageIds as $id) {
  159.             if (isset($indexed[$id])) {
  160.                 $ordered[] = $indexed[$id];
  161.             }
  162.         }
  163.         return $ordered;
  164.     }
  165.     /**
  166.      * Recherche des offres par keyword exact dans le champ keywords uniquement
  167.      *
  168.      * @param string $keyword Le mot-clé exact à chercher
  169.      * @param string $locale Langue (fr, en)
  170.      * @param int $limit Nombre max de résultats
  171.      * @return Jobs[]
  172.      */
  173.     public function findByKeyword(string $keywordstring $locale 'fr'int $limit 4): array
  174.     {
  175.         $keyword trim($keyword);
  176.         if (empty($keyword)) {
  177.             return [];
  178.         }
  179.         return $this->createQueryBuilder('j')
  180.             ->where('j.online = :online')
  181.             ->andWhere('j.locale = :locale')
  182.             ->andWhere('j.keywords LIKE :kw')
  183.             ->setParameter('online'true)
  184.             ->setParameter('locale'$locale)
  185.             ->setParameter('kw''%' $keyword '%')
  186.             ->orderBy('j.createdAt''DESC')
  187.             ->setMaxResults($limit)
  188.             ->getQuery()
  189.             ->getResult();
  190.     }
  191.     /**
  192.      * Recommande des offres similaires aux offres likées par le candidat
  193.      * (même catégorie ou mêmes compétences requises)
  194.      *
  195.      * @param array $likedJobIds IDs des jobs déjà likés
  196.      * @param int $limit Nombre max de résultats
  197.      * @return Jobs[]
  198.      */
  199.     public function findRecommendedByLikedJobs(array $likedJobIdsint $limit 10): array
  200.     {
  201.         if (empty($likedJobIds)) {
  202.             return [];
  203.         }
  204.         // Récupérer les catégories et compétences des jobs likés
  205.         $likedJobs $this->createQueryBuilder('lj')
  206.             ->select('lj.category, lj.requiredSkills')
  207.             ->where('lj.id IN (:ids)')
  208.             ->setParameter('ids'$likedJobIds)
  209.             ->getQuery()
  210.             ->getResult();
  211.         $categories = [];
  212.         $skillTerms = [];
  213.         foreach ($likedJobs as $lj) {
  214.             if (!empty($lj['category'])) {
  215.                 $categories[] = $lj['category'];
  216.             }
  217.             if (!empty($lj['requiredSkills'])) {
  218.                 // Extraire quelques mots-clés des compétences
  219.                 $words array_filter(explode(','$lj['requiredSkills']), function($w) {
  220.                     return strlen(trim($w)) >= 3;
  221.                 });
  222.                 foreach (array_slice($words03) as $word) {
  223.                     $skillTerms[] = trim($word);
  224.                 }
  225.             }
  226.         }
  227.         $categories array_unique($categories);
  228.         $skillTerms array_unique(array_slice($skillTerms05));
  229.         $qb $this->createQueryBuilder('j')
  230.             ->where('j.online = :online')
  231.             ->andWhere('j.id NOT IN (:excludeIds)')
  232.             ->setParameter('online'true)
  233.             ->setParameter('excludeIds'$likedJobIds);
  234.         // Construire les conditions OR pour catégories et skills
  235.         $orConditions = [];
  236.         $i 0;
  237.         foreach ($categories as $cat) {
  238.             $param 'cat' $i;
  239.             $orConditions[] = 'j.category = :' $param;
  240.             $qb->setParameter($param$cat);
  241.             $i++;
  242.         }
  243.         foreach ($skillTerms as $skill) {
  244.             $param 'skill' $i;
  245.             $orConditions[] = 'j.requiredSkills LIKE :' $param;
  246.             $qb->setParameter($param'%' $skill '%');
  247.             $i++;
  248.         }
  249.         if (empty($orConditions)) {
  250.             return [];
  251.         }
  252.         $qb->andWhere('(' implode(' OR '$orConditions) . ')')
  253.             ->orderBy('j.createdAt''DESC')
  254.             ->setMaxResults($limit);
  255.         return $qb->getQuery()->getResult();
  256.     }
  257.     /**
  258.      * Recommande des offres provenant des entreprises likées par le candidat
  259.      *
  260.      * @param array $likedEnterpriseIds IDs des entreprises likées
  261.      * @param array $excludeJobIds IDs des jobs à exclure (déjà likés)
  262.      * @param int $limit Nombre max de résultats
  263.      * @return Jobs[]
  264.      */
  265.     public function findRecommendedByLikedEnterprises(array $likedEnterpriseIds, array $excludeJobIds = [], int $limit 10): array
  266.     {
  267.         if (empty($likedEnterpriseIds)) {
  268.             return [];
  269.         }
  270.         $qb $this->createQueryBuilder('j')
  271.             ->where('j.online = :online')
  272.             ->andWhere('j.enterprise IN (:enterpriseIds)')
  273.             ->setParameter('online'true)
  274.             ->setParameter('enterpriseIds'$likedEnterpriseIds);
  275.         if (!empty($excludeJobIds)) {
  276.             $qb->andWhere('j.id NOT IN (:excludeIds)')
  277.                 ->setParameter('excludeIds'$excludeJobIds);
  278.         }
  279.         $qb->orderBy('j.createdAt''DESC')
  280.             ->setMaxResults($limit);
  281.         return $qb->getQuery()->getResult();
  282.     }
  283.     /**
  284.      * Trouve des offres similaires à une offre donnée
  285.      * (même catégorie, mêmes compétences, même ville, ou même entreprise)
  286.      *
  287.      * @param Jobs $job L'offre de référence
  288.      * @param int $limit Nombre max de résultats
  289.      * @return Jobs[]
  290.      */
  291.     public function findSimilarJobs(Jobs $jobint $limit 5): array
  292.     {
  293.         $qb $this->createQueryBuilder('j')
  294.             ->where('j.online = :online')
  295.             ->andWhere('j.locale = :locale')
  296.             ->andWhere('j.id != :currentId')
  297.             ->setParameter('online'true)
  298.             ->setParameter('locale'$job->getLocale())
  299.             ->setParameter('currentId'$job->getId());
  300.         $orConditions = [];
  301.         $i 0;
  302.         // Même catégorie
  303.         if (!empty($job->getCategory())) {
  304.             $param 'cat' $i;
  305.             $orConditions[] = 'j.category = :' $param;
  306.             $qb->setParameter($param$job->getCategory());
  307.             $i++;
  308.         }
  309.         // Même ville
  310.         if (!empty($job->getCity())) {
  311.             $param 'city' $i;
  312.             $orConditions[] = 'j.city = :' $param;
  313.             $qb->setParameter($param$job->getCity());
  314.             $i++;
  315.         }
  316.         // Compétences similaires (mots-clés extraits de requiredSkills)
  317.         if (!empty($job->getRequiredSkills())) {
  318.             $skills array_filter(explode(','$job->getRequiredSkills()), function($w) {
  319.                 return strlen(trim($w)) >= 3;
  320.             });
  321.             foreach (array_slice($skills03) as $skill) {
  322.                 $param 'skill' $i;
  323.                 $orConditions[] = 'j.requiredSkills LIKE :' $param;
  324.                 $qb->setParameter($param'%' trim($skill) . '%');
  325.                 $i++;
  326.             }
  327.         }
  328.         // Mots-clés du titre similaires
  329.         if (!empty($job->getJobTitle())) {
  330.             $titleWords array_filter(explode(' '$job->getJobTitle()), function($w) {
  331.                 return strlen(trim($w)) >= 4;
  332.             });
  333.             foreach (array_slice($titleWords02) as $word) {
  334.                 $param 'title' $i;
  335.                 $orConditions[] = 'j.jobTitle LIKE :' $param;
  336.                 $qb->setParameter($param'%' trim($word) . '%');
  337.                 $i++;
  338.             }
  339.         }
  340.         if (empty($orConditions)) {
  341.             // Fallback : retourner les offres les plus récentes
  342.             $qb->orderBy('j.createdAt''DESC')
  343.                 ->setMaxResults($limit);
  344.             return $qb->getQuery()->getResult();
  345.         }
  346.         $qb->andWhere('(' implode(' OR '$orConditions) . ')')
  347.             ->orderBy('j.createdAt''DESC')
  348.             ->setMaxResults($limit);
  349.         $results $qb->getQuery()->getResult();
  350.         // Si pas assez de résultats, compléter avec des offres récentes
  351.         if (count($results) < $limit) {
  352.             $excludeIds array_map(function($j) { return $j->getId(); }, $results);
  353.             $excludeIds[] = $job->getId();
  354.             $complement $this->createQueryBuilder('j')
  355.                 ->where('j.online = :online')
  356.                 ->andWhere('j.locale = :locale')
  357.                 ->andWhere('j.id NOT IN (:excludeIds)')
  358.                 ->setParameter('online'true)
  359.                 ->setParameter('locale'$job->getLocale())
  360.                 ->setParameter('excludeIds'$excludeIds)
  361.                 ->orderBy('j.createdAt''DESC')
  362.                 ->setMaxResults($limit count($results))
  363.                 ->getQuery()
  364.                 ->getResult();
  365.             $results array_merge($results$complement);
  366.         }
  367.         return $results;
  368.     }
  369.     /**
  370.      * Compte le nombre total d'offres en ligne sur tout le site (toutes locales confondues)
  371.      *
  372.      * @return int
  373.      */
  374.     public function countAllOnlineJobs(): int
  375.     {
  376.         return (int) $this->createQueryBuilder('j')
  377.             ->select('COUNT(j.id)')
  378.             ->where('j.online = :online')
  379.             ->setParameter('online'true)
  380.             ->getQuery()
  381.             ->getSingleScalarResult();
  382.     }
  383. }