phpstan/phpstan-doctrine

PHPStan 的 Doctrine 扩展

安装次数: 40,787,212

依赖者: 946

建议者: 4

安全: 0

星标: 583

关注者: 9

分支: 96

开放问题: 73

类型:phpstan-extension

1.5.3 2024-09-01 13:17 UTC

README

Build Latest Stable Version License

此扩展提供以下功能

  • DQL 验证用于解析错误、未知实体类和未知持久字段。也支持 QueryBuilder 验证。
  • 识别 EntityRepository 上的魔法方法 findBy*findOneBy*countBy*
  • 验证在 repository 的 findByfindBy*findOneByfindOneBy*countcountBy* 方法调用中的实体字段。
  • 正确解释 phpDocs 中的 EntityRepository,以进行对在 repository 上调用的方法的进一步类型推理。
  • Doctrine\ORM\EntityManager::getRepository() 提供正确的返回值。
  • 当将 Foo::class 实体类名作为第一个参数提供时,为 Doctrine\ORM\EntityManager::findgetReferencegetPartialReference 提供正确的返回类型。
  • 添加 Doctrine\Common\Collections\Collection 上缺失的 matching 方法。可以通过将 parameters.doctrine.allCollectionsSelectable 设置为 false 来关闭此功能。
  • 也支持 Doctrine ODM。
  • 分析实体列类型与属性字段类型之间的差异。可以通过将 allowNullablePropertyForRequiredField: true 设置来放松此限制。
  • Doctrine\ORM\Query::getResultgetOneOrNullResultgetSingleResulttoIterableexecuteHYDRATE_OBJECT 模式下提供返回类型(见下文)。

安装

要使用此扩展,请在 Composer 中要求它

composer require --dev phpstan/phpstan-doctrine

如果您还安装了 phpstan/extension-installer,那么您就准备好了!

手动安装

如果您不想使用 phpstan/extension-installer,请将 extension.neon 包含在您项目的 PHPStan 配置中

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

如果您对 DQL/QueryBuilder 验证感兴趣,请还包含 rules.neon(您还需要提供 objectManagerLoader,见下文)

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

配置

如果您的工作库有一个公共基类,您可以在 phpstan.neon 中配置它,PHPStan 将看到您在其中定义的额外方法

parameters:
	doctrine:
		ormRepositoryClass: MyApp\Doctrine\BetterEntityRepository
		odmRepositoryClass: MyApp\Doctrine\BetterDocumentRepository

您可以选择通过提供自己的应用程序中的对象管理器来获得更高级的分析。这将启用 DQL 验证

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

对称法 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();

对称法 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();

查询类型推理

如果提供 objectManagerLoader,则此扩展可以推断 DQL 查询的结果类型。

示例

$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>

查询是在静态方式下分析的,不需要运行中的数据库服务器。这使用了 Doctrine DQL 解析器和实体元数据。

支持大多数 DQL 功能,包括 GROUP BYDISTINCT、所有种类的 JOIN、算术表达式、函数、聚合、NEW 等。子查询和 INDEX BY 还不支持(推断的类型将是 mixed)。

表达式查询类型推理

例如,SUM(e.column) 是否作为 floatnumeric-stringint 获取高度依赖于驱动程序、它们的设置和 PHP 版本。此扩展自动检测您的设置,并为 pdo_mysqlmysqlipdo_sqlitesqlite3pdo_pgsqlpgsql 提供相当准确的结果。

支持的方法

支持在没有参数调用时,或者将 hydrateMode 参数设置为 Query::HYDRATE_OBJECT 时调用 getResult 方法。

$query = $entityManager->createQuery('SELECT u FROM Acme\User u');

$query->getResult(); // array<User>

$query->getResult(Query::HYDRATE_OBJECT); // array<User>

当显式地将hydrateMode参数设置为Query::HYDRATE_OBJECT时,支持以下方法:getOneOrNullResultgetSingleResulttoIterableexecute

$query = $entityManager->createQuery('SELECT u FROM Acme\User u');

$query->getOneOrNullResult(); // mixed

$query->getOneOrNullResult(Query::HYDRATE_OBJECT); // User

这是由于Query类的结构设计,除非在调用时明确指定,否则无法确定这些函数所使用的 hydration 模式。

有问题的方法

并非每个QueryBuilder都可以进行静态分析,以下是一些最大化类型推断的建议

  • 不要将QueryBuilder传递给方法
  • 不要在QueryBuilder方法中使用动态表达式(主要是在select/join/from/set中)

您可以通过以下方式启用报告不可推断的位置:

parameters:
	doctrine:
		reportDynamicQueryBuilders: true

自定义类型

如果您的应用程序使用自定义Doctrine类型,您可以编写自己的类型描述符以正确分析它们。类型描述符实现接口PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor,如下所示

<?php

public function getType(): string;

public function getWritableToPropertyType(): Type;

public function getWritableToDatabaseType(): Type;
  • getType()方法简单返回自定义类型的类名。
  • getWritableToPropertyType()方法返回自定义类型将要写入实体属性字段的PHPStan类型。基本上,它是自定义类型convertToPHPValue()方法的返回类型。
  • getWritableToDatabaseType()方法返回可以写入实体属性字段并写入自定义类型的PHPStan类型。再次,基本上,它是自定义类型convertToDatabaseValue()的第一个参数所允许的类型。

一般来说,至少对于大多数Doctrine的本地类型,这两个最后的方法将返回相同类型,但这并不总是如此。一个例子是datetime类型,它可以设置任何\DateTimeInterface到属性字段,但在从数据库加载时始终包含\DateTime类型。

可空类型

类型描述符无需处理可空类型,因为这些类型可以透明地根据需要添加/删除到描述符的类型中。因此,即使您的自定义类型允许null,您也不必从描述符的方法中返回自定义类型和NullType的联合类型。

ReflectionDescriptor

如果您的自定义类型的convertToPHPValue()convertToDatabaseValue()方法有正确的类型提示,则不需要为它编写自己的描述符。PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor可以分析类型提示,并为您完成剩余工作。

如果您的类型的父类是Doctrine的非抽象类型之一,则ReflectionDescriptor将重用其描述符,甚至在表达式解析(例如AVG(t.cost))中。例如,如果您扩展了Doctrine\DBAL\Types\DecimalType,它将知道sqlite将其作为float|int检索,而其他驱动程序作为numeric-string。如果您仅扩展了Doctrine\DBAL\Types\Type,则应使用自定义描述符,并可选择实现DoctrineTypeDriverAwareDescriptor以提供特定驱动程序的解析。

注册类型描述符

当您编写自定义类型描述符时,您必须让PHPStan了解它。将以下内容添加到您的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]

确保类型具有描述符

如果您想确保您永远不会忘记某个自定义类型的描述符,您可以启用

parameters:
	doctrine:
		reportUnknownTypes: true

这将在您的实体使用自定义类型而没有描述符时导致失败

#[Entity]
abstract class Uuid7Entity
{

    #[Id]
    #[Column(type: Uuid7Type::NAME)] // reported when descriptor for such type is missing
    private Uuid7 $hsCode;

自定义DQL函数

任何实现Doctrine的TypedExpression自定义DQL函数都被此扩展理解,并以其在getReturnType()方法中使用的类型进行推断。所有其他自定义DQL函数都被推断为mixed。请注意,您不能使用原生StringType进行类型转换(和推断)字符串结果(请参阅ORM问题)。

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\AST\TypedExpression;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;

class Floor extends FunctionNode implements TypedExpression
{
    private AST\Node|string $arithmeticExpression;

    public function getSql(SqlWalker $sqlWalker): string
    {
        return 'FLOOR(' . $sqlWalker->walkSimpleArithmeticExpression($this->arithmeticExpression) . ')';
    }

    public function parse(Parser $parser): void
    {
        $parser->match(TokenType::T_IDENTIFIER);
        $parser->match(TokenType::T_OPEN_PARENTHESIS);

        $this->arithmeticExpression = $parser->SimpleArithmeticExpression();

        $parser->match(TokenType::T_CLOSE_PARENTHESIS);
    }


    public function getReturnType(): Type
    {
        return Type::getType(Types::INTEGER);
    }
}

文字字符串

在phpstan-doctrine中,占位文件包含许多用literal-string标记的参数。这是一种以安全为中心的类型,只允许将用代码编写的文字字符串传递到这些参数中。

这降低了SQL注入的风险,因为不允许用用户输入的动态字符串代替literal-string

使用此类型的一个例子是Doctrine\Dbal\Connection::executeQuery()中的$sql参数。

要在phpstan-doctrine中启用此高级类型,请使用此配置参数

parameters:
	doctrine:
		literalString: true