Skip to content

Support query inference with multiple entity managers#756

Open
LubuSeb wants to merge 8 commits into
phpstan:2.0.xfrom
LubuSeb:codex/phpstan-doctrine-multiple-ems
Open

Support query inference with multiple entity managers#756
LubuSeb wants to merge 8 commits into
phpstan:2.0.xfrom
LubuSeb:codex/phpstan-doctrine-multiple-ems

Conversation

@LubuSeb
Copy link
Copy Markdown

@LubuSeb LubuSeb commented May 22, 2026

Summary

Fixes return type inference for projects that use multiple Doctrine entity managers by allowing objectManagerLoader to return a Doctrine ManagerRegistry.

When the loader returns a registry, PHPStan Doctrine now resolves the object manager from the entity class found in the DQL before validating or inferring query result types. This keeps the existing single-manager loader behavior intact while supporting Symfony applications where each entity namespace is owned by a different manager.

Details

  • Add ManagerRegistry support to ObjectMetadataResolver.
  • Resolve entity metadata and DQL/query-builder analysis through getManagerForClass() when possible.
  • Add a regression fixture with two in-memory entity managers mapped to disjoint namespaces.
  • Document returning the Doctrine registry from Symfony objectManagerLoader setups.

Verification

Fixes #655

Comment thread src/Type/Doctrine/ObjectMetadataResolver.php
@VincentLanglet
Copy link
Copy Markdown
Contributor

Looking at https://github.com/phpstan/phpstan-doctrine/pull/757/changes#diff-e1bfea71addecdb2a3f73f91d95ed61b789458e5107d9efbbc39937d35b880fd some updates are also needed in GetRepositoryDynamicReturnTypeExtension no ?

@LubuSeb
Copy link
Copy Markdown
Author

LubuSeb commented May 24, 2026

Addressed the repository return-type path as well: the extension now reads the optional manager name argument and resolves the repository class through the matching object manager. I also fixed the lowest-dependency ObjectManager fixture compatibility issue and the coding-standard findings from the last run. CI is green on the latest head.

Comment thread src/Type/Doctrine/ObjectMetadataResolver.php Outdated
Comment thread src/Type/Doctrine/ObjectMetadataResolver.php
Comment thread src/Type/Doctrine/ObjectMetadataResolver.php Outdated
@LubuSeb
Copy link
Copy Markdown
Author

LubuSeb commented May 24, 2026

Addressed the latest resolver simplification feedback in 3568a87.

Changes made:

  • getObjectManagerLoaderResult() now normalizes a ManagerRegistry with exactly one manager into that single ObjectManager when caching the loader result.
  • getObjectManager() and getObjectManagerForClass() now return early when the cached loader result is not a registry.
  • getObjectManagerForDql() now avoids the DQL class parsing path entirely for non-registry loader results.
  • Removed the separate single-manager helper.

Validation:

  • parallel-lint on ObjectMetadataResolver.php passed.
  • Targeted PHPUnit for multiple-EM, named-manager repository, and ManagerRegistry inference passed: 8 tests / 8 assertions.
  • Broader tests/Type/Doctrine passed: 279 tests / 8072 assertions.
  • PHPStan analysis of ObjectMetadataResolver.php and GetRepositoryDynamicReturnTypeExtension.php passed.
  • phpcs on ObjectMetadataResolver.php passed.
  • GitHub CI is green on head 3568a87.

Comment on lines +103 to +104
preg_match_all('~\b(?:FROM|UPDATE)\s+([\\\\A-Za-z_][\\\\A-Za-z0-9_]*)(?:\s+|$)~i', $dql, $matches);
preg_match_all('~\bDELETE\s+(?:FROM\s+)?([\\\\A-Za-z_][\\\\A-Za-z0-9_]*)(?:\s+|$)~i', $dql, $deleteMatches);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if you don't use doctrine ; I saw you worked a lot with regex, so I'd like your opinion on this strategy @staabm

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do I see this correctly, that we need this regex-magic, because we cannot get a AST for the DQL since we are in the process of creating a entity manager?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will the DQL always contain a full-qualified class-name in the query?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a test which contains a DELETE statement.. is this line tested?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the reason for the pre-parse step. At this point we need a candidate entity class before handing the DQL to Doctrine's EM-bound parser/metadata validation; otherwise the default manager can reject a tenant entity before we know which manager should parse it. The regex is intentionally a narrow heuristic to find an autoloadable root entity class, then the actual manager choice is delegated to ManagerRegistry::getManagerForClass().

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not universally. This only supports the autoloadable class-string path. QueryBuilder/repository-generated DQL uses class metadata names, and direct createQuery() inference can resolve those FQCN-style class strings. If the DQL uses a short alias or anything not autoloadable, this falls back to the default configured manager rather than guessing.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I added focused coverage in badaaae using Doctrine's no-FROM form, DELETE QueryResult\MultipleEntityManagers\Tenant\App a, so it exercises the DELETE-specific extraction branch rather than the generic FROM branch.

Verification run in Docker with PHP 8.4:

  • vendor/bin/phpunit tests/Type/Doctrine/MultipleEntityManagersQueryTypeInferenceTest.php -> 9 tests / 9 assertions
  • vendor/bin/phpunit tests/Type/Doctrine -> 280 tests / 8073 assertions
  • git diff --check -> clean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof do you forsee whether this regex-matching is "good enough" for most DQL to identify the used entity-class?
(I am no DQL expert and don't know which syntax people use/is supported)

in case we can think about more DQL syntax edge cases, this might help to add more tests and harden the regex pattern

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repository return type errors when using multiple entity managers

4 participants