👏

[PHP]foreach可能かつ、各要素に対してランタイムで型保証できるようなファーストクラスコレクションを作る。

2021/11/24に公開

はじめに

以前こんな記事を書きました。PHPでは配列の中身の型を縛ることはできないので、可変超変数を使ってファーストクラスコレクションを作れるよ、という内容です。
https://zenn.dev/tasteck/articles/94cad3eea8c99a
反復処理のロジックをコレクションにカプセル化するならばforeachなどできる必要はないのですが、やろうとするとどうなるのかなというのを考えてみました。

課題: foreachで回した時にIDEの補完が効かない

下記の記事などで紹介されている方法で、Iteratorは簡単に使れます。
https://qiita.com/Hiraku/items/14722922441f9ed3fbbb
ただ、foreachを回して取れる各要素の型をIDEが解決できないという課題があります。以下は上記の記事のようにIteratorAggregateを使ってGreeterクラスのコレクションGreetersを実装した例です。
Greeterクラスはgreetingメソッドを実装しているのですが、IDEが型を解決出来ないので補完が効かない事がわかります。(ランタイムで解釈される返り値のタイプヒントではmixedになっているため)

ただし、foreachの中でassertを入れたり、静的解析ツールが解釈できるようなコメントを入れてあげる事でIDEに型を伝える事は可能です。

以降で紹介するのは、実行時(ランタイム)で仮に違う型だった場合にもTypeErrorを吐いてくれるようにできる、とうものです。

どうやるか

Iteratorを実装し、currentメソッドの返り値にタイプヒントを指定する事で、先述の課題を解決する事ができます。
https://www.php.net/manual/ja/class.iterator.php
具体的には以下のような実装になりました。

/**
 * Iteratorを実装する抽象クラス
 * currentメソッドのタイプヒントは子クラスで指定したいので、抽象メソッドにしておく
 * が、具体的なメソッドの実装はどの子クラスでも共通なので、_currentメソッドを作っておく
 */
abstract class Collection implements \Iterator
{
    private int $position = 0;

    public function __construct(private array $values)
    {
    }

    abstract public function current(): mixed;

    public function _current(): mixed
    {
        return $this->values[$this->position];
    }

    public function next(): void
    {
        $this->position++;
    }

    public function key(): int
    {
        return $this->position;
    }

    public function valid(): bool
    {
        return isset($this->values[$this->position]);
    }

    public function rewind(): void
    {
        $this->position = 0;
    }
}
/**
 * サンプルコレクションクラス。コンストラクタの引数は可変超変数を受け取る事で、配列の中身の型を縛る。
 * currentメソッドを実装し、返り値のタイプヒントを追加する。実装の中身は_currentに委譲する。
 */
class Greeters extends Collection
{
    public function __construct(Greeter ...$values)
    {
        parent::__construct($values);
    }

    public function current(): Greeter
    {
        return $this->_current();
    }
}
/**
 * 今回コレクションを作りたいサンプルクラス
 */
class Greeter
{
    public function greeting(): void
    {
        echo 'hello';
    }
}

補完がちゃんと効くようになりました

おわりに

紹介しておいてなんですが、筆者自身「こんなめんどくさい事しなくてもPsalmとかでCIで担保したらええやん」って思いました。
静的解析ツールと仲良くなりましょう!

Discussion