Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

ManyToMany Between Task and Tag

ManyToMany Between Task and Tag

~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

ONE task can have MULTIPLE tags, ONE tag can apply to MULTIPLE tasks – the CLASSIC many-to-many relationship, EXACTLY as in the Symfony course (chapter 32).

The tags property on Task

api/src/Entity/Task.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

// ... inside the class:

#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'tasks')]
#[Groups(['task:read', 'task:write'])]
private Collection $tags;

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

public function getTags(): Collection
{
    return $this->tags;
}

public function addTag(Tag $tag): static
{
    if (!$this->tags->contains($tag)) {
        $this->tags->add($tag);
    }

    return $this;
}

public function removeTag(Tag $tag): static
{
    $this->tags->removeElement($tag);

    return $this;
}

Achtung: NO set setter for tags, but add/remove instead – the Symfony Serializer AUTOMATICALLY recognizes this pattern and calls the matching add/remove methods WHEN deserializing a collection, EXACTLY as with a normal Symfony form.

The inverse side on Tag

api/src/Entity/Tag.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

// ... inside the class:

#[ORM\ManyToMany(targetEntity: Task::class, mappedBy: 'tags')]
private Collection $tasks;

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

DELIBERATELY WITHOUT #[Groups] on tasksTag has been read-only since chapter 12 and doesn't NEED to show its related tasks in the API response to make the relationship USABLE.

Creating a migration

docker compose exec php bin/console make:migration
docker compose exec php bin/console doctrine:migrations:migrate --no-interaction

CREATES an ADDITIONAL join table (task_tag) – EXACTLY the USUAL database pattern for many-to-many relationships, FULLY managed by Doctrine.

Creating a task with tags

curl -k -X POST https://localhost/api/tasks \
  -H 'Content-Type: application/json' \
  -d '{"title": "Prepare deployment", "project": "/api/projects/1", "tags": ["/api/tags/1", "/api/tags/2"]}'

tags as an ARRAY of IRIs – EXACTLY the same principle as the SINGLE project field, just PLURAL for a many-to-many relationship.

Tipp: #[Assert\Count(min: 1)] from chapter 21 could be used HERE to enforce AT LEAST one tag per task – for OUR project, tags stay OPTIONAL.