
Relations entre entités
Relation onetoumani / manitouone :
Un objet peut être associé à plusieurs autres objets.
Relation manitoumani :
Plusieurs objets peuvent être associés à plusieurs objets.
—> Une table d’association associe les id de la première table avec les id de la deuxième table.
Relation onetouone :
Un objet peut être associé à un et un seul objet.
Pas très utilisé : souvent on lui met en attribut et pas dans une table.
Utile par exemple pour l’authentification.
Relation entre objets PHP :
Une entité est liée à une autre lorsqu’elle porte un attribut qui porte le même nom que l’entité à laquelle elle est liée (on peut typer en PHP8).
BP : Si l’attribut est en fait un tableau d’objets, on le met au pluriel.
Créer une relation entre deux entités
- créer les deux entités à associer (sans relation) :
symfony console make:entity
- ajouter la relation :
symfony console make:entity>NomEntite>attributClasseVoulue> type d’attributrelation> type de relation (onetomani, manitoumani…)
- mettre à jour la base de données :
symfony console doctrine:schema:update --force
L’entité liée est importée.
Les attributs respectifs portent des attributs (eh oui c’est le même mot……) @ORM\TypeRelation et leurs getter/setter ont été ajoutés.
addEntite() et removeEntite() avec l’initialisation du tableau dans un constructeur __construct().// entite Country, liée en onetoumani avec Movie
<?php
namespace App\Entity;
use App\Repository\CountryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CountryRepository::class)]
class Country
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $code = null;
#[ORM\Column(length: 2)]
private ?string $alpha2 = null;
#[ORM\Column(length: 3)]
private ?string $alpha3 = null;
#[ORM\Column(length: 45)]
private ?string $name_english = null;
#[ORM\Column(length: 45)]
private ?string $name_french = null;
#[ORM\OneToMany(mappedBy: 'country', targetEntity: Movie::class)]
private Collection $movies;
public function __construct()
{
$this->movies = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getCode(): ?int
{
return $this->code;
}
public function setCode(int $code): self
{
$this->code = $code;
return $this;
}
public function getAlpha2(): ?string
{
return $this->alpha2;
}
public function setAlpha2(string $alpha2): self
{
$this->alpha2 = $alpha2;
return $this;
}
public function getAlpha3(): ?string
{
return $this->alpha3;
}
public function setAlpha3(string $alpha3): self
{
$this->alpha3 = $alpha3;
return $this;
}
public function getNameEnglish(): ?string
{
return $this->name_english;
}
public function setNameEnglish(string $name_english): self
{
$this->name_english = $name_english;
return $this;
}
public function getNameFrench(): ?string
{
return $this->name_french;
}
public function setNameFrench(string $name_french): self
{
$this->name_french = $name_french;
return $this;
}
/**
* @return Collection<int, Movie>
*/
public function getMovies(): Collection
{
return $this->movies;
}
public function addMovie(Movie $movie): self
{
if (!$this->movies->contains($movie)) {
$this->movies->add($movie);
$movie->setCountry($this);
}
return $this;
}
public function removeMovie(Movie $movie): self
{
if ($this->movies->removeElement($movie)) {
// set the owning side to null (unless already changed)
if ($movie->getCountry() === $this) {
$movie->setCountry(null);
}
}
return $this;
}
}
Relation bidirectionnelle
Dans la majorité des cas, c’est ce qu’on veut. Les deux classes sont au courant de la relation. Un seul côté est propriétaire, indiqué avec mappedBy tandis que l’autre est indiqué avec inversedBy.
Le côté qui porte le “one” de la relation est propriétaire. Si onetouone , on peut faire comme on veut (Symfony décide souvent pour nous à la création). Si manitoumani, Symfony crée la table d’association, à laquelle on ne peut pas ajouter de colonnes.
Traduction en base de données avec Doctrine :
@ORM\ManyToOne et @ORM\OneToMany :
• la clé étrangère s'ajoute dans la table côté Many
@ORM\OneToOne() :
• on choisit où s'ajoute la clé étrangère
@ORM\ManyToMany() :
• Doctrine crée la table de pivot !
• Le choix de nom de table est personnalisable
• Impossibilité d'ajouter des colonnes supplémentaires
Récupérer une entité associée à une autre
Il suffit d’utiliser l’attribut objet de l’entité.
Exemples :
- dans un Twig, afficher le pays (attribut objet) de l’entité Movie
{% for movie in movies %}
<li>
<a href="{{ path('movie_detail', {'id': movie.id}) }}">
{{ movie.title }}
</a>
({{ movie.country.nameFrench }}) sorti en {{ movie.releaseYear }}
</li>
{% else %}
Aucun film n'a été ajouté pour l'instant.
{% endfor %}- dans un Twig, afficher les saisons (attribut objet) de l’entité Série avec une boucle
foret afficher le nombre de saisons quand on hover sur l’image en ajoutant une propriété
titledans la balise image
{% for season in serie.seasons %}
<article>
<img
src="{{ asset('img/poster/seasons/'~season.poster }}"
alt="{{ serie.name }}"
title="{{ serie.seasons | length }}"
>
</article
{% endfor %}
Améliorer les requêtes
Doctrine se charge des requêtes, mais c’est souvent moche : au lieu de faire une jointure, il fait plusieurs requêtes SELECT. Dans ce cas-là, on fait des requêtes personnalisées : possible en DQL ou avec le QueryBuilder.
Exemple pour les saisons des séries :
- dans le SerieRepository :
On crée une méthode findAllWithSeasons() avec une jointure sur l’attribut s.seasons (dont l’alias est seasons), trié par ordre décroissant de popularité.
Puis on crée une requête et on récupère le résultat qu’on retourne.
public function findAllWithSeasons() {
$queryBuilder = ($this->createQueryBuilder('s'))
->leftJoin('s.seasons', 'seasons'); // pour aussi avoir les series sans saison
->addSelect('seasons')
->addOrderBy('s.popularity', 'DESC');
$query = $queryBuilder->getQuery();
$query->setMaxResults(10);
return $query->getResult();
}- dans le SerieController :
On utilise la méthode findAllWithSeasons() à la place du findAll().
Gérer la pagination
Paginator est un outil pour faire le group by sur un ensemble d’enregistrements et les compter correctement (ex: plusieurs saisons pour une seule série).
setMaxResults(), on obtient le nombre de lignes (et pas le nombre de séries !!).On ajoute le paginator à la fin et c’est lui qu’on retourne.
public function findAllWithSeasons() {
$queryBuilder = ($this->createQueryBuilder('s'))
->leftJoin('s.seasons', 'seasons'); // pour aussi avoir les series sans saison
->addSelect('seasons')
->addOrderBy('s.popularity', 'DESC');
$query = $queryBuilder->getQuery();
$query->setMaxResults(10);
$paginator = new Paginator($query);
return $paginator;
}
Afficher un champ objet dans un formulaire
Dans la classe du formulaire,
- on précise que le type du champ est une entité :
EntityType::class
- on lie l’entité concernée :
"class" => Serie::class
- on choisit l’attribut à afficher : “
choice_label" => "name"
// MovieFormType (relation avec Country)
<?php
namespace App\Form;
use App\Entity\Country;
use App\Entity\Movie;
use App\Repository\CountryRepository;
use Doctrine\Migrations\Tests\Provider\C;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MovieFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, [
"label" => "Titre ",
"required" => true
])
->add('releaseYear', IntegerType::class, ["label" => "Année de sortie "])
->add('country', EntityType::class, [
"class" => Country::class,
"choice_label" => "nameFrench",
"label" => "Pays "
])
->add('wasSeen', CheckboxType::class, [
"label" => "Déjà vu ?",
"required" => false
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Movie::class,
]);
}
}
Enregistrer, supprimer et automatiser
Pour enregistrer une entité associée à une autre, il faut faire attention à l’ordre : on crée d’abord l’instance de l’entité à associer puis l’entité qui la contiendra et on lie les deux grâce au setter dédié.
Exemple : enregistrer un article avec ses commentaires
Dans le SerieController,
- on crée une instance de commentaire, on complète ses attributs
- on crée une instance d’article, on complète ses attributs — dont l’attribut objet commentaire
- on enregistre les deux instances
- on envoie les modifications en base de données
public function enregistrerArticleAvecCommentaires(
EntityManagerInterface $em
){
$commentaire = (new Commentaire())
->setContent("Un super commentaire gentil pour une fois");
$article = (new Article())
->setTitle("Un article sur la zététique"
->setCommentaire($commentaire); //associe le commentaire avec l'article
$em->persist($commentaire);
$em->persist($article);
$em->flush();
return $this->render('unTwig.html.twig');
}
Pour supprimer une entité associée à une autre, il faut faire attention à l’ordre : on supprime d’abord l’entité liée puis l’entité principale (qui contient l’autre).
Exemple : supprimer un article avec ses commentaires
Dans le SerieController,
- on récupère l’instance d’article avec une méthode du Repository
- on supprime l’instance de commentaire
- on supprime l’instance d’article
- on envoie les modifications en base de données
public function supprimerArticleAvecCommentaires(
EntityManagerInterface $em
){
$em->remove($commentaire);
$em->remove($article);
$em->flush();
return $this->render('unTwig.html.twig');
}
Pour automatiser ces processus, on peut utiliser l’option cascade dans l’attribut #[ORM\...] : persist | remove | merge | detach | refresh | all.
#[ORM\OneToMany(mappedBy: 'country', targetEntity: Movie::class, cascade:['persist', 'remove'])]