ManyToMany Relationship

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Let's talk about the famous, ManyToMany relationship. We already have a Genus entity and also a User entity. Before this tutorial, I updated the fixtures file. It still loads genuses, but it now loads two groups of users:

... lines 1 - 22
AppBundle\Entity\User:
user_{1..10}:
email: weaverryan+<current()>@gmail.com
plainPassword: iliketurtles
roles: ['ROLE_ADMIN']
avatarUri: <imageUrl(100, 100, 'abstract')>
user.aquanaut_{1..10}:
email: aquanaut<current()>@example.org
plainPassword: aquanote
isScientist: true
firstName: <firstName()>
lastName: <lastName()>
universityName: <company()> University
avatarUri: <imageUrl(100, 100, 'abstract')>

The first group consists of normal users, but the second group has an isScientist boolean field set to true. In other words, our site will have many users, and some of those users happen to be scientists.

That's not really important for the relationship we're about to setup, the point is just that many users are scientists. And on the site, we want to keep track of which genuses are being studied by which scientists, or really, users. So, each User may study many genuses. And each Genus, may be studied by many Users.

This is a ManyToMany relationship. In a database, to link the genus table and user table, we'll need to add a new, middle, or join table, with genus_id and user_id foreign keys. That isn't a Doctrine thing, that's just how it's done.

Mapping a ManyToMany in Doctrine

So how do we setup this relationship in Doctrine? It's really nice! First, choose either entity: Genus or User, I don't care. I'll tell you soon why you might choose one over the other, but for now, it doesn't matter. Let's open Genus. Then, add a new private property: let's call it $genusScientists:

This could also be called users or anything else. The important thing is that it will hold the array of User objects that are linked to this Genus:

... lines 1 - 14
class Genus
{
... lines 17 - 74
private $genusScientists;
... lines 76 - 172
}

Above, add the annotation: @ORM\ManyToMany with targetEntity="User".

... lines 1 - 14
class Genus
{
... lines 17 - 71
/**
* @ORM\ManyToMany(targetEntity="User")
*/
private $genusScientists;
... lines 76 - 172
}

Doctrine ArrayCollection

Finally, whenever you have a Doctrine relationship where your property is an array of items, so, ManyToMany and OneToMany, you need to initialize that property in the __construct() method. Set $this->genusScientists to a new ArrayCollection():

... lines 1 - 14
class Genus
{
... lines 17 - 76
public function __construct()
{
... line 79
$this->genusScientists = new ArrayCollection();
}
... lines 82 - 172
}

Creating the Join Table

Next... do nothing! Or maybe, high-five a stranger in celebration... because that is all you need. This is enough for Doctrine to create that middle, join table and start inserting and removing records for you.

It can be a bit confusing, because until now, every table in the database has needed a corresponding entity class. But the ManyToMany relationship is special. Doctrine says:

You know what? I'm not going to require you to create an entity for that join table. Just map a ManyToMany relationship and I will create and manage that table for you.

That's freaking awesome! To prove it, go to your terminal, and run:

./bin/console doctrine:schema:update --dump-sql

Boom! Thanks to that one little ManyToMany annotation, Doctrine now wants to create a genus_user table with genus_id and user_id foreign keys. Pretty dang cool.

JoinTable to control the... join table

But before we generate the migration for this, you can also control the name of that join table. Instead of genus_user, let's call ours genus_scientists - it's a bit more descriptive. To do that, add another annotation: @ORM\JoinTable. This optional annotation has just one job: to let you control how things are named in the database for this relationship. The most important is name="genus_scientist":

... lines 1 - 14
class Genus
{
... lines 17 - 71
/**
* @ORM\ManyToMany(targetEntity="User")
* @ORM\JoinTable(name="genus_scientist")
*/
private $genusScientists;
... lines 77 - 173
}

With that, find your terminal again and run:

./bin/console doctrine:migrations:diff

Ok, go find and open that file!

... lines 1 - 10
class Version20160921164430 extends AbstractMigration
{
... lines 13 - 15
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 genus_scientist (genus_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_66CF3FA885C4074C (genus_id), INDEX IDX_66CF3FA8A76ED395 (user_id), PRIMARY KEY(genus_id, user_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA885C4074C FOREIGN KEY (genus_id) REFERENCES genus (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE genus_scientist ADD CONSTRAINT FK_66CF3FA8A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE');
}
... lines 25 - 28
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 genus_scientist');
}
}

Woohoo!

Now it creates a genus_scientist table with those foreign keys. Execute the migration:

./bin/console doctrine:migrations:migrate

Guys: with about 5 lines of code, we just setup a ManyToMany relationship. Next question: how do we add stuff to it? Or, read from it?

Leave a comment!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}