doctrine-orm
Skill35 starsChanged 5 months agoMITView on GitHub
Use when creating Doctrine ORM 3 entities, associations, repositories, enums, or UUID identifiers in Symfony. Defines entity conventions, attribute mapping, fetch strategy, and repository structure. Use when the task mentions entity, Doctrine, repository, association, or mapping.
In this repository16 other files it gives its agents.
- api-platform-resourcesSkill
- bundle-organizationSkill
- console-commandsSkill
- doctrine-migrationsSkill
- doctrine-query-optimizationSkill
- domain-driven-designSkill
- dto-and-validationSkill
- hexagonal-architectureSkill
- jwt-authenticationSkill
- layered-architectureSkill
- messenger-asyncSkill
- problem-details-rfc9457Skill
- rest-controller-conventionsSkill
- security-authenticationSkill
- testing-pyramidSkill
- transactional-patternsSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 81
Keep both sides in sync with a helper on the owning aggregate.
- Very specific: line 96
Use cascade: ['persist'] deliberately.
- Very specific: line 133
Never put business logic in repositories (see layered-architecture).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Doctrine ORM 3 (Symfony)
Entity conventions
#[ORM\Entity(repositoryClass: OrderRepository::class)]
#[ORM\Table(name: 'orders')]
class Order
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)] // symfony/uid type
private Uuid $id;
#[ORM\Column(length: 255)]
private string $customerEmail;
#[ORM\Column(enumType: OrderStatus::class)] // backed enum, stored as string
private OrderStatus $status;
/** @var Collection<int, OrderItem> */
#[ORM\OneToMany(targetEntity: OrderItem::class, mappedBy: 'order', cascade: ['persist'], orphanRemoval: true)]
private Collection $items;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
// private constructor — force creation through a named factory
private function __construct(string $customerEmail)
{
$this->id = Uuid::v7(); // sortable UUID
$this->customerEmail = $customerEmail;
$this->status = OrderStatus::Pending;
$this->items = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
}
public static function create(string $customerEmail): self
{
return new self($customerEmail);
}
// Behavior, not a setter
public function addItem(Uuid $productId, int $quantity): void
{
$this->items->add(new OrderItem($this, $productId, $quantity));
}
public function getId(): Uuid { return $this->id; }
public function getStatus(): OrderStatus { return $this->status; }
/** @return Collection<int, OrderItem> */
public function getItems(): Collection { return $this->items; }
}Rules
- IDs:
symfony/uidUuid(prefer UUID v7 — time-ordered, index-friendly). Never expose auto-increment integers in a public API. - Enums:
#[ORM\Column(enumType: OrderStatus::class)]with a backed enum (string). Never store the ordinal/int. - Dates:
Types::DATETIME_IMMUTABLE+\DateTimeImmutable. Never mutable\DateTime. - Collections: initialize in the constructor (
new ArrayCollection()), typedCollection<int, X>. Never null. - No public setters — expose behavior methods; getters are fine.
- Factory creation — private constructor + named static factory (
Order::create()), so an entity can't be built in an invalid state. declare(strict_types=1);in every file.
Associations & fetch strategy
#[ORM\ManyToOne]and#[ORM\ManyToMany]are LAZY by default — keep it that way. Neverfetch: 'EAGER'.- The owning side holds the FK column (
#[ORM\JoinColumn]); the inverse side usesmappedBy. - Keep both sides in sync with a helper on the owning aggregate.
// Child owns the FK
#[ORM\ManyToOne(targetEntity: Order::class, inversedBy: 'items')]
#[ORM\JoinColumn(name: 'order_id', nullable: false, onDelete: 'CASCADE')]
private Order $order;// ❌ BAD — eager fetch loads the whole graph on every query
#[ORM\ManyToOne(fetch: 'EAGER')]
private Customer $customer;Use cascade: ['persist'] deliberately; avoid cascade: ['remove'] — prefer orphanRemoval or explicit deletion so you don't accidentally wipe shared records.
Repositories
/** @extends ServiceEntityRepository<Order> */
final class OrderRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Order::class);
}
public function save(Order $order, bool $flush = false): void
{
$this->getEntityManager()->persist($order);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/** @return Order[] */
public function findActiveByCustomer(string $email): array
{
return $this->createQueryBuilder('o')
->andWhere('o.customerEmail = :email')
->andWhere('o.status = :status')
->setParameter('email', $email)
->setParameter('status', OrderStatus::Active)
->getQuery()
->getResult();
}
}- Type the repository with
@extends ServiceEntityRepository<Entity>so static analysis knows the return types. exists*checks:SELECT 1 … setMaxResults(1)is faster than loading the entity.- Never put business logic in repositories (see
layered-architecture).
Value objects: embeddables & custom types
#[ORM\Embedded(class: Address::class)]
private Address $shippingAddress;For single-column value objects (Email, Money), write a custom DBAL type so the model stays rich and the schema stays flat.
Gotchas
- Agent uses
fetch: 'EAGER'on associations — keepManyToOne/ManyToManyLAZY; fetch explicitly when needed (seedoctrine-query-optimization). - Agent uses auto-increment
intIDs — usesymfony/uidUuid(v7). - Agent stores enums as int / uses
enumTypewith a pure (non-backed) enum — use a string-backed enum. - Agent adds public setters to entities — use behavior methods + a factory.
- Agent uses mutable
\DateTime— use\DateTimeImmutablewithDATETIME_IMMUTABLE. - Agent forgets
orphanRemoval: trueon owningOneToMany— orphaned child rows pile up. - Agent adds
cascade: ['remove']casually — preferorphanRemoval/ explicit deletes to avoid wiping shared rows. - Agent leaves collections uninitialized —
new ArrayCollection()in the constructor. - Agent uses Doctrine annotations (
@ORM\Entity) — use PHP 8 attributes. - Agent forgets the
@extends ServiceEntityRepository<Entity>generic — analysis loses return types.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

