Doctrine Entities — Complete Guide to Entity Mapping and Lifecycle
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
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