<?php
namespace App\Service\Scraper;
use App\Entity\Enum\FrenchRegion;
use App\Entity\Offer;
use App\Repository\OfferRepository;
use App\Repository\SocietyRepository;
use App\Repository\UserRepository;
use App\Service\CityExtractor;
use App\Service\Scraper\ScraperInterface;
use App\Service\SlugService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;
use DateTime;
class HaysScraper implements ScraperInterface
{
private $client;
private $entityManager;
private $societyRepository;
private $slugService;
private $userRepository;
private $offerRepository;
private $cityExtractor;
public function __construct(
EntityManagerInterface $entityManager,
SocietyRepository $societyRepository,
SlugService $slugService,
UserRepository $userRepository,
OfferRepository $offerRepository,
CityExtractor $cityExtractor
) {
$this->client = HttpClient::create();
$this->entityManager = $entityManager;
$this->societyRepository = $societyRepository;
$this->slugService = $slugService;
$this->userRepository = $userRepository;
$this->offerRepository = $offerRepository;
$this->cityExtractor = $cityExtractor;
}
public function supports(string $url): bool
{
// Logique pour vérifier si le scraper doit gérer cette URL
return strpos($url, 'https://mapi.hays.com/jobportalapi/int/s/fr/fr/jobportal/job/browse/v1/jobsweb') !== false;
}
public function scrape(string $url): array
{
$authData = $this->getToken();
$token = $authData['token'] ?? null;
$sessionId = $authData['sessionId'] ?? null;
// dump("Auth Data - Token: " . substr($token, 0, 10) . "..., SessionId: " . $sessionId);
$pageToken = "";
$data = [];
while (true) {
$response = $this->callApi($url, $pageToken, $token, $sessionId);
dump("Jobs found in this page: " . count($response['content']['jobs'] ?? []));
foreach ($response['content']['jobs'] ?? [] as $result) {
// $currentTitle = $result['nonFilterableCustomFields']['JobTitle']['values'][0] ?? 'N/A';
// dump("Checking: " . $currentTitle);
// if ($currentTitle === 'Ingénieur Bases de Données (DBA) H/F') {
// dd($result);
// }
$title = $result['nonFilterableCustomFields']['JobTitle']['values'][0];
$link = $result['applicationUrl'];
$createDate = $result['createDate'];
$date = DateTime::createFromFormat('Y-m-d', "{$createDate['year']}-{$createDate['month']}-{$createDate['day']}");
if ($this->isOlderThanOneMonth($date)) {
echo "date dépassé" . "\n";
return $data;
}
$typeContrat = $result['nonFilterableCustomFields']['xjobType']['values'][0];
switch ($typeContrat) {
case 'T':
$typeContrat = "CDD";
break;
case 'P':
$typeContrat = "CDI";
break;
case 'C':
$typeContrat = "Freelance";
break;
}
$input = $this->getCurrentRegionName($result['location']);
$location = "Toute la France";
$ville = null;
if ($this->getDepartementKey($result['location']) !== null) {
$location = $this->getDepartementKey($input);
} elseif ($this->getRegionKey($input) !== null) {
$location = $this->getRegionKey($input);
} else {
// Priorité spéciale pour Vitrolles
if (strpos(strtolower($input), 'vitrolles') !== false) {
$location = '13 Bouches-du-Rhône';
$ville = 'Vitrolles';
} else {
// Utiliser le CityExtractor service (qui a maintenant un timeout et un fallback)
$details = $this->cityExtractor->extractCityDetails($input);
if ($details['city'] !== null) {
$ville = $details['city'];
}
if ($details['postcode'] !== null) {
$location = $this->getDepartmentDetails($details['postcode']) ?? "Toute la France";
} elseif ($details['region'] !== null) {
$location = $details['region'];
// Si c'est le contexte de l'API Adresse, on prend le dernier élément si c'est une chaîne longue
if (strpos($location, ',') !== false) {
$contextParts = explode(", ", $location);
$location = end($contextParts);
}
}
}
}
$tjmMax = null;
$tjmMin = null;
$salaireMax = null;
$salaireMin = null;
if (array_key_exists('DaySalaryMax', $result['nonFilterableCustomFields'])) {
$tjmMax = intval($result['nonFilterableCustomFields']['DaySalaryMax']['values'][0]);
}
if (array_key_exists('DaySalaryMin', $result['nonFilterableCustomFields'])) {
$tjmMin = intval($result['nonFilterableCustomFields']['DaySalaryMin']['values'][0]);
}
if (array_key_exists('AnnualSalaryMax', $result['nonFilterableCustomFields'])) {
$salaireMax = intval($result['nonFilterableCustomFields']['AnnualSalaryMax']['values'][0]);
}
if (array_key_exists('AnnualSalaryMin', $result['nonFilterableCustomFields'])) {
$salaireMin = intval($result['nonFilterableCustomFields']['AnnualSalaryMin']['values'][0]);
}
$data[] = [
'title' => $title,
'link' => $link,
'publicationDate' => $date,
'contractTypes' => [$typeContrat],
'ville' => $ville != null ? $ville : null,
'location' => $location,
'salaire_min' => $salaireMin,
'salaire_max' => $salaireMax,
'tjm_min' => $tjmMin,
'tjm_max' => $tjmMax,
'teletravail' => 'A discuter',
'experience' => 'Selon profil',
'description' => $result['description']
];
}
}
return $data;
}
public function insertion(array $data): void
{
$existSlug = [];
foreach ($data as $offerData) {
$offer = new Offer();
if (!in_array('CDI', $offerData['contractTypes']) && ($offerData['salaire_max'] !== null || $offerData['salaire_min'] !== null)) {
$offerData['contractTypes'][] = 'CDI';
}
$offer->setContrats($offerData['contractTypes']);
$offer->setLocation($offerData['location']);
$offer->setVille($offerData['ville']);
$offer->setTitle($offerData['title']);
$offer->setDescription($offerData['description']);
$offer->setMaxTjm($offerData['tjm_max']);
$offer->setMinTjm($offerData['tjm_min']);
$offer->setMontantMax($offerData['salaire_max']);
$offer->setMontantMin($offerData['salaire_min']);
$offer->setLinkExtern($offerData['link']);
$offer->setCheckLinkExtern(true);
$offer->setDurationType('mois');
$offer->setModeRemote($offerData['teletravail']);
$experience = $offerData['experience']; // Supposons que $offerData['experience'] contienne la valeur d'expérience
$yearRequired = $experience;
$offer->setYearRequired($yearRequired);
$offer->setRenew(true);
$offer->setStartingType('ASAP');
// Récupérer la date de publication et la convertir en objet DateTime
$publicationDate = $offerData['publicationDate'];
// Définir la date de création (CreatedAt)
$offer->setCreatedAt(clone $publicationDate);
// Ajouter 2 mois à la copie pour définir la date d'expiration (ExpireAt)
$expirationDate = clone $publicationDate;
$expirationDate->add(new \DateInterval('P2M'));
// Définir la date d'expiration (ExpireAt)
$offer->setExpireAt($expirationDate);
// Récupérer l'email de l'utilisateur
$email = 'hays@workdispo.com'; // Remplace par l'email de l'utilisateur
// Récupérer l'utilisateur en fonction de son email
$user = $this->userRepository->findOneBy(['email' => $email]);
$offer->setSociety($user->getSociety());
$offer->setOpenSociety(false);
$existingOffer = $this->entityManager->getRepository(Offer::class)
->findOneBy(['linkExtern' => $offerData['link']]);
if (!$existingOffer) {
$this->entityManager->persist($offer);
// UPDATE URL
$slug = $this->slugService->generateUniqueSlug($offer->getTitle(), Offer::class, $existSlug);
$existSlug[] = $slug;
$offer->setSlug($slug);
$this->entityManager->persist($offer);
} else {
if (!$existingOffer->getEditedByAdmin()) {
// L'offre existe déjà
$existingOffer->setContrats($offerData['contractTypes']);
$existingOffer->setLocation($offerData['location']);
$existingOffer->setVille($offerData['ville']);
$existingOffer->setTitle($offerData['title']);
$existingOffer->setDescription($offerData['description']);
$existingOffer->setMaxTjm($offerData['tjm_max']);
$existingOffer->setMinTjm($offerData['tjm_min']);
$existingOffer->setMontantMax($offerData['salaire_max']);
$existingOffer->setMontantMin($offerData['salaire_min']);
$existingOffer->setLinkExtern($offerData['link']);
$existingOffer->setCheckLinkExtern(true);
$existingOffer->setDurationType('mois');
$existingOffer->setModeRemote($offerData['teletravail']);
$existingOffer->setYearRequired($yearRequired);
$existingOffer->setRenew(true);
$existingOffer->setStartingType('ASAP');
$existingOffer->setCreatedAt(clone $publicationDate);
$existingOffer->setExpireAt($expirationDate);
$existingOffer->setSociety($user->getSociety());
$existingOffer->setOpenSociety(false);
$this->entityManager->persist($existingOffer);
}
echo "L'offre existe déjà dans la base de données.";
}
}
$this->entityManager->flush();
}
private function getDepartementKey(string $departementNom)
{
$exactMatch = null;
$partialMatch = null;
foreach (FrenchRegion::DEPARTEMENT as $key => $departement) {
// Extraire uniquement le nom du département (sans le numéro)
$nomSansNumero = preg_replace('/^\d+\s/', '', $key);
// Vérification exacte (prioritaire)
if (strcasecmp($nomSansNumero, $departementNom) === 0) {
return $key; // On retourne directement si c'est une correspondance exacte
}
// Vérification partielle (si aucune correspondance exacte n'a été trouvée)
if (stripos($nomSansNumero, $departementNom) !== false) {
$partialMatch = $key; // On stocke la première correspondance partielle trouvée
}
}
// Retourner la correspondance partielle si disponible, sinon null
return $partialMatch;
}
private function getToken()
{
// Créer un client HTTP
$client = HttpClient::create();
// Créer un HttpBrowser
$browser = new HttpBrowser($client);
// Effectuer la requête GET sur la page cible
$browser->request('GET', 'https://www.hays.fr/recherche-emploi');
$cookies = $browser->getCookieJar()->all();
// Chercher le cookie 'AhaysToken'
$token = null;
$sessionId = null;
// Boucle sur les cookies pour récupérer les valeurs souhaitées
foreach ($cookies as $cookie) {
if ($cookie->getName() === 'AhaysToken') {
$token = $cookie->getValue();
}
if ($cookie->getName() === 'Asessionid') {
$sessionId = $cookie->getValue();
}
}
// Retourner un tableau contenant à la fois le token et le sessionId
return [
'token' => $token,
'sessionId' => $sessionId
];
}
private function callApi(string $url, ?string $pageToken = "", string $BearerToken, string $sessionId)
{
$client = HttpClient::create();
$data = [
"cookieDomain" => ".hays.fr",
"crossCountryUrl" => "",
"facetLocation" => "",
"flexibleWorking" => "false",
"fullTime" => "false",
"industry" => ["Nouvelles Technologies Et Internet"],
"isCrossCountry" => false,
"isResponseCountry" => false,
"isSponsored" => false,
"jobId" => "",
"jobRefrence" => "",
"jobType" => [],
"locations" => "",
"pageToken" => $pageToken,
"partTime" => "false",
"payType" => "",
"query" => "",
"radius" => 100,
"responseSiteLocale" => "",
"salMax" => "",
"salMin" => "",
"sortType" => "PUBLISHED_DATE_DESC",
"specialismId" => "",
"subSpecialismId" => "",
"type" => "search",
"typeOnlyFilter" => "",
"userAgent" => "-Desktop"
];
try {
$response = $client->request('POST', $url, [
'headers' => [
'Authorization' => 'Bearer ' . $BearerToken, // Ajouter le token
'Content-Type' => 'application/json', // Facultatif si Guzzle le gère déjà
'x-session' => $sessionId
],
'json' => $data, // Spécifiez le body en JSON
]);
// Récupérer le contenu de la réponse
$statusCode = $response->getStatusCode();
$content = $response->toArray(); // Décoder automatiquement le JSON
return [
'statusCode' => $statusCode,
'content' => $content['data']['result'],
];
} catch (\Exception $e) {
// Gérer les erreurs
return [
'error' => $e->getMessage(),
];
}
}
// Fonction pour vérifier si une date est déjà passée depuis plus d'un mois
private function isOlderThanOneMonth(DateTime $publishDate): bool
{
$now = new DateTime();
$interval = $publishDate->diff($now);
return $interval->m >= 1 || $interval->y > 0; // Plus d'un mois
}
private function getDepartmentDetails(string $postcode): ?string
{
$department = 'France';
// Extraire les 2 premiers chiffres du code postal
$departmentCode = substr($postcode, 0, 2);
// Vérifier si le code postal extrait existe dans la constante DEPARTEMENT
foreach (FrenchRegion::DEPARTEMENT as $key => $value) {
// Extraire le code département du nom du département, ici par exemple '75' pour Paris
$deptCode = substr($key, 0, 2);
// Comparer le code postal avec le code département
if ($deptCode === $departmentCode) {
$department = $value; // Retourner le département trouvé
}
}
return $department;
}
private function getRegionKey($regionName)
{
foreach (FrenchRegion::REGION_DEPARTEMENT_WITH_ALL as $region => $departements) {
if (strtolower($region) == strtolower($regionName)) {
return $region; // Retourne la clé de la région trouvée
}
}
return null; // Retourne null si la région n'est pas trouvée
}
private function getCurrentRegionName($oldRegionName)
{
// Tableau associatif des anciennes régions avec leur nouvelle région
$regionsMapping = [
'Alsace' => 'Grand Est',
'Champagne-Ardenne' => 'Grand Est',
'Lorraine' => 'Grand Est',
'Aquitaine' => 'Nouvelle-Aquitaine',
'Limousin' => 'Nouvelle-Aquitaine',
'Poitou-Charentes' => 'Nouvelle-Aquitaine',
'Auvergne' => 'Auvergne-Rhône-Alpes',
'Rhône-Alpes' => 'Auvergne-Rhône-Alpes',
'Bourgogne' => 'Bourgogne-Franche-Comté',
'Franche-Comté' => 'Bourgogne-Franche-Comté',
'Midi-Pyrénées' => 'Occitanie',
'Languedoc-Roussillon' => 'Occitanie',
'Basse-Normandie' => 'Normandie',
'Haute-Normandie' => 'Normandie',
'Nord-Pas-de-Calais' => 'Hauts-de-France',
'Picardie' => 'Hauts-de-France',
];
// Si l'ancienne région est dans le tableau, retourner la nouvelle région
if (array_key_exists($oldRegionName, $regionsMapping)) {
return $regionsMapping[$oldRegionName];
}
// Sinon, retourner le nom donné si pas dans la liste
return $oldRegionName;
}
}