ManyToMany Relationship

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We need the ability to add tags to each Article. And that means, we need a new relationship! Like always, we could add this by hand. But, the generator can help us. At your terminal, run:

php bin/console make:entity

But, hmm. Which entity should we update? We could add a new property to Article called tags or... I guess we could also add a new property to Tag called articles. That's really the same relationship, just viewed from two different sides.

And... yea! We could choose to update either class. The side you choose will make a subtle difference, and we'll learn about it soon. Let's update Article. For the property name, use tags. Remember, we need to stop thinking about the database and only think about our objects. In PHP, I want an Article object to have many Tag objects. So, the property should be called tags.

For type, use the fake relation type to activate the relationship wizard. We want to relate this to Tag and... perfect! Here is our menu of relationship options! I already hinted that this will be a ManyToMany relationship. But, let's look at the description to see if it fits. Each article can have many tags. And, each tag can relate to many articles. Yep, that's us! This is a ManyToMany relationship.

And just like last time, it asks us if we also want to map the other side of the relationship. This is optional, and is only for convenience. If we map the other side, we'll be able to say $tag->getArticles(). That may or may not be useful for us, but let's say yes. Call the field articles, because it will hold an array of Article objects.

And, that's it! Hit enter to finish.

Looking at the Generating Entities

Exciting! Let's see what changes this made. Open Article first:

... lines 1 - 15
class Article
{
... lines 18 - 68
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="articles")
*/
private $tags;
... lines 73 - 245
}

Yes! Here is the new tags property: it's a ManyToMany that points to the Tag entity. And, like we saw earlier with comments, whenever you have a relationship that holds many objects, in your constructor, you need to initialize that property to a new ArrayCollection:

... lines 1 - 15
class Article
{
... lines 18 - 73
public function __construct()
{
... line 76
$this->tags = new ArrayCollection();
}
... lines 79 - 245
}

The generator did that for us.

At the bottom, instead of a getter & setter, we have a getter, adder & remover:

... lines 1 - 15
class Article
{
... lines 18 - 220
/**
* @return Collection|Tag[]
*/
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(Tag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags[] = $tag;
}
return $this;
}
public function removeTag(Tag $tag): self
{
if ($this->tags->contains($tag)) {
$this->tags->removeElement($tag);
}
return $this;
}
}

There's no special reason for that: the adder & remover methods are just convenient.

Next, open Tag:

103 lines src/Entity/Tag.php
... lines 1 - 13
class Tag
{
... lines 16 - 35
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Article", mappedBy="tags")
*/
private $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
}
... lines 45 - 74
/**
* @return Collection|Article[]
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles[] = $article;
$article->addTag($this);
}
return $this;
}
public function removeArticle(Article $article): self
{
if ($this->articles->contains($article)) {
$this->articles->removeElement($article);
$article->removeTag($this);
}
return $this;
}
}

The code here is almost identical: a ManyToMany pointing back to Article and, at the bottom, getter, adder & remover methods.

Owning Versus Inverse Sides

Great! But, which side is the owning side and which is the inverse side of the relationship? Open Comment:

... lines 1 - 10
class Comment
{
... lines 13 - 31
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $article;
... lines 37 - 94
}

Remember, with a ManyToOne / OneToMany relationship, the ManyToOne side is always the owning side of the relation. That's easy to remember, because this is where the column lives in the database: the comment table has an article_id column.

But, with a ManyToMany relationship, well, both sides are ManyToMany! In Article, ManyToMany. In Tag, the same! So, which side is the owning side?

The answer lives in Article. See that inversedBy="articles" config?

... lines 1 - 15
class Article
{
... lines 18 - 68
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="articles")
*/
private $tags;
... lines 73 - 245
}

That points to the articles property in Tag. On the other side, we have mappedBy="tags", which points back to Article:

103 lines src/Entity/Tag.php
... lines 1 - 13
class Tag
{
... lines 16 - 35
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Article", mappedBy="tags")
*/
private $articles;
... lines 40 - 101
}

Here's the point: with a ManyToMany relationship, you choose the owning side by where the inversedBy versus mappedBy config lives. The generator configured things so that Article holds the owning side because that's the entity we chose to update with make:entity.

Remember, all of this owning versus inverse stuff is important because, when Doctrine saves an entity, it only looks at the owning side of the relationship to figure out what to save to the database. So, if we add tags to an article, Doctrine will save that correctly. But, if you added articles to a tag and save, Doctrine would do nothing. Well, in practice, if you use make:entity, that's not true. Why? Because the generated code synchronizes the owning side. If you call $tag->addArticle(), inside, that calls $article->addTag():

103 lines src/Entity/Tag.php
... lines 1 - 13
class Tag
{
... lines 16 - 82
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles[] = $article;
$article->addTag($this);
}
return $this;
}
... lines 92 - 101
}

Generating the Migration

Enough of that! Let's generate the migration:

php bin/console make:migration

Cool! Go open that file:

... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20180501143055 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE article_tag (article_id INT NOT NULL, tag_id INT NOT NULL, INDEX IDX_919694F97294869C (article_id), INDEX IDX_919694F9BAD26311 (tag_id), PRIMARY KEY(article_id, tag_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F97294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F9BAD26311 FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE');
}
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE article_tag');
}
}

Woh! It creates a new table! Of course! That's how you model a ManyToMany relationship in a relational database. It creates an article_tag table with only two fields: article_id and tag_id.

This is very different than anything we've seen so far with Doctrine. This is the first time - and really, the only time - that you will have a table in the database, that has no direct entity class. This table is created magically by Doctrine to help us relate tags and articles. And, as we'll see next, Doctrine will also automatically insert and delete records from this table as we add and remove tags from an article.

Now, run the migration:

php bin/console doctrine:migrations:migrate

Let's go tag some articles!

Leave a comment!

  • 2020-02-06 Victor Bocharsky

    Hey Mike,

    Wow, a lot of updates :D Yes, good catch! That;'s because of ManyToMany relation, it's a tricky relation... technically you're operating 2 entities, but behind the scene there're 3 tables, one of them is auxiliary table. Probably there're some technical reasons behind the scene, but I'm not sure what exactly. I bet it might be related to the code complexity. Anyway, I'm glad you figured out the working solution for you by yourself

    Cheers!

  • 2020-02-05 Mike

    UPDATE2://
    The reason seems to be that its a ManyToMany relationship.
    (And not a ManyToOne Relationship, like in the working a.author column.)

    Unsuccessful code:


    $qb
    ->leftJoin('r.categories', 'c')
    ->addSelect('c');
    ->andWhere('a.categories IN (:categories)')
    ->setParameter('categories', $this->categoryRepository->findAll()) // Just for testing purposes I dependency injected the categoryRepository

    Output:
    [Semantical Error] line 0, col 70 near 'categories IN': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.

    Successful code:

    A.)


    $qb
    ->leftJoin('r.categories', 'c')
    ->addSelect('c');
    ->andWhere(':categoriesId MEMBER OF a.categories')
    ->setParameter('categoriesId', $this->categoryRepository->findAll())

    B.)


    $qb
    ->leftJoin('r.categories', 'c')
    ->addSelect('c');
    ->andWhere('c.id IN (:categoriesId)')
    ->setParameter('categoriesId', $categories);

    Final questions:
    1.) But why does the new code work and the old doesn't?
    Because of the ManyToMany connection instead of ManyToOne? I don't understand why I have to explicitly select the .id instead of giving just the array of category entitys.
    2.) If the user visits: "./categories/cat1,cat2", he should get every article that has cat1 AND cat2.
    The IN query above is not suitable (because it is treating like cat1 OR cat2). I've tried another method:


    foreach ($categories as $key => $category) {
    $qb->andWhere('c.id = :categoryId'.$key)
    ->setParameter('categoryId'.$key, $category);
    }


    But this doesn't work as well (it querys in the article_category ManyToMany table for rows that have category_id = cat1 AND category_id = cat2, which of course would NEVER happen, because it should instead look for article_id's which has both category_id's (so it should output only articles with 2 datasets(one dataset for each category)))
    Do you know what kind of query is necessary to do exactly this, to show articles that have BOTH categories set on it? (ManyToMany of Articles and Categories)

    UPDATE3://
    I've found an answer to question 2.):


    foreach ($categories as $key => $category) {
    $qb->andWhere(':categoriesId'.$key.' MEMBER OF a.categories')
    ->setParameter('categoriesId'.$key, $category)
    ;
    }

    So only 1.) is open :)

  • 2020-02-05 Mike

    Goal:
    Just a simpel "left join" for a ManyToMany relation (Many Articles have Many Categories)

    Code (minified to just reproduce the error):


    class ArticleRepository extends ServiceEntityRepository
    ...
    public function getTest()
    {
    $qb = $this->createQueryBuilder('a')
    ->leftJoin('a.categories', 'c')
    ->addSelect('c')
    ->andWhere('a.categories IN (:categories)')
    ->setParameter('categories', $this->categoryRepository->findAll()) // Just for testing purposes I dependency injected the categoryRepository
    ;
    dd($qb
    ->getQuery()
    ->getResult())
    ;
    }

    Problem:
    This code results in a:
    [Semantical Error] line 0, col 70 near 'categories IN': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.

    $this->categoryRepository->findAll() is definitely returning an array of Category Entitys.

    getDQL() returns the correct query:

    "SELECT a, c FROM App\Entity\Article a LEFT JOIN a.categories c WHERE a.categories IN (:categories)"


    So strange... can you help?

    UPDATE://
    If I change the Repo to UserRepository and set a.author IN (:authors) with $userRepo->findAll(), its working.
    Conclusion: The problem only occurs with my category entity / repository.

  • 2019-06-05 Diego Aguiar

    You mean the "placeholder" way, right? If that's the case you only have to set up the "placeholder" option :)
    Here is an example: https://symfony.com/doc/cur...
    Just don't forget to make that field optional

    Cheers!

  • 2019-06-04 Giacomo Balloccu

    Lovely your first answer is perfect and it's what I'm trying to do. How can I do? I load from the datebase the values , can't find an option to set the default value empty

  • 2019-06-04 Diego Aguiar

    Ok, so, I still believe by adding a placeholder and making the field optional you can accomplish it. Otherwise I'm not sure what you can do, probably add a "All" tag record into the database, and explicitly look for it when handling your form

  • 2019-06-03 Giacomo Balloccu

    Sorry i didn't express myself in a correct way. Check how it is:
    http://i68.tinypic.com/e9x8...

    The goal is to have in that entitytype field as default the value all, that means: 'I don't care about the tag search no matter what's the tag'.

    How can I do that?

  • 2019-06-03 Diego Aguiar

    That's great, you did it, congrats!

    About your "ALL" option question... I'm not sure about your intention but what you can do is to add a placeholder that says "All options" and then, on submit, you check if that field is empty so you don't add any filters. I hope it makes any sense to you

    Cheers!

  • 2019-06-03 Giacomo Balloccu

    Nice idea! But I solved, for whom is interested:
    /**
    * @param string|null $term
    * @param Tag|null $tag
    * @return QueryBuilder
    */
    public function getQueryBuilderSearchAll(?string $term, ?Tag $tag)
    {
    $qb = $this->createQueryBuilder('a');

    $qb->leftJoin('a.tag', 'at')
    ->andWhere('a.content LIKE :term OR a.title LIKE :term OR at = :tag_id')
    ->setParameter('term', '%' . $term . '%')
    ->setParameter('tag_id', $tag)
    ->orderBy('a.date', 'DESC');
    ;

    return $qb;
    }

    That's the working query, now I'm going to do a research with date intervall, type article selection, tag and text together, I'll probably use a raw query for this.

    I have one question in my mind right know,how can I set a entitytype default value like: ALL in the form?

    I'm using:
    ->add('tag', EntityType::class, [
    'label' => 'Tag di Ricerca',
    'class' => Tag::class,
    'choices' => $this->tagRepository->findAllTagsAlphabetical(),
    'choice_label' => 'name',
    'by_reference' => false,

    ]);

    But I can't find online a way to put as default ALL

  • 2019-06-03 Diego Aguiar

    Hey Giacomo Balloccu

    I think this use case is complex for doing it with Doctrine ORM, have you tried writing a raw sql? I think it will be easier. If you have problems hydrating the results what you can do is to fetch first all the matching ids and then in a second query fetch just those id objects

    Cheers!

  • 2019-06-03 Giacomo Balloccu

    I got now, I have tried both passing id and obj:
    __clone method called on non-object
    :(
    Yes i'ts a many to many is exactly how is done in the course, but I would like to add a filter that allow me to search with a key word and search if the keyword is a tag. All should be fine but doctrine doesn't like my queries

  • 2019-06-03 Vladimir Sadicov

    Could you please check what exact type of $tag variable. I think I got it, you have Many-To-Many relation here? so you need leftJoin for search, something like

    ->leftJoin('a.tag', 'at')->andWhere('a.content LIKE :term OR a.title LIKE :term OR at.id = :tag_id')

    not sure about example, but something like this.

    Cheers!

  • 2019-06-03 Giacomo Balloccu

    Thanks but doesn't work, I have tried both with passing object or id, I still get this:
    [Semantical Error] line 0, col 89 near 'tag = :tag ORDER': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.
    doing:
    /**
    * @param string|null $term
    * @param int|null $tag
    * @return QueryBuilder
    */
    public function getQueryBuilderSearchAll(?string $term, ?int $tag)
    {
    $qb = $this->createQueryBuilder('a');

    $qb->andWhere('a.content LIKE :term OR a.title LIKE :term OR a.tag = :tag')
    ->setParameters(array('term' => '%' . $term . '%', 'tag' => $tag))
    ->orderBy('a.date', 'DESC');

    return $qb;
    }

    and passing a object same, I get semantical error.

  • 2019-06-03 Vladimir Sadicov

    Hey Giacomo Balloccu

    You got interesting error, I'm not quite sure, but you can't use LIKE expression with objects or ids. If you want to use ID of tag, than try OR a.tag_id = :id or if you have tag object than a.tag = :tag also you can use JOINs but it's not necessary.

    Cheers!

  • 2019-06-01 Giacomo Balloccu

    I use the paginator with the query builder, but it doesn't work same error, is because i'm not using a join? How can i do that?

  • 2019-05-29 Giacomo Balloccu

    I use the paginator:

    /**@var Tags $tags*/
    $tags = $repositoryt->findAllTagsAlphabetical();

    $pagination = $paginator->paginate(
    $queryBuilder, /* query NOT result */
    $request->query->getInt('page', 1), /*page number*/
    5 /*limit per page*/
    );

    return $this->render('articles/allarticles.twig', [
    'title' => 'Ricerca',
    'articles' => $pagination,
    'tags' => $tags
    ]);

  • 2019-05-29 Diego Aguiar

    Ok, so the error changed and it's related to this problem: https://stackoverflow.com/a...

    What do you with the QB after calling $queryBuilder = $repository->getQueryBuilderSearchAll($q, $searchedTag);?

  • 2019-05-29 Giacomo Balloccu

    I have tried also giving it a object, check it out:

    Controller:
    $repositoryt = $em->getRepository(Tag::class);

    $q = $request->query->get('search') ?? null;
    /** @var Tag $searchedTag */
    $searchedTag = $repositoryt->findOneBy(array('name' => $q));

    $queryBuilder = $repository->getQueryBuilderSearchAll($q, $searchedTag);

    Repository:
    /**
    * @param string|null $term
    * @param Tag|null $tag
    * @return QueryBuilder
    */
    public function getQueryBuilderSearchAll(?string $term, ?Tag $tag)
    {
    $qb = $this->createQueryBuilder('a');
    if ($term && $tag) {
    $qb->andWhere('a.content LIKE :term OR a.title LIKE :term OR a.tag LIKE :tag')
    ->setParameters(array('term' => '%' . $term . '%', 'tag' => $tag))
    ->orderBy('a.date', 'DESC');
    ;
    }elseif($term && !($tag)){
    $qb->andWhere('a.content LIKE :term OR a.title LIKE :term')
    ->setParameter('term', '%' . $term . '%')
    ->orderBy('a.date', 'DESC');
    ;
    }
    return $qb;
    }

    I get:
    [Semantical Error] line 0, col 89 near 'tag LIKE :tag': Error: Invalid PathExpression. Must be a StateFieldPathExpression.

    Either using equal or like

  • 2019-05-28 Diego Aguiar

    Hey Giacomo Balloccu

    Doctrine is expecting an entity object instead of an ID. Do that change and try again :)

    Cheers!

  • 2019-05-28 Giacomo Balloccu

    Hi! I'm doing a custom query to select from manytomany, I have articles and tags like you have but I would like when i search a word to check if the word is a category or not.
    I have done that: in controller i pass :term which is the word to:

    /**
    * @param string|null $term
    * @param int|null $id
    * @return QueryBuilder
    */
    public function getQueryBuilderSearchAll(?string $term, ?int $id)
    {
    $qb = $this->createQueryBuilder('a');
    if ($term && $id) {
    $qb->andWhere('a.content LIKE :term OR a.title LIKE :term OR a.tag = :id')
    ->setParameters(array('term' => '%' . $term . '%', 'id' => $id))
    ->orderBy('a.date', 'DESC');
    ;
    }elseif($term && !($id)){
    $qb->andWhere('a.content LIKE :term OR a.title LIKE :term')
    ->setParameter('term', '%' . $term . '%')
    ->orderBy('a.date', 'DESC');
    ;
    }
    return $qb;
    }

    And the id from controller of the corresponding tag for the :term if exist. It seems ok to me but I get the error:
    [Semantical Error] line 0, col 89 near 'tag = :id ORDER': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.

    Can't understand why

  • 2019-04-24 rumen

    Thanks Vladimir.

    Data Transformer sounds like an option. My situation is a little bit different. I have an offer search form on the main page and all it's fields are not obligatory. That's why Ajax call submits null when no category is selected. I made it like I said in the post marking all the fields as mapped false in the Form Type and take what I need in the controller from the Request object.
    But I will try with the data transformation too.

    Thanks a lot and cheers!

  • 2019-04-23 Vladimir Sadicov

    Hey rumen

    It's really difficult to say something without code example. Sounds like your form tries to save NULL value to "category" field for "Offer" entity. There could be several solutions, probably form field definition cannot process ManyToMany relation, or it's misconfigured. Or you should create a DataTransformer for it. You can check how it's working here https://symfonycasts.com/sc... or look in Symfony Docs.

    Cheers!

  • 2019-04-21 rumen

    Hello all,
    I have the same problem like cybernet2u's
    My situation is as follows: I have an Offer entity with the following relations
    Offer -> Country - ManyToOne
    Offer->City - ManyToOne
    Offer->Category - ManyToMany

    I also have a relation ManyToMany between the Country and City entities.

    I have a search form created with the form builder. I made the city field dependant on the selected country value. If i mark the category element in the FormType as 'mapped' => false, everything with the dependant fields works ok but when I submit the form, the form data for the category field is null as exected.
    When I remove the mapped => false part for the category element in the FormType, when I change the country the ajax call is submitted but the result gives me the same exception as the one cybernet2u's got.

    Could not determine access type for property "category" in class "App\Entity\Offer": The property "category" in class "App\Entity\Offer" can be defined with the methods "addCategory()", "removeCategory()" but the new value must be an array or an instance of \Traversable, "NULL" given.


    I have both the methods in the Offer entity (all entities I've made with the make:entity)

    Any ideas how to fix?
    Thanks in addvance.

  • 2019-02-28 Diego Aguiar

    Hey cybernet2u

    I have an idea. Try syncing both sides when adding/removing items


    public function addShip(Ships $ship): self
    {
    if (!$this->ship->contains($ship)) {
    $this->ship[] = $ship;
    $ship->addMemeber($this);
    }

    If it doesn't work I may have too give it a look to your code. Cheers!

  • 2019-02-28 cybernet2u

    any idea howto fix this ?

    Could not determine access type for property "ship" in class "App\Entity\Members": The property "ship" in class "App\Entity\Members" can be defined with the methods "addShip()", "removeShip()" but the new value must be an array or an instance of \Traversable, "App\Entity\Ships" given.

    i have a ManyToMany relation between them two
    in a form i get this error
    even if i have the add & remove methods


    // Members

    public function addShip(Ships $ship): self
    {
    if (!$this->ship->contains($ship)) {
    $this->ship[] = $ship;
    }


    return $this;
    }


    public function removeShip(Ships $ship): self
    {
    if ($this->ship->contains($ship)) {
    $this->ship->removeElement($ship);
    }


    return $this;
    }

  • 2019-01-11 Diego Aguiar

    Cool, I'm glad to know that you could fix your problem :)

  • 2019-01-11 Emin

    *UPDATE*
    I have found the problem. I don't know why but apperently the index was UNIQ_389B783989D9B62 instead of slug, that was the reason why i gave an error :)

    Cheers!

  • 2019-01-11 Emin

    Hey Diego,

    I do have a column slug and when i try to run your code i get the same error :(

    Cheers!

  • 2019-01-10 Diego Aguiar

    Hey Emin

    This line is the problem $this->addSql('DROP INDEX slug ON tag'); Are you sure the "slug" INDEX exists on the Tag table?
    I believe what you want to drop is the field "slug", so you would need to add a SQL code like this:


    ALTER TABLE your_table
    DROP COLUMN column_name

    Cheers!

  • 2019-01-10 Emin

    Hey,

    When i try to migrate i get the following error which is weird cause there shouldn't go anything wrong.
    "1091 Can't DROP 'slug'; check that column/key exists"

    The following code is what is in my migration file and i dont see anything wrong with it.


    public function up(Schema $schema) : void
    {
    // this up() migration is auto-generated, please modify it to your needs
    $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

    $this->addSql('CREATE TABLE article_tag (article_id INT NOT NULL, tag_id INT NOT NULL, INDEX IDX_919694F97294869C (article_id), INDEX IDX_919694F9BAD26311 (tag_id), PRIMARY KEY(article_id, tag_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
    $this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F97294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE');
    $this->addSql('ALTER TABLE article_tag ADD CONSTRAINT FK_919694F9BAD26311 FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE');
    $this->addSql('DROP INDEX slug ON tag');
    $this->addSql('DROP INDEX slug_2 ON tag');
    $this->addSql('ALTER TABLE tag CHANGE slug slug VARCHAR(255) NOT NULL');
    }

    public function down(Schema $schema) : void
    {
    // this down() migration is auto-generated, please modify it to your needs
    $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

    $this->addSql('DROP TABLE article_tag');
    $this->addSql('ALTER TABLE tag CHANGE slug slug VARCHAR(191) NOT NULL COLLATE utf8mb4_unicode_ci');
    $this->addSql('CREATE UNIQUE INDEX slug ON tag (slug)');
    $this->addSql('CREATE INDEX slug_2 ON tag (slug)');
    }

  • 2018-12-13 weaverryan

    Oh, and to add one more thing! It's not important (for Doctrine) to set the "inverse" side... but we *could* still do this technically, just to keep all our code "in sync". The reason we don't do this (well, technically, the reason that MakerBundle does not do this) is that calling $tag->addArticle() would cause Doctrine to query for all of the Articles for this Tag so that it could then add the Article to that collection. In other words, "synchronizing" the inverse side could have some performance impacts - so we avoid it.

    Cheers!

  • 2018-12-13 Victor Bocharsky

    Hey Larry,

    Thank you for your feedback! We're glad you understand our explanation better, though it's sad official docs do it bad, probably someone will create a PR to tweak it and make it more understandable for newcomers. Maybe you're that guy who can suggest some improvements in those docs. It's always tricky to write a good docs, many edge cases, but it always open for improvements.

    About your question, it's all about "owning vs inverse sides", you always have an owning and an inverse sides, we explain it here: https://symfonycasts.com/sc... . So basically, because of Tag entity is an inverse side of Article-Tag relation, you need to call that extra "$article->addTag()". In other words, Doctrine just ignores any changes on inverse side.

    I hope it helps!

    Cheers!

  • 2018-12-11 Larry Lu

    This video explains better than the one in the Doctrine document. Their examples are terrible and the explanation does not make sense:
    https://www.doctrine-projec....

    Though there's are one thing here I see here that doesn't get explained. Why is there a $article->addTag() call in the addArticle method of the Tag entity but not vise versa in the Article entity?