【PHPフレームワークFlow】findByIdentifierの戻り値をPHPStanに認識させる

# 初めに
PHPフレームワークFlowの`findByIdentifier()`メソッドは動的に戻り値を変えるメソッドです。そのため、PHPStanの解析時に型の定義ができません。今回は、PHPStanの動的戻り値拡張を用いてこのメソッドに型を定義していきたいと思います。

# 前提
FlowはDoctrineORMを採用&拡張しており、エンティティからテーブルを自動生成できます。生成したテーブルには`persistence_object_identifier`というカラムが自動で作成され、これがテーブルの主キーになっています。

例えば、以下のようなクラスがあったとします。

```php
<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 * @ORM\Table(name="item")
 */
class Item
{

    /**
     * @ORM\Column(type="string", name="item_name", length=20, nullable=false, unique=true)
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column(type="string", length=100)
     * @var string
     */
    protected $description;

    /**
     * @ORM\Column(type="integer", nullable=false)
     * @var int
     */
    protected $price;

    public function __construct(string $name, string $description, int $price)
    {
        $this->name = $name;
        $this->description = $description;
        $this->price = $price;
    }

}
```

このクラスから以下のようなテーブルが自動生成されます。
```sql
mysql> show create table item\G
*************************** 1. row ***************************
       Table: item
Create Table: CREATE TABLE `item` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `item_name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `price` int(11) NOT NULL,
  PRIMARY KEY (`persistence_object_identifier`),
  UNIQUE KEY `UNIQ_1F1B251E96133AFD` (`item_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)
```

# findByIdentifier()とは、UUIDからエンティティを取得するメソッド
`findByIdentifier()`はUUIDからエンティティを取得できるメソッドです。
このメソッドはRepositoryの親クラスにあるため、どのRepositoryからでも利用できます。

```php: findByIdentifierの例
$entity = $this->itemlRepository->findByIdentifier('dummy');
```

# findByIdentifier()は実行されるまで戻り値の型が分からない
前述の通り、`findByIdentifier()`は親Repositoryのメソッドのため、呼び出されるまでreturnの型を定義することができません。
以下のように、PHPDocは`@return object|null`となっています。

```php
    /**
     * Finds an object matching the given identifier.
     *
     * @param string $identifier The identifier of the object to find
     * @return object|null The matching object if found, otherwise NULL
     * @api
     */
    public function findByIdentifier($identifier)
    {
        return $this->persistenceManager->getObjectByIdentifier($identifier, $this->entityClassName);
    }
```

# 動的戻り値拡張を使う
ということで、動的戻り値拡張を使って解決していきましょう。
動的戻り値拡張についてはこちらの記事で記載をしたので是非ご覧ください。

https://qiita.com/nnhkrnk/items/8016ff8b948b6c9fa789




Flowでは、Repositoryとエンティティは同じ名前にしなければいけないというルールがあります。そして、RepositoryのfindByIdentifier()で取得する型は対応したエンティティになります。

そのため、**呼び出しているRepositoryさえわかればfindByIdentifier()で取得する型の定義が可能**ということです。

※エンティティとRepositoryの関係は以下をご参照ください
```
Classes/
   └ Domain/
        ├ Model
        |    └ Item.php（★）
        └ Repository
             └ ItemRepository.php（★）← ItemRepositoryではItem型のエンティティを取得する
```

ということで拡張機能を作ってみました。
Repository名からModelの型を定義しています。nullもあり得るので忘れずに定義。

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

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class FindByIdentifierDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
    public function getClass(): string
	{
		return \Neos\Flow\Persistence\Repository::class;
        // return \Neos\Welcome\Domain\Repository\FunctionalRepository::class;
	}
	public function isMethodSupported(MethodReflection $methodReflection): bool
	{
		return $methodReflection->getName() === 'findByIdentifier';
	}
	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);

		$types[] = new ObjectType($domainModelClassName);
		$types[] = new NullType();

		return TypeCombinator::union(...$types);
	}
}

```

動作確認のために以下のクラスを用意しました。
`itemRepository`を用いているので、結果が`Neos\Welcome\Domain\Model\Item`クラスになっていればOKです。
```php: 解析対象（一部抜粋）
class Test
{
    public function findDetailAction(): void
    {
        $result = $this->itemlRepository->findByIdentifier('dummy');
        \PHPStan\dumpType($result);
    } 
}
```

解析結果が以下です。ちゃんと型が取得できました！
```sh: 解析結果（一部抜粋）
$ ./bin/phpstan analyse
 1/1 [============================] 100%

 ------ ----------------------------------------------------------------------------
  Line   Packages/Application/Neos.Welcome/Classes/Util/Test.php
 ------ ----------------------------------------------------------------------------
  :43    Dumped type: Neos\Welcome\Domain\Model\Item|null
 ------ ----------------------------------------------------------------------------
```

# 終わりに
解析自体では扱うことができましたが、エディタ上での型補完は利きません。ひとまず一歩前進しましたが、型補完できる方法を探したいですね。
また何か学んだことがあれば記載します。
ここまで読んでいただきありがとうございました！