👏
[PHP]foreach可能かつ、各要素に対してランタイムで型保証できるようなファーストクラスコレクションを作る。
はじめに
以前こんな記事を書きました。PHPでは配列の中身の型を縛ることはできないので、可変超変数を使ってファーストクラスコレクションを作れるよ、という内容です。
反復処理のロジックをコレクションにカプセル化するならばforeachなどできる必要はないのですが、やろうとするとどうなるのかなというのを考えてみました。課題: foreachで回した時にIDEの補完が効かない
下記の記事などで紹介されている方法で、Iteratorは簡単に使れます。IteratorAggregate
を使ってGreeter
クラスのコレクションGreeters
を実装した例です。
Greeterクラスはgreetingメソッドを実装しているのですが、IDEが型を解決出来ないので補完が効かない事がわかります。(ランタイムで解釈される返り値のタイプヒントではmixedになっているため)
ただし、foreach
の中でassert
を入れたり、静的解析ツールが解釈できるようなコメントを入れてあげる事でIDEに型を伝える事は可能です。
以降で紹介するのは、実行時(ランタイム)で仮に違う型だった場合にもTypeError
を吐いてくれるようにできる、とうものです。
どうやるか
Iteratorを実装し、currentメソッドの返り値にタイプヒントを指定する事で、先述の課題を解決する事ができます。
具体的には以下のような実装になりました。/**
* 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