11 - Symfony : Les relations bidirectionnelles avec Doctrine
Objectifs
- ✅ Créer une relation bidirectionnelle entre deux entités.
- ✅ Récupérer la catégorie d'une série,
- ✅ Récupérer les séries d'une catégorie.
Pré-requis
Avoir terminé la quête
Introduction
User stories 📋
en tant que visiteur, je souhaite récupérer les données d'une série
en tant que visiteur, je souhaite récupérer la liste des séries
en tant que contributeur, je souhaite ajouter une série
In progress 🤸
en tant que visiteur, je souhaite afficher les séries par catégories
Doing 🏗️
mise en place de la relation bidirectionnelle
Done ✅
en tant que visiteur, je souhaite visualiser la fiche d'une série
création de la méthode show dans le contrôleur CategoryController
mise en place de la relation entre les entités Program et Category
Dans les quêtes précédentes, tu as vu les relations de type Many-To-One à travers les entités :
- Category
- Program
N'oublie pas que "Program" est le terme utilisé pour désigner la classe utilisée dans ton code et qui correspond au concept de série, employé dans le contexte utilisateur.
Grâce à ça, tu as pu lier des "Séries Télé" avec des "Catégories". Ainsi, dans ton application tu es capable de récupérer la catégorie d'une série depuis ta classe Program. Mais ! tu ne peux pas encore faire l'inverse : récupérer une liste de programmes depuis l'entité "Category". Vu que l'association n'est accessible que depuis une seule entité, on dit que c'est une relation "unidirectionnelle".
Aujourd'hui, tu verras qu'il est possible d'effectuer une relation "bidirectionnelle", afin de récupérer aussi les différentes séries télé qui appartiennent à une même catégorie depuis ta classe Category.
Sommaire
Récapitulatif sur les deux entités
D'abord, il faut savoir que, pour les relations, tout n'est qu'une question de point de vue. Du point de vue de Program, on a une relation Many-To-One (plusieurs séries peuvent appartenir à une seule catégorie). Du point de vue de Category, on a une relation One-To-Many (une catégorie peut avoir plusieurs séries).
Dans une quête précédente, tu as créé la relation Many-To-One afin d’associer un objet de type Category à un objet de type Program. Tu as alors dans ta classe Program une propriété $category qui fait "référence" à la catégorie de ta série en question.
Ainsi, tu es capable, depuis l’instance d’un objet "Program", de faire : $category = $program->getCategory(), ce qui te renvoie une instance de la classe Category avec comme valeurs les informations contenues en base de données.
Par contre, cette relation ne va que dans un seul sens, et tu ne peux pas effectuer la demande inverse, à savoir : $category->getPrograms(). C'est cela que l’on appelle relation unidirectionnelle.
Pour que ta relation soit ”bidirectionnelle”, il faut que ta classe Category ait aussi une "référence" vers ses programs. Pour cela, il faut lui ajouter une propriété $programs.
Ainsi, tu auras une relation à "double sens" et tu seras capable de récupérer la catégorie d'une série, mais aussi les séries d'une catégorie.
Nouveau diagramme de classe

Au niveau BDD, le MLD ne change pas

Mais... pourquoi programs est-il de type ArrayCollection?
Parce que ta propriété $programs sera un tableau qui contiendra plusieurs instances de la classe Program. D’où également le nom de la propriété au pluriel.
Le type est ArrayCollection, en effet, Symfony (ou plutôt Doctrine) possède une classe appelée ArrayCollection dont les instances fonctionnent de manière similaire à un tableau PHP, mais avec des options en plus. Sache juste que tu peux boucler sur un ArrayCollection avec un foreach comme tu le ferais sur un Array simple.
Créer la relation avec Symfony
Tout ça, c'est bien beau, mais concrètement, comment je fais ?
Eh bien, de la même façon que lorsque tu avais créé ta relation Many-To-One de manière unidirectionnelle.
Bah... si c'est pareil, je vais créer une relation unidirectionnelle, non ?
Tu te rappelles, dans la quête précédente, quand Symfony te propose :
`Do you want to add a new property to Category so that you can access/update Program objects from it - e.g. $category->getPrograms()? (yes/no)` [yes]:
> no
Tu avais répondu "no". Eh bien, si tu avais répondu oui, Doctrine aurait directement créé les propriétés et les méthodes pour gérer la bidirectionnalité.
Et... C'est tout ?
Oui, c'est tout. Symfony crée alors la relation de Program vers Category, mais aussi la relation inverse (de Category vers Program).
Ce qui se passe vraiment dans le code
Sauf que tu n'avais pas répondu oui à la question, donc tu vas devoir faire à la main ce que Symfony aurait fait. Parce que le “tout automatisé”, c'est pratique et ça marche bien, mais l'important c'est de savoir ce qu'il se passe au niveau du code de nos classes Program et Category.
Dans la classe Program, voici ce que tu dois avoir :
namespace App\Entity;
use App\Repository\ProgramRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProgramRepository::class)]
class Program
{
// ...
#[ORM\ManyToOne(inversedBy: 'programs')]
#[ORM\JoinColumn(nullable: false)]
private ?Category $category = null;
// ...
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
}
Observe bien l'attribut #[ORM\ManyToOne] de la propriété $category. Tu ne remarques rien ?
Eh oui ! Il faut ajouter une nouvelle mention inversedBy: 'programs' dans l’attribut.
Cela fait référence à la propriété $programs dans la classe Category que tu t’apprêtes à ajouter.
Dans la classe Category :
Il y a plus de changements à effectuer dans la classe Category, voici les étapes :
- L'ajout d'une propriété
$programs(au pluriel) et son attribut :
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Program::class)]
private $programs;
Dans l'attribut ci-dessus, on retrouve comme valeur de "targetEntity", le nom de la classe Program::class avec laquelle on crée l’association (pas besoin d'importer l'entité Program ici, puisqu'elle est dans le même namespace que l'entité Category). Pour "mappedBy", on retrouve la valeur "category" qui correspond au nom de la propriété de la classe Program.
- L'ajout du constructeur avec à l'intérieur l'initialisation de la propriété
$programsqui est une nouvelle instance deArrayCollection:
public function __construct()
{
$this->programs = new ArrayCollection();
}
- L'ajout du getter de cette propriété :
public function getPrograms(): Collection
{
return $this->programs;
}
- L'ajout d’une méthode pour ajouter une série à une catégorie :
public function addProgram(Program $program): self
{
if (!$this->programs->contains($program)) {
$this->programs->add($program);
$program->setCategory($this);
}
return $this;
}
- L'ajout d’une méthode pour supprimer une série :
public function removeProgram(Program $program): self
{
if ($this->programs->removeElement($program)) {
// set the owning side to null (unless already changed)
if ($program->getCategory() === $this) {
$program->setCategory(null);
}
}
return $this;
}
Dans l'attribut de la propriété $programs, il y a une mention mappedBy: 'category' qui fait référence à la propriété $category de la classe Program.
Pour le constructeur, c'est simple. La propriété $programs contient toutes les instances de la classe Program représentant chaque tuple de la base de données. Donc, c'est un tableau (ou plus précisément une ArrayCollection ce qui est relativement similaire).
Comme il ne s’agit plus d’un élément simple, mais d’une liste d’éléments, il ne faut plus un setter basique, mais une méthode addProgram pour ajouter une série à ce tableau, et une autre méthode removeProgram pour retirer une série.
Si tu as copié/collé le code, n’oublie pas le use Doctrine\Common\Collections\ArrayCollection; ainsi que le use Doctrine\Common\Collections\Collection;. Sinon, en écrivant le code à la main, ton IDE a dû normalement déjà ajouter le use tout seul.
💪 Challenge
Attention
Ce challenge va te prendre un petit plus de temps que lors de la plupart des quêtes précédentes. Cela est nécessaire pour construire le socle de ta future application, et pouvoir travailler sur les différents autres concepts de Symfony. 💪😉
C'est meilleur quand c'est de saison
User stories 📋
en tant que visiteur, je souhaite récupérer les données d'une série
en tant que visiteur, je souhaite récupérer la liste des séries
en tant que contributeur, je souhaite ajouter une série
In progress 🤸
en tant que visiteur, je souhaite afficher les saisons par série
en tant que visiteur, je souhaite afficher les épisodes par saison
Doing 🏗️
création des entités Season et Episode en relations bidirectionnelles
création des fixtures pour les entités Season et Episode
Done ✅
en tant que visiteur, je souhaite afficher les séries par catégories
en tant que visiteur, je souhaite visualiser la fiche d'une série
À partir de l’extrait du modèle logique ci-dessous, crée les relations bidirectionnelles entre les entités Program, Season et Episode.
Cette fois, tu pourras répondre [yes] lorsque la CLI te demandera d'ajouter une propriété pour que la relation soit ”bidirectionnelle”.
Voici ce que tu dois obtenir côté classes et le résultat de la migration côté SGBD. ⚠️ Fais bien attention aux noms des propriétés des classes.
N’oublie pas, qui dit modification des entités, dit migrations !
Jeu de données
Afin de tester cette fonctionnalité et valider cette quête, tu dois créer en BDD quelques épisodes ainsi qu'une ou plusieurs saisons associées à une série déjà existante.
Dans la prochaine quête, nous utiliserons un nouvel outil pour générer des données en masse aléatoirement. Mais sache d'ores et déjà qu'il est aussi important de disposer d'un jeu de données simple et testable sur tous les environnements (en local pour chaque développeur, sur un serveur de tests pour un Product Owner, etc.). N’hésite pas à aller sur imdb si tu as besoin d'inspiration pour créer une saison et les 2 ou 3 épisodes associés.
Utilise les fixtures (avec addReference() et getReference()) comme tu l'as fait précédemment pour accomplir cette tâche.
Pour exemple, regarde ci-dessous un bref aperçu de ce que tu pourrais mettre en place.
Program
//src/DataFixtures/ProgramFixtures.php
$program = new Program();
$program->setTitle('Arcane');
$program->setCategory($this->getReference('category_Animation'));
//... set other program's properties
$this->addReference('program_Arcane', $program);
Season
//src/DataFixtures/SeasonFixtures.php
$season = new Season();
$season->setNumber(1);
$season->setProgram($this->getReference('program_Arcane'));
//... set other season's properties
$this->addReference('season1_Arcane', $season);
Episode
//src/DataFixtures/EpisodeFixtures.php
$episode = new Episode();
$episode->setTitle('Welcome to the Playground');
$episode->setNumber(1);
$episode->setSeason($this->getReference('season1_Arcane'));
//... set other episode's properties
//... create 2 more episodes
Affichage
- Dans la méthode
show(int $id)de la classeProgramController, récupère le Program en question grâce à l'id passé dans l’url. - Passe le programme que tu viens de récupérer en contexte à ta vue program/show.html.twig.
- Dans la vue, affiche la liste des saisons disponibles (propriété bidirectionnelle
program.seasons😉) avec un lien qui permet d'aller vers la page détaillée de la saison (tu implémenteras cette page dans la suite du challenge, l'URL sera de la forme :/program/{programId}/season/{seasonId}).
- Dans la vue, affiche la liste des saisons disponibles (propriété bidirectionnelle
- Dans la classe ProgramController, crée une méthode
showSeason(int $programId, int $seasonId)- La route de cette méthode sera donc de la forme
/program/{programId}/seasons/{seasonId}et le nom de la route seraprogram_season_show - Récupère le Program qui correspond à l'identifiant du programme passé en paramètre dans l'URL
- Récupère la Season du programme qui correspond à l'identifiant de la saison passé en paramètre.
- Cette méthode retournera une nouvelle vue : templates/program/season_show.html.twig.
- Dans cette vue nouvellement créée, affiche les informations du programme et de la saison sélectionnée.
- Tu dois également afficher la liste des épisodes de la saison. Opte pour la méthode qui te convient le mieux afin d'obtenir le résultat suivant (bien sûr l'apparence variera en fonction de ton styleguide) :
- La route de cette méthode sera donc de la forme

Critères de validation
- Lorsque l’utilisateur navigue de séries en saisons
- Sur la page d'une série on voit bien la liste des saisons de la série
- Lorsqu'on clique sur une saison on est redirigé vers la page de la saison
- Sur la page d'une saison on voit bien la liste des épisodes de la saison
- Les deux classes Season et Episode sont présentes.
- Les annotations
inversedByetmappedBysont présentes dans toutes les relations - Les méthodes
addProgram()etremoveProgram()sont présentes dans l'entité Category, comme présenté dans la quête, - Les méthodes
addSeason(),removeSeason()etgetSeasons()sont présentes dans l'entité Program, comme demandé dans le challenge, - Les méthodes
addEpisode(),removeEpisode()etgetEpisodes()sont présentes dans l'entité Season, comme demandé dans le challenge, - Les méthodes
getProgram()etsetProgram()sont présentes dans l'entité Season, comme demandé dans le challenge, - Les méthodes
getSeason()etsetSeason()sont présentes dans l'entité Episode, comme demandé dans le challenge,
Ressources partagées sur cette quête
- attention de mettre un champ texte pour synopsis de l'entitée program avant de source le fichierhttps://gist.github.com/Bruno-Fernandes-37/0661f60f762a20e73c867ced02c51c99