Les relations
Avec un ORM, nous allons réfléchir notre base de données sous forme objet.
Cela signifie que dans l'exemple que nous réalisons, c'est-à-dire un site d'actualités, nous avons créé une entité Article, correspondant à la table article de notre base de données.
Si nous souhaitons que nos articles aient une catégorie, alors en base de données, on sait que ce type de besoin correspond à une association 1-N, avec ajout d'une clé étrangère dans la table article, vers une table de catégories.
Avec Doctrine, ce type d'association se représente sous la forme d'une relation. Chaque relation disponible va représenter un type d'association qui serait présent dans notre MCD/MLD.
Ajout d'une relation OneToMany
Avec Doctrine, on va donc créer une entité Category, mais le but est de pouvoir manipuler nos instances de classes de la manière suivante par exemple : si je dispose d'une instance de Category, alors j'aimerais pouvoir récupérer tous les articles de la catégorie avec $category->getArticles();.
Dans le même esprit, si je consulte un article, donc que je dispose d'une instance d'Article, j'aimerais pouvoir récupérer sa catégorie avec $article->getCategory().
Tout ça va pouvoir être géré automatiquement avec le maker, qui va nous inscrire le code approprié au bon endroit. Il s'agira donc de comprendre ce qui a été ajouté lorsqu'on crée une relation entre deux entités.
On va donc créer un attribut articles dans notre entité Category. Mais dans le maker, nous allons pouvoir définir cette propriété comme étant une relation, et plus précisément une relation OneToMany reliée à l'entité Article.
L'assistant va également nous demander si on souhaite créer un nouvel attribut dans Article, afin de créer une relation bi-directionnelle, nous permettant de récupérer les articles d'une catégorie, et la catégorie d'un article. On confirme la création d'un attribut category dans l'entité Article.
L'entité Category est alors créée, et l'entité Article a été modifiée en conséquence :
class Article
{
//...
#[ORM\ManyToOne(inversedBy: 'articles')]
private ?Category $category = null;
//...
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
}
class Category
{
//...
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Article::class)]
private Collection $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
}
//...
/**
* @return Collection<int, Article>
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles->add($article);
$article->setCategory($this);
}
return $this;
}
public function removeArticle(Article $article): self
{
if ($this->articles->removeElement($article)) {
// set the owning side to null (unless already changed)
if ($article->getCategory() === $this) {
$article->setCategory(null);
}
}
return $this;
}
}
L'attribut articles est créé et géré du point de vue de Doctrine comme une ArrayCollection, une collection d'objets à laquelle on peut enlever/ajouter des éléments
Ce qu'on constate donc, c'est que de notre point de vue, c'est-à-dire notre application Symfony, on ne se concentre que sur la gestion de nos objets. Il n'y a nulle part dans la classe Article une référence à une clé étrangère vers la table Category. À la place, nous avons un attribut portant le type Category. C'est Doctrine qui se chargera donc de récupérer la catégorie correspondante, si on en a besoin.
Vu qu'on vient de créer une nouvelle entité, il faut impacter notre base de données avec ces changements : on va générer une nouvelle migration puis l'exécuter pour déclencher la création de la nouvelle table.
À l'exécution de la migration, Doctrine va automatiquement modifier la table article pour ajouter la clé étrangère vers la nouvelle table category. La clé étrangère existe donc bel et bien dans notre base de données, mais nous n'avons pas à la gérer manuellement via le code. Ce qu'on va essayer de gérer, ce sont des relations entre nos objets.
Et nos fixtures ?
Si nous créons une nouvelle entité, comme ici avec Category, alors il convient de l'ajouter à nos fixtures.
Dans ce cas, nous avons 2 options :
- Utiliser nos fixtures pour intégrer les catégories, puis créer les articles avec des catégories reliées automatiquement via deux boucles : une pour les catégories et une pour les articles
- Utiliser l'intégration d'ORM présente dans la librairie Faker pour lui laisser deviner les valeurs à générer pour chaque colonne. Faker s'assurera également de la bonne liaison entre un article et une catégorie
Conclusion
Les relations entre entités s'établissent directement au sein des classes concernées.
Dans ces classes, il n'est pas question de déclarer des clés étrangères, mais des liens directs vers des instances de classes. C'est Doctrine qui se chargera de créer l'élément de structure approprié dans la base de données.
Enfin, on remarque une chose : avec la console de Symfony, depuis la création de la base de données jusqu'à l'ajout de nouvelles tables ou de liens entre les tables, nous ne touchons plus à la structure manuellement. Nous utilisons les migrations et les commandes de la console pour déléguer ce travail.