PHPの配列は何でも入るのが利点ですが、あまりに奔放すぎて、逆に特定の型の値しか入れられないようなコレクションを作るのが難しいです。
要するに$array = array<String>()
って書きたいということですが。
そのような需要はある程度存在するようで、古代よりそのような試みが幾度となく試みられています。
なんか先日いいかんじのライブラリを見つけたので紹介してみます。
ramsey/collection
インストール
composer require ramsey/collection dev-master
リリース版は2018/11/19現在、2016年リリースの0.3.0で古いです。
dev-master
は現在のmasterブランチを拾ってくるという指定で、開発バージョンという位置付けなので品質が問題となりますが、ソースを見るかぎりでは大丈夫なんじゃないかな。
保証はしませんが。
対応PHPバージョン
PHP7.2以降。
バージョン0.3.0であればPHP5.6以降。
使用方法
\Ramsey\Collection\Collection
のコンストラクタに、限定したい型名を渡すだけ。
$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のようにaddしてもいいし、ArrayAccessを実装しているので配列形式でアクセスすることもできます。
もちろんnamespaceやinterfaceにも対応。
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
を継承して独自のコレクションクラスを作ることもできます。
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;
独自のコレクションでは、配列を直接コンストラクタに渡すことも可能です。
これを使って配列の型チェックができないこともない。
$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
も配列からのコレクション生成はできるのですが、この場合は第一引数に型、第二引数に値の配列を指定する形となります。
うっかり第一引数に値の配列を入れてしまうと正しく動きません。
ちょっとわかりにくいので、引数の形を同じにするか、例外を出すなどの対応をしてほしいところです。
$foos = [
new Foo(),
new Foo(),
];
$collection = new \Ramsey\Collection\Collection('Foo', $foos); // OK
$collection = new \Ramsey\Collection\Collection($foos); // エラーは出ないが正しく動かない
メソッド
\Ramsey\Collection\Collection
はIteratorAggregate、ArrayAccess、Serializable、Countableを実装しているので、それらが提供する機能には対応しています。
ここでは上記以外の独自メソッドについて紹介してみます。
$collection = new \Ramsey\Collection\Collection('Foo');
$collection->add(new Foo());
// 配列にする
$array = $collection->toArray();
// 全要素を削除
$collection->clear();
// 要素が空であればtrue
$isEmpty = $collection->isEmpty();
以上です。
その他のコレクション
Exampleには一切書かれていないのですが、Collection以外のコレクションクラスも幾つか用意されています。
Set
Setは値の重複を許さないコレクションです。
$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です。
Queue
Queueは先入れ先出しのコレクションです。
とはいえPHPの配列は元々順序付きなので、何も考えなくてもFIFOになりますが。
取り出してもなくならないpeek・element、取り出したらなくなるpoll・removeが実装されています。
$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
型を制限しないコレクションです。
$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はキーの型も制限できるコレクションです。
$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だけなので、それ以外の型を与えてもうまく動きません。
floatなどはintegerに勝手にキャストされるし、object等を使おうとするとIllegal offset typeのE_WARNINGが発生します。
純粋配列を作るか、純粋連想配列を作るか、程度にしか使えません。
純粋連想配列ってなんだ。
NamedParameterMap
これだけ他のコレクションと毛色が違っていて、コンストラクタで与えたキーしか登録できなくなる連想配列です。
$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はさくっと使えて便利なので、そのような必要性があるならば使ってみるといいかもしれません。
あとソースを読んだ感想として、コメントがしっかり充実していて素晴らしく読みやすいです。
みんながこんなソースを書いてくれると助かるんですけどね。
コメントがちゃんとしていると、ライブラリとしての信頼性も高いように思えてくる不思議。
まあ信頼性とか言うと、この人array_columnとか実装した人なので高くて当然というかんじではありますが。