ManyToMany Relationship
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?
How to define in witch entity User or Genus we must use @ManyToMany annotation. Why do you use It in User instead of Genus class?