
Symfony:doctrine Relations Skill
- 499 installs
- 190 repo stars
- Updated August 6, 2026
- makfly/superpowers-symfony
symfony:doctrine-relations is a Claude Code skill that models OneToMany, ManyToMany, and inheritance mappings in Doctrine entities with correct owning sides, cascades, and fetch strategies for Symfony backend developers.
About
symfony:doctrine-relations is a skill in makfly/superpowers-symfony that guides Doctrine ORM relationship mapping for Symfony applications. Developers use it when defining OneToMany, ManyToMany, or inheritance hierarchies and need correct owning versus inverse sides, cascade persist and remove behavior, and lazy versus eager fetch plans. The skill fits agent sessions writing Entity classes, migration-safe association changes, or fixing bidirectional sync bugs and N+1 query patterns in PHP backends. Reach for symfony:doctrine-relations when association annotations or attributes must align with Symfony best practices instead of generic SQL or Laravel Eloquent patterns.
- Configure bidirectional associations correctly
- Choose lazy, eager, or extra-lazy fetch modes
- Set cascade persist, remove, and orphan removal
- Design join tables for ManyToMany cleanly
- Prevent N+1 queries in API serialization
Symfony:Doctrine Relations by the numbers
- 499 all-time installs (skills.sh)
- Ranked #125 of 922 Databases skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/makfly/superpowers-symfony --skill symfonydoctrine-relationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 499 |
|---|---|
| repo stars | ★ 190 |
| Last updated | August 6, 2026 |
| Repository | makfly/superpowers-symfony ↗ |
How do you map Doctrine entity relations in Symfony?
Model OneToMany, ManyToMany, and inheritance mappings in Doctrine entities with correct owning sides, cascades, and fetch strategies.
Who is it for?
Symfony backend developers defining or fixing Doctrine entity associations who need owning-side and cascade guidance.
Skip if: Developers on non-PHP stacks, raw SQL schema design without Doctrine, or Symfony routes and controllers with no entity changes.
When should I use this skill?
The user asks to model Doctrine relations, fix owning side errors, configure cascades, or set fetch strategies in Symfony entities.
What you get
Doctrine entity relation mappings, cascade and fetch configuration, and association definitions ready for migrations.
- entity relation mappings
- cascade and fetch configuration
Files
Doctrine Relations (Symfony)
Use when
- Designing entity relations or schema evolution.
- Improving Doctrine correctness/performance.
Default workflow
1. Model ownership/cardinality and transactional boundaries. 2. Apply mapping/schema changes with migration safety. 3. Tune fetch/query behavior for hot paths. 4. Verify lifecycle behavior with targeted tests.
Guardrails
- Keep owning/inverse sides coherent.
- Avoid destructive migration jumps in one release.
- Eliminate accidental N+1 and over-fetching.
Progressive disclosure
- Use this file for execution posture and risk controls.
- Open references when deep implementation details are needed.
Output contract
- Entity/migration changes.
- Integrity and performance decisions.
- Validation outcomes and rollback notes.
References
reference.mddocs/complexity-tiers.md
Doctrine Relations Reference (Symfony)
Use this reference for implementation details and review criteria specific to doctrine-relations.
Mapping is done with PHP attributes (#[ORM\...]). The mapping API is stable across ORM 2.x/3.x; on ORM 3 the targetEntity can also be inferred from the property type-hint, but passing it explicitly stays valid and is clearer.
Owning vs Inverse side
| Owning side | Inverse side | |
|---|---|---|
| Mapping | ManyToOne (or owning ManyToMany/OneToOne) | OneToMany (or inverse side) |
| Database | holds the foreign key | no column |
| Updates DB | yes | only if the owning side is updated |
The single most common Doctrine bug: setting the relation on the inverse side only. Doctrine persists what the owning side holds. Always set the owning side (the ManyToOne reference).
ManyToOne / OneToMany (the common pair)
Owning side — Product holds the FK
<?php
// src/Entity/Product.php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Product
{
#[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Category $category = null;
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
}Inverse side — Category owns a collection
<?php
// src/Entity/Category.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Category
{
/** @var Collection<int, Product> */
#[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', orphanRemoval: true)]
private Collection $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
/** @return Collection<int, Product> */
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
$product->setCategory($this); // keep both sides in sync
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->removeElement($product)) {
// unset the owning side only if it points here
if ($product->getCategory() === $this) {
$product->setCategory(null);
}
}
return $this;
}
}Persisting — set the owning side, then flush
$product->setCategory($category); // owning side carries the FK
$em->persist($category);
$em->persist($product);
$em->flush();orphanRemoval vs cascade
- `cascade: ['persist', 'remove']` — operations on the parent propagate to
the associated entities. Useful for aggregate roots you always save together. Avoid cascade: ['remove'] on large collections (issues a DELETE per row).
- `orphanRemoval: true` — when a child is removed from the collection (no
longer referenced by the parent), Doctrine deletes it. Use it for true composition (a Product cannot exist without its Category), not for shared entities.
#[ORM\OneToMany(targetEntity: OrderItem::class, mappedBy: 'order',
cascade: ['persist'], orphanRemoval: true)]
private Collection $items;Fetching — avoid N+1
// LAZY (default): triggers one extra query per access
$post = $em->find(Post::class, 1);
$post->getAuthor()->getName(); // 2nd query here
// Fetch join: one query, relation hydrated
$post = $repository->createQueryBuilder('p')
->addSelect('a')
->leftJoin('p.author', 'a')
->where('p.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();Pitfall: contains() on large inverse collections
On a plain OneToMany, calling $category->getProducts()->contains($product) forces Doctrine to hydrate the entire collection into memory before checking — disastrous for collections with thousands of rows.
// BAD on a huge collection — loads everything
if ($category->getProducts()->contains($product)) { /* ... */ }Mitigations:
- Mark the collection
fetch: 'EXTRA_LAZY'socontains(),count()and
slice() issue targeted SQL (EXISTS, COUNT, LIMIT) instead of loading the whole collection:
#[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', fetch: 'EXTRA_LAZY')]
private Collection $products;- Or check membership from the owning side:
$product->getCategory() === $category.
Always review the generated add*/remove* helpers (from make:entity) — they call contains() and become a hot spot on large inverse collections.
Multiple Entity Managers
A relation may not span two entity managers. When the schema is split, each EM owns its own set of entities.
# config/packages/doctrine.yaml
doctrine:
dbal:
connections:
default: { url: '%env(resolve:DATABASE_URL)%' }
customer: { url: '%env(resolve:CUSTOMER_DATABASE_URL)%' }
default_connection: default
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
Main:
is_bundle: false
dir: '%kernel.project_dir%/src/Entity/Main'
prefix: 'App\Entity\Main'
customer:
connection: customer
mappings:
Customer:
is_bundle: false
dir: '%kernel.project_dir%/src/Entity/Customer'
prefix: 'App\Entity\Customer'Autowiring a named EM (by variable name)
The FrameworkBundle registers an alias per EM. Type-hint EntityManagerInterface and name the variable after the EM to inject the right one:
use Doctrine\ORM\EntityManagerInterface;
public function __construct(
private EntityManagerInterface $entityManager, // default EM
private EntityManagerInterface $customerEntityManager, // "customer" EM
) {}Relying on the variable name for autowiring resolution is convenient, but be
aware Symfony 8.1+ deprecates name-based alias resolution in the general case;
for Doctrine EMs the named alias is still the documented mechanism. Use
ManagerRegistry::getManager('customer') when you need it explicitly.Repositories with multiple EMs
getRepository() takes the EM name as a second argument:
use Doctrine\Persistence\ManagerRegistry;
$default = $registry->getRepository(Product::class); // default EM
$customer = $registry->getRepository(Customer::class, 'customer'); // named EMFor a custom repository on an entity managed by a non-default EM, extend Doctrine\ORM\EntityRepository (not ServiceEntityRepository, whose constructor resolves the EM from the entity class via the default manager) and obtain it through ManagerRegistry::getRepository().
Console / migrations with --em
php bin/console doctrine:database:create --connection=customer
php bin/console doctrine:migrations:diff --em=customer
php bin/console doctrine:migrations:migrate --em=customerSkill Operating Checklist
Design checklist
- Confirm operation boundaries and invariants first.
- Minimize scope while preserving contract correctness.
- Test both happy path and negative path behavior.
Validation commands
- php bin/console doctrine:migrations:diff
- php bin/console doctrine:migrations:migrate
- ./vendor/bin/phpunit --filter=Doctrine
Failure modes to test
- Invalid payload or forbidden actor.
- Boundary values / not-found cases.
- Retry or partial-failure behavior for async flows.
Related skills
How it compares
Use symfony:doctrine-relations for Symfony Doctrine entity graphs; use raw migration skills when only SQL DDL changes are needed without ORM mapping.
FAQ
Which relation types does symfony:doctrine-relations cover?
symfony:doctrine-relations covers OneToMany, ManyToMany, and inheritance mappings in Doctrine entities. The skill emphasizes correct owning sides, cascades, and fetch strategies for Symfony backends.
Does symfony:doctrine-relations help with cascade and fetch settings?
symfony:doctrine-relations helps configure cascade persist and remove behavior plus lazy or eager fetch strategies. Developers use it to avoid bidirectional sync bugs and N+1 queries in Doctrine associations.