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

Beziehungen: OneToMany und ManyToOne

Beziehungen: OneToMany und ManyToOne

~17 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026

Jetzt modellieren wir die erste ECHTE Beziehung aus Kapitel 5: ein Project hat MEHRERE Tasks, jede Task gehört zu GENAU einem Project.

Die Task-Entity generieren

php bin/console make:entity Task

Bei der interaktiven Abfrage nach Feldern wählen Sie project als "relation"-Feld – make:entity fragt dann nach dem Beziehungstyp (ManyToOne) und erzeugt automatisch BEIDE Seiten der Beziehung:

src/Entity/Task.php
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\TaskRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: TaskRepository::class)]
class Task
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $titel = '';

    #[ORM\Column(length: 20)]
    private string $status = 'offen';

    #[ORM\Column(nullable: true)]
    private ?\DateTimeImmutable $faelligAm = null;

    #[ORM\ManyToOne(inversedBy: 'tasks')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Project $project = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getTitel(): string
    {
        return $this->titel;
    }

    public function setTitel(string $titel): static
    {
        $this->titel = $titel;

        return $this;
    }

    public function getStatus(): string
    {
        return $this->status;
    }

    public function setStatus(string $status): static
    {
        $this->status = $status;

        return $this;
    }

    public function getFaelligAm(): ?\DateTimeImmutable
    {
        return $this->faelligAm;
    }

    public function setFaelligAm(?\DateTimeImmutable $faelligAm): static
    {
        $this->faelligAm = $faelligAm;

        return $this;
    }

    public function getProject(): ?Project
    {
        return $this->project;
    }

    public function setProject(?Project $project): static
    {
        $this->project = $project;

        return $this;
    }
}

Die andere Seite der Beziehung: Project ergänzen

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

class Project
{
    // ... bisherige Felder aus Kapitel 19 ...

    /**
     * @var Collection<int, Task>
     */
    #[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'project', orphanRemoval: true)]
    private Collection $tasks;

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

    /**
     * @return Collection<int, Task>
     */
    public function getTasks(): Collection
    {
        return $this->tasks;
    }

    public function addTask(Task $task): static
    {
        if (!$this->tasks->contains($task)) {
            $this->tasks->add($task);
            $task->setProject($this);
        }

        return $this;
    }

    public function removeTask(Task $task): static
    {
        if ($this->tasks->removeElement($task)) {
            if ($task->getProject() === $this) {
                $task->setProject(null);
            }
        }

        return $this;
    }
}

Owning Side vs. Inverse Side: der entscheidende Unterschied

Eine Beziehung wird in Doctrine NUR auf EINER Seite tatsächlich in der Datenbank gespeichert – die Owning Side:

EntityRolle
Task (Owning Side)Trägt #[ORM\ManyToOne] UND #[ORM\JoinColumn] – GENAU hier entsteht die tatsächliche project_id-Fremdschlüsselspalte in der Datenbank.
Project (Inverse Side)Trägt #[ORM\OneToMany(mappedBy: 'project')] – KEINE eigene Datenbankspalte, mappedBy verweist auf den Property-Namen (project) auf der Owning Side.

Achtung: EIN häufiger Anfängerfehler: $task->setProject($project) vergessen und NUR $project->addTask($task) aufgerufen – da Project die INVERSE Seite ist, hat das KEINE Auswirkung auf die Datenbank! Genau deshalb ruft addTask() INTERN selbst $task->setProject($this) auf – diese Synchronisierung auf BEIDEN Seiten ist Standard-Best-Practice für bidirektionale Beziehungen.

Migration für die neue Beziehung

php bin/console make:migration
php bin/console doctrine:migrations:migrate

Erzeugt eine task-Tabelle mit project_id-Fremdschlüssel UND der zugehörigen FOREIGN KEY-Constraint – GENAU der Fünf-Schritte-Workflow aus Kapitel 20.

orphanRemoval: verwaiste Tasks automatisch löschen

orphanRemoval: true auf der Project-Seite sorgt dafür: entfernen Sie eine Task aus $project->getTasks() (via removeTask()), löscht Doctrine sie AUTOMATISCH auch aus der Datenbank, statt sie "verwaist" mit project_id = NULL zurückzulassen (was ohnehin unmöglich wäre, da JoinColumn(nullable: false) das verbietet).