[PHPStan] Consider adding something like array<Foo> to the PHPDoc. が Level=max で発生する場合の対処法

<h2>💡 Consider adding something like <code>array&lt;Foo&gt;</code> to the PHPDoc.</h2>

> Function `myFunction()` has parameter `$hoge` with no value type specified in iterable type array. 

PHPStan を `--level=max` で実行すると `no value type specified in iterable type array.` と叱られる。「PHPDoc に `array<Foo>` っぽいものを書け」と言われるも、何をいっているのか分からない。

```
 ------ ------------------------------------------------------------------ 
  Line   classes/MyClass.php                                                   
 ------ ------------------------------------------------------------------ 
  9      Property KEINOS\Sample\MyClass::$dummy type has no value type  
         specified in iterable type array.                                 
         💡 Consider adding something like array<Foo> to the PHPDoc.
         You can turn off this check by setting                            
         checkMissingIterableValueType: false in your phpstan.neon. 
 ------ ------------------------------------------------------------------ 
```

例えば、以下のように `@param` で `array` と定義しつつ、引数の<ruby>[型宣言](https://www.php.net/manual/ja/functions.arguments.php#functions.arguments.type-declaration)<rt>タイプ・ヒント</rt></ruby>も `array` と定義しているのに叱られる。

```php:PHPStanレベルMAXに叱られる例
/**
 * myDemoFunction
 *
 * @param  array $data
 * @return string
 */
function myDemoFunction(array $data): string
{
   ...
}
```
さらに「[Consider adding something like "array\<Foo\>" to the PHPDoc.](https://www.google.com/search?q=Consider+adding+something+like+%22array%3CFoo%3E%22+to+the+PHPDoc.)」とググってみてもピンと来ないので、自分のググラビリティとして。

## TL; DR

> `Consider adding something like ??????<Foo> to the PHPDoc.` とは
> 「**?????? を構成する要素の型が不明確**なので PHPDoc で明示してね」という意味。


つまり、PHPDoc コメントに `@param array` と引数の型宣言を `array` と指定しても、その配列の中身である**各要素が、どのように構成されているかわからない**状態であるということです。（詳しくは TS; DR）

例えば、配列の各要素が `key` は `int` 型、値は `string` 型で構成されている場合は、`array<int,string>` と下記のように記載して明示します。

この、<strong><code>型&lt;キーの型, 値の型&gt;</code>のような記法を「[ジェネリクス記法](https://wiki.php.net/rfc/generics)」と呼びます</strong>。

以下が修正例の `diff`（差分）です。`-` が修正前、`+` が修正後です。

```diff:引数の型宣言のarrayをPHPDocに記載する際に、その構成内容をジェネリクス記法で明示する場合の変更例
 /**
  * myDemoFunction
  *
- * @param  array $data
+ * @param  array<int,string> $data
  * @return string
  */
 function myDemoFunction(array $data): string
 {
    ...
 }
```

多次元配列の場合も、同様に各々指定が必要です。例えば、クラスのプロパティが `array` で、要素のキーが `int` で値がさらに `array`（多次元配列）の場合は、PHPDoc 形式の `@var` でその旨を指定してあげます。

```php:多次元配列の型宣言（@varのコメント行に注目）
class MyClass
{
    /** @var array<int,array<string,string>> */
    private array $dummy;

    public function setValue(string $key, string $value)
    {
        $len_key  = 8; //8byte
        $hash_str = substr(hash('sha256', $key), 0, $len_key);
        $hash_int = intVal(hexdec($hash_str));

        // $this->dummy のキーは int, 値は array、値の array は string => string
        // ∴ PHPDoc の型宣言は array<int,array<string,string>> となる
        $this->dummy[$hash_int] = [
            'key' => $key,
            'value' => $value,
        ];
    }
}
```

## TS;DR

### `no value type specified in iterable type array.` の説明

`iterable type` とは「<ruby>[イテレーション](https://ja.wikipedia.org/wiki/%E5%8F%8D%E5%BE%A9)<rt>反復</r></ruby>で処理が可能な型」のことです。具体的には、`foreach` などのループで処理できる型を言います。今回の例では `array` なので、`iterable type` とは「配列」型のことを指します。

そして `value type` は「値の型」です。`no value type specified` とは「値の型が指定されていません」という意味になります。

総合すると、「`iterable` な `array` の、`value` の `type` が指定されていません」という意味です。日本語でいうと<strong>「`foreach` ループをした際の、各々の値（要素）の型が明記されていない状態である」と PHPStan が型々言っている</strong>のです。

> 関数／メソッドの引数と返り値、そしてオブジェクトのプロパティは比較的に型がつけやすいところですが、現状で無法地帯な箇所があります。
> 
> そうです、配列の内部構造です。実際のところ、PHPDocに `@param array` や `@return array` と書くことは `mixed` と書くのとあまり大きな違いはありません。
>
> <sup>(「[array shapes記法(Object-like arrays)と旧PSR-5記法で型をつける](https://qiita.com/tadsan/items/bfa9465166c351da37e5)」 @ Qiita より）</sup>

つまり、下記のように PHPDoc に引数の型を記載したとしても、問題は同じということです。

```php
$items[] = new Foo(1);
$items[] = new Foo(2);
$items[] = new Foo(3);

/**
 * @param  array $items
 * @return string
 */
function myFunc(array $items): string
    foreach($items as $item){
       // ここで $items が配列というのはわかっていても、
       // $item が「文字列」「配列」「クラスオブジェクト」 etc.
       // どの型なのか、全体を見ないとわからない。
    }
    return $result;
}
```

では、どうやって子要素を指定してあげればいいのでしょうか。

> phpDocumentor や PhpStorm を含めた複数の処理系がサポートする PHPDoc の[デファクト](https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%95%E3%82%A1%E3%82%AF%E3%83%88%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%80%E3%83%BC%E3%83%89)な仕様では `Book[]` のように記述することで `[new Book("a"), new Book("b"), new Book("c")]` のような **Book クラスのオブジェクトだけが並んだ配列**を表現することができます。
> ...
> 
> ```php
> /**
>  * @return Books[]
>  */
> function getBook(): array
> {
>     // ...
> }
> ```
> <sup>(「[array shapes記法(Object-like arrays)と旧PSR-5記法で型をつける](https://qiita.com/tadsan/items/bfa9465166c351da37e5)」 @ Qiita より）</sup>

では、以下のように `@param Foo[] $items` と指定すれば良い気がします。

```php
$items[] = new Foo(1);
$items[] = new Foo(2);
$items[] = new Foo(3);

/**
 * @param  Foo[] $items
 * @return string
 */
function myFunc(array $items): string
    foreach($items as $item){
        // ここで $items が配列というのはわかっていて、
        // $item が Foo クラスのオブジェクトとわかっていても
        // $item が Iterable（foreach 可能か）は、全体を
        //見ないとわからない。
    }
    return $result;
}
```

しかし、上記のように記載しても PHPStan のワーニングは回避できません。

> この記法には弱点があります。PHPには複数のイテレータがありますが、`[]`は飽くまで配列を表すので、ジェネレータや `ArratIterator`、あるいはその他のコレクションクラスなどは表現できないのです。
>
> <sup>(「[array shapes記法(Object-like arrays)と旧PSR-5記法で型をつける](https://qiita.com/tadsan/items/bfa9465166c351da37e5)」 @ Qiita より）</sup>


### `💡 Consider adding something like array<Foo> to the PHPDoc.` の説明

先のワーニングの後にサジェスチョンされている `💡 Consider adding something like array<Foo> to the PHPDoc.` は、「PHPDoc に `array<Foo>` っぽい何かを追記することを検討してください」という意味です。

では「**`array<Foo>`っぽい何か**」（"something like `array<Foo>`"）とは「A型 < B型 >」の形式で「親要素の型<子要素の型>」と「何か指定してください」という意味です。つまり、<code>array<Foo></code> の場合は「配列で、各要素は `Foo` クラスである」ということを意味します。

> 従来記法では値の型のみを指定できましたが、ジェネリクス記法はキーと値の型を同時に指定できます。
>
>    ```php
>    /**
>     * @return ArraryObject<int, Book>
>     */
>    function getBook(): array
>    {
>        // ...
>    }
>    ```
> <sup>(「[array shapes記法(Object-like arrays)と旧PSR-5記法で型をつける](https://qiita.com/tadsan/items/bfa9465166c351da37e5)」 @ Qiita より）</sup>

このジェネリクス記法でやっと `--level=max` で PHPStan を実行してもワーニングが表示されなくなりました。 🙏 

### 具体例

```shell-session:PHPStanを--level=maxで実行する例
$ ./vendor/bin/phpstan analyse src --level=max
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ------------------------------------------------------------------------- 
  Line   functions.php                                                            
 ------ ------------------------------------------------------------------------- 
  12     Function KEINOS\MyApp\myDemoFunction() has parameter $data with no       
         value type specified in iterable type array.                             
         💡 Consider adding something like array<Foo> to the                       
         PHPDoc.                                                                  
         You can turn off this check by setting                                   
         checkMissingIterableValueType: false in your                             
         phpstan.neon.                                                            
  12     Function KEINOS\MyApp\myDemoFunction() return type has no value type     
         specified in iterable type array.                                        
         💡 Consider adding something like array<Foo> to the                       
         PHPDoc.                                                                  
         You can turn off this check by setting                                   
         checkMissingIterableValueType: false in your                             
         phpstan.neon.                                                            
 ------ ------------------------------------------------------------------------- 

                                                                                  
 [ERROR] Found 2 errors 
```

```php:問題のソース
<?php
declare(strict_types=1);

namespace KEINOS\MyApp;

/**
 * myDemoFunction
 *
 * @param  array $data
 * @return array
 */
function myDemoFunction(array $data): array
{
    $result = [];

    foreach($data as $item){
        // ここで $item の型がわからない
        $result[] = $item;
    }

    // $result の型も不明
    return $result;
}
```

```php:修正後のソース
<?php
declare(strict_types=1);

namespace KEINOS\MyApp;

/**
 * myDemoFunction
 *
 * @param  array<int,string> $data
 * @return array<int,string>
 */
function myDemoFunction(array $data): array
{
    $result = [];

    foreach($data as $item){
        // $data は key=int 型、value=string 型の配列と宣言されているので
        // $item の値は string 型なのがわかる。
        $result[] = $item;
    }

    // $result も key=int型、value=string型の配列であることが宣言されている
    return $result;
}
```

```shell-session:環境情報
$ ./vendor/bin/phpstan --version              
PHPStan - PHP Static Analysis Tool 0.12.17

$ php --version
PHP 7.4.4 (cli) (built: Mar 19 2020 22:02:49) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Xdebug v2.9.3, Copyright (c) 2002-2020, by Derick Rethans
```

## 参考文献

- [array shapes記法(Object-like arrays)と旧PSR-5記法で型をつける](https://qiita.com/tadsan/items/bfa9465166c351da37e5) @ Qiita
- [PSR-5ジェネリクス記法についての個人的見解](https://qiita.com/tadsan/items/c9685be8e8297bd74cb6#psr-5%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%82%AF%E3%82%B9%E8%A8%98%E6%B3%95%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%81%AE%E5%80%8B%E4%BA%BA%E7%9A%84%E8%A6%8B%E8%A7%A3) | PhpStormに実装されてない未来のPHPDocとPSR-5ジェネリクス記法 @ Qiita