0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

初めに

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

FlowのcreateQuery()とは

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

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

createQuery()の使い方

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

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

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()経由で取得した結果の型を認識することができません。

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

解析対象
$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);
解析結果
 ./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に認識させてみましょう。

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

以下実装です。

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;
}

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;
}

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)]
		);
	}
}

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

実際に動作確認した結果がこちらです。
型が認識されていることが確認できました!

./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の各拡張機能は便利ですが、組み合わせることでさらに便利になります。いろいろ組み合わせて、フレームワーク固有の問題を解決していきたいです。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?