agentleFS
AnyOrgNameAnyOrgName/php-skills/skills/doctrine/orm-basics/SKILL.md

orm-basics

Skill4 starsChanged 4 months agoMITView on GitHub

Persist PHP objects with Doctrine ORM — the EntityManager, the unit of work, and the persist/flush lifecycle, so entities move through managed, detached, and removed states predictably. Use when the user needs to reason about entity states — new, managed, detached, or removed — and how the EntityManager tracks them; the user is unsure when Doctrine writes changes and needs to understand persist, flush, and the unit of work; the user wants to store and load plain PHP objects in a relational database using Doctrine ORM. Keywords: doctrine, orm, entity-manager, unit-of-work, persist, flush, sql.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    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
  1. Distinctive:

    Do not apply Doctrine's data-mapper patterns when the project uses an Active Record ORM such as Eloquent, where the model saves itself.

    line 65
  2. Distinctive:

    Do not hydrate entities for large bulk updates, imports, or read-only reporting queries where raw SQL or DBAL is a better fit.

    line 66
  3. Very specific:

    Call $em->flush() once at the end of the use case so Doctrine runs the INSERT inside a single transaction and populates the generated …

    line 81
  4. Very specific:

    Delete an entity with $em->remove($entity) followed by $em->flush()

    line 90

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- generated: do not edit — source: knowledge/atoms/doctrine/orm-basics/ --><!-- schema: atomVersion=3 skillVersion=2 graphVersion=1 --><!-- compile: @php-skills/compiler v1.0.0 (deterministic build) -->

Doctrine ORM Basics

When to use

  • The user needs to reason about entity states — new, managed, detached, or removed — and how the EntityManager tracks them.
  • The user is unsure when Doctrine writes changes and needs to understand persist, flush, and the unit of work.
  • The user wants to store and load plain PHP objects in a relational database using Doctrine ORM.

When NOT to use

  • Do not apply Doctrine's data-mapper patterns when the project uses an Active Record ORM such as Eloquent, where the model saves itself.
  • Do not hydrate entities for large bulk updates, imports, or read-only reporting queries where raw SQL or DBAL is a better fit.

Core guidance

  • Flush and clear in batches to bound memory — When processing many entities in a loop, flush() every N iterations (for example 100) and then $em->clear() to detach the flushed entities so the unit of work does not accumulate every object in memory.
  • persist() schedules, flush() executes — Call $em->persist($entity) to schedule a new entity for insertion, then a single $em->flush() to run all pending inserts, updates, and deletes in one transaction. Do not treat persist() as an immediate write.

Quick recipe

  1. Instantiate the entity with its required constructor data, then call $em->persist($order) to schedule it for insertion. (verify: The entity is in the managed state and appearing in the pending inserts of the unit of work.)
  2. Call $em->flush() once at the end of the use case so Doctrine runs the INSERT inside a single transaction and populates the generated identifier. (verify: $order->getId() is non-null after flush and the row exists in the table.)
  3. Fetch the entity back with $em->getRepository(Order::class)->find($id) in a fresh unit of work (or after clear()) to confirm it round-trips from the database. (verify: The reloaded entity matches the persisted data and is a distinct managed instance.)

Best practices

  • For an entity already loaded through the EntityManager (a managed entity), mutate it and call flush() — do not call persist() again
  • Delete an entity with $em->remove($entity) followed by $em->flush()
  • Query and load entities through a repository ($em->getRepository(Order::class) or an injected custom repository) and reserve the EntityManager itself for writes — persist(), remove(), and flush()

Anti-patterns

Anti-patternWhy it failsDo instead
Injecting the EntityManager into an entityIt couples the domain object to persistence, breaks the data-mapper separation, and makes the entity impossible to unit-test in isolation.Keep entities persistence-ignorant; coordinate persist/flush from a service or repository that receives the EntityManager.
One flush() per individual writeMultiple flushes fragment what should be one atomic transaction, so a later failure leaves partial data committed.Group the whole use case behind a single flush() so all writes commit or roll back together.
Constructing a fresh EntityManager per operationA fresh EntityManager has an empty unit of work and connection pool, so identity map benefits vanish and connections churn.Resolve a single shared EntityManager from the DI container (the framework already manages its lifecycle) and inject it.

See references/anti-patterns.md.

Common mistakes

  1. Using a detached entity after clear() silently no-ops — After $em->clear() (or across a serialized boundary) an entity becomes detached, so mutating it and flushing writes nothing. Fix: Re-fetch the entity by id, or merge it back with $em->find(Entity::class, $id) before mutating, so it is managed again.
  2. Flushing inside a tight loop issues one transaction per row — Calling flush() on every iteration of a large loop runs a separate transaction and changeset computation per row. Fix: Batch the writes — flush once at the end, or every N rows paired with clear(), so one transaction covers many entities.
  3. Changes lost because flush() was never called — Calling persist() or mutating a managed entity without a later flush() silently discards the change. Fix: Ensure a single flush() runs at the end of the use case (often in a service or a request middleware), and assert persistence in a test.

Version notes

  • Doctrine ORM 3 requires PHP 8.1+ and DBAL 4 (PHP >=8.1) — Doctrine ORM 3.x requires PHP 8.1 or newer and Doctrine DBAL 4; on older PHP runtimes stay on ORM 2.x, which still supports PHP 7.x.

Security

  • Never concatenate input into DQL or SQL — When querying through the EntityManager, bind user input with parameters (->setParameter() on a query builder or DQL) rather than concatenating it into the DQL or native SQL string.

Testing

  • Activation and decision behavior are verified in eval/scenarios/doctrine/orm-basics.yaml.

Additional resources

  • references/anti-patterns.md
  • references/core.md
  • references/examples.md
  • references/pitfalls.md
  • references/recipes.md
  • references/version-differences.md

Knowledge graph

Related: doctrine/associations, doctrine/entities-mapping, laravel/eloquent-relationships, security/sql-injection-prevention

<!-- graph-fragment: graph/fragments/doctrine/orm-basics.yaml -->

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.