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 TaskBei 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:
<?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
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:
| Entity | Rolle |
|---|---|
| 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:migrateErzeugt 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).