Login to bookmark this video
Buy Access to Course
15.

Many-To-Many Relationship

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Alrighty, 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 Droids 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 many Droids, and each Droid can serve on many Starships. 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:

224 lines | src/Entity/Starship.php
// ... 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():

91 lines | src/Entity/Droid.php
// ... 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:

38 lines | migrations/Version20250311014256.php
// ... 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!