18 - Symfony : Doctrine relations “Many-To-Many”
Objectifs
- ✅ Savoir mettre en place une relation ManyToMany.
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 consulter les acteurs d'une série
Doing 🏗️
création de l'entité Actor
Done ✅
en tant qu'administrateur, je souhaite ajouter, éditer ou supprimer une saison
en tant qu'administrateur, je souhaite ajouter, éditer ou supprimer un épisode
Maintenant que tu maîtrises les relations ManyToOne/OneToMany, allons plus loin avec les relations ManyToMany dans Doctrine. Tu connais le principe. Un élément d'une base de données, contenu dans une table, peut être lié à 0 ou n éléments d'une autre table qui, eux-mêmes, peuvent être liés à 0 ou n éléments de la première table. Pour la matérialiser, l'association est stockée dans une table intermédiaire. Dans cette table sont stockées les clés étrangères correspondant aux clés primaires de chaque table que nous souhaitons associer.
Tu vas voir qu'il va être nécessaire de bousculer un peu cette logique avec l’ORM.
Sommaire
Création de l'entité Actor
Tu as précédemment mis en place les catégories sur ton Wild Series, ce qui te permet de regrouper et d’organiser tes séries par thématique. Maintenant, tu vas ajouter la gestion des acteurs. Plusieurs acteurs pourront se trouver dans plusieurs séries.
Si tu souhaites ajouter la possibilité de gérer les acteurs à ton application Wild Séries, tu dois rajouter une classe d’entité Actor qui, pour être associée à nos éléments de Program, engendre la création d'une table intermédiaire actor_program. Cette dernière contient la paire :
iddu Program,iddu Actor.
Pour ça, tu vas pouvoir commencer par créer une entité Actor en utilisant l'outil de génération en ligne de commandes :
symfony console make:entity
Ce qui va t’entraîner sur cette liste de questions, réponds de la même manière à chaque question :
`Class name of the entity to create or update (e.g. BravePuppy)`:
> Actor
`Add the ability to broadcast entity updates using Symfony UX Turbo? (yes/no)` [no]:
> no
created: src/Entity/Actor.php
created: src/Repository/ActorRepository.php
Entity generated! Now let's add some fields!
You can always add more fields later manually or by re-running this command.
`New property name (press <return> to stop adding fields)`:
> name
`Field type (enter ? to see all types) [string]`:
>
`Field length [255]`:
>
`Can this field be null in the database (nullable) (yes/no)` [no]:
>
updated: src/Entity/Actor.php
`Add another property? Enter the property name (or press <return> to stop adding fields)`:
> programs
`Field type (enter ? to see all types)` [string]:
> ManyToMany
`What class should this entity be related to?`:
> Program
`Do you want to add a new property to Program so that you can access/update Actor objects from it - e.g. $program->getActors()? (yes/no)` [yes]:
> yes
A new property will also be added to the Program class so that you can access the related Actor objects from it.
`New field name inside Program` [actors]:
> actors
updated: src/Entity/Actor.php
updated: src/Entity/Program.php
Add another property? Enter the property name (or press <return> to stop adding fields):
>
`Success!`
Next: When you're ready, create a migration with `symfony console make:migration`
Si maintenant tu jettes un œil à la classe App\Entity\Actor, tu peux retrouver des éléments que tu as déjà vus auparavant dans les quêtes "Many-To-One" et "Bidirectionnalités" :
-
Une propriété
$programsavec l'attribut adapté#[ORM\ManyToMany(targetEntity: Program::class, inversedBy: 'actors')]; -
Un constructeur avec la définition de la propriété
programsen tant que collection d'objets :$this->programs = new ArrayCollection();; -
Une méthode
getPrograms()qui permet de récupérer l'ensemble des séries associées à unActor, -
Deux méthodes
addProgrametremoveProgramqui permettent respectivement d'ajouter et de retirer une série associée à cet acteur.
De la même manière, si tu vas dans la classe App\Entity\Program, tu vas retrouver les mêmes éléments "inverses". C'est-à-dire :
-
Une propriété
$actorsavec l'annotation adaptée#[ORM\ManyToMany(targetEntity: Actor::class, mappedBy: 'programs')]; -
Un constructeur avec la définition de la propriété
actorsen tant que collection d'objets :$this->actors = new ArrayCollection();; -
Une méthode
getActors()qui permet de récupérer l'ensemble des acteurs associés à unProgram, -
Deux méthodes,
addActoretremoveActor, qui permettent respectivement d'ajouter et de retirer un acteur relié à une série.
Enfin, si tu regardes la classe App\Entity\ActorProgram...eh bien, il n'y en a pas ! C'est normal. Avec Doctrine, on s'affranchit de cette notion de classe intermédiaire. Étant donné qu'elle n'est là que pour stocker des identifiants, et qu'avec l'ORM, on manipule directement des objets plutôt que des entiers représentant d'autres entités, on n’en a plus besoin.
Attention ! Ce n'est pas parce que l'entité ActorProgram n'existe pas, que la table n'est plus nécessaire ! Cela ne change en rien le fonctionnement de nos bases SQL.
Ainsi, si tu génères une nouvelle classe de migration, tu dois retrouver, quelque part dans la méthode up();, la requête SQL suivante :
CREATE TABLE actor_program (actor_id INT NOT NULL, program_id INT NOT NULL, INDEX IDX_D3CCCBF710DAF24A (actor_id), INDEX IDX_D3CCCBF75E3A35BB (program_id), PRIMARY KEY(actor_id, program_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB
Comme tu peux le constater, malgré l’absence d’une entité ActorProgram, Doctrine a bien pris en compte la nécessité de cette table intermédiaire. Tu ne dois en aucun cas manipuler celle-ci directement depuis le serveur MySQL. Comme pour toute partie de ta DB, tu dois passer par l’ORM pour altérer la structure des bases de données.
Remarque : Symfony gère tout seul la table intermédiaire, du moment que celle-ci ne nécessite pas de champ supplémentaire. Dans le cas contraire, il faudra créer une classe pour la table intermédiaire et gérer alors deux relations ManyToOne entre celle-ci et les deux autres tables à relier. https://symfonycasts.com/screencast/collections/many-to-many-extra-fields
Si tu as besoin d'une piqûre de rappel sur les relations Many-to-Many, voici une vidéo qui peut t'aider :
💪 Challenge
Affichage des acteurs jouant dans une série
Peut-être que tu as eu le réflexe de le faire, mais dans les consignes précédentes, tu n'as pas encore impacté la création de tes entités sur la base de données. Je te laisse retrouver les commandes de migration qui te permettent de le faire.
💻 Une fois ta base de données à jour, il te faut des données dans la nouvelle table Actor. Tu vas à nouveau utiliser les compétences acquises lors des précédentes quêtes pour utiliser les fixtures.
- Crée ActorFixtures. A l'aide de Faker, crée 10 acteurs ou actrices, puis ajoute aléatoirement 3 programmes à chaque acteur.
💻 Ensuite, crée une page répondant la route /actor/{id} et affichant les informations suivantes :
-
Le nom de l’acteur,
-
La liste des séries associées à cet acteur.
Pour cela, tu vas évidemment créer une classe "ActorController", avec une méthode "show". 😏
💻 Maintenant, reprends la page de détail d'une série, disponible à l'url program/{id} et ajoute en bas de ta vue le code suivant :
<strong>Acteurs :</strong>
<ul>
{% for actor in program.actors %}
<li>{{ actor.name }}</li>
{% else %}
<li>Aucun acteur pour cette série</li>
{% endfor %}
</ul>
Ça fonctionne ?!? Pourtant, à aucun moment tu n’as récupéré les acteurs depuis le contrôleur ! C’est normal ! Car depuis Twig, en appelant program.actors, tu fais l’équivalent en PHP de $program->getActors(). Ce qui te donne le tableau des acteurs associés à la série. Pas besoin de le faire au niveau du contrôleur. Grâce à la bidirectionnelle, que l’on soit en OneToMany ou en ManyToMany ne change donc rien.
Critères de validation
- Le code est disponible sur un repository GitHub, avec une branche correspondant à cette quête.
- Ton entité
App\Entity\Actorest bien présente, - Ta classe de migration générant les 2 nouvelles tables et les contraintes d'intégrité fonctionnent,
- Les fixtures ajoutent 10 acteurs, et 3 programmes à chaque acteur.
- La route vers la page
/actor/{id}est fonctionnelle, - Sur la page d’un acteur, la liste des séries associées s'affiche.
- Sur la page d’une série, la liste des acteurs associés s'affiche.
- Sur la page de l’acteur, lors du clic sur une série de la liste, l'utilisateur est redirigé vers la page de la série sélectionnée.
- Sur la page d’une série, lors du clic sur un acteur de la liste, l'utilisateur est redirigé vers la page de l’artiste sélectionné.
Ressources partagées sur cette quête
- La seconde partie explique bien les fixtureshttps://www.youtube.com/watch?v=rUr4PrN-fqo