Doctrine: Associations, N+1 & Migrations
Associations & Cascading
class Post
{
#[ORM\ManyToOne(targetEntity: User::class)]
private User $author;
}
class User
{
// cascade: ['remove'] -- deleting a User also deletes their Posts
// automatically, without manually iterating and deleting each one
#[ORM\OneToMany(mappedBy: 'author', targetEntity: Post::class, cascade: ['remove'])]
private Collection $posts;
}Lazy Loading & the N+1 Problem
// N+1: fetches N posts with one query, then triggers a SEPARATE
// query for EACH post's author when getAuthor() is first accessed
foreach ($posts as $post) {
echo $post->getAuthor()->getName();
}
// FIXED: eager-load author with a JOIN -- one single query total
$query = $entityManager->createQuery(
'SELECT p, a FROM App\Entity\Post p JOIN p.author a'
);
$posts = $query->getResult();QueryBuilder for Conditional Queries
$qb = $repository->createQueryBuilder('u');
// Conditionally add a WHERE clause -- more readable than manually
// concatenating raw DQL strings
if ($emailFilter) {
$qb->andWhere('u.email = :email')->setParameter('email', $emailFilter);
}
$users = $qb->getQuery()->getResult();Migrations
# Generates a migration reflecting the diff between entity
# definitions and the current database schema
php bin/console doctrine:migrations:diff
# Applies pending migrations in order -- consistent across
# dev/staging/production rather than ad-hoc manual SQL changes
php bin/console doctrine:migrations:migrateDoctrine vs. Eloquent
Doctrine follows the Data Mapper pattern -- entities are plain objects with no built-in save/query methods; a separate EntityManager handles persistence. Laravel's Eloquent follows Active Record instead ($user->save()), where the model itself knows how to persist. Both are legitimate ORM philosophies with different coupling tradeoffs.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free