Doctrine n'est pas forcément un ORM facile à utiliser quand on débute et afin de ne pas tomber dans des pièces voici comment créer une ManyToOne facilement.
Vous avez sans doute déjà entendu parler de l'ORM (Object Relational Mapper) Doctrine et de ses liaisons que l'on peut y faire (oneToOne, manyToOne, oneToMany ou manyToMany). Mais savez-vous comment programmer ces liaisons dans vos entités ?
Avant de commencer il est intéressant que l'on se mette d'accord sur le vocabulaire qui sera utilisé par la suite :
Il y a une chose très importante à savoir dans Doctrine. Il ne gère QUE le owning side car c'est ce côté qui contient la clé étrangère. Le inverse side n'est là que pour vous, afin que vous puissiez visualiser facilement les différentes liaisons.
J'ai mis parfaite entre guillemets car cela va correspondre à 99% de vos liaisons.
Comme Doctrine ne gère que le owning side, il se peut que vous ayez des bugs dans vos entités car vous pensez qu'il va sauvegarder les données côté inverse side lors du persist
alors qu'en fait, comme ce n'est là que pour faire joli, vous vous retrouviez avec aucune donnée, un null
dans le owning side ou des données orphelines dans la table inverse side.
Prenons par exemple un blog avec des articles et des commentaires. Ce qui nous fait :
class Article
{
/** @var Collection|Comment[] */
protected $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function setComments(iterable $comments): self
{
$this->clearComments();
/** @var Comment $comment */
foreach ($comments as $comment) {
$this->addComment($comment);
}
return $this;
}
public function addComment(Comment $comment): self
{
if (false === $this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setArticle($this);
}
return $this;
}
/** @return Collection|Comment[] */
public function getComments(): Collection
{
return $this->comments;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
$comment->setArticle(null);
}
return $this;
}
public function clearComments(): self
{
foreach ($this->getComments() as $comment) {
$this->removeComment($comment);
}
$this->comments->clear();
return $this;
}
}
class Article
{
/** @var Collection|Comment[] */
protected $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function setComments(iterable $comments): self
{
$this->clearComments();
/** @var Comment $comment */
foreach ($comments as $comment) {
$this->addComment($comment);
}
return $this;
}
public function addComment(Comment $comment): self
{
if (false === $this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setArticle($this);
}
return $this;
}
/** @return Collection|Comment[] */
public function getComments(): Collection
{
return $this->comments;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
$comment->setArticle(null);
}
return $this;
}
public function clearComments(): self
{
foreach ($this->getComments() as $comment) {
$this->removeComment($comment);
}
$this->comments->clear();
return $this;
}
}
Notre propriété $comments
est à la fois une Collection
et un tableau de Comment
afin d'avoir l'auto-complétion des deux classes. Collection
est utile car il est compatible avec ArrayCollection
et PersistentCollection
qui vous seront retournés par Doctrine.
Dans le __construct
il est important d'initialiser $comments
avec ArrayCollection
sinon nous ne pourrons jamais ajouter de commentaires à notre article. Impossible de l'initialiser avec Collection
car, comme son nom ne l'indique pas du tout, c'est une interface.
setComments
ne fait pas de $this->comments = new ArrayCollection();
car les commentaires qui existent déjà sur notre article ne seront jamais supprimés. C'est pour cela qu'il y a les méthodes removeComment
et clearComments
.
clearComments
ne doit pas non plus faire $this->comments = new ArrayCollection();
pour la même raison. Par contre, il faut bien penser à faire un clear
pour remettre le pointeur du tableau à zéro.
Dans le addComment
il est intéressant de noter deux choses. La première qu'il faut d'abord vérifier que notre Collection
ne contient pas déjà notre commentaire et surtout, comme dit plus haut, comme le inverse side n'est là que pour faire joli, il faut notifier le owning side qu'il est lié à un article.
Comme pour le addComment
, dans le removeComment
il faut notifier le owning side qu'il n'est plus lié à un article avec $comment->setArticle(null);
. Celui-ci sera supprimé grâce à la propriété orphanRemoval
.
Article:
oneToMany:
comments:
targetEntity: Comment
mappedBy: article
orphanRemoval: true
cascade: [persist, remove]
Article:
oneToMany:
comments:
targetEntity: Comment
mappedBy: article
orphanRemoval: true
cascade: [persist, remove]
Comme nous comme inverse side, il faut mettre mappedBy
pour indiquer avec quelle propriété notre entité est liée.
orphanRemoval
permet de supprimer automatiquement les relations qui sont à null
. Côté logique, il devrait être plutôt placé du coté owning side mais non, c'est bien ici qu'il faut le placer.
cascade persist
permet de déclencher la sauvegarde des commentaires lors de la sauvegarde d'un article.
cascade remove
permet de déclencher la suppression des commentaires lors de la suppression d'un article.
class Comment
{
/** @var ?Article */
protected $article;
public function setArticle(?Article $article): self
{
$this->article = $article;
if ($article instanceof Article) {
$article->addComment($this);
}
return $this;
}
public function getArticle(): ?Article
{
return $this->article;
}
}
class Comment
{
/** @var ?Article */
protected $article;
public function setArticle(?Article $article): self
{
$this->article = $article;
if ($article instanceof Article) {
$article->addComment($this);
}
return $this;
}
public function getArticle(): ?Article
{
return $this->article;
}
}
Dans setArticle
on regarde si $article
est bien une instance de la classe Article
pour eviter une boucle infinie lors des notifications.
Comment:
manyToOne:
article:
targetEntity: Article
inversedBy: comments
joinColumn:
nullable: false
Comment:
manyToOne:
article:
targetEntity: Article
inversedBy: comments
joinColumn:
nullable: false
Comme nous sommes du coté owning side, il faut renseigner la propriété inversedBy
.
Le joinColumn
à null
est nécessaire car par defaut, Doctrine laisse la possibilité de mettre une clé étrangère à null
. Et c'est grâce à cela que orphanRemoval
va pouvoir faire son travail et supprimer les liaisons inutiles.