Archives Wild Code School
Symfony

11 - Symfony : Les relations bidirectionnelles avec Doctrine

Quête 14 sur 35 · 2 images archivées

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

Quêtes liées
title-book
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
Info

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.

Attention

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.

Le récit

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 :

ruby
`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é.

Le récit

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 :

php
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 :

  1. L'ajout d'une propriété $programs (au pluriel) et son attribut :
php
#[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.

  1. L'ajout du constructeur avec à l'intérieur l'initialisation de la propriété $programs qui est une nouvelle instance de ArrayCollection :
php
public function __construct()
{
    $this->programs = new ArrayCollection();
}
  1. L'ajout du getter de cette propriété :
php
public function getPrograms(): Collection
{
    return $this->programs;
}
  1. L'ajout d’une méthode pour ajouter une série à une catégorie :
php
public function addProgram(Program $program): self
{
    if (!$this->programs->contains($program)) {
        $this->programs->add($program);
        $program->setCategory($this);
    }

    return $this;
}
  1. L'ajout d’une méthode pour supprimer une série :
php
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.

Attention

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.

↗
Association Mapping
doctrine-project.org
↗
Mapping the ManyToOne relationship
symfony.com

💪 Challenge

Info

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.

contains
1
0..*
contains
1
0..*
contains
1
0..*
Category
-id: int
-name: string
-programs: ArrayCollection
Program
-id: int
-title: string
-synopsis: string
-poster: string
-category: Category
-seasons: ArrayCollection
Season
-id: int
-number: int
-year: int
-description: string
-program: Program
-episodes: ArrayCollection
Episode
-id: int
-title: string
-synopsis: string
-number: int
-season: Season
Diagramme de classe
categoryintidvarcharnameprogramintidvarchartitletextsynopsisvarcharposterintcategory_idseasonintidintnumberintyearvarchardescriptionintprogram_idepisodeintidvarchartitleintnumbertextsynopsisintseason_idcontainscontainscontainsMLD avec les tables season et episode
Attention

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.

Info

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

php
//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

php
//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

php
//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

  1. Dans la méthode show(int $id) de la classe ProgramController, récupère le Program en question grâce à l'id passé dans l’url.
  2. 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}).
  3. 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 sera program_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) :

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 inversedBy et mappedBy sont présentes dans toutes les relations
  • Les méthodes addProgram() et removeProgram() sont présentes dans l'entité Category, comme présenté dans la quête,
  • Les méthodes addSeason(), removeSeason() et getSeasons()sont présentes dans l'entité Program, comme demandé dans le challenge,
  • Les méthodes addEpisode(), removeEpisode() et getEpisodes()sont présentes dans l'entité Season, comme demandé dans le challenge,
  • Les méthodes getProgram() et setProgram()sont présentes dans l'entité Season, comme demandé dans le challenge,
  • Les méthodes getSeason() et setSeason()sont présentes dans l'entité Episode, comme demandé dans le challenge,

Ressources partagées sur cette quête