Many-To-Many 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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeAlrighty, we've got a Starship
entity and a Droid
entity set up and ready to mingle. How do we get these two entities to connect?
Picture it this way: Each Starship
is going to need a crew of Droids
to keep things running smoothly... and for the occasional comic relief. Each Droid
, in turn, should be able to serve on many Starships
. Forget about the database and just focus on the objects. Our Starship
entity needs a droids
property that holds a collection of all the Droid
s assigned to it.
Cool! Head back to your terminal and run:
symfony console make:entity
Update Starship
and add a droids
property. Use "relation" to get into the handy wizard. This time, we need a ManyToMany
relationship:
Each
Starship
can have manyDroids
, and eachDroid
can serve on manyStarships
. That sounds perfect!
Next, it asks us if we want to map the inverse side of the relationship. This is asking if we want to give our Droids
the ability to list all the Starships
they're connected to: $droid->getShips()
. That sounds useful. So let's say "yes". For the new field name inside Droid
, starships
will do just fine.
Notice it's updated both Starship
and Droid
. Take a peek at the changes in each.
The 'ManyToMany' Magic
In Starship
, we now have a new droids
property, which is a ManyToMany
. It also initialized droids
to the ArrayCollection
and added getDroids()
, addDroid()
, and removeDroid()
methods:
// ... lines 1 - 15 | |
class Starship | |
{ | |
// ... lines 18 - 50 | |
/** | |
* @var Collection<int, Droid> | |
*/ | |
#[ORM\ManyToMany(targetEntity: Droid::class, inversedBy: 'starships')] | |
private Collection $droids; | |
public function __construct() | |
{ | |
// ... line 59 | |
$this->droids = new ArrayCollection(); | |
} | |
// ... lines 62 - 199 | |
/** | |
* @return Collection<int, Droid> | |
*/ | |
public function getDroids(): Collection | |
{ | |
return $this->droids; | |
} | |
public function addDroid(Droid $droid): static | |
{ | |
if (!$this->droids->contains($droid)) { | |
$this->droids->add($droid); | |
} | |
return $this; | |
} | |
public function removeDroid(Droid $droid): static | |
{ | |
$this->droids->removeElement($droid); | |
return $this; | |
} | |
} |
If you're thinking this looks a lot like a OneToMany
relationship, ding, ding! Order yourself a pizza! Because it totally is!
Over in Droid
, it's a similar story. We have a starships
property, which is a ManyToMany
, and it's initialized in the constructor. Then we have the same getStarships()
, addStarship()
, and removeStarship()
:
// ... lines 1 - 10 | |
class Droid | |
{ | |
// ... lines 13 - 23 | |
/** | |
* @var Collection<int, Starship> | |
*/ | |
#[ORM\ManyToMany(targetEntity: Starship::class, mappedBy: 'droids')] | |
private Collection $starships; | |
public function __construct() | |
{ | |
$this->starships = new ArrayCollection(); | |
} | |
// ... lines 34 - 63 | |
/** | |
* @return Collection<int, Starship> | |
*/ | |
public function getStarships(): Collection | |
{ | |
return $this->starships; | |
} | |
public function addStarship(Starship $starship): static | |
{ | |
if (!$this->starships->contains($starship)) { | |
$this->starships->add($starship); | |
$starship->addDroid($this); | |
} | |
return $this; | |
} | |
public function removeStarship(Starship $starship): static | |
{ | |
if ($this->starships->removeElement($starship)) { | |
$starship->removeDroid($this); | |
} | |
return $this; | |
} | |
} |
Generate the migration for this. Go back to the terminal and run:
symfony console make:migration
Unveiling the Join Table
Marvelous! Take a peek at what it generated: it's fascinating. We have a new table called starship_droid
! It features a starship_id
foreign key to starship
and a droid_id
foreign key to droid
:
// ... lines 1 - 12 | |
final class Version20250311014256 extends AbstractMigration | |
{ | |
// ... lines 15 - 19 | |
public function up(Schema $schema): void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql('CREATE TABLE starship_droid (starship_id INT NOT NULL, droid_id INT NOT NULL, PRIMARY KEY(starship_id, droid_id))'); | |
$this->addSql('CREATE INDEX IDX_1C7FBE889B24DF5 ON starship_droid (starship_id)'); | |
$this->addSql('CREATE INDEX IDX_1C7FBE88AB064EF ON starship_droid (droid_id)'); | |
$this->addSql('ALTER TABLE starship_droid ADD CONSTRAINT FK_1C7FBE889B24DF5 FOREIGN KEY (starship_id) REFERENCES starship (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | |
$this->addSql('ALTER TABLE starship_droid ADD CONSTRAINT FK_1C7FBE88AB064EF FOREIGN KEY (droid_id) REFERENCES droid (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | |
} | |
// ... lines 29 - 36 | |
} |
This is how you structure a ManyToMany
relationship in the database: with a join table. The real magic of Doctrine is that we only need to think about objects. A Starship
object has many Droid
objects, and a Droid
object has many Starship
objects. Doctrine handles the tedious details of saving that relationship to the database.
Before we move on, run that migration. Spin back to the terminal and do it:
symfony console doctrine:migrations:migrate
Cool! We now have a shiny new join table. Ok... but how do we relate Droid
objects to Starship
objects? That's next... and you're gonna love it!