Terrain 5 — Doctrine : entités & relations
1. Entité simple ⭐
🕹️ Mission : créez une entité Provider (id, name, slug unique).
✅ Solution
#[ORM\Entity]
class Provider {
#[ORM\Id, ORM\GeneratedValue, ORM\Column] private ?int $id = null;
#[ORM\Column(length: 255)] private string $name;
#[ORM\Column(length: 255, unique: true)] private string $slug;
}2. Enum en colonne ⭐⭐
🕹️ Mission : mappez BookingStatus sur une colonne.
✅ Solution
#[ORM\Column(enumType: BookingStatus::class)]
private BookingStatus $status = BookingStatus::Pending;3. ManyToOne ⭐⭐
🕹️ Mission : reliez Slot à Service (un slot appartient à un service). Quel côté porte la FK ?
✅ Solution
Côté Slot (propriétaire, porte la FK) :
#[ORM\ManyToOne(inversedBy: 'slots')]
#[ORM\JoinColumn(nullable: false)]
private ?Service $service = null;4. OneToMany + collection ⭐⭐⭐
🕹️ Mission : côté Service, la collection slots avec helpers synchronisant les deux côtés.
✅ Solution
/** @var Collection<int, Slot> */
#[ORM\OneToMany(targetEntity: Slot::class, mappedBy: 'service', orphanRemoval: true)]
private Collection $slots;
public function __construct() { $this->slots = new ArrayCollection(); }
public function addSlot(Slot $s): static {
if (!$this->slots->contains($s)) { $this->slots->add($s); $s->setService($this); }
return $this;
}5. Timestamps automatiques ⭐⭐⭐
🕹️ Mission : ajoutez createdAt/updatedAt auto. Quelle annotation sur la classe ne pas oublier ? 🔍 6.5.
✅ Solution
#[ORM\HasLifecycleCallbacks] sur la classe, + #[ORM\PrePersist]/#[ORM\PreUpdate] sur des méthodes qui posent les dates.
6. Générer une migration ⭐⭐
🕹️ Mission : après avoir ajouté une propriété, quelles commandes ? Quelle étape ne pas sauter ?
✅ Solution
make:migration → relire le fichier généré → doctrine:migrations:migrate → committer. Ne pas sauter la relecture.
7. Slug via entity listener ⭐⭐⭐⭐
🕹️ Mission : générez le slug d’un Provider en prePersist via un service Slugger. Callback ou listener ? 🔍 6.5.
✅ Solution
Un entity listener (injectable), car un callback d’entité ne peut pas recevoir de service :
final class ProviderSlugListener {
public function __construct(private Slugger $slugger) {}
public function prePersist(Provider $p): void { if ($p->getSlug() === null) $p->setSlug($this->slugger->slugify($p->getName())); }
}8. Montant en centimes ⭐⭐
🕹️ Mission : stockez un montant sans erreur d’arrondi.
✅ Solution
Un int en centimes (private int $amountCents;) ou un decimal — jamais un float.
9. OneToOne ⭐⭐⭐
🕹️ Mission : reliez Booking et Payment (au plus un paiement par réservation).
✅ Solution
// Payment (propriétaire)
#[ORM\OneToOne(inversedBy: 'payment')]
#[ORM\JoinColumn(nullable: false)]
private ?Booking $booking = null;
// Booking (inverse)
#[ORM\OneToOne(mappedBy: 'booking')]
private ?Payment $payment = null;10. Cascade prudente ⭐⭐⭐⭐
🕹️ Mission : supprimer un Service doit effacer ses Slot mais jamais les Booking. Comment ?
✅ Solution
orphanRemoval: true (ou cascade: ['remove']) sur Service::$slots ; aucune cascade remove vers Booking, et onDelete: 'RESTRICT' sur la FK des réservations (ou archivage) pour les protéger.
Terrain suivant : Doctrine : requêtes & perf.