Unlike one-to-one or one-to-many relationships, you don't really need to use cascade={"remove"} or orphanRemoval=true in ManyToMany relationships so this post is just about how you should use ManyToMany relationships. The reason why we're using bidirectional relationship is because it provides navigational access in both directions. Our example is football focused.


Info


If you try to delete a record in Team or Tournament entity, process will automatically delete associated records in junction table team_tournament in database. This table is automatically created by doctrine so we won't create an entity for it.


Design




class Team
{
/**
* @ORM\ManyToMany(targetEntity="Tournament", mappedBy="teams")
*/
private $tournaments;

public function __construct()
{
$this->tournaments = new ArrayCollection();
}

public function addTournament(Tournament $tournament)
{
if (!$this->tournaments->contains($tournament)) {
$this->tournaments[] = $tournament;
$tournament->addTeam($this);
}

return $this;
}

public function removeTournament(Tournament $tournament)
{
if ($this->tournaments->contains($tournament)) {
$this->tournaments->removeElement($tournament);
$tournament->removeTeam($this);
}
}
}

class Tournament
{
/**
* @ORM\ManyToMany(targetEntity="Team", inversedBy="tournaments")
* @ORM\JoinTable(
* name="team_tournament",
* joinColumns={
* @ORM\JoinColumn(name="tournament_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* @ORM\JoinColumn(name="team_id", referencedColumnName="id")
* }
* )
*/
private $teams;

public function __construct()
{
$this->teams = new ArrayCollection();
}

public function addTeam(Team $team)
{
if (!$this->teams->contains($team)) {
$this->teams[] = $team;
$team->addTournament($this);
}

return $this;
}

public function removeTeam(Team $team)
{
if ($this->teams->contains($team)) {
$this->teams->removeElement($team);
$team->removeTournament($this);
}
}
}

`team` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
)

`tournament` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
)

`team_tournament` (
`tournament_id` int(11) NOT NULL,
`team_id` int(11) NOT NULL,
PRIMARY KEY (`tournament_id`,`team_id`),
KEY `IDX_8386CA1C33D1A3E7` (`tournament_id`),
KEY `IDX_8386CA1C296CD8AE` (`team_id`),
CONSTRAINT `FK_8386CA1C296CD8AE` FOREIGN KEY (`team_id`) REFERENCES `team` (`id`),
CONSTRAINT `FK_8386CA1C33D1A3E7` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`)
)

mysql> SELECT
-> team.id AS TaamId,
-> team.name AS TeamName,
-> tournament.id AS TournamentId,
-> tournament.name AS TournamentName
-> FROM team
-> INNER JOIN team_tournament ON team.id = team_tournament.team_id
-> INNER JOIN tournament ON team_tournament.tournament_id = tournament.id;
+--------+-------------------+--------------+-----------------------+
| TaamId | TeamName | TournamentId | TournamentName |
+--------+-------------------+--------------+-----------------------+
| 1 | Fenerbahce | 1 | UEFA Champions League |
| 4 | Arsenal | 1 | UEFA Champions League |
| 7 | Bayern Munich | 1 | UEFA Champions League |
| 2 | Galatasaray | 2 | UEFA Europa League |
| 5 | Liverpool | 2 | UEFA Europa League |
| 8 | Borussia Dortmund | 2 | UEFA Europa League |
| 3 | Besiktas | 3 | UEFA Super Cup |
| 6 | Manchester United | 3 | UEFA Super Cup |
| 9 | Werder Bremen | 3 | UEFA Super Cup |
+--------+-------------------+--------------+-----------------------+

As you can see above, ManyToMany relationship automatically creates a junction table team_tournament in database to keep the relationships. We don't create an entity for it so doctrine handles it itself for us. Check extended version.


Tests


Deleting Team: Team gets deleted as well as all Team related records in team_tournament table.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM team WHERE id = ? LIMIT 1 ["1"] []
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM team_tournament WHERE team_id = ? [1] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Deleting Tournament: Tournament gets deleted as well as all Tournament related records in team_tournament table.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM tournament WHERE id = ? LIMIT 1 ["1"] []
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM team_tournament WHERE tournament_id = ? [1] []
doctrine.DEBUG: DELETE FROM tournament WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Readings