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

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
api/src/Entity/Task.php
<?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-interaction

Creating 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.