PHPでジェネリクスっぽいやつ

PHPの配列は何でも入るのが利点ですが、あまりに奔放すぎて、逆に特定の型の値しか入れられないようなコレクションを作るのが難しいです。
要するに`$array = array<String>()`って書きたいということですが。
そのような需要はある程度存在するようで、古代よりそのような試みが[幾度となく](https://qiita.com/mpyw/items/2376f311a9db0370bcbb)[試みられて](https://qiita.com/Hiraku/items/bfab969ee5424e4da734)[います](https://qiita.com/oubakiou/items/ac0dbd3118add58baa98)。

なんか先日いいかんじの[ライブラリを見つけた](https://github.com/ramsey/collection)ので紹介してみます。

# ramsey/collection

## インストール

```
composer require ramsey/collection dev-master
```

[リリース版](https://packagist.org/packages/ramsey/collection)は2018/11/19現在、[2016年リリースの0.3.0](https://packagist.org/packages/ramsey/collection#0.3.0)で古いです。
`dev-master`は現在のmasterブランチを拾ってくるという指定で、開発バージョンという位置付けなので品質が問題となりますが、ソースを見るかぎりでは大丈夫なんじゃないかな。
保証はしませんが。


## 対応PHPバージョン

PHP7.2以降。
バージョン0.3.0であればPHP5.6以降。

## 使用方法

`\Ramsey\Collection\Collection`のコンストラクタに、限定したい型名を渡すだけ。

```php
$collection = new \Ramsey\Collection\Collection('integer');
$collection->add(1);
$collection[] = 2; // これでもいい
$collection->add('3'); // Fatal error: Uncaught InvalidArgumentException: Value must be of type integer;

foreach ($collection as $k=>$v) { // foreachで回せる
    echo $v; // 1, 2
}

class Foo{}
class Bar extends Foo{}
class Baz extends Bar{}
class Qux{}

$collection = new \Ramsey\Collection\Collection('Foo');
$collection->add(new Foo());
$collection->add(new Bar()); // 子クラスもOK
$collection[] = new Baz(); // []でもいい
$collection->add(new Qux()); // Fatal error: Uncaught InvalidArgumentException: Value must be of type Foo;
```

元々そういう書式がないので型名は文字列で渡さないといけないところが残念ですが、ジェネリクスを概ね実現できています。

追加は[List](https://docs.oracle.com/javase/jp/8/docs/api/java/util/List.html)のようにaddしてもいいし、[ArrayAccess](http://php.net/manual/ja/class.arrayaccess.php)を実装しているので配列形式でアクセスすることもできます。

もちろんnamespaceやinterfaceにも対応。

```php
namespace A {
    interface IFoo{}
    class Foo implements IFoo{}
}

namespace B {
    class Foo implements \A\IFoo{}
}

namespace C {
    interface IFoo{}
    class Foo implements IFoo{}
}

namespace D {
    $collection = new \Ramsey\Collection\Collection('\\A\\IFoo');
    $collection->add(new \A\Foo());
    $collection->add(new \B\Foo());
    $collection->add(new \C\Foo()); // Fatal error: Uncaught InvalidArgumentException: Value must be of type \A\IFoo
}
```

同じ型のコレクションをたくさん扱いたい場合は、`AbstractCollection`を継承して独自のコレクションクラスを作ることもできます。

```php
class FooCollection extends \Ramsey\Collection\AbstractCollection{
    /**
    * @Override
    * Fooクラスしか受け付けないコレクション
    * @return string 型クラス/インターフェイス名
    */
    public function getType(){
        return 'Foo';
    }
}

class Foo{}
class Bar{}

$collection = new FooCollection();
$collection->add(new Foo());
$collection->add(new Bar()); // Fatal error: Uncaught InvalidArgumentException: Value must be of type Foo;
```

独自のコレクションでは、配列を直接コンストラクタに渡すことも可能です。
これを使って配列の型チェックができないこともない。

```php
$foos = [
    new Foo(),
    new Foo(),
];
$collection = new FooCollection($foos); // OK

$foos = [
    new Foo(),
    new Bar(), // Fooではない
];
$collection = new FooCollection($foos); // Fatal error: Uncaught InvalidArgumentException: Value must be of type Foo;
```

なお、実は`\Ramsey\Collection\Collection`も配列からのコレクション生成はできるのですが、この場合は第一引数に型、第二引数に値の配列を指定する形となります。
うっかり第一引数に値の配列を入れてしまうと正しく動きません。
ちょっとわかりにくいので、引数の形を同じにするか、例外を出すなどの対応をしてほしいところです。

```php
$foos = [
    new Foo(),
    new Foo(),
];
$collection = new \Ramsey\Collection\Collection('Foo', $foos); // OK
$collection = new \Ramsey\Collection\Collection($foos); // エラーは出ないが正しく動かない
```


## メソッド

`\Ramsey\Collection\Collection`は[IteratorAggregate](https://secure.php.net/manual/ja/class.iteratoraggregate.php)、[ArrayAccess](https://php.net/manual/ja/class.arrayaccess.php)、[Serializable](https://secure.php.net/manual/ja/class.serializable.php)、[Countable](https://secure.php.net/manual/ja/class.countable.php)を実装しているので、それらが提供する機能には対応しています。
ここでは上記以外の独自メソッドについて紹介してみます。

```php
$collection = new \Ramsey\Collection\Collection('Foo');
$collection->add(new Foo());

// 配列にする
$array = $collection->toArray();
// 全要素を削除
$collection->clear();
// 要素が空であればtrue
$isEmpty = $collection->isEmpty();
```

以上です。


## その他のコレクション

[Example](https://github.com/ramsey/collection#examples)には一切書かれていないのですが、Collection以外のコレクションクラスも幾つか用意されています。

### Set

Setは値の重複を許さないコレクションです。

```php
$collection = new \Ramsey\Collection\Set('integer');
$collection->add(1);
$collection->add(2);
$collection->add(3);
$collection->add(2);

foreach($collection as $v){
    echo $v; // 1, 2, 3
}
```

なお、重複しているか否かの判断は[第三引数がtrueのin_array](https://php.net/manual/ja/function.in-array.php)です。


### Queue

Queueは先入れ先出しのコレクションです。
とはいえPHPの配列は元々順序付きなので、何も考えなくてもFIFOになりますが。
取り出してもなくならないpeek・element、取り出したらなくなるpoll・removeが実装されています。

```php
$collection = new \Ramsey\Collection\Queue('integer');
$collection->add(1);
$collection->offer(2); // addと同じ

// 値を覗くがなくならない
echo $collection->peek(); // 1

// 取り出すとなくなる
while($v = $collection->poll()){
    echo $v; // 1, 2
}

// なくなった
echo $collection->peek(); // null
```

なぜかStackはない。


### GenericArray

型を制限しないコレクションです。

```php
$collection = new \Ramsey\Collection\GenericArray();

$collection[] = 1;
$collection[] = 'a';
$collection[] = new stdClass();

foreach($collection as $k=>$v){
    echo $v; // 1, 'a', object(stdClass)
}
```

要するに普通の配列。


### TypedMap

TypedMapはキーの型も制限できるコレクションです。

```php
$collection = new \Ramsey\Collection\Map\TypedMap('string', 'string');

$collection['a'] = '1';
$collection['a'] = 1; // Fatal error: Uncaught InvalidArgumentException: Value must be of type string
$collection[1] = '1'; // Fatal error: Uncaught InvalidArgumentException: Key must be of type string
```

とはいえ、元々配列のキーとして許されているのは[integerとstringだけ](http://php.net/manual/ja/language.types.array.php)なので、それ以外の型を与えてもうまく動きません。
floatなどはintegerに勝手にキャストされるし、object等を使おうとすると[Illegal offset typeのE_WARNINGが発生](https://qiita.com/mpyw/items/5a30250ba2d622352959#%E9%80%A3%E6%83%B3%E9%85%8D%E5%88%97%E3%81%AE%E3%82%AD%E3%83%BC%E3%81%AB%E4%BD%BF%E3%81%88%E3%82%8B%E3%82%82%E3%81%AE)します。
純粋配列を作るか、純粋連想配列を作るか、程度にしか使えません。
純粋連想配列ってなんだ。


### NamedParameterMap

これだけ他のコレクションと毛色が違っていて、コンストラクタで与えたキーしか登録できなくなる連想配列です。

```php
$pararms = [
    'id' => 'integer',
    'name' => 'string',
    'amount' => 'float',
    'delete_flag' => 'bool',
    'callback' => 'callable',
    'updated' => '\DateTimeInterface',
];

$map = new \Ramsey\Collection\Map\NamedParameterMap($pararms);

$map['id'] = 42;
$map['name'] = 'John Doe';
$map['amount'] = 3.14;
$map['delete_flag'] = false;
$map['callback'] = function(){ return true; };
$map['updated'] = new \DateTimeImmutable();

$map['amount'] = 42; // Fatal error: Uncaught InvalidArgumentException: Value for 'amount' must be of type float
$map['invalidkey'] = 1; // Fatal error: Uncaught InvalidArgumentException: Attempting to set value for unconfigured parameter
```

これDTOじゃね。


# 感想

さすがに`'T extends Foo'`とかは書けない。

Collectionはさくっと使えて便利なので、そのような必要性があるならば使ってみるといいかもしれません。

あと[ソース](https://github.com/ramsey/collection/blob/master/src/Collection.php)を読んだ感想として、コメントがしっかり充実していて素晴らしく読みやすいです。
みんながこんなソースを書いてくれると助かるんですけどね。
コメントがちゃんとしていると、ライブラリとしての信頼性も高いように思えてくる不思議。

まあ信頼性とか言うと、この人[array_column](https://php.net/manual/ja/function.array-column.php)とか[実装した人](https://github.com/php/php-src/pull/257)なので高くて当然というかんじではありますが。