【PHPフレームワークFlow】動的戻り値拡張、スタブ、ジェネリクスを使ってcreateQueryの型をPHPStanに認識させる

# 初めに
Flowの`Repository`には、DBへのシンプルな問い合わせを定義する`createQuery()`が用意されています。これにより、SQLを作成することなくDBアクセスができます。しかし、PHPStanではデフォルトで`createQuery()`を認識してくれません。
今回はFlowのRepositoryクラスの`createQuery()`をPHPStanに認識させる方法を紹介します。

# FlowのcreateQuery()とは
`createQuery()`は、Flowの`Repository`のメソッドです。
DBへの問い合わせを行うQueryクラスを作成することができます。
（使用例は後述します）

```php: RepositoryInterface（一部抜粋）
interface RepositoryInterface
{
    /**
     * Returns a query for objects of this repository
     *
     * @return QueryInterface
     * @api
     */
    public function createQuery(): QueryInterface;
}
```

## createQuery()の使い方
取得したい条件を指定後、`execute()`を行い、`QueryResult`（取得結果を抽象化したクラス）を作成します。
その後、`QueryResult`の`getFirst()`や`toArray()`でDBのデータを取得することができます。

取得結果の型は`Repository`に対応するドメインモデルとなります。

```php: createQuery()の使用例
$query = $this->hogeRepository->createQuery(); // Query型

// QueryResult型
$queryResult = $query->matching(
    $query->equals('column', 'value')
)->execute();

$resultData =  $queryResult->getFirst();  // Hoge型（hogeRepositoryに対応するドメインモデル型）
$resultDataList = $queryResult->toArray();  // Array<Hoge>型
```


# PHPStanはcreateQuery()を認識しない
PHPStanでは`createQuery()`経由で取得した結果の型を認識することができません。

実際に先ほどの例で取得した値の型を出力してみましたが、型が`object`や`array`などになってしまい、詳細な型が認識できていません。

```php: 解析対象
$query = $this->hogeRepository->createQuery(); // Query型

// QueryResult型
$queryResult = $query->matching(
    $query->equals('column', 'value')
)->execute();

$resultData =  $queryResult->getFirst();  // Hoge型（hogeRepositoryに対応するドメインモデル型）
$resultDataList = $queryResult->toArray();  // Array<Hoge>型

// 型を確認
\PHPStan\dumpType($query);
\PHPStan\dumpType($queryResult);
\PHPStan\dumpType($resultData);
\PHPStan\dumpType($resultDataList);
```

```sh: 解析結果
 ./bin/phpstan analyse .\Packages\Application\Neos.Welcome\Classes\Controller\FunctionalTestController.php 
Note: Using configuration file C:\Users\rogto\workspace\php\flow\Quickstart\phpstan.neon.
 1/1 [============================] 100%

 ------ ---------------------------------------------------------------------------------------
  Line   Packages/Application/Neos.Welcome/Classes/Controller/FunctionalTestController.php
 ------ ---------------------------------------------------------------------------------------
  :97    Dumped type: Neos\Flow\Persistence\QueryInterface
  :99    Dumped type: Neos\Flow\Persistence\QueryResultInterface
  :101   Dumped type: object|null
  :103   Dumped type: array
 ------ ---------------------------------------------------------------------------------------
```


動的戻り値拡張を用いたいところですが、`createQuery()`の戻り値は`Query`であり、`QueryResult`を経由してデータを取得します。そのため、動的戻り値拡張だけではうまくいきません。

# PHPStanにcreateQueryを認識させる
ということで動的戻り値拡張に加えて、スタブ、ジェネリクスを贅沢に使用し、createQuery()の結果をPHPStanに認識させてみましょう。

* **スタブ & ジェネリクス**
  * `QueryInterface`と`QueryResultInterface`のスタブを作成
  * ジェネリクスを使い、ドメインモデルの型を動的に受け取れるようにする
* **動的戻り値拡張**
  * `createQuery()`の動的戻り値拡張を作成
  * 戻り値を`QueryInterface`から`QueryInterface<Object>`に変更
    * `Object`は`Repository`に対応するドメインモデルをリポジトリ名から判別

以下実装です。

```php: QueryInterFace.stub
<?php
namespace Neos\Flow\Persistence;

/**
 * @template T of object
 */
interface QueryInterface
{
    /**
     * @param bool $cacheResult If the result cache should be used
     * @return QueryResultInterface<T> The query result
     */
    public function execute(bool $cacheResult = false): QueryResultInterface;

    /**
     * @param array $orderings The property names to order by
     * @return QueryInterface<T>
     */
    public function setOrderings(array $orderings): QueryInterface;

    /**
     * @param integer|null $limit
     * @return QueryInterface<T>
     */
    public function setLimit(?int $limit): QueryInterface;

    /**
     * @param boolean $distinct
     * @return QueryInterface<T>
     */
    public function setDistinct(bool $distinct = true): QueryInterface;

    /**
     * @param integer|null $offset
     * @return QueryInterface<T>
     */
    public function setOffset(?int $offset): QueryInterface;

    /**
     * @param object $constraint Some constraint, depending on the backend
     * @return QueryInterface<T>
     */
    public function matching($constraint): QueryInterface;
}

```

```php: QueryResultInterface.stub
<?php
namespace Neos\Flow\Persistence;

/**
 * @template T of object
 */
interface QueryResultInterface extends \Countable, \Iterator, \ArrayAccess
{
    /**
     * @return QueryInterface<T>
     */
    public function getQuery(): QueryInterface;

    /**
     * @return T
     */
    public function getFirst();

    /**
     * @return array<T>
     */
    public function toArray(): array;
}

```

```php: createQueryDynamicMethodReturnTypeExtension
<?php
namespace Neos\Welcome\PHPStan\Extension;

use Neos\Flow\Persistence\QueryInterface;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Type;

class createQueryDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
    public function getClass(): string
	{
		return \Neos\Flow\Persistence\Repository::class;
	}
	public function isMethodSupported(MethodReflection $methodReflection): bool
	{
		return $methodReflection->getName() === 'createQuery';
	}
	public function getTypeFromMethodCall(
		MethodReflection $methodReflection,
		MethodCall $methodCall,
		Scope $scope
	): ?Type
	{
        $repositoryClassName = $scope->getType($methodCall->var)->getClassName();

        if (!str_ends_with($repositoryClassName, 'Repository')) {
            return null;
        }

        $withoutRepository = preg_replace('/Repository$/', '', $repositoryClassName);
        $domainModelClassName = str_replace('Repository', 'Model', $withoutRepository);

		return new GenericObjectType(
			QueryInterface::class,
			[0 => new ObjectType($domainModelClassName)]
		);
	}
}

```

:::note info
今回の動作確認では不要ですが、`QueryInterface`のスタブでは`execute()`だけでなく`QueryInterface`と`QueryResultInterface`を返却するすべてのメソッドでジェネリクスを定義しています。
:::


実際に動作確認した結果がこちらです。
型が認識されていることが確認できました！
```sh
./bin/phpstan analyse .\Packages\Application\Neos.Welcome\Classes\Controller\FunctionalTestController.php 
Note: Using configuration file C:\Users\rogto\workspace\php\flow\Quickstart\phpstan.neon.
 1/1 [============================] 100%

 ------ ---------------------------------------------------------------------------------------
  Line   Packages/Application/Neos.Welcome/Classes/Controller/FunctionalTestController.php
 ------ ---------------------------------------------------------------------------------------
  :97    Dumped type: Neos\Flow\Persistence\QueryInterface<Neos\Welcome\Domain\Model\Hoge>
  :99    Dumped type: Neos\Flow\Persistence\QueryResultInterface<Neos\Welcome\Domain\Model\Hoge>
  :101   Dumped type: Neos\Welcome\Domain\Model\Hoge
  :103   Dumped type: array<Neos\Welcome\Domain\Model\Hoge>
 ------ ---------------------------------------------------------------------------------------

```

# 終わりに
PHPStanの各拡張機能は便利ですが、組み合わせることでさらに便利になります。いろいろ組み合わせて、フレームワーク固有の問題を解決していきたいです。

ここまでご覧いただきありがとうございました！