Open-source project
phpstan/phpstan-doctrine avatar
phpstan/phpstan-doctrine

phpstan-doctrine: teaching PHPStan what your Doctrine entities really contain

Doctrine extensions for PHPStan

682 stars123 forksPHPMIT

At a glance

What is it?
phpstan/phpstan-doctrine is a PHPStan extension for Doctrine ORM and ODM. It adds DQL validation, typed repository calls and entity mapping checks, but the deepest features only activate once you hand it an object manager from your own application.
Who is it for?
Teams running Doctrine ORM through PHPStan should adopt this extension, since the repository typing and mapping checks cost nothing to enable. Skip it if your codebase writes DQL by hand in large volume, because sub queries still infer as mixed.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

DQL validation only switches on when you supply an object manager

phpstan/phpstan-doctrine has no command of its own. It is a PHPStan extension that teaches the analyser about Doctrine ORM and Doctrine ODM, and most of what makes it worth installing sits behind a single parameter called objectManagerLoader. Without it you still get the static half of the extension: repository method recognition, return types for EntityManager calls, mapping diagnostics and dead code awareness. With it, the extension also validates DQL for parse errors, unknown entity classes and unknown persistent fields, and the same validation applies to QueryBuilder calls. The catch is that the object manager has to come from your own application code, which means the loader file boots your kernel and returns a manager, or a manager registry when you run several. That is a deliberate design choice rather than an oversight. The extension reads your real mapping configuration, so it can tell you that a field name in a DQL string does not exist on the entity. The price is that your analysis environment now depends on your application booting successfully.

Repository magic methods become ordinary typed calls

The part of this extension most teams feel first has nothing to do with DQL. Doctrine repositories expose magic finders such as findBy, findOneBy and countBy, plus their suffixed variants, and plain static analysis sees none of them. The extension recognises findBy* , findOneBy* and countBy* on EntityRepository, and it validates the entity fields passed to findBy, findBy* , findOneBy, findOneBy* , count and countBy* calls. A typo in a field name becomes a reported error instead of a runtime query that returns nothing. The extension also interprets an EntityRepository<MyEntity> annotation, so methods you call on that repository inherit the entity type for further inference, and it gives EntityManager::getRepository() a correct return type. The same treatment applies to find, getReference and getPartialReference when you pass a class name literal such as Foo::class as the first argument, which removes a whole family of nullable return type complaints. If your repositories share a custom base class, you can point the extension at it through the ormRepositoryClass and odmRepositoryClass parameters, and your own helper methods become visible to the analyser.

DQL result types come from the parser and the metadata, not a live database

The query type inference feature reads a DQL string and works out what the result looks like, using the Doctrine DQL parser and your entity metadata. The documentation is explicit that queries are analysed statically and do not require a running database server, so you get useful types in CI where no MySQL instance exists. Selecting whole entities gives an array of that entity type. Selecting individual columns gives a shaped array of column types, and a COALESCE expression in the select list changes the inferred type accordingly. Hydration mode matters too: with Query::HYDRATE_OBJECT the extension provides return types for getResult, getOneOrNullResult, getSingleResult, toIterable and execute. A short example shows the shape of the inference.

Scalar aggregates depend on the PDO driver underneath

Aggregate expressions are where static typing meets a runtime detail that no amount of metadata can settle. Whether SUM(e.column) arrives as a float, a numeric-string or an int depends on the driver, its configuration and the PHP version in use, and the project links to a driver comparison to make that point. The extension autodetects your setup and returns reasonably accurate results for pdo_mysql, mysqli, pdo_sqlite, sqlite3, pdo_pgsql and pgsql. If your project runs on a driver outside that list, the inferred scalar type is a guess rather than a fact, and the resulting analysis error may point at your code when the real cause is the driver.

Final classes and proxy names are reported as errors

Doctrine generates proxy subclasses for your entities, and that mechanism has edges. The extension reports final entity classes that can cause problems with proxy generation, though the report is allowed when native lazy objects are enabled, and it reports final entity constructors for the same reason. It also forbids direct use of Doctrine proxy class names, since referencing a generated proxy in application code ties you to an implementation detail that Doctrine owns. Mapping configuration itself is checked as well, with errors for annotation and attribute parsing issues, which catches a malformed mapping that would otherwise fail at runtime. Alongside that, the extension supplies precise throw types for EntityManager::flush(), naming ORMException and UniqueConstraintViolationException, so a flush call inside a try block is no longer a blind catch. These checks need no object manager and work from the mapping metadata alone.

Dead code detection stops flagging entity properties as unused

PHPStan reports properties that are never written. Doctrine writes many of them for you, through the constructor, a setter, a lifecycle callback or the proxy, so a plain entity class can fill up with false positives. This extension integrates with PHPStan dead code detection and excludes entity properties from those reports. It also recognises generated identifiers, version fields and read-only entities as always written, which covers the cases where a value exists only in the database and is never assigned in PHP. Support for Gedmo doctrine-extensions extends the same idea to properties managed by annotations and attributes such as Timestampable, Blameable and Slug, which Doctrine extensions write rather than your code. The practical effect is that enabling this extension can remove a batch of dead code errors that had been buried in a baseline file, and the baseline becomes smaller rather than larger.

Installing the extension and pointing it at your manager

The package is a normal Composer dependency, installed as a development requirement:

bash
composer require --dev phpstan/phpstan-doctrine

With phpstan/extension-installer present the extension registers itself and there is nothing else to do. Without it, include the extension configuration in your phpstan.neon by hand:

yaml
includes:
    - vendor/phpstan/phpstan-doctrine/extension.neon

DQL and QueryBuilder validation live in a separate rules file, and they need the object manager loader as well:

yaml
includes:
    - vendor/phpstan/phpstan-doctrine/rules.neon

The loader is a PHP file that boots your application and returns a Doctrine manager:

php
// tests/object-manager.php

use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;

require __DIR__ . '/../vendor/autoload.php';

(new Dotenv())->bootEnv(__DIR__ . '/../.env');

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();
return $kernel->getContainer()->get('doctrine')->getManager();

That path is referenced from the configuration:

yaml
parameters:
	doctrine:
		objectManagerLoader: tests/object-manager.php

If your application uses several entity managers, return the Doctrine manager registry from the loader instead, and the extension picks the manager that owns the entity being analysed. Because the loader executes application bootstrap code, it needs a working environment file and container, which is the part most likely to fail on a fresh CI machine.

The boundaries: sub queries, collection matching and relaxed nullability

Several documented limits shape how far you can trust the output. Sub queries are not yet supported for type inference, so the inferred type falls back to mixed. The extension also adds a missing matching method on Doctrine Common Collections Collection, because matching is not declared on the interface, and that behaviour can be turned off by setting parameters.doctrine.allCollectionsSelectable to false. On the mapping side, the extension analyses discrepancies between entity column types and property field types, and between entity relation types and property field types for to-one and to-many relations. That strictness has an escape hatch: the allowNullablePropertyForRequiredField setting relaxes the check for a nullable property mapped to a required field. Because the check is on by default, adding this extension can surface mapping mismatches that your code has carried for a long time. The repository covers Doctrine ODM as well, and it holds an MIT licence, so vendoring and redistribution carry no obligations beyond keeping the licence text.

Release rhythm and what the source tree shows about scope

The project is not archived, and the last push was on 2026-10-20. Recent tagged releases include 2.0.28 on 2026-07-13, 2.0.27 on 2026-06-10 and 2.0.26 on 2026-06-10, and the default branch is 2.1.x, which suggests a 2.x line still being worked on. The repository layout explains the breadth of the checks: extension.neon and rules.neon hold the two layers of configuration, while a series of baseline files covers Doctrine ORM 2, ORM 3, DBAL 3 and DBAL 4 separately, which is what you would expect from a package that has to analyse against several supported versions at once. A Makefile drives the contributor workflow with parallel-lint, phpcs from a shared build-cs repository, phpunit and a PHPStan self-analysis target. Those baselines are the honest measure of the current state: the project still needs a baseline to analyse itself cleanly against multiple Doctrine versions.

Editorial conclusion

Teams running Doctrine ORM through PHPStan should adopt this extension, since the repository typing and mapping checks cost nothing to enable. Skip it if your codebase writes DQL by hand in large volume, because sub queries still infer as mixed. Before enabling DQL validation, check that the object manager loader you point at resolves in the analysis environment, since that is what decides whether the deepest rules run at all.

Frequently asked questions

How do I install phpstan/phpstan-doctrine?

Require it with Composer as a development dependency using composer require --dev phpstan/phpstan-doctrine. If you also install phpstan/extension-installer, the extension is picked up automatically; otherwise include vendor/phpstan/phpstan-doctrine/extension.neon in your PHPStan configuration.

Does phpstan-doctrine need a running database to analyse DQL?

No, queries are analysed statically using the Doctrine DQL parser and entity metadata, so no running database server is required. DQL and QueryBuilder validation do require an objectManagerLoader that returns the manager from your application.

Why does my DQL sub query resolve to mixed?

Sub queries are not yet supported by the query type inference, so the inferred type for a query containing one is mixed. Most other DQL features are supported, including GROUP BY, INDEX BY, DISTINCT, JOIN variants, arithmetic expressions, functions, aggregations and NEW.

How can I stop the extension reporting a missing matching method on Collection?

Set parameters.doctrine.allCollectionsSelectable to false in your PHPStan configuration. That turns off the added matching method on Doctrine Common Collections Collection.

Official sources

  1. Issues
  2. License: MIT
  3. phpstan/phpstan-doctrine on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/phpstan-phpstan-doctrine.svg)](https://hysenlabs.com/projects/phpstan-phpstan-doctrine)

Was this useful?