PHPStanのカスタムPHPDoc型作ってみる

# 初めに
PHPStanには様々な拡張機能があり、プロジェクトに応じたルールを柔軟に作成することができます。前回は[こちら](https://qiita.com/nnhkrnk/items/936dd295132065e04999)の記事でPHPStanのカスタムルールを作成してみました。今回は**カスタムPHPDoc型**を作ってみようと思います。

# カスタムPHPDoc型とは
カスタムPHPDocとはPHPDocで定義する型を独自で作成することができる機能です。

https://phpstan.org/developing-extensions/custom-phpdoc-types


以下のように、独自の型`CustomType`というエイリアスで定義しておき、PHPDocの`@phpstan-param`に指定することでPHPStanに独自の型を認識させることができます。

```php
    /**
     * @phpstan-param CustomType<''> $param
     */
    public function testAction(array $param): void
    {
        // PHPStanは$paramをCustomTypeという型だと認識する
        \PHPStan\dumpType($param); 
    }
```

# カスタムPHPDoc型の作り方
ということで作り方を紹介します。
今回は`CustomTypeTest`というエイリアスで`array{key1: string, key2: string}`という型を定義してみましょう。

## 1. TypeNodeResolverExtensionインターフェースを継承する
PHPstanには`TypeNodeResolverExtension`というインターフェースがあり、カスタムPHPDoc型はこのインターフェースを継承して作成する必要があります。

```php: TypeNodeResolverExtension
interface TypeNodeResolverExtension
{

	public const EXTENSION_TAG = 'phpstan.phpDoc.typeNodeResolverExtension';

	public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type;

}
```

resoleveメソッドの戻り値は`?Type`であり、このメソッドを実装することで型の定義が可能になります。

今回は以下のように実装しました。

```php: CustomTypeTest
<?php
declare(strict_types = 1);

namespace Hoge\fuga;

use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class CustomTypeTest implements TypeNodeResolverExtension
{

    public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
    {

        // ポイント1：型のエイリアスがCustomTypeTestかどうかをチェック
		$typeName = $typeNode->type;
		if ($typeName->name !== 'CustomTypeTest') {
			return null;
		}

        // ポイント2：型を定義
        $newTypeBuilder = ConstantArrayTypeBuilder::createEmpty();
        $newTypeBuilder->setOffsetValueType( 
            new ConstantStringType("key1"),
            new StringType()
        );
        $newTypeBuilder->setOffsetValueType( 
            new ConstantStringType("key2"),
            new StringType()
        );
        $newTypes[] = $newTypeBuilder->getArray();
        return TypeCombinator::union(...$newTypes);
    }
}
```

* **ポイント1：エイリアスの確認**
  * このメソッドに入ってきた段階ではエイリアスの特定ができてないので、エイリアスの名前を確認しこのクラスで定義している対象かどうかを確認する
* **ポイント2：型の定義**
  * 定義した型をreturnする


## 2. 設定ファイルへ記述
作成したカスタムPHPDoc型は、phpstan.neonに以下のように記載することで読み込ませます。

```yaml: phpstan.neon
services:
    -
        class: Test\rule\CustomTypeTest
        tags:
            - phpstan.phpDoc.typeNodeResolverExtension
```


## 3. カスタムPHPDoc型を使用する
作成した型は以下のようにPHPDocに定義します。

```php
    /**
     * @phpstan-param CustomTypeTest<''> $param
     */
    public function testAction(array $param): void
    {
        \PHPStan\dumpType($param); // array{key1: string, key2: string}
    }
```

`CustomTypeTest<''>`のように`<''>`を書いているのはPHPStanにカスタムPHPDoc型であることを認識させるためです。書かないと通常のクラスだと認識するため必要になります。
また、`CustomTypeTest<'arg1', 'arg2'>`のように引数を渡すことも可能です。[公式ドキュメント](https://phpstan.org/developing-extensions/custom-phpdoc-types)に例が書いてあるので気になる方はご参照ください。

---
ということで解析してみました。
`\PHPStan\dumpType($param)`で$paramの型を出力した結果がこちらです。

```sh
> .\vendor\bin\phpstan analyse
Note: Using configuration file C:\Path\to\project\phpstan.neon.
 1/1 [============================] 100%

 ------ ------------------------------------------------ 
  Line   MethodExtension.php
 ------ ------------------------------------------------
  :11    Dumped type: array{key1: string, key2: string}
         ✏️  MethodExtension.php
 ------ ------------------------------------------------

```
無事に定義した型がついていることが確認できました！


# 終わりに
今回はPHPStanのカスタムPHPDoc型について解説しました。
ネストの深い複雑な配列の型を定義する場合や、複数の箇所で繰り返し使用されるような型を定義する必要がある場合に利用してみるのはアリな気がします。

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