Laravelコレクション掘り下げシリーズ第四弾。今回は本体配列の一部を新たなコレクションとして取得するメソッドに関して確認していきます。
なお「サブセットの取得」が主目的かどうかははっきりしないものも含まれています。例えば最初に紹介するdiffなども目的はあくまで「比較」であり、差分があったことを示す方法が差分に当たる要素のコレクション提供であっただけなのかもしれません。その場合、サブセットに要素が存在するかどうかのみが重要で、サブセットの中身に関しては全く関心がない可能性もあります。
ただ、この辺になると使い方次第なので、とりあえず結果として本体配列のサブセットコレクションが生成される操作を全てここに集めてみました。
また、サブセットコレクション生成に際して、基本的には以下の前提が成立します。
- 元コレクションにおけるキー(インデックス)は保持される
- 元コレクションには影響(変化)がない
よって上記が成立する場合に関しては特に言及せず、例外的処理の場合のみその旨言及するようにします。
diff
$col1 = collect([1,2,3,4,5]);
$col2 = collect([1,3,5,7]);
$col3 = $col1->diff($col2);
引数で指定したコレクションもしくは配列と比較して、値が合致しない要素で新しいコレクションを生成します。
上記の実行結果は以下のようになります。
array:2 [
1 => 2
3 => 4
]
col1に含まれてcol2に含まれない要素のみで構成された新しいコレクションが生成されました。
最初に触れたように、diffの引数は配列でも同様に機能します。
つまり先の例であれば、以下のように実行した場合と同じです。
$col3 = $col1->diff([1,3,5,7]);
diffは比較する2つのコレクション(配列)が一次元の場合のみ有効なようで、多次元の場合はエラーとなります。
なお、一次元であれば連想配列を対象とすることもできるようですが、キーは無視され、値のみの比較が行われるようです。
例えば以下のような比較を行います。
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4,
]);
$col2 = collect([
'b' => 0,
'c' => 3,
'dx' => 4,
'e' => 5,
]);
$col3 = $col1->diff($col2);
結果は以下の通り。
array:2 [
"a" => 1
"b" => 2
]
キー「b」に関しては同じキーが存在しますが、値が異なっているため一致とは見なされず、新規コレクションに反映されます。
キー「c」に関しては値まで同じであるため除外されています。
キー「d」に関しては比較対象側に同じキーは存在しないものの、同じ値「4」が含まれているため、こちらも除外されてしまっています。
では次のようなケースはどうでしょう?
$col1 = collect([
1,
2,
3,
1,
2,
3,
]);
$col2 = collect([
3,
1,
]);
$col3 = $col1->diff($col2);
結果は以下の通り。
array:2 [
1 => 2
4 => 2
]
つまり、比較対象に同じ値が存在しないことのみが重要で、順番や個数は関係ないようです。
diffと言うとLinuxのdiffコマンドをイメージしてしまいますが、機能的には全く似ていませんね。
値だけの単純比較って、使い所があまり想定できませんが…
diffAssoc
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4,
]);
$col2 = collect([
'b' => 0,
'c' => 3,
'dx' => 4,
'e' => 5,
]);
$col3 = $col1->diffAssoc($col2);
diffと基本的には同じで2つのコレクション(配列)を比較するのですが、違いはキーと値の組み合わせまで含めて判断する点です。
diffで例示した2つのコレクションに関する処理結果を見てみましょう。
array:3 [
"a" => 1
"b" => 2
"d" => 4
]
diffでは元配列の「’d’ => 4」と比較対象側の「’dx’ => 4」が値のみの一致で除外されていましたが、diffAssocではキーと値の組み合わせとしては一致しないと言うことで新規コレクションに反映されています。
ただ、これはこれで注意が必要です。
例えば以下のようなケース。
$col1 = collect([
1,
2,
3,
]);
$col2 = collect([
2,
3,
]);
$col3 = $col1->diffAssoc($col2);
なんとなく、「1」のみが含まれたコレクションが生成されそうに見える処理ですが、残念ながら結果は以下です。
array:3 [
0 => 1
1 => 2
2 => 3
]
同じ値「2」を持つ要素でも元配列は厳密には「1 => 2」であり、比較対象側は「0 => 2」であるため、不一致となって新規コレクションに反映されてしまいます。理屈は分かりますが、直感的には気付き難く、間違い易い印象です。
根本的にdiffAssocはあくまで連想配列に対して有効であり、通常の配列(連番をキーとする形)に用いるものではないと考えるべきですね。
なお、多次元構造に対応していない点もdiffと同様です。
diffKeys
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4,
]);
$col2 = collect([
'b' => 0,
'c' => 3,
'dx' => 4,
'e' => 5,
]);
$col3 = $col1->diffKeys($col2);
これもdiffと同様の機能ですが、比較対象が値ではなくキーになります。
例によって同じ比較対象に対する結果を見てみましょう。
array:2 [
"a" => 1
"d" => 4
]
今回は値が異なるもののキーが一致する「b」が除外され、値が同じでもキーが異なる「d」は反映されています。
なお、diffやdiffAssocとは異なり、diffKeysは多次元配列を対象とできます。ただし、その用法には注意が必要です。
例えば以下のようなケース。
$col1 = collect([
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 3,
'b-2' => 4,
]
]);
$col2 = $col1->diffKeys([
'a' => [
'a-1' => 0,
],
'bx' => [
'b-1' => 0,
'b-2' => 0,
],
]);
結果は以下の通り。
array:1 [
"b" => array:2 [
"b-1" => 3
"b-2" => 4
]
]
比較対象として多次元配列を扱えますが、比較されるのはあくまで一次元目のキーのみです。
duplicates
$col1 = collect([1,2,3,2,4,2,1]);
$col2 = $col1->duplicates();
コレクションの中から値が重複する要素を抽出して新しいコレクションを生成します。
上記処理結果は以下の通り。
array:3 [
3 => 2
5 => 2
6 => 1
]
なお、本機能は多次元構造にも対応しているようです。
$col1 = collect([
[1,2],
[3,4],
[1,3],
[2,4],
[1,2],
[
[1,2],
[3,4]
],
[
[1,2],
[3,4]
],
[
[1,2],
[3,5]
],
]);
$col2 = $col1->duplicates();
結果は以下の通り。
array:2 [
4 => array:2 [
0 => 1
1 => 2
]
6 => array:2 [
0 => array:2 [
0 => 1
1 => 2
]
1 => array:2 [
0 => 3
1 => 4
]
]
]
多次元構造の中身(二次元以降)に関してもチェックしているようですが、重複と見なされるのは二次元目以降の要素を含む一次元目の要素全体が完全一致した場合のみであり、階層内で部分的に一致したケースまでを指摘してくれる機能ではなさそうです。
ただ、多次元構造におけるどの部分を重複チェックの対象とするかは指定できるようです。
下記例では二次元配列の二次元目第一要素(インデックス0)を比較対象としています。
$col1 = collect([
[
[1,2],
[3,4]
],
[
[1,2],
[3,4]
],
[
[1,2],
[3,5]
],
[
[1,5],
[3,4]
],
]);
$col2 = $col1->duplicates(0);
結果は以下の通り。
array:2 [
1 => array:2 [
0 => 1
1 => 2
]
2 => array:2 [
0 => 1
1 => 2
]
]
指定した部分の重複結果が得られました。
さらに深い階層の指定も可能です。
なお、階層が多段になった際の要素の指定方法(キーをドットで連結)はLaravel内での共通ルールですね。
$col1 = collect([
[
[
[1,2],
[3,4],
],
[
[5,6],
[7,8],
],
],
[
[
[0,0],
[0,0],
],
[
[5,6],
[0,0],
],
],
]);
$col2 = $col1->duplicates("1.0");
結果は以下の通り(指定位置の重複を検出できています)。
array:1 [
1 => array:2 [
0 => 5
1 => 6
]
]
なお、上記で示した多段構造における比較位置指定は、本来であれば連想配列を想定したものでしょうね。
$col1 = collect([
[
'a' => [
'a-1' => [1,2],
'a-2' => [3,4],
],
'b' => [
'b-1' => [5,6],
'b-2' => [7,8],
],
],
[
'a' => [
'a-1' => [0,0],
'a-2' => [0,0],
],
'b' => [
'b-1' => [5,6],
'b-2' => [0,0],
],
],
]);
$col2 = $col1->duplicates("b.b-1");
結果は以下の通り。
array:1 [
1 => array:2 [
0 => 5
1 => 6
]
]
蛇足ながら一次元目が連想配列の場合も機能します。
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 1,
]);
$col2 = $col1->duplicates();
結果は以下の通り。
array:1 [
"c" => 1
]
本機能はあくまで値の重複を判断材料とするのみでキーには関心がないため上記のような結果になるのでしょうね。
使い所はあまり思いつきませんが…
duplicatesStrict
duplicatesの厳格版です。
比較がより厳密になるだけなので確認は割愛。
except
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
]);
$col2 = $col1->except('b');
指定したキーを除外して残りの要素から新たなコレクションを生成します。
上記結果は以下の通り。
array:2 [
"a" => 1
"c" => 3
]
当然ながら複数要素指定もOK。
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
]);
$col2 = $col1->except(['a','c']);
結果は以下の通り。
array:1 [
"b" => 2
]
当然ながら多次元構造にも対応。
$col1 = collect([
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 1,
'b-2' => 2,
],
]);
$col2 = $col1->except(['a.a-1','b']);
結果は以下の通り。
array:1 [
"a" => array:1 [
"a-2" => 2
]
]
蛇足ながら通常配列の多次元構造にも適用可能。
$col1 = collect([
[
1,
2,
],
[
3,
4,
],
]);
$col2 = $col1->except([0,'1.1']);
結果は以下の通り。
array:1 [
1 => array:1 [
0 => 3
]
]
まぁ、通常配列に対して使用することは考え難いですが。やはり、対象は連想配列でしょう。
filter
$col1 = collect([
1,
2,
3,
4,
]);
$col2 = $col1->filter(function($val, $key) {
return $val > 2;
});
各要素に対して引数として指定したコールバックを実行し、戻り値が真だった要素のみで新たなコレクションを生成します。
上記結果は以下の通り。
array:2 [
2 => 3
3 => 4
]
コールバックの第一引数に一次元目の要素が渡されるので、多次元構造内の所定の要素での判定も可能です。
$col1 = collect([
[
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 3,
'b-2' => 4,
],
],
[
'a' => [
'a-1' => 5,
'a-2' => 6,
],
'b' => [
'b-1' => 7,
'b-2' => 8,
],
],
]);
$col2 = $col1->filter(function($val, $key) {
return $val['b']['b-1'] >= 7;
});
結果は以下の通り。
array:1 [
1 => array:2 [
"a" => array:2 [
"a-1" => 5
"a-2" => 6
]
"b" => array:2 [
"b-1" => 7
"b-2" => 8
]
]
]
forPage
$col1 = collect([1,2,3,4,5,6,7,8,9]);
$col2 = $col1->forPage(2,3);
第二引数で指定した数に要素を分割し、第一引数で指定した位置のセットで新たなコレクションを生成します。
上記実行結果は以下の通り。
array:3 [
3 => 4
4 => 5
5 => 6
]
注意すべきは第一引数が配列のインデックスのように0スタートではなく、1スタートである点です。
メソッド名からしてページネーションを意識していると思われ、その際に「1ページ目のデータ」と言う認識をそのまま引数に適用したのだと思いますが、配列のインデックスに馴染んでしまっていると逆に間違いそうなところです。
なお、蛇足ながら、一応連想配列にも適用可能です。
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4,
]);
$col2 = $col1->forPage(2,2);
結果は以下の通り。
array:2 [
"c" => 3
"d" => 4
]
このような使い方はしないでしょうけどね。
intersect
$col1 = collect([1,2,3,4,5]);
$col2 = collect([1,3,5,7]);
$col3 = $col1->intersect($col2);
引数で指定したコレクションもしくは配列と比較して、共通する要素から構成されるコレクションを返します。
共通するものを採用するか排除するかと言う点ではdiffと真逆の処理ですが、その他の点(一次元構造にしか適用できない点など)はdiffと共通しているようです。
と言うことで、結果は以下の通り。
array:3 [
0 => 1
2 => 3
4 => 5
]
intersectByKeys
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4,
]);
$col2 = collect([
'b' => 0,
'c' => 3,
'dx' => 4,
'e' => 5,
]);
$col3 = $col1->intersectByKeys($col2);
こちらはdiffKeysの逆で、共通するキーの要素から構成されるコレクションを返します。
結果は以下の通り。
array:2 [
"b" => 2
"c" => 3
]
しかし、対比されるような機能であるにも関わらず、「diffKeys」に対して「intersectByKeys」のように命名規則が微妙に違っていたり、「diffAssoc」に対応する「intersectAssoc」が存在しなかったりと、若干のアンバランスさを感じます。
この辺は何か深い意味があるんでしょうかね?
nth
$col1 = collect([1,2,3,4,5,6,7,8]);
$col2 = $col1->nth(3);
指定した間隔で要素を抽出し、新しいコレクションを生成します。
上記結果は以下の通り。
array:3 [
0 => 1
1 => 4
2 => 7
]
インデックスが振り直される点に要注意です。
なお、第二引数でオフセットの指定もできるようです。
$col1 = collect([1,2,3,4,5,6,7,8]);
$col2 = $col1->nth(3,1);
結果は以下の通り。
array:3 [
0 => 2
1 => 5
2 => 8
]
このインタフェースで、間隔(第一引数)よりオフセット(第二引数)の方が大きくなったらどうなるんでしょう?
$col1 = collect([1,2,3,4,5,6,7,8]);
$col2 = $col1->nth(3,4);
結果は以下の通り。
[]
さらっと空配列になってしまいました…
例によって、意味はなさそうですが連想配列に関しても試してみましょう。
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4,
]);
$col2 = $col1->nth(2);
結果は以下の通り。
array:2 [
0 => 1
1 => 3
]
指定の間隔で要素を抽出し、インデックスを振り直す。
まぁ、そうなんでしょうけど…
only
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
]);
$col2 = $col1->only(['a','c']);
exceptの逆で、指定したキーと一致する要素から新たなコレクションを生成します。
結果は以下の通り。
array:2 [
"a" => 1
"c" => 3
]
その他特徴もexceptと同じ…と言おうとして一応確認してみたところ、意外な結果が。
具体的には以下のケース。
$col1 = collect([
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 1,
'b-2' => 2,
],
]);
$col2 = $col1->only(['a.a-1','b']);
上記結果に関しては当然「a-2」を除いた要素が反映されると思いきや、結果は以下の通り。
array:1 [
"b" => array:2 [
"b-1" => 1
"b-2" => 2
]
]
あれ?「a-1」は反映してくれないんですかね?
確かに特定の要素だけを除外する処理よりも特定の要素のみを採用する処理の方が面倒そうではありますが、公式サイトの説明でも「onlyの正反対の機能は、exceptメソッドです」と書かれているのに、この微妙な違いはいかがなものでしょうか?
と言っていても仕方がないので、onlyで指定できるキーは一次元目に限ると覚えておくしかなさそうです。
partition
$col1 = collect([
1,
-2,
-3,
4
]);
list($col2, $col3) = $col1->partition(function($val) {
return $val >= 0;
});
各要素に対して引数で指定したコールバックを実行し、戻り値が真の要素を一つ目のコレクションに、偽の要素を二つ目のコレクションに振り分けます。つまりコレクションが2つ返されます。
結果は以下の通り。
// 一つ目のコレクション
array:2 [
0 => 1
3 => 4
]
// 二つ目のコレクション
array:2 [
1 => -2
2 => -3
]
連想配列に関しても適用可能です。
$col1 = collect([
'a' => 1,
'b' => -2,
'c' => -3,
'd' => 4
]);
list($col2, $col3) = $col1->partition(function($val) {
return $val >= 0;
});
結果は以下の通り。
// 一つ目のコレクション
array:2 [
"a" => 1
"d" => 4
]
// 二つ目のコレクション
array:2 [
"b" => -2
"c" => -3
]
reject
$col1 = collect([
1,
2,
3,
4,
]);
$col2 = $col1->reject(function($val, $key) {
return $val > 2;
});
filterとは逆に各要素に対して引数として指定したコールバックを実行し、戻り値が偽だった要素のみで新たなコレクションを生成します。
結果は以下の通り。
0 => 1
1 => 2
]
多次元構造内の所定の要素での判定も可能です。
$col1 = collect([
[
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 3,
'b-2' => 4,
],
],
[
'a' => [
'a-1' => 5,
'a-2' => 6,
],
'b' => [
'b-1' => 7,
'b-2' => 8,
],
],
]);
$col2 = $col1->reject(function($val, $key) {
return $val['b']['b-1'] >= 7;
});
結果は以下の通り。
array:1 [
0 => array:2 [
"a" => array:2 [
"a-1" => 1
"a-2" => 2
]
"b" => array:2 [
"b-1" => 3
"b-2" => 4
]
]
]
しかし、コールバックが真偽のどちらを返した場合を有効とするかだけの違いしかないのであれば、filterだけあれば良くないですかね?
skip
$col1 = collect([1,2,3,4,5]);
$col2 = $col1->skip(2);
先頭から指定数の要素を飛ばした以降の要素で新しいコレクションを生成します。
結果は以下の通り。
array:3 [
2 => 3
3 => 4
4 => 5
]
実は引数にはマイナスの数字も指定できます。
$col1 = collect([1,2,3,4,5]);
$col2 = $col1->skip(-2);
結果は以下の通り。
array:2 [
3 => 4
4 => 5
]
マイナス指定は往々にして末尾からを意味するので「末尾から2個の要素までを飛ばして」と言う意味で上記結果になるのでしょうけど、個人的には末尾から2個の要素を除外した残りの要素で新しいコレクションが生成するかと思ってしまいました。
引数は除外する要素数を意味するのではなく新規コレクションに反映する要素の先頭位置(オフセット)を意味すると言うことですね。
蛇足ながら、連想配列にも適用可能です。
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4
]);
$col2 = $col1->skip(2);
結果は以下の通り。
array:2 [
"c" => 3
"d" => 4
]
例によって、このような使い方をすることはないでしょうけど。
slice
$col1 = collect([1,2,3,4,5]);
$col2 = $col1->slice(2);
指定したインデックス以降の要素で新しいコレクションを生成します。
結果は以下の通り。
array:3 [
2 => 3
3 => 4
4 => 5
]
しかしこれって「skip(2)」と変わらないんですよね。例示しませんがskipと同様にマイナスの数字も指定できて、こちらの結果もskipと同じです。
なお、第二引数で取得する要素数を指定できる点でsliceの方が芸達者です。
$col1 = collect([1,2,3,4,5]);
$col2 = $col1->slice(2,2);
結果は以下の通り。
array:2 [
2 => 3
3 => 4
]
だったら、sliceだけあれば良くて、skip不要では?と思ってしまいますが…
splice
$col1 = collect([1,2,3,4,5]);
$col2 = $col1->splice(2);
指定したインデックス以降の要素で新しいコレクションを生成し、元コレクションから取得した要素を削除します。
sliceと似ていますが、元コレクションにも変更が生じる点に違いがあります。
上記結果は以下の通り。
// 新規コレクション($col2)
array:3 [
0 => 3
1 => 4
2 => 5
]
// 元コレクション($col1)
array:2 [
0 => 1
1 => 2
]
新規コレクションにおいてキー(インデックス)が振り直されている点も特徴です。例示しませんがマイナスの数字も指定できて、やはり末尾からの位置を示します。
第二引数で取得する要素数を指定できる点もsliceと同じです。
$col1 = collect([1,2,3,4,5]);
$col2 = $col1->splice(2,2);
結果は以下の通り。
// 新規コレクション($col2)
array:2 [
0 => 3
1 => 4
]
// 元コレクション($col1)
array:3 [
0 => 1
1 => 2
2 => 5
]
元コレクションのキー(インデックス)も振り直されるようです。
さらには抜き出した要素の代わりに別の要素を元コレクションに設定することも可能です。
$col1 = collect([1,2,3,4,5]);
$col2 = $col1->splice(2,2,[6,7,8]);
上記のように抜き出す要素数と追加する要素数に違いがあっても問題ないようです。
結果は以下の通り。
// 新規コレクション($col2)
array:2 [
0 => 3
1 => 4
]
// 元コレクション($col1)
array:6 [
0 => 1
1 => 2
2 => 6
3 => 7
4 => 8
5 => 5
]
キーが振り直されると言うことは、連想配列に適用するとどうなるんでしょう?
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 3,
'd' => 4,
'e' => 5
]);
$col2 = $col1->splice(2,2,[
'f' => 6,
'g' => 7,
'h' => 8
]);
実行結果はなかなか意外なものです。
// 新規コレクション($col2)
array:2 [
"c" => 3
"d" => 4
]
// 元コレクション($col1)
array:6 [
"a" => 1
"b" => 2
0 => 6
1 => 7
2 => 8
"e" => 5
]
連想配列の場合、新規コレクションおよび元コレクション共にキーは保持するんですね。
PHPにおける通常配列は連想配列の特殊形(キーが連番となっているだけ)のような位置付けなので、連想配列を対象とした場合もキーは強制的に連番に変更されてしまうと予測したのですが、どうもキーが数字(型的に文字列で内容が数字の場合も含む)であれば振り直し、数字以外であればキーは保持されるようです。
一方で、追加する要素に関してはキーを完全無視して値だけが所定の場所に所定の並びで追加されるようです。
と、興味本位で色々試してみましたが、実際には連想配列にspliceを適用することはないかと思いますし、要素の追加も使い所が思いつきません。
「元コレクションから取得した要素を削除する機能を有した、sliceの発展形」くらいの使い方までかと思います。
take
$col1 = collect([1,2,3,4,5,6]);
$col2 = $col1->take(2);
先頭から指定した個数の要素で新しいコレクションを生成します。この意味ではskipの逆と言ったところでしょうか。
結果は以下の通り。
array:2 [
0 => 1
1 => 2
]
引数にマイナスの値を指定すると末尾から指定した個数の要素で新しいコレクションを生成します。
$col1 = collect([1,2,3,4,5,6]);
$col2 = $col1->take(-2);
結果は以下の通り。
array:2 [
4 => 5
5 => 6
]
こちらの結果は「skip(-2)」と同じになってしまいました。skipと対比するのは適切ではなさそうです…
unique
$col1 = collect([1,2,3,3,2,1,4]);
$col2 = $col1->unique();
重複する要素を除外して新しいコレクションを生成します。
結果は以下の通り。
array:4 [
0 => 1
1 => 2
2 => 3
6 => 4
]
多次元構造を持つ場合は、一意性を判断する要素を指定できるようです。
$col1 = collect([
[
'a' => 1,
'b' => 2,
],
[
'a' => 3,
'b' => 4,
],
[
'a' => 1,
'b' => 3,
],
[
'a' => 2,
'b' => 4,
],
]);
$col2 = $col1->unique('a');
結果は以下の通り。
array:3 [
0 => array:2 [
"a" => 1
"b" => 2
]
1 => array:2 [
"a" => 3
"b" => 4
]
3 => array:2 [
"a" => 2
"b" => 4
]
]
元コレクションの一次元目の1番目と3番目の要素は、二次元目の要素「b」に関しては値が異なっていますが、あくまで引数で指定した二次元目の要素「a」のみを判断材料に除外対象を決定しています。
除外されるのは引数で指定した要素だけでなく、その要素を含む一次元目の要素全体である点に注意が必要です。
また、引数にコールバックも指定できるため、上記のように単独の要素での比較で問題があれば、コールバック内で複数要素を組み合わせて比較用情報を加工生成することもできます。
$col1 = collect([
[
'a' => 1,
'b' => 2,
],
[
'a' => 1,
'b' => 1,
],
[
'a' => 1,
'b' => 2,
],
[
'a' => 2,
'b' => 2,
],
]);
$col2 = $col1->unique(function($val) {
return $val['a'].'/'.$val['b'];
});
結果は以下の通り。
array:3 [
0 => array:2 [
"a" => 1
"b" => 2
]
1 => array:2 [
"a" => 1
"b" => 1
]
3 => array:2 [
"a" => 2
"b" => 2
]
]
例によって、蛇足ながら連想配列に対しても適用可能です。
$col1 = collect([
'a' => 1,
'b' => 2,
'c' => 1,
'd' => 3
]);
$col2 = $col1->unique();
結果は以下の通り。
array:3 [
"a" => 1
"b" => 2
"d" => 3
]
まぁ、意味はないでしょうけど。
uniqueStrict
uniqueの厳格版です。
比較がより厳密になるだけなので確認は割愛。
where
$col1 = collect([
[
'a' => 1,
'b' => 1,
],
[
'a' => 1,
'b' => 2,
],
[
'a' => 2,
'b' => 1,
],
[
'a' => 2,
'b' => 2,
],
]);
$col2 = $col1->where('b', 1);
第一引数にキー、第二引数に値を指定することで同キーと値のセットを持つ要素を抽出し、新しいコレクションを生成します。
結果は以下の通り。
array:2 [
0 => array:2 [
"a" => 1
"b" => 1
]
2 => array:2 [
"a" => 2
"b" => 1
]
]
階層が階層が深くても大丈夫です。
$col1 = collect([
[
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 3,
'b-2' => 4,
],
],
[
'a' => [
'a-1' => 5,
'a-2' => 6,
],
'b' => [
'b-1' => 7,
'b-2' => 8,
],
],
[
'a' => [
'a-1' => 9,
'a-2' => 10,
],
'b' => [
'b-1' => 11,
'b-2' => 12,
],
],
[
'a' => [
'a-1' => 13,
'a-2' => 14,
],
'b' => [
'b-1' => 3,
'b-2' => 15,
],
],
]);
$col2 = $col1->where('b.b-1', 3);
結果は以下の通り。
array:2 [
0 => array:2 [
"a" => array:2 [
"a-1" => 1
"a-2" => 2
]
"b" => array:2 [
"b-1" => 3
"b-2" => 4
]
]
3 => array:2 [
"a" => array:2 [
"a-1" => 13
"a-2" => 14
]
"b" => array:2 [
"b-1" => 3
"b-2" => 15
]
]
]
なお、第一引数にキー、第二引数に比較演算子、第三引数に値と言う指定もできます。
先の例の同じコレクションに対して以下のようにwhereの設定を変更してみます。
$col2 = $col1->where('b.b-1', '>', 5);
結果は以下の通り。
array:2 [
1 => array:2 [
"a" => array:2 [
"a-1" => 5
"a-2" => 6
]
"b" => array:2 [
"b-1" => 7
"b-2" => 8
]
]
2 => array:2 [
"a" => array:2 [
"a-1" => 9
"a-2" => 10
]
"b" => array:2 [
"b-1" => 11
"b-2" => 12
]
]
]
コレクションの本体配列としてはEloquentを要素とする配列であるケースが一番多いと思いますが、このようなケースにwhereが適用できるかどうかは重要です。
以下のようなケースを試してみます。
$col1 = Tsample::get();
$col2 = $col1->where('column1', 2);
元コレクションはTSample(Eloquent)の配列になりますが、中身を展開すると以下のような状態になっています。
array:3 [
0 => array:6 [
"id" => 1
"column1" => 1
"column2" => "データ1"
]
1 => array:6 [
"id" => 2
"column1" => 2
"column2" => "データ2"
]
2 => array:6 [
"id" => 3
"column1" => 3
"column2" => "データ3"
]
]
つまり、各Eloquentのプロパティ「column1」の値で要素を絞り込めるかどうかと言うことです。
結果は以下の通り。
array:1 [
1 => array:6 [
"id" => 2
"column1" => 2
"column2" => "データ2"
]
]
無事絞り込めました。
whereStrict
whereの厳格版です。
比較がより厳密になるだけなので確認は割愛。
whereBetween
$col1 = collect([
[
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 3,
'b-2' => 4,
],
],
[
'a' => [
'a-1' => 5,
'a-2' => 6,
],
'b' => [
'b-1' => 7,
'b-2' => 8,
],
],
[
'a' => [
'a-1' => 9,
'a-2' => 10,
],
'b' => [
'b-1' => 11,
'b-2' => 12,
],
],
[
'a' => [
'a-1' => 13,
'a-2' => 14,
],
'b' => [
'b-1' => 15,
'b-2' => 16,
],
],
]);
$col2 = $col1->whereBetween('b.b-1', [7,11]);
whereの派生形で、第二引数に2つの値を持つ配列を指定し、第一引数で指定したキーの値が第二引数で指定した2つの値の間に含まれる要素のみで新しいコレクションを生成します。
なおwhereの派生形は数も多いため、細かいパターンの確認はサボって、比較的複雑な構造を持つコレクションに関する動作確認のみ行いたいと思います。
と言うことで前述の実行結果は以下の通り。
array:2 [
1 => array:2 [
"a" => array:2 [
"a-1" => 5
"a-2" => 6
]
"b" => array:2 [
"b-1" => 7
"b-2" => 8
]
]
2 => array:2 [
"a" => array:2 [
"a-1" => 9
"a-2" => 10
]
"b" => array:2 [
"b-1" => 11
"b-2" => 12
]
]
]
上記から分かるように、第二引数で示した2つの値のいずれかと一致する場合(つまり指定した数値の幅の両端)も新規コレクションに含まれます。
whereIn
$col1 = collect([
[
'a' => [
'a-1' => 'str1',
'a-2' => 'str2',
],
'b' => [
'b-1' => 'str3',
'b-2' => 'str4',
],
],
[
'a' => [
'a-1' => 'str5',
'a-2' => 'str6',
],
'b' => [
'b-1' => 'str7',
'b-2' => 'str8',
],
],
[
'a' => [
'a-1' => 'str9',
'a-2' => 'str10',
],
'b' => [
'b-1' => 'str11',
'b-2' => 'str12',
],
],
[
'a' => [
'a-1' => 'str13',
'a-2' => 'str14',
],
'b' => [
'b-1' => 'str15',
'b-2' => 'str16',
],
],
]);
$col2 = $col1->whereIn('b.b-1', ['str3','str10','str15']);
whereBetweenに似ていますが、第二引数で指定する配列が数値の幅を示すものから、一致する値のセットに変わります。
当然ながら第二引数の配列に指定できる値は数値だけではなく文字列でもOKで、指定できる数も制限はなさそうです(あまり多くなるとメモリ容量やパフォーマンス面で問題になるとは思いますが)。
結果は以下の通り。
array:2 [
0 => array:2 [
"a" => array:2 [
"a-1" => "str1"
"a-2" => "str2"
]
"b" => array:2 [
"b-1" => "str3"
"b-2" => "str4"
]
]
3 => array:2 [
"a" => array:2 [
"a-1" => "str13"
"a-2" => "str14"
]
"b" => array:2 [
"b-1" => "str15"
"b-2" => "str16"
]
]
]
whereInStrict
whereInの厳格版です。
比較がより厳密になるだけなので確認は割愛。
whereInstanceOf
$col1 = collect([
new sub1,
new sub2,
new sub2,
new sub1
]);
$col2 = $col1->whereInstanceOf(sub1::class);
指定したクラスのインスタンスのみで新たなコレクションを生成します。
結果は以下のようになります。
array:2 [
0 => sub1のインスタンス
3 => sub1のインスタンス
]
しかし、そもそも異なるクラスのインスタンスで構成されたコレクションが生成されるケースを思いつかないのですが?
DBからの複数データ取得結果としてEloquentコレクションが頻繁に登場しますが、当然ながら同一クラスのインスタンスから構成されたものです。
とりあえず、このような機能が存在すると言うことを覚えておく程度にしておきます。
whereNotBetween
$col1 = collect([
[
'a' => [
'a-1' => 1,
'a-2' => 2,
],
'b' => [
'b-1' => 3,
'b-2' => 4,
],
],
[
'a' => [
'a-1' => 5,
'a-2' => 6,
],
'b' => [
'b-1' => 7,
'b-2' => 8,
],
],
[
'a' => [
'a-1' => 9,
'a-2' => 10,
],
'b' => [
'b-1' => 11,
'b-2' => 12,
],
],
[
'a' => [
'a-1' => 13,
'a-2' => 14,
],
'b' => [
'b-1' => 15,
'b-2' => 16,
],
],
]);
$col2 = $col1->whereNotBetween('b.b-1', [7,11]);
whereBetweenの逆で、指定した範囲に含まれない要素から新しいコレクションを生成します。
結果は以下の通り。
array:2 [
0 => array:2 [
"a" => array:2 [
"a-1" => 1
"a-2" => 2
]
"b" => array:2 [
"b-1" => 3
"b-2" => 4
]
]
3 => array:2 [
"a" => array:2 [
"a-1" => 13
"a-2" => 14
]
"b" => array:2 [
"b-1" => 15
"b-2" => 16
]
]
]
whereNotIn
$col1 = collect([
[
'a' => [
'a-1' => 'str1',
'a-2' => 'str2',
],
'b' => [
'b-1' => 'str3',
'b-2' => 'str4',
],
],
[
'a' => [
'a-1' => 'str5',
'a-2' => 'str6',
],
'b' => [
'b-1' => 'str7',
'b-2' => 'str8',
],
],
[
'a' => [
'a-1' => 'str9',
'a-2' => 'str10',
],
'b' => [
'b-1' => 'str11',
'b-2' => 'str12',
],
],
[
'a' => [
'a-1' => 'str13',
'a-2' => 'str14',
],
'b' => [
'b-1' => 'str15',
'b-2' => 'str16',
],
],
]);
$col2 = $col1->whereNotIn('b.b-1', ['str3','str10','str15']);
whereInの逆で、指定したセットに該当しない要素から新しいコレクションを生成します。
結果は以下の通り。
array:2 [
1 => array:2 [
"a" => array:2 [
"a-1" => "str5"
"a-2" => "str6"
]
"b" => array:2 [
"b-1" => "str7"
"b-2" => "str8"
]
]
2 => array:2 [
"a" => array:2 [
"a-1" => "str9"
"a-2" => "str10"
]
"b" => array:2 [
"b-1" => "str11"
"b-2" => "str12"
]
]
]
whereNotInStrict
whereNotInの厳格版です。
比較がより厳密になるだけなので確認は割愛。
whereNull, whereNotNull
$col1 = collect([
0,
1,
'',
'str1',
null,
new sub1
]);
$col2 = $col1->whereNull();
$col3 = $col1->whereNotNull();
表裏をなす処理なのでセットで。
whereNullは値がNULLの要素のみ抽出し新たなコレクションを生成します。
whereNotNullは値がNULLでない要素のみ抽出し新たなコレクションを生成します。
結果は以下の通り。
// whereNull
array:1 [
4 => null
]
// whereNotNull
array:5 [
0 => 0
1 => 1
2 => ""
3 => "str1"
5 => sub1のインスタンス
]
ポイントは、曖昧な比較ではNULLと等価に扱われそうな数値の0や空文字もNULLとは区別されている点です。
総括
使い方がイメージし易いものとし難いものがありますが、いずれにしてもまずは全般覚えておいて、必要な局面で必要なものを使えるようにしたいと思います。filterとrejectやskipとsliceのように片方で良くない?と思うものもありますが。
要素の部分抽出を行ってみると、改めてPHPでは配列(キーが連番)は連想配列の特殊形に過ぎないと言うことが良く分かります。
インデックスの振り直しは原則行われませんが、これは「インデックス」はあくまで数字を用いた「キー」だと考えれば自然なことかもしれません。
配列ではキー(インデックス)を明示しなくても定義できるためどうしてもキーの存在を忘れがちですが、diffAssocを配列に適用した場合のように思わぬ所でその存在を意識させられることもあります(まぁ、diffAssocの例はかなり強引ではありますが)。
この辺は改めてしっかり認識しておきたいと思います。