Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpstan Doctrine Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install as a dev dependency and let the extension installer wire it up:

composer require --dev phpstan/phpstan-doctrine phpstan/extension-installer

If you don't use extension-installer, add the base include manually:

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

That baseline gives you the cheap, high-value checks immediately: EntityRepository<MyEntity> generics resolution, magic findBy* / findOneBy* / countBy* recognition, correct return types for EntityManager::find()/getReference(), entity column-vs-property type mismatches, and final-entity/proxy warnings.

Where to look first, in order:

  1. Existing repository baseline — run vendor/bin/phpstan analyse at your current level before touching config. Most teams get their first wins here with zero configuration (typed find() returns, magic finders, collection matching()).
  2. Repository base class — if you extend a common repository, register it:
    parameters:
        doctrine:
            ormRepositoryClass: App\Doctrine\BetterEntityRepository
    
    This is the single config line that most improves inference on custom repository methods.
  3. objectManagerLoader — only add this once the baseline is green, because it unlocks DQL/QueryBuilder validation and query result type inference:
    parameters:
        doctrine:
            objectManagerLoader: tests/object-manager.php
    
    The loader must return a booted EntityManager (or ODM DocumentManager). Queries are analysed statically against entity metadata — no database connection is required, which matters for CI.

The first use case worth targeting is DQL/QueryBuilder result inference. A typo in a DQL field name or a wrong hydration assumption becomes a PHPStan error instead of a runtime exception:

$query = $em->createQuery('SELECT u FROM App\Entity\User u');
$query->getResult(); // array<App\Entity\User>

$qb->select('u.id', 'u.email')->from(User::class, 'u')->getQuery()->getResult();
// array<array{id: int, email: string}>

Note for Laravel shops: this package targets Doctrine ORM/ODM, not Eloquent. It's relevant only where you've adopted Doctrine (e.g. laravel-doctrine/orm) alongside or instead of Eloquent. The literalString and DQL rules are useless against Eloquent DB::select() calls — don't expect cross-benefit.

Implementation Patterns

Progressive enablement. The extensions are layered; turn them on incrementally so you can attribute failures:

Stage Config What you gain
1 extension.neon only Entity/repository inference, column type checks
2 + ormRepositoryClass Custom repository method inference
3 + rules.neon + objectManagerLoader DQL/QueryBuilder validation & result types
4 + literalString: true SQL-injection-adjacent types on executeQuery

Repository generics as documentation. Push teams to annotate repositories with the generic form; the extension interprets it and propagates types:

/** @extends EntityRepository<User> */
final class UserRepository extends EntityRepository
{
    /** @return User[] */
    public function findActive(): array { /* ... */ }
}

Then $em->getRepository(User::class) resolves to UserRepository and findActive() returns array<User> — no manual docblocks at call sites.

QueryBuilder discipline for inference. The analyser needs static structure. Encode these as review rules:

  • Never pass a QueryBuilder into another method — the type is lost at the call boundary.
  • No dynamic expressions in select/from/join/set (no $field = $request->input(...) inside ->select()).
  • Prefer ->select('u.id', 'u.email') over string concatenation.

Turn on the guard-rail to catch violations in CI:

parameters:
    doctrine:
        reportDynamicQueryBuilders: true

Hydration mode must be explicit. getResult() works bare, but getOneOrNullResult(), getSingleResult(), toIterable(), and execute() require an explicit Query::HYDRATE_OBJECT argument before any type is inferred — otherwise you get mixed. Standardise on explicit modes in a codebase-wide rule rather than sprinkling it ad hoc:

$user = $query->getOneOrNullResult(Query::HYDRATE_OBJECT); // User|null

Custom DBAL types via ReflectionDescriptor. If your custom type has proper typehints on convertToPHPValue()/convertToDatabaseValue(), don't hand-write a descriptor:

services:
    -
        factory: PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor('App\Doctrine\Type\MoneyType')
        tags: [phpstan.doctrine.typeDescriptor]

Only fall back to a full DoctrineTypeDescriptor implementation when the typehints are insufficient. Better still, extend a non-abstract Doctrine type (e.g. DecimalType) and the descriptor is picked up automatically, including driver-specific expression resolution (AVG, SUM).

Custom DQL functions. Implement TypedExpression::getReturnType() and inference follows. Functions without it are silently mixed — worth a grep if you rely on aggregate helpers.

Type coverage hygiene. Enable reportUnknownTypes: true to fail when an entity uses a column type with no descriptor. This is the pattern that keeps new custom types from degrading inference over time:

#[Column(type: Uuid7Type::NAME)] // fails until a descriptor is registered
private Uuid7 $code;

Dead-code integration. If you run phpstan-strict-rules / dead code detection, entity properties read only by Doctrine are recognized (generated IDs, version fields, Gedmo-managed properties for Timestampable/Blameable/Slug). No need for @property-read suppressions or @phpstan-ignore spam.

Symfony-style loader for the object manager. The loader script pattern from the README applies almost verbatim under any framework — boot the kernel/container, resolve the manager, return it. Keep it in tests/ and point objectManagerLoader at it, so it runs in CI without a booted web app.

Gotchas and Tips

  • rules.neon is not included by extension.neon. If DQL validation appears to do nothing, you almost certainly forgot includes: vendor/phpstan/phpstan-doctrine/rules.neon. Also note rules.neon requires objectManagerLoader — it silently degrades without it.
  • extension.neon alone does not validate DQL. You get repository/column inference but no query parsing. This trips up people who expect the headline feature on install.
  • objectManagerLoader failures are opaque. A loader that throws (env vars missing, kernel not bootable in the test env, cache dir permissions) produces confusing downstream errors. Test the loader standalone: php -r 'var_dump((require "tests/object-manager.php") instanceof Doctrine\ORM\EntityManagerInterface);' before blaming the analyser.
  • reportUnknownTypes is a ratchet, not a switch. Turning it on for an existing codebase with many legacy custom types will bury you in errors. Introduce it alongside a descriptor per type, or scope with a baseline.
  • literalString: true changes signatures broadly. Enabling it retroactively flags every executeQuery($sql) with a non-literal string — including legitimate builder-style helpers. Adopt it early in a project's life; retrofitting large codebases on a deadline is painful. It's also the highest-value security setting here.
  • getOneOrNullResult() returning mixed is expected, not a bug. The Query class design means hydration mode can't be inferred without the explicit argument. Don't chase it — just pass Query::HYDRATE_OBJECT.
  • Subqueries infer as mixed. Known limitation. If a repository method returns mixed and you're using a subquery, add an explicit @return docblock or restructure — don't wait for the extension to figure it out.
  • Aggregate result types are driver-dependent. SUM() → float vs numeric-string vs int depends on driver, driver setup, and PHP version. The extension autodetects for pdo_mysql, mysqli, pdo_sqlite, sqlite3, pdo_pgsql, pgsql. CI running a different driver config than prod will produce different inferred types — pin the CI database driver to match production or accept the drift knowingly.
  • Native StringType won't cast-infer custom DQL results (upstream ORM issue). Implement TypedExpression and return the real type.
  • Final entity classes and final constructors are reported because they break proxy generation. The exception is when native lazy objects are enabled — if you're on PHP 8.4+ with lazy objects, configure that or you'll fight false positives on a legitimately final entity.
  • Direct proxy class name usage is forbidden. instanceof \Proxies\__CG__\App\Entity\User fails; use instanceof User.
  • Nullable property vs required column is the most common column-type false positive. Before rewriting entities, consider doctrine.allowNullablePropertyForRequiredField: true to relax it — but understand you're suppressing a real discrepancy.
  • allCollectionsSelectable: false removes the injected matching() method on Collection. Set it if you're not on ORM and your collections legitimately lack it, otherwise you'll get phantom method resolutions.
  • Extension points to know: type descriptors (phpstan.doctrine.typeDescriptor tag) and DoctrineTypeDriverAwareDescriptor for driver-specific expression types. There's no supported hook for the DQL result inference itself — if a query shape isn't supported, your only lever is restructuring the query.
  • Debugging approach: when a type is unexpectedly mixed, bisect by (a) confirming the object manager loads, (b) replacing the query with a trivial SELECT u FROM Entity u to confirm baseline inference works, (c) checking whether the builder is passed across a method boundary or built dynamically, and (d) enabling reportDynamicQueryBuilders to let PHPStan point at the exact unsupported expression rather than you guessing.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
ably/pubsub-server
mstfkhazaal/defender
mfonte/base62x
karnoweb/metapilot
tobya/bcsapiwrapper
swalker2/cpanel
vish4395/laravel-translation-manager
morningtrain/economic
teksite/systeminfo-laravel
atomcms/voting
run6/weather
siokas/laravelembeddirectives
adamlien/pkuuids
gilsonsouza/lara-doc-generator