phpstan/phpstan-doctrine
PHPStan extension for Doctrine that validates DQL and QueryBuilder, checks repository findBy and magic methods, improves EntityManager and repository type inference, supports ORM and ODM, and reports mapping, entity type, and proxy issues.
Architecture fit. phpstan/phpstan-doctrine is a static-analysis extension, not a runtime library. It hooks into PHPStan via extension.neon (auto-registered through phpstan/extension-installer) and optionally rules.neon for DQL/QueryBuilder validation. This makes it architecturally low-risk: it is a require-dev dependency that never ships to production and imposes no runtime contract on the application. The critical fit question for a Laravel codebase is whether Doctrine ORM is actually in use. Laravel's default persistence layer is Eloquent, so a conventional Laravel app gets almost none of the headline value (DQL validation, EntityRepository<MyEntity> inference, proxy/final-entity rules). The package becomes relevant in three scenarios: (1) Laravel applications using laravel-doctrine/orm or a hand-rolled Doctrine ORM integration, (2) modular/DDD Laravel codebases where Doctrine owns the domain model and Eloquent is peripheral, and (3) narrower DBAL-only wins, since the bundled stubs harden Doctrine\DBAL\Connection signatures (including opt-in literal-string for executeQuery) even where no ORM EntityManager exists.
Integration feasibility. Straightforward mechanically: composer require --dev phpstan/phpstan-doctrine, then either rely on the extension installer or add one includes: line. The real integration work is the objectManagerLoader — a bootstrap PHP file that boots the Laravel kernel and returns the Doctrine EntityManager. The README's Symfony examples translate to Laravel but require care: booting a full Laravel kernel inside a static-analysis run can pull in environment files, service providers, cache warmers, and outbound HTTP calls. A dedicated analysis-only bootstrap (test env, array/sqlite connection, no queue or mail providers) is usually necessary. Note that query analysis uses Doctrine's DQL parser and entity metadata, so no live database is required — a significant feasibility advantage. Custom DBAL types require either a hand-written DoctrineTypeDescriptor or the ReflectionDescriptor shortcut, and reportUnknownTypes: true can be used to enforce coverage.
Technical risk. Moderate, concentrated in version coupling rather than code risk. The extension tracks PHPStan 2.x and Doctrine ORM 2.x/3.x and DBAL majors; a PHPStan or Doctrine upgrade can force a coordinated bump across the toolchain (Larastan, extension-installer, PHPStan itself). Second risk: false positives. The final entity and final constructor rules, column/property type discrepancy rules, and literalString mode can surface large error volumes on a legacy codebase and block CI if enabled without a baseline. Third risk: silent false negatives — subqueries infer as mixed, all non-TypedExpression custom DQL functions infer as mixed, and getOneOrNullResult/getSingleResult/toIterable/execute only infer when Query::HYDRATE_OBJECT is passed explicitly. Teams can mistake "green build" for "fully typed queries."
Key questions.
doctrine/dbal transitively via Laravel schema tooling? If the latter, scope the value to DBAL stubs only.pdo_mysql, pdo_pgsql, pdo_sqlite, etc.), so driver choice affects result accuracy.objectManagerLoader and its analysis-only environment? An unowned loader is a recurring CI outage.TypedExpression?Stack fit. Targets PHP 8.x, PHPStan 2.x, Doctrine ORM/DBAL/ODM. In Laravel it co-exists with Larastan; both are PHPStan extensions and both contribute stubs and rules. There is no known service-tag collision, but the includes must be resolved deterministically — prefer phpstan/extension-installer over manual includes: ordering, and pin PHPStan, Larastan, and phpstan-doctrine together in composer.json so constraint solving does not drift mid-sprint. If the app is Eloquent-only, do not adopt; the maintenance overhead buys nothing.
Migration path. Stage the rollout so each step is independently revertible:
extension.neon only, no objectManagerLoader. Gains repository magic-method recognition, EntityManager::getRepository()/find/getReference return types, collection matching/first/last narrowing, and proxy-class prohibitions. Zero bootstrapping cost.objectManagerLoader and rules.neon. Unlocks DQL parse validation, unknown entity/field detection, column-type and relation-type discrepancy analysis, mapping-configuration errors, and precise flush() throw types.reportDynamicQueryBuilders, reportUnknownTypes, and finally literalString, each behind a fresh baseline entry so the delta is attributable.Compatibility. Verify PHPStan major alignment before anything else; the 2.x line assumes PHPStan 2.x. Doctrine ORM 3 changes (native lazy objects) alter the final entity rules — an ORM 2→3 upgrade will change the error set. Test the loader against the same PHP version CI uses, and pin the extension to a caret range that matches the Doctrine major in composer.lock.
Sequencing. (1) Audit Doctrine surface area: entity count, DQL string count, QueryBuilder call sites, custom types, custom DQL functions. (2) Spike on a branch at the current level, measuring new-error count as the adoption cost signal. (3) Build and commit the analysis-only tests/object-manager.php. (4) Enable rules.neon, triage errors into true bugs vs. baseline. (5) Register custom type descriptors — prefer ReflectionDescriptor where convertToPHPValue/convertToDatabaseValue are properly typed. (6) Turn on the opt-in flags in the order above. (7) Gate phpstan analyse on PRs and add the loader to the same review ownership as the CI config.
Maintenance. Dev-only, so no production blast radius, no runtime performance cost, no security surface in deployed artifacts. The recurring cost is toolchain upkeep: PHPStan and Doctrine majors move, and this extension moves with them. Budget a small, recurring upgrade task per quarter and treat it as part of the static-analysis platform rather than a per-team concern. MIT license and PHPStan-org stewardship keep legal and abandonment risk low; 681 stars understates real usage, and the listed "0 dependents" appears to be a metadata artifact — validate via Packagist install counts before citing it either way. The README is unusually good for a PHPStan extension: configuration, custom types, custom DQL functions, and the literal-string security mode are all documented with runnable examples.
Support. Community-supported only — GitHub issues, no vendor SLA. The failure surface is narrow enough that in-house expertise is achievable: one engineer can become the internal owner in a day. Escalation for novel false positives typically means a minimal reproduction against the upstream repo, so budget for that latency on blocking CI failures. Deep DQL-inference quirks (driver-dependent SUM/AVG result types, unsupported subqueries) are documented but easy to misread as extension bugs.
Scaling. Analysis time grows roughly with entity count, DQL call-site count, and repository method count. The objectManagerLoader boots the framework once per analysis process and parses all entity metadata; on a large domain model this is measurable but typically seconds, not minutes. PHPStan's result cache absorbs most of it on incremental runs — ensure the cache directory is persisted in CI. Memory is the more likely ceiling on very large metadata graphs. For monorepos or multi-app Laravel setups, run PHPStan per application with its own loader rather than one aggregate config.
Failure modes.
object-manager.php takes down the entire analysis run, not just Doctrine rules. Highest-severity, highest-likelihood operational risk.literalString or reportUnknownTypes late produces a large error wall; teams respond by widening the baseline, permanently muting the signal.mixed — subqueries and non-TypedExpression custom DQL functions degrade to mixed without error. A clean build does not mean fully typed queries; track this explicitly if type coverage is a stated goal.final entity/constructor rules and column/property discrepancy rules will flag older code aggressively; scoping them via allowNullablePropertyForRequiredField and selective ignores is a policy decision, not a fix.Ramp-up. A mid-level engineer can complete the base integration (composer, extension include, loader, baseline) in roughly one to three days; the strictness opt-ins and custom type descriptors add another two to four days depending on how many bespoke DBAL types exist. The genuine value — DQL typos for unknown fields and entity classes caught pre-merge, correct repository return types, precise flush() exceptions — is realized at step 2 of the migration path, which is a favorable value-to-effort ratio. The main organizational risk is adopting it into an Eloquent-dominant codebase on the strength of its opportunity score alone; confirm Doctrine ORM is load-bearing before committing the maintenance budget.
How can I help you explore Laravel packages today?