PHPStanの動的戻り値拡張作ってみる

# 初めに
前略、PHPStanの動的戻り値拡張を試してみたのでまとめます。

# 動的戻り値拡張とは
動的戻り値拡張とは、動的に戻り値が決まるメソッドの戻り値を定義することができる機能です。

極端な例ですが、以下のdynamicReturnMethod()メソッドは受け取った引数をそのまま返すメソッドです。~~そのため、戻り値は呼び出し側の引数に何が詰められるかによって決まり、PHPDocに記載することができません。~~

```php: 例）動的に戻り値が決まるメソッド
<?php
namespace Path\To\Project\analyseObject;

class DynamicMehodReturn
{
    function dynamicReturnMethod(mixed $arg)
    {
        // ★戻り値がメソッド引数の型に左右されてしまうため定義ができない
        return $arg;
    }
}
```

:::note warn
**2025/01/07 追記**
上記の例について、執筆当初はできないと思っていたのですが、コメントでご指摘いただき以下のように`@template`を利用することで定義できることが分かりました:bow:

```php
    /**
     * @template T
     * @param T $arg
     * @return T
     */
    function dynamicReturnMethod(mixed $arg)
    {
        return $arg;
    }
```
※この記事では動的戻り値拡張を扱いたいので、`@template`を用いずに進めます。
:::


そのため、呼び出し側では戻り値の型がmixedになってしまい、正しい解析を行うことができなくなってしまいます。

```php: 呼び出し側
<?php
namespace Path\To\Project\analyseObject;

use DateTime;

class CallDynamicMethod
{

    function classDynamicMethod(int $arg): void
    {
        $dynamicMethodClass = new DynamicMehodReturn();
        $response1 = $dynamicMethodClass->dynamicReturnMethod($arg);
        \PHPStan\dumpType($response1);
        $response2 = $dynamicMethodClass->dynamicReturnMethod("test");
        \PHPStan\dumpType($response2);
        $response3 = $dynamicMethodClass->dynamicReturnMethod(new DateTime());
        \PHPStan\dumpType($response3);
    }
}

```

```sh: 解析結果
> ./vendor/bin/phpstan analyse .\src\analyseObject\CallDynamicMethod.php -v
Note: Using configuration file C:\Path\to\project\phpstan\phpstan.neon.
 1/1 [============================] 100%  1 sec

 ------ -------------------------- 
  Line   CallDynamicMethod.php     
 ------ --------------------------
  :13    Dumped type: mixed
         ✏️  CallDynamicMethod.php
  :15    Dumped type: mixed
         ✏️  CallDynamicMethod.php
  :17    Dumped type: mixed
         ✏️  CallDynamicMethod.php
 ------ --------------------------
```

# 動的戻り値拡張を作ってみる

そこで、PHPStanの動的戻り値拡張を利用してみましょう。
動的戻り値拡張は以下のクラスを継承することで作成することができます。

```php: DynamicMethodReturnTypeExtension
namespace PHPStan\Type;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;

interface DynamicMethodReturnTypeExtension
{
	public function getClass(): string;

	public function isMethodSupported(MethodReflection $methodReflection): bool;

	public function getTypeFromMethodCall(
		MethodReflection $methodReflection,
		MethodCall $methodCall,
		Scope $scope
	): ?Type;
}
```
それぞれのメソッドの役割はこちらです。

| メソッド | 役割 |
|:-:|:-:|
| `getClass()` | 動的戻り値拡張の対象メソッドがあるクラスを指定|
| `isMethodSupported()` | 動的戻り値拡張の対象のメソッド名を指定 |
| `getTypeFromMethodCall()` | 型を戻り値で指定する |

今回は以下のように作成しました。

```php: 動的戻り値拡張を実装
<?php
namespace Path\To\Project\extension;

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

class TestDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
    public function getClass(): string
	{
		return \Path\To\Project\analyseObject\DynamicMehodReturn::class;
	}

	public function isMethodSupported(MethodReflection $methodReflection): bool
	{
		return $methodReflection->getName() === 'dynamicReturnMethod';
	}

	public function getTypeFromMethodCall(
		MethodReflection $methodReflection,
		MethodCall $methodCall,
		Scope $scope
	): ?Type
	{
		if (count($methodCall->getArgs()) !== 1) {
			return null;
		}
        
		$arg = $methodCall->getArgs()[0]->value;
        return $scope->getType($arg); 
	}
}

```

その後、`phpstan.neon`に以下のように記載し、PHPStanに拡張を認識させます。
以上で準備OKです。

```yaml: phpstan.neon
services:
    -
        class: Path\To\Project\extension\TestDynamicMethodReturnTypeExtension
        tags:
            - phpstan.broker.dynamicMethodReturnTypeExtension
```

PHPStanで解析を行うと、戻り値が設定されていました。
```sh: 解析結果
> ./vendor/bin/phpstan analyse .\src\analyseObject\CallDynamicMethod.php -v
Note: Using configuration file C:\Path\to\project\phpstan\phpstan.neon.
 1/1 [============================] 100%  1 sec

 ------ -------------------------- 
  Line   CallDynamicMethod.php     
 ------ --------------------------
  :13    Dumped type: int
         ✏️  CallDynamicMethod.php
  :15    Dumped type: 'test'
         ✏️  CallDynamicMethod.php
  :17    Dumped type: DateTime
         ✏️  CallDynamicMethod.php
 ------ --------------------------
```
※15行目の型がStringではなく'test'と出ているのは、PHPStanが`ConstantStringType`と判断しているためです。

# 終わりに
PHPStanには様々な拡張機能があって便利ですね、他にも調べてみようと思います。
ここまでご覧いただきありがとうございました！