<?phpnamespace App\Entity\Cvs;use Doctrine\ORM\Mapping as ORM;/** * ============================================================================= * JobPublication — le cycle de vie d'une offre * ============================================================================= * * ⚠️ UNE TABLE À PART, ET C'EST DÉLIBÉRÉ. * * On aurait pu ajouter une colonne `expires_at` à `cvs_jobs`. Techniquement, * une colonne NULLABLE est additive : rien n'aurait cassé, ni l'app mobile, * ni Grapajobs. * * Mais tu as demandé « sans modifier », et je le prends au mot : `Jobs.php` * n'est pas touché. Zéro ligne. Ce qui rend impossible, par construction, de * casser ce qui marche. * * ⚠️ ET CETTE TABLE APPORTE CE QUE `Jobs` N'A PAS. * * `Jobs` a `createdAt` et `updatedAt`. Il n'a PAS de date de PUBLICATION. * * Ce n'est pas la même chose : une offre peut être créée en brouillon le 3 * janvier, publiée le 12, modifiée le 20. `updatedAt` dit « le 20 », et * compter 90 jours à partir de là ferait vivre l'offre plus longtemps à * chaque correction de faute de frappe. * * Google, lui, veut `datePosted` — la vraie. Et `validThrough` — la vraie. * * · published_at → `datePosted` * · expires_at → `validThrough` * · renewed_at → un renouvellement est une décision, pas un effet de bord * * ⚠️ L'INDEX LE PLUS RENTABLE DE TOUT LE SCHÉMA. * * `excludeExpired` ajoute « il n'existe pas de publication expirée pour * cette offre » à CHAQUE requête publique — liste, recherche, landing, * accueil. Sans index sur (job_id, expires_at), cette sous-requête * relit la table pour chaque offre examinée. * * @ORM\Table(name="cvs_job_publication", indexes={ * @ORM\Index(name="idx_pub_job_expires", columns={"job_id", "expires_at"}), * @ORM\Index(name="idx_pub_closed", columns={"closed_reason"}) * }) * @ORM\Entity(repositoryClass="App\Repository\Cvs\JobPublicationRepository") * ============================================================================= */class JobPublication{ /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * ⚠️ OneToOne : une offre, une publication. * * `onDelete="CASCADE"` : si l'offre disparaît, sa publication n'a plus de * sens. Une ligne orpheline ici, c'est un `validThrough` qui pointe vers * une annonce qui n'existe plus. * * @ORM\OneToOne(targetEntity="App\Entity\Cvs\Jobs") * @ORM\JoinColumn(name="job_id", referencedColumnName="id", nullable=false, onDelete="CASCADE", unique=true) */ private $job; /** * La date de PUBLICATION. Pas de création, pas de modification. * * C'est `datePosted` dans le JSON-LD que lit Google. * * @ORM\Column(name="published_at", type="datetime") */ private $publishedAt; /** * La date d'expiration. `validThrough` pour Google. * * ⚠️ SANS ELLE, GOOGLE FOR JOBS DÉCLASSE OU RETIRE L'ANNONCE. * * C'est le champ que Google utilise pour savoir si une offre est encore * valide. Une annonce sans `validThrough` est traitée comme suspecte — * et pour un site d'emploi, Google Jobs représente couramment un tiers à * la moitié du trafic. * * @ORM\Column(name="expires_at", type="datetime") */ private $expiresAt; /** * Le dernier renouvellement. `null` si jamais renouvelée. * * @ORM\Column(name="renewed_at", type="datetime", nullable=true) */ private $renewedAt; /** * Combien de fois. Un compteur, pas une opinion : il dira un jour si les * offres qu'on renouvelle sans arrêt sont celles qui ne recrutent jamais. * * @ORM\Column(name="renew_count", type="integer", options={"default": 0}) */ private $renewCount = 0; /** * Pourquoi elle n'est plus en ligne, si elle ne l'est plus. * * 'expired' → les 90 jours sont passés * 'filled' → le poste est pourvu (le recruteur l'a dit) * 'closed' → le recruteur l'a retirée * * ⚠️ Un poste POURVU et un poste EXPIRÉ ne se disent pas pareil au * candidat. « Cette offre a expiré » et « Ce poste a été pourvu » ne * produisent pas la même émotion — ni la même action. * * @ORM\Column(name="closed_reason", type="string", length=16, nullable=true) */ private $closedReason; /** * @ORM\Column(name="created_at", type="datetime") */ private $createdAt; /** * @ORM\Column(name="updated_at", type="datetime") */ private $updatedAt; // ========================================================================= public function getId(): ?int { return $this->id; } public function getJob(): ?Jobs { return $this->job; } public function setJob(Jobs $job): self { $this->job = $job; return $this; } public function getPublishedAt(): ?\DateTimeInterface { return $this->publishedAt; } public function setPublishedAt(\DateTimeInterface $publishedAt): self { $this->publishedAt = $publishedAt; return $this; } public function getExpiresAt(): ?\DateTimeInterface { return $this->expiresAt; } public function setExpiresAt(\DateTimeInterface $expiresAt): self { $this->expiresAt = $expiresAt; return $this; } public function getRenewedAt(): ?\DateTimeInterface { return $this->renewedAt; } public function setRenewedAt(?\DateTimeInterface $renewedAt): self { $this->renewedAt = $renewedAt; return $this; } public function getRenewCount(): int { return (int) $this->renewCount; } public function setRenewCount(int $renewCount): self { $this->renewCount = $renewCount; return $this; } public function getClosedReason(): ?string { return $this->closedReason; } public function setClosedReason(?string $closedReason): self { $this->closedReason = $closedReason; return $this; } public function getCreatedAt(): ?\DateTimeInterface { return $this->createdAt; } public function setCreatedAt(\DateTimeInterface $createdAt): self { $this->createdAt = $createdAt; return $this; } public function getUpdatedAt(): ?\DateTimeInterface { return $this->updatedAt; } public function setUpdatedAt(\DateTimeInterface $updatedAt): self { $this->updatedAt = $updatedAt; return $this; } // ========================================================================= /** L'offre a-t-elle dépassé sa date ? */ public function isExpired(): bool { return $this->expiresAt < new \DateTime(); } /** * Combien de jours reste-t-il ? * * Négatif si l'offre est expirée — on ne renvoie pas 0. La différence * compte : « expirée depuis 40 jours » et « expirée hier » n'appellent pas * la même décision. */ public function daysLeft(): int { $diff = (new \DateTime())->diff($this->expiresAt); $days = (int) $diff->days; return $diff->invert ? -$days : $days; }}