Search code examples
phpdoctrine-ormdatabase-relations

Doctrine2 - Saving custom join relation table


Basically, I have two tables (Article and Tag) and I want to make many-to-many (one article can have many tags, one tag can be assigned to many articles) relation with some extra attributes. I can write this in Doctrine2 by breaking it into two separate relations (one-to-many, many-to-one) and one relation table ArticleTag with my extra attributes.

My problem is that I don't know if I can make Doctrine2 to create also the join table entities for me. What I mean is when I call:

$article = /* create new article, etc... */
$tag = /* create new tag, etc... */
$article->addTag($tag);

$em->persist($article);
$em->flush();

It DOES create both Article and Tag entities in the database but it DOES NOT create ArticleTag entity (in other words, it doesn't create the connection between Article and Tag). I could create it on my own but I would rather rely on Doctrine2.
Of course, it works fine when I use standard join table generated by Doctrine2 but I need those extra attributes.

Does anyone have any idea or do I really have to do it manually?

EDIT: source codes

/**
 * @ORM\Entity
 */
class Article {
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\OneToMany(targetEntity="Tag", mappedBy="article", cascade={"persist"})
     * @ORM\JoinTable(name="ArticleTag", joinColumns={@ORM\JoinColumn(name="article_id", referencedColumnName="id")})
     * )
     */
    protected $tags;

    ...
}

/**
 * @ORM\Entity
 */
class ArticleTag {

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Article")
     */
     private $article;

    /**
     * @ORM\ManyToOne(targetEntity="Tag")
     * @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
     */
     private $tag;

    /**
     * @ORM\Column(type="float")
     */
    protected $priority = 0.5;

}

/**
 * @ORM\Entity
 */
class Tag {

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=32)
     */
    protected $name;

}

Solution

  • There's not a way to do this automatically, per se, but you can change the addTag method on the Article class to something like this:

    public function addTag(Tag $tag, $priority)
    {
        $articleTag = new ArticleTag();
        $articleTag->setTag($tag);
        $articleTag->setArticle($this);
        $articleTag->setPriority($priority);
        $this->addArticleTag($articleTag);
        return $this;
    }
    

    That way, you keep your code centralized and hide the creation of the ArticleTag entry. For an additional explanation, here's the reasoning behind this logic:

    A class in the eyes of Doctrine represents an entity source, and each instance of that class represents an entity. In a simple many-to-many table, the many-to-many table is not an entity. In fact, it is merely a relationship between two entities, which is why Doctrine allows you to bypass this logic and doesn't require an ArticleTag entity if it only has foreign keys.

    However, once you add additional metadata to that table, it is no longer a relationship table. I have discussed this with many people who see things differently, but it's just not. Yes, it's defining that entity 1 and entity 2 are related, but that additional column defines additional metadata that is required with the association. Therefore it is an entity on its own and must be reflected as such.

    I had struggled with this for quite some time until I ended up adding the code that I showed above.