# doctrine/orm: the mature PHP mapper and where its sharp edges are

> Five maintained branches, an object query language on top of SQL, and two decades of accumulated design decisions. A look at what Doctrine ORM is good at and where it fights you.

**doctrine/orm** — Doctrine Object Relational Mapper (ORM)

- Repository: https://github.com/doctrine/orm
- Website: https://www.doctrine-project.org/projects/orm.html
- Stars: 10,166 · Forks: 2,550
- Language: PHP
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/doctrine-orm

## What sits between your objects and the database

The README describes Doctrine ORM as an object-relational mapper for PHP 8.1 or newer that provides transparent persistence for PHP objects, and it says plainly that it sits on top of a database abstraction layer, DBAL. That layering is the first thing to understand, because it decides where a problem lives when something breaks.

The unit of work is the mechanism that makes persistence feel transparent. You mark objects dirty, call flush, and the mapper inspects them, computes the SQL and executes it inside a transaction. The identity map guarantees that asking twice for the same row gives you the same PHP object, so you can compare identity rather than re-query. Lazy loading means an association you never touched never generated a query.

None of that is free. Every one of those conveniences is a layer between your code and the wire protocol, and each has a documented failure mode. Lazy loading produces the N plus 1 query problem. The identity map means stale objects until you refresh them. The unit of work means the flush boundary, not the method boundary, is where your writes happen.

The alternative the README names is a proprietary object-oriented SQL dialect called Doctrine Query Language, inspired by Hibernate's HQL. DQL is where the mapper's abstraction is most visible and most debatable, because it is a second query language to learn and it still has to be translated into vendor SQL.

## Five branches from 2.20 to 4.0

The README's badge table spans five version lines: 4.0, 3.8, 3.7, 2.21 and 2.20, each with its own build status and coverage badge. That is an unusually wide spread for a project at this maturity, and it tells you something about the installed base. Applications that shipped on 2.20 years ago are still receiving maintenance.

The default branch is `3.7.x`, so that is where a new installation starts. The release history in the same period shows the maintenance running in parallel: version 3.7.1 shipped on 2026-09-14 and version 3.6.9 on 2026-09-07, minutes apart, with the 3.6 line taking bugfixes while the 3.7 line was still fresh.

The 3.6.9 notes are a good sample of what a mature patch release contains. Fixing stale original-data tracking for native lazy objects, fixing eager collection duplicates when a second-level cache is in play, fixing hydration of collections indexed by an inherited field when SQL filters change, and reverting a use of the editor API that had landed earlier. None of these are features. They are the accumulated consequences of earlier design decisions meeting real applications.

Version 3.7.1 is smaller still: two bugfixes, avoiding a duplicate index for conflicting single-table-inheritance associations and ensuring a schema tool event fires once rather than twice.

## The feature release that shows where the project is going

Version 3.7.0, published 2026-09-07, is the interesting one because it is a feature release: thirty-five pull requests from thirteen contributors. The list reads as a map of where the friction has been.

```bash
git clone https://github.com/doctrine/orm
cd orm
composer install
```

Two items are about the PHP runtime itself. One uses native lazy objects, which is the language mechanism added in recent PHP versions and which the following patch release then had to fix stale original-data tracking for. Another exposes `EntityRepository::expr()` on an RFC, which is a small ergonomic change with a long history behind it.

Two are about schema generation: configurable foreign key names, and making the default string length configurable in the SchemaTool. Both are complaints about the same underlying thing, that the tool had opinions it did not expose.

Two are about pagination, which is the clearest sign of a web application focus. Cursor-based pagination was added, and `OffsetPaginator` was introduced alongside a realignment of `CursorPaginator` onto an immutable page API. That pairing suggests the project noticed offset pagination is wrong for large tables and is steering people toward keyset pagination without breaking the existing class names.

The remaining items allow a `NULL` literal in `SELECT` and `CASE` expressions and allow defining an explicit identifier position.

## Mapping XML, static analysis and benchmarking in the tree

Two files at the root say more about the project's engineering than the README does. `doctrine-mapping.xsd` is the XML Schema definition for mapping files, which means the XML mapping format is validated against a published schema rather than being whatever the parser happens to accept. And `phpbench.json` is a PHPBench configuration, so the repository carries its own benchmark harness rather than leaving performance claims unmeasured.

Static analysis is the more elaborate corner. There is `phpstan.neon`, a `phpstan-baseline.neon` recording accepted errors, `phpstan-params.neon` for shared parameters, and `phpstan-dbal3.neon` for running the analysis against DBAL 3. That last file is the interesting one: it means the analysis is run twice, against two supported versions of the layer underneath, because an ORM that supports both DBAL 3 and DBAL 4 has two code paths to keep honest.

Alongside those sit `phpcs.xml.dist` for coding standards and `phpunit.xml.dist` for the tests, `ci/` for the pipeline, `docs/` for documentation, and `UPGRADE.md` and `SECURITY.md` at the root. The project is MIT licensed and its topics list currently contains only `hacktoberfest`, which is a snapshot of a seasonal campaign rather than a description of what the library is.

## Where Doctrine ORM costs you and what you use instead

The real cost of this library is conceptual weight. A developer joining the project has to understand the unit of work, the identity map, lazy loading, proxy objects, the change tracking policy, DQL, the query builder, and the mapping formats. That is a lot of surface before the first feature ships, and it is the reason many teams reach for something thinner.

Eloquent, which is Laravel's mapper, is the comparison that comes up most often in practice, and the difference is architectural rather than a feature list. Eloquent's active record model puts persistence behaviour on the model itself, so you need less setup and there is no separate unit of work to reason about. The cost is that the models carry database concerns directly and testing them in isolation takes more work. Doctrine's data mapper keeps entities free of persistence logic, which is better for domain models and worse for getting started.

The other comparison is with using DBAL directly. For a reporting endpoint or a job that reads a few rows and formats JSON, an ORM is the wrong tool and a query is the right one. The README is honest that ORM sits on DBAL rather than competing with it, so you can drop down a layer when a hot path demands it without rewriting the rest of the application.

The case where Doctrine earns its keep is a domain model with real relationships, associations and invariants, where the same graph is loaded repeatedly across endpoints and where N plus 1 queries would otherwise be the alternative.

## What the README settles and what it does not

The README is four sentences and two links. It states the PHP version requirement, the relationship to DBAL, the existence of DQL and its inspiration in HQL, and then points at the website and the documentation. It does not show a single line of mapping code.

For a library with this many users that is defensible, because the documentation is versioned and the stable documentation link in the README is the one you want. But it means the questions that actually decide an adoption are all unanswered on the repository front page: how inheritance is mapped, what the proxies do to your objects, whether the second-level cache is something you end up maintaining, and how the schema tool behaves on a production-sized database.

The release notes answer some of these indirectly and honestly. A fix for duplicate indexes on conflicting single-table-inheritance associations tells you STI is still generating edge cases. A fix for eager collection duplicates with the L2 cache tells you the cache is a component people hit problems with. A fix for hydration of collections indexed by an inherited field tells you inheritance and collections interact badly sometimes.

None of that is an argument against the library. It is an argument for reading `UPGRADE.md` and the versioned documentation before committing, rather than after. The last push was on 2026-09-21.

## Conclusion

Doctrine ORM is a good fit when your domain model is rich, your queries recur, and you want the same object graph in tests and in production. It is a poor fit when your access pattern is mostly key-value lookups, because the unit of work and identity map add cost you do not need there. Start on 3.7.x, which is the default branch and had version 3.7.1 published on 2026-09-14, and read `UPGRADE.md` when crossing a major line, since 4.0 changes the runtime requirements. If your only question is how to run a select, DBAL below it is the more honest dependency.

## FAQ

### What is an ORM and why is it used?

An object-relational mapper lets you treat database rows as PHP objects instead of writing SQL by hand, and Doctrine ORM adds a change-tracking unit of work so that modified objects are detected and written back on flush. The README describes it as providing transparent persistence for PHP 8.1+ objects on top of the DBAL abstraction layer.

### Should I use an ORM or not?

An ORM pays off when your domain has relationships and invariants and you load the same graph repeatedly, since it removes N plus 1 queries and keeps queries consistent. It costs you when your access pattern is mostly key-value lookups or when you need a query shape the mapper cannot express. Doctrine ORM is built on DBAL, so dropping to the lower layer for a hot path is an option rather than a rewrite.

### What is Doctrine Query Language and why not just use SQL?

DQL is the object-oriented SQL dialect the README describes as inspired by Hibernate's HQL. It queries your mapped classes rather than tables, so an association can be traversed without you writing the join yourself. The trade-off is a second query language to learn and a translation step to vendor SQL.

### Which Doctrine ORM version should I install?

The default branch is 3.7.x, and version 3.7.1 was published on 2026-09-14. The repository maintains branches from 2.20 up to 4.0, so an existing application can stay on its line with patch releases, while a new one should start on 3.7. Read UPGRADE.md before crossing a major version, since 4.0 raises the PHP requirement.

## Sources

- [doctrine/orm on GitHub](https://github.com/doctrine/orm)
- [License: MIT](https://github.com/doctrine/orm/blob/3.7.x/LICENSE)
- [Project website](https://www.doctrine-project.org/projects/orm.html)
- [README](https://github.com/doctrine/orm/blob/3.7.x/README.md)
- [Releases](https://github.com/doctrine/orm/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/doctrine-orm
