Comment programmer une ManyToOne sous Doctrine
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 ?
Vocabulaire
Avant de commencer il est intéressant que l'on se mette d'accord sur le vocabulaire qui sera utilisé par la suite :
- owning side : Côté propriétaire, là où la clé étrangère sera stockée. Toujours du coté many.
- inverse side : Côté inverse. Toujours du coté one.
- manyToOne : Mon entité A contient une ou plusieurs données de mon entité B. Je suis donc le owning side.
- oneToMany : Mon entité B est donc reliée à une ou plusieurs données de mon entité A. Je suis donc le inverse side.
- oneToOne : Chacune de mes deux entités ne contient qu'une seule donnée de chaque. Comme le owning side est toujours du côté many, logiquement elles devraient être toutes les deux inverse side. Sauf que pour faire la liaison, il faut choisir qui sera le owning side et qui sera le inverse side.
- manyToMany : Chacune de mes deux entités contient une ou plusieurs données de chaque. Elles sont donc toutes les deux le owning side grâce à une table associative entre les deux. C'est donc en faite deux manyToOne vers la table associative. Mais cela est invisible pour vous.
Owning side dans Doctrine
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.
Programmer une liaison "parfaite"
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 :
- Article > oneToMany > Comment
- Comment > manyToOne > Article
oneToMany
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]
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.
manyToOne
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
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.
Source
Steevan BARBOYON, comment bien coder une manyToOne bidirectionnelle
Articles liés
0 commentaire
Soyez le premier à commenter l'article.