Updated June 2026. Tested on Symfony 7.x and Doctrine ORM 3. Part of the Techalyst Symfony series.

Doctrine is the ORM that ships with Symfony, and it lets you work with database rows as plain PHP objects. You define entities, classes that map to tables, and the EntityManager handles turning your object changes into SQL. Once you understand those two pieces, working with data feels like working with objects, which is the whole point.

Defining an entity

An entity is a regular class annotated with Doctrine ORM attributes that describe how its properties map to columns:

use Doctrine\ORM\Mapping as ORM;

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

    #[ORM\Column(length: 255)]
    private string $name;

    #[ORM\Column]
    private int $price;

    public function getId(): ?int { return $this->id; }
    public function getName(): string { return $this->name; }
    public function setName(string $name): static { $this->name = $name; return $this; }
    // ...and so on
}

#[ORM\Entity] marks the class as mapped, #[ORM\Id] with #[ORM\GeneratedValue] is the auto-increment primary key, and each #[ORM\Column] is a field. You rarely write all this by hand, php bin/console make:entity scaffolds the class and its getters and setters for you, and you adjust from there.

The EntityManager and the unit of work

You do not write SQL to save an entity. You ask the EntityManagerInterface to track it, then to flush. The mental model that matters here is the unit of work: Doctrine watches the objects it manages, and when you call flush, it works out everything that changed and runs the queries in one batch.

use Doctrine\ORM\EntityManagerInterface;

public function create(EntityManagerInterface $em): void
{
    $product = new Product();
    $product->setName('Keyboard');
    $product->setPrice(80);

    $em->persist($product); // start tracking this new object
    $em->flush();           // write all pending changes to the database
}

persist tells Doctrine to start tracking a new object. flush is what actually talks to the database. Nothing is written until you flush.

Updating needs no persist

Here is a detail that surprises people. To update an entity you already fetched, you do not call persist again. Doctrine is already tracking it, so you just change it and flush:

$product = $repository->find($id); // now managed by Doctrine
$product->setPrice(90);
$em->flush(); // Doctrine sees the change and updates the row

The unit of work compared the object to its loaded state, saw the price changed, and issued an UPDATE. You only persist brand new objects.

Removing

Deleting follows the same shape, schedule the removal, then flush:

$em->remove($product);
$em->flush();

Wrapping up

A Doctrine entity is a PHP class mapped to a table with #[ORM\Entity] and #[ORM\Column] attributes, scaffolded with make:entity. You save changes through the EntityManagerInterface: persist to track a new object, then flush to write all pending changes in one unit of work. Updates to already-managed entities need only a flush, and removals use remove then flush. Think in objects, flush when you are done, and let Doctrine generate the SQL. Fetching those objects back out is the job of repositories.