The Task Entity with ManyToOne to Project
The Task Entity with ManyToOne to Project
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
EXACTLY the domain model from the Symfony course: ONE Project has MANY Tasks. This relationship now gets mapped as a REAL Doctrine association – AND automatically becomes part of the API.
Creating the Task entity
docker compose exec php bin/console make:entity Task<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\TaskRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
normalizationContext: ['groups' => ['task:read']],
denormalizationContext: ['groups' => ['task:write']]
)]
#[ORM\Entity(repositoryClass: TaskRepository::class)]
class Task
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Groups(['task:read', 'task:write'])]
private string $title = '';
#[ORM\Column]
#[Groups(['task:read', 'task:write'])]
private bool $done = false;
#[ORM\ManyToOne(inversedBy: 'tasks')]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['task:read', 'task:write'])]
private ?Project $project = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function isDone(): bool
{
return $this->done;
}
public function setDone(bool $done): static
{
$this->done = $done;
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
}#[ORM\ManyToOne(inversedBy: 'tasks')] is EXACTLY the same annotation as in the Symfony course (chapter 30) – inversedBy points to a tasks property, which gets added to Project in chapter 38.
Creating a migration
docker compose exec php bin/console make:migration
docker compose exec php bin/console doctrine:migrations:migrate --no-interactionCreating a task
curl -k -X POST https://localhost/api/tasks \
-H 'Content-Type: application/json' \
-d '{"title": "Create wireframes", "project": "/api/projects/1"}'Achtung: "project": "/api/projects/1" – NOT the bare number 1, but the FULL API path as a STRING. Chapter 40 explains IN DETAIL why API Platform maps relationships through such IRIs (Internationalized Resource Identifiers), instead of raw database IDs.
Tipp: #[ORM\JoinColumn(nullable: false)] enforces that EVERY task belongs to a project MANDATORILY – a POST WITHOUT a project field fails with 422, EXACTLY like a missing #[Assert\NotBlank] field from chapter 19.