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.
In this repository119 other files it gives its agents.
- strategy-bitrix-d7-incrementalSkill
- strategy-explicit-over-magicSkill
- strategy-framework-conventionsSkill
- strategy-legacy-safe-editsSkill
- strategy-minimal-diffSkill
- strategy-no-untested-refactorSkill
- strategy-preserve-public-apiSkill
- strategy-readonly-immutabilitySkill
- strategy-security-firstSkill
- strategy-test-drivenSkill
- openapiSkill
- paginationSkill
- rest-designSkill
- versioningSkill
- cqrsSkill
- dependency-inversionSkill
- hexagonalSkill
- repository-patternSkill
- service-layerSkill
- bitrixSkill
- findings-reportSkill
- laravelSkill
- performanceSkill
- phpSkill
- project-analysisSkill
- refactoringSkill
- roi-prioritizationSkill
- securitySkill
- stack-detectionSkill
- cache-layersSkill
- component-engineSkill
- d7-eventsSkill
- d7-loaderSkill
- d7-orm-queriesSkill
- iblock-elementsSkill
- kernel-main-moduleSkill
- localizationSkill
- controllersSkill
- ormSkill
- validationSkill
- controllersSkill
- query-builderSkill
- routingSkill
- autoload-psr4Skill
- dependency-managementSkill
- scriptsSkill
- connection-poolingSkill
- mysql-indexingSkill
- redisSkill
- transactionsSkill
- ci-github-actionsSkill
- deployment-strategiesSkill
- docker-phpSkill
- env-configSkill
- associationsSkill
- entities-mappingSkill
- entity-apiSkill
- module-developmentSkill
- routing-controllersSkill
- formsSkill
- The first 60 shown.
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
- Distinctive: line 65
Do not apply Doctrine's data-mapper patterns when the project uses an Active Record ORM such as Eloquent, where the model saves itself.
- Distinctive: line 66
Do not hydrate entities for large bulk updates, imports, or read-only reporting queries where raw SQL or DBAL is a better fit.
- Very specific: line 81
Call $em->flush() once at the end of the use case so Doctrine runs the INSERT inside a single transaction and populates the generated …
- Very specific: line 90
Delete an entity with $em->remove($entity) followed by $em->flush()
≈ “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
<!-- 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 treatpersist()as an immediate write.
Quick recipe
- 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.) - 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.) - Fetch the entity back with
$em->getRepository(Order::class)->find($id)in a fresh unit of work (or afterclear()) 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 callpersist()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(), andflush()
Anti-patterns
| Anti-pattern | Why it fails | Do instead |
| Injecting the EntityManager into an entity | It 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 write | Multiple 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 operation | A 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
- 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. - 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 withclear(), so one transaction covers many entities. - Changes lost because flush() was never called — Calling
persist()or mutating a managed entity without a laterflush()silently discards the change. Fix: Ensure a singleflush()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 -->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.

