Stars: 528
Forks: 98
Pull Requests: 220
Issues: 251
Watchers: 7
Last Updated: 2023-09-16 07:45:01
Doctrine extensions for PHPStan
License: MIT License
Languages: PHP, Makefile
This extension provides following features:
findBy*, findOneBy* and countBy* methods on EntityRepository.findBy, findBy*, findOneBy, findOneBy*, count and countBy* method calls.EntityRepository<MyEntity> correctly in phpDocs for further type inference of methods called on the repository.Doctrine\ORM\EntityManager::getRepository().Doctrine\ORM\EntityManager::find, getReference and getPartialReference when Foo::class entity class name is provided as the first argumentmatching method on Doctrine\Common\Collections\Collection. This can be turned off by setting parameters.doctrine.allCollectionsSelectable to false.allowNullablePropertyForRequiredField: true setting.Doctrine\ORM\Query::getResult, getOneOrNullResult, getSingleResult, toIterable and execute in HYDRATE_OBJECT mode (see below).To use this extension, require it in Composer:
composer require --dev phpstan/phpstan-doctrineIf you also install phpstan/extension-installer then you're all set!
If you don't want to use phpstan/extension-installer, include extension.neon in your project's PHPStan config:
includes:
- vendor/phpstan/phpstan-doctrine/extension.neonIf you're interested in DQL/QueryBuilder validation, include also rules.neon (you will also need to provide the objectManagerLoader, see below):
includes:
- vendor/phpstan/phpstan-doctrine/rules.neonIf your repositories have a common base class, you can configure it in your phpstan.neon and PHPStan will see additional methods you define in it:
parameters:
doctrine:
ormRepositoryClass: MyApp\Doctrine\BetterEntityRepository
odmRepositoryClass: MyApp\Doctrine\BetterDocumentRepositoryYou can opt in for more advanced analysis by providing the object manager from your own application. This will enable DQL validation:
parameters:
doctrine:
objectManagerLoader: tests/object-manager.phpExample for Symfony 4:
// tests/object-manager.php
use App\Kernel;
require __DIR__ . '/../config/bootstrap.php';
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();
return $kernel->getContainer()->get('doctrine')->getManager();Example for Symfony 5:
// 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();This extension can infer the result type of DQL queries when an objectManagerLoader is provided.
Examples:
$query = $entityManager->createQuery('SELECT u FROM Acme\User u');
$query->getResult(); // array<Acme\User>
$query = $entityManager->createQuery('SELECT u.id, u.email, u.name FROM Acme\User u');
$query->getResult(); // array<array{id: int, email: string, name: string|null}>
$query = $entityManager->createQuery('
SELECT u.id, u.email, COALESCE(u.name, "Anonymous") AS name
FROM Acme\User u
');
$query->getSingleResult(Query::HYDRATE_OBJECT); // array{id: int, email: string, name: string}>
$query = $entityManager->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->getQuery();
$query->getResult(); // array<Acme\User>Queries are analyzed statically and do not require a running database server. This makes use of the Doctrine DQL parser and entities metadata.
Most DQL features are supported, including GROUP BY, DISTINCT, all flavors of JOIN, arithmetic expressions, functions, aggregations, NEW, etc. Sub queries and INDEX BY are not yet supported (infered type will be mixed).
The getResult method is supported when called without argument, or with the hydrateMode argument set to Query::HYDRATE_OBJECT:
$query = $entityManager->createQuery('SELECT u FROM Acme\User u');
$query->getResult(); // array<User>
$query->getResult(Query::HYDRATE_OBJECT); // array<User>The methods getOneOrNullResult, getSingleResult, toIterable, and execute are supported when the hydrateMode argument is explicitly set to Query::HYDRATE_OBJECT:
$query = $entityManager->createQuery('SELECT u FROM Acme\User u');
$query->getOneOrNullResult(); // mixed
$query->getOneOrNullResult(Query::HYDRATE_OBJECT); // UserThis is due to the design of the Query class preventing from determining the hydration mode used by these functions unless it is specified explicitly during the call.
If your application uses custom Doctrine types, you can write your own type descriptors to analyse them properly.
Type descriptors implement the interface PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor which looks like this:
<?php
public function getType(): string;
public function getWritableToPropertyType(): Type;
public function getWritableToDatabaseType(): Type;getType() method simply returns the class name of the custom type.getWritableToPropertyType() method returns the PHPStan type that the custom type will write into the entity's property field. Basically it is the return type of the custom type's convertToPHPValue() method.getWritableToDatabaseType() method returns the PHPStan type that can be written from the entity's property field into the custom type. Again, basically it's the allowed type for the custom type's convertToDatabaseValue()'s first argument.Generally, at least for most of Doctrine's native types, these last two methods will return the same type, but it is not always the case. One example would be the datetime type, which allows you to set any \DateTimeInterface into to property field, but will always contain the \DateTime type when loaded from the database.
Type descriptors don't have to deal with nullable types, as these are transparently added/removed from the descriptor's types as needed. Therefore you don't have to return the union type of your custom type and NullType from the descriptor's methods, even if your custom type allows null.
If your custom type's convertToPHPValue() and convertToDatabaseValue() methods have proper typehints, you don't have to write your own descriptor for it. The PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor can analyse the typehints and do the rest for you.
When you write a custom type descriptor, you have to let PHPStan know about it. Add something like this into your phpstan.neon:
services:
-
class: MyCustomTypeDescriptor
tags: [phpstan.doctrine.typeDescriptor]
# in case you are using the ReflectionDescriptor
-
factory: PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor('MyApp\MyCustomTypeName')
tags: [phpstan.doctrine.typeDescriptor]