昔、こんな記事を書いたことがあるのですが、
PHPで高速オシャレな配列操作を求めて - Qiita
やっぱりforeachを書きまくってて疲れてきたので、コレクションライブラリとして仕立てました。
spindle/spindle-collection: The fastest php collection library
※割とAPIに悩んでいるので、まだしばらくは0.X系にしておくつもり。0.0.2までは無かったことにして下さい。
使い方
$ composer require spindle/collection
<?php
require 'vendor/autoload.php';
use Spindle\Collection\Collection;
(new Collection(range(1, 100))
->filter('$_ % 10 === 0')
->map('$_ * 2')
->assignTo($collection);
echo $collection->join(', ');
//20, 40, 60, 80, 100, 120, 140, 160, 180, 200
キーワード
- evalこそパワー(危険だが高速)
- 短い記法
- もちろんクロージャでも書けるよ
- 代入をひっくり返して書ける終端assignToメソッド
- 手書きforeachの次に高速
- laravelの
Illuminate\Support\Collection
の2倍弱速い - CakePHP3の
Cake\Collection\Collection
の4倍速い
- laravelの
- 個人的に欲しい機能を入れる
- 安定ソート版のusort
usortStable
- シュワルツ変換つきソート
mapSort
- N+1問題対策でよく書く
leftJoin
- 安定ソート版のusort
短い記法
よくあるコレクションライブラリはクロージャを使って書きますので、こんな見た目になります。
use Cake\Collection\Collection;
echo (new Collection(range(1, 1000)))
->map(function($v){ return $v * 2; })
->filter(function($v) { return $v % 3 === 0; })
->sumOf();
spindle/collectionの場合、式を文字列で渡すとそれっぽく評価してくれます。
要素は$_
として受け取ることができます。
use Spindle\Collection\Collection;
Collection::range(1, 1000)
->map('$_ * 2')
->filter('$_ % 3 === 0')
->assignTo($col);
echo $col->sum();
もちろん、普通のクロージャや関数名などのcallableを渡すこともでき、これも動作します。
use Spindle\Collection\Collection;
Collection::range(1, 1000)
->map(function($_) { return $_ * 2; })
->filter(function($_) { return $_ % 3 === 0; })
->assignTo($col);
echo $col->sum();
速さ
適当なベンチマークで実行時間測定してみました。実行時間だから棒が短いほうが速い。
(あとでリポジトリ公開予定)
- spindle-range ... spindle/collectionの普通の書き方
- spindle-range-closure ... spindle/collectionでクロージャで記述
- cakephp/collection ... https://github.com/cakephp/collection
- illuminate/support ... https://github.com/illuminate/support
- native-for ... 温かみのある手書きfor文
- native-array ... array_xxxで頑張って記述
流石に生で書いたfor文には及びませんが、他のコレクションライブラリよりは高速に処理できています。
これは、ソースコードを生成して最後に1回eval()して実行しているためです。
->filter()
や ->map()
の度にループを回さないし、Iteratorによる都度メソッド実行のオーバーヘッドもありません。その分高速に処理できるわけです。
懸念
- 素直にevalしているので、ユーザー入力値をそのままmapなどに渡したら即死レベルのセキュリティの穴になる
- evalはopcacheが効かないので、Webアプリケーションにおけるパフォーマンスも測定した方が良さそう。
API抜粋
ライブラリと銘打ってますが、Spindle\Collection\Collection
というクラスが一個あるだけです。
配列やTraversableなオブジェクトをこのクラスに変換すると、map/reduceといったメソッドが使えるようになります。
大抵のメソッドは自分自身を返すため、メソッドチェーンでコードを組み立てていくことができます。
オブジェクトの生成
$seedとして配列や、Traversableなオブジェクトを渡してnewしてください。
(new \Spindle\Collection\Collection($seed))
クラス名がPSR-4仕様で長ったらしいので、適当にuse文を書いておくと便利です。
直接newする代わりに、staticメソッドがいくつか使えます。
<?php
use Spindle\Collection\Collection;
$col = new Collection(range(1, 10));
// newと同じ意味、メソッドチェーンする時はカッコが一つ外せるので楽
$col = Collection::from(range(1, 10));
// 1 ~ 10の連番を作る。ジェネレータ式なのでメモリに優しい
$col = Collection::range(1, 10);
// 文字列も対応。
$col = Collection::range('a', 'z');
// 指定回数繰り返し同じ要素を返す
$col = Collection::repeat('yes', 10);
要素を変換する系
->map($fn)
一要素ごとに加工します。$fnは引数1個です。
->mapWithKey($fn)
$fnに渡される引数が(key, value)になります。
->column($columns)
->flip()
集合操作系
->chunk($size)
->groupBy($fn)
->leftJoin($map, callable $fetch, callable $combine)
ソート系
全件見ないといけないので、配列化されます。seedが無限長だとうまく動きません。
sort, rsort, usort, usortStable, mapSort
(今後拡張予定)
終端系
計算結果を返すので、メソッドチェーンが途切れます。
->join($separator)
全要素を文字列として連結した結果を返します。要するにimplode
です。
->sum()
全要素が数字でないと成功しません。足し合わせた結果を返します。
->product()
全要素が数字でないと成功しません。掛け合わせた結果を返します。
->count()
数を数えます。
->toArray()
普通のPHPの連想配列にして返します。
->reduce($fn, $_carry=null)
いわゆるreduceです。
今後の展望
- APIの拡充(sort系とか特に)