Skip to content

Doctrine Entities — Complete Guide to Entity Mapping and Lifecycle

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Doctrine Entities. We cover key concepts, practical examples, and best practices to help you master this topic.

Doctrine entities are plain PHP classes with mapping metadata using PHP 8 attributes that define how objects relate to database tables and columns.

What You'll Learn

By the end of this tutorial, you'll design Doctrine entities with attributes, map all column types, use lifecycle callbacks, implement inheritance, and create custom mapping types.

Why Entity Mapping Matters

Entity mapping is the foundation of Doctrine ORM. Proper mapping ensures correct database schema generation, efficient queries, and maintainable domain models.

Real-World Use

A content management system uses entities for Article, Author, Category, and Comment. Inheritance mapping handles content types. Lifecycle callbacks automatically set timestamps.

Entity Path

flowchart LR
  A[Doctrine ORM] --> B[Entity Mapping]
  B --> C[Relations]
  B --> D[Embeddables]
  B --> E[Inheritance]
  B --> F[Custom Types]
  B --> G{You Are Here}
  style G fill:#f90,color:#fff

Attribute Mapping

Use PHP 8 attributes to map entities.

<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: "app_users")]
class User {
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "IDENTITY")]
    #[ORM\Column(type: "integer")]
    private int|null $id = null;
    #[ORM\Column(type: "string", length: 180, unique: true)]
    private string $email;
    #[ORM\Column(type: "string", length: 255)]
    private string $password;
    #[ORM\Column(type: "json")]
    private array $roles = [];
    #[ORM\Column(type: "boolean", options: ["default" => true])]
    private bool $isActive = true;
    #[ORM\Column(type: "datetime_immutable")]
    private DateTimeImmutable $createdAt;
}

Embedded Entities

Embeddables are reusable value objects mapped to the same table.

<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Embeddable]
class Address {
    #[ORM\Column(type: "string", length: 255)]
    private string $street;
    #[ORM\Column(type: "string", length: 100)]
    private string $city;
    #[ORM\Column(type: "string", length: 20)]
    private string $zipCode;
    #[ORM\Column(type: "string", length: 2)]
    private string $country;
}
#[ORM\Entity]
class Customer {
    #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
    private int|null $id = null;
    #[ORM\Embedded(class: Address::class)]
    private Address $address;
}

Lifecycle Callbacks

Hook into entity lifecycle events.

<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Article {
    #[ORM\PrePersist]
    public function setCreatedAt(): void {
        $this->createdAt = new DateTimeImmutable();
        $this->slug = strtolower(str_replace(" ", "-", $this->title));
    }
    #[ORM\PreUpdate]
    public function setUpdatedAt(): void {
        $this->updatedAt = new DateTimeImmutable();
    }
    #[ORM\PostLoad]
    public function onLoad(): void {
        $this->popularity = $this->views > 1000 ? "high" : "low";
    }
}

Inheritance Mapping

Map class hierarchies to tables using joined or single-table inheritance.

<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\InheritanceType("JOINED")]
#[ORM\DiscriminatorColumn(name: "type", type: "string")]
#[ORM\DiscriminatorMap(["video" => Video::class, "article" => Article::class])]
class Media {
    #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
    protected int|null $id = null;
    #[ORM\Column(type: "string")]
    protected string $title;
    #[ORM\Column(type: "datetime_immutable")]
    protected DateTimeImmutable $createdAt;
}

Common Mistakes

1. Using JSON Type Without Indexing

JSON columns cannot be indexed efficiently in MySQL. Consider separate tables for searchable data.

2. Not Specifying Fetch Mode

Default fetch is lazy. For commonly accessed associations, use EAGER fetch to avoid N+1 queries.

3. Ignoring Nullable Columns

Null values in required fields cause runtime errors. Always specify nullable: true when the field can be null.

4. Overusing Inheritance

Deep inheritance hierarchies are hard to query efficiently. Prefer composition with embeddables.

5. Not Using Immutable DateTime Types

DateTime objects can be modified after persistence. Use DateTimeImmutable to prevent unintended changes.

Practice Questions

1. What is an embeddable entity?

A value object whose fields are mapped to the same table as the parent entity.

2. How do you register lifecycle callbacks?

Use #[HasLifecycleCallbacks] and annotate methods with #[PrePersist], #[PreUpdate], etc.

3. What is the discriminator map for?

Maps class types to discriminator column values in inheritance hierarchies.

4. How do you map a JSON column?

Use #[Column(type: "json")] on an array property.

5. Challenge: Create an entity with lifecycle callbacks and an embeddable.

<?php
#[ORM\Embeddable]
class Metadata {
    #[ORM\Column]
    private DateTimeImmutable $createdAt;
    #[ORM\Column(nullable: true)]
    private DateTimeImmutable|null $updatedAt;
}
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Post {
    #[ORM\Embedded(class: Metadata::class)]
    private Metadata $meta;
    #[ORM\PrePersist]
    public function onCreate(): void {
        $this->meta = new Metadata(new DateTimeImmutable(), null);
    }
}

FAQ

What is the difference between mappedSuperclass and inheritance?

MappedSuperclass is not an entity. Inheritance types map to separate tables.

Can I use YAML or XML mapping?

Yes. Doctrine supports YAML, XML, and PHP array mapping in addition to attributes.

What is the default fetch mode?

Lazy. Most associations load on demand.

How do I map a UUID primary key?

Use #[Column(type: 'guid')] and #[GeneratedValue(strategy: 'UUID')].

What is soft delete?

Not deleting records but marking them as deleted. Implement with a deletedAt column and a filter.

Mini Project: Complete Entity Model

Create a complete set of entities for a blog application.

<?php
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class BlogPost {
    #[ORM\Embedded(class: Metadata::class)]
    private Metadata $meta;
    #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: "posts")]
    private Author $author;
    #[ORM\OneToMany(mappedBy: "post", targetEntity: Comment::class)]
    private Collection $comments;
    #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: "posts")]
    private Collection $tags;
    #[ORM\Column(type: "text")]
    private string $content;
}

What's Next

Doctrine Relations Doctrine DQL Doctrine Migrations

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro