Edited at

【5.5対応】Laravel の Collection を使い倒してみたくなった 〜 サンプルコード 115 連発 1/3

More than 1 year has passed since last update.


はじめに

思ったより数が多かったので、3回に分けることにしました。


目次


概要

Laravel には、Collection という、配列のラッパークラスがあります。

Laravel 標準の ORM である Eloquent で複数レコードを取得する際に、この Collection のインスタンス (正確には Collection を継承したクラスのインスタンス) で返ってきたりしますが、もちろん、アプリケーション内で自由に使うことができます。

これまで、いくつか Laravel を使ったアプリケーションを見てきましたが、あまり Collection クラスを使ってないなぁ、と思っていたので、一回ちゃんと調べてみるかと思った次第です。

2017-12-13 追記

5.5 で導入されたメソッドについては、メソッド名の後ろに (5.5) と記載しました。


環境

Laravel 5.5.25

PHP 7.1.4


メソッド数

Collection クラスにある public メソッドの数は以下のとおりです(Macroable トレイトを使っています)。

public
public
static
total

Collection class
104
5
109

Macroable trait
1
4
5


105
9
114

上記に加えて、おまけで collect ヘルパーも入れて、115 個になります。

本当は、カテゴリー別にして、3分割したかったんですが、力尽きたので、ソースコードの上から順に掲載します(Macroable トレイトのメソッドは最後に持ってきています)。

では、第一弾として 1. __construct - 38. get までをお送りします。


サンプルコード

サンプルコードは PHPUnit のテストコードとして記載しています。


1. __construct

void __construct(mixed $items = array())

通常のコンストラクタです。 new Collection したときに自動的に呼ばれます。

// 空で初期化

$emptyItems = new Collection();
$this->assertEquals([], $emptyItems->toArray());
// 配列で初期化
$filledItems = new Collection([1, 2, 3]);
$this->assertEquals([1, 2, 3], $filledItems->toArray());


2. make

static Collection make(mixed $items = array())

静的メソッドで初期化することもできます。中身はコンストラクタ呼んでるだけなので、new するか make するかはお好みでどうぞ。

// 空で初期化

$emptyItems = Collection::make();
$this->assertEquals([], $emptyItems->toArray());
// 配列で初期化
$filledItems = Collection::make([1, 2, 3]);
$this->assertEquals([1, 2, 3], $filledItems->toArray());


3. wrap (5.5)

static Collection wrap(mixed $value)

$value が配列でなくても、Collection インスタンスを生成してくれます。

$items = Collection::wrap(1);

$this->assertEquals([1], $items->toArray());


4. unwrap (5.5)

static array unwrap(array|Collection $value)

$value が Collection インスタンスなら、生の配列を取り出し(内部で all() を呼んでます)、生の配列ならそのまま返します。

$collection = Collection::make([1, 2, 3]);

$items = Collection::unwrap($collection);

$this->assertEquals([1, 2, 3], $items);

$items = Collection::unwrap([1, 2, 3]);
$this->assertEquals([1, 2, 3], $items);


5. times

static Collection times(int $amount, callable $callback)

$amount で指定された回数分、コールバック関数を呼んでリストを構築します(コールバック関数に渡ってくる引数の数字は 1 から始まるのでご注意ください)。

$items = Collection::times(3, function ($n) {

return 'test-' . $n;
});
$this->assertEquals(['test-1', 'test-2', 'test-3'], $items->toArray());


6. all

array all()

要素を配列に変換して返します(変換するというよりは内部では配列で要素を持っているのでそれをそのまま返すだけです。後述の toArray は異なる振る舞いをします)。

$items = new Collection([1, 2, 3]);

$values = $items->all();
$this->assertEquals([1, 2, 3], $values);


7. avg

mixed avg(callable|string|array $callback = null)

平均値を算出します。引数の指定の仕方によって、3つのパターンがあります。


7.1. 配列の値を集計するパターン

// all

$items = new Collection([1, 2, 3]);
$avg = $items->avg();
$this->assertEquals(2, $avg);


7.2. 連想配列のキーを指定して集計するパターン

$items = new Collection([

['id' => 1, 'score' => 4],
['id' => 2, 'score' => 3],
['id' => 3, 'score' => 5]
]);
$avg = $items->avg('score');
$this->assertEquals(4, $avg);


7.3. コールバック関数を使って集計するパターン

$items = new Collection([

['id' => 1, 'birthday' => '2000-5-1'],
['id' => 2, 'birthday' => '2001-5-1'],
['id' => 3, 'birthday' => '2002-5-1']
]);
Carbon::setTestNow('2017-05-01 00:00:00');
$avg = $items->avg(function ($a) {
return (new Carbon($a['birthday']))->startOfDay()->age;
});
$this->assertEquals(16, $avg);


8. average

mixed average(callable|string|null $callback = null)

avg のエイリアスなので省略。


9. median

mixed median(null $key = null)

メディアン(中央値)を算出します。


9.1. 配列の値を集計するパターン

$items = new Collection([1, 2, 3, 4, 5]);

$median = $items->median();
$this->assertEquals(3, $median);


9.2. 連想配列のキーを指定して集計するパターン

$items = new Collection([

['id' => 1, 'score' => 1],
['id' => 2, 'score' => 2],
['id' => 3, 'score' => 3],
['id' => 4, 'score' => 4],
['id' => 5, 'score' => 5],
]);
$median = $items->median('score');
$this->assertEquals(3, $median);


10. mode

array|null mode(mixed $key = null)

モード(最頻値)を算出します。戻り値は配列になるので注意が必要です(空の配列を渡すと null が返ります)。


10.1. 配列の値を集計するパターン

$items = new Collection([1, 2, 2, 3, 3, 4]);

$mode = $items->mode();
$this->assertEquals([2, 3], $mode);

$items = new Collection([]);
$mode = $items->mode();
$this->assertEquals(null, $mode);


10.2. 連想配列のキーを指定して集計するパターン

$items = new Collection([

['id' => 1, 'score' => 1],
['id' => 2, 'score' => 2],
['id' => 3, 'score' => 3],
['id' => 4, 'score' => 4],
['id' => 5, 'score' => 4],
]);
$mode = $items->mode('score');
$this->assertEquals([4], $mode);

存在しないキーを指定すると、null になるかと思いきや、以下のような奇妙な結果になりました。

$items = new Collection([

['id' => 1, 'score' => 1],
['id' => 2, 'score' => 2],
['id' => 3, 'score' => 3],
['id' => 4, 'score' => 4],
['id' => 5, 'score' => 4],
]);
$mode = $items->mode('scole'); // typo
$this->assertEquals([0, 1, 2, 3, 4], $mode);

注意が必要です。


11. collapse

Collection collapse()

二次元の配列を一次元の配列に押し潰します(collapse = 潰す)。

$items = new Collection([

[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]);
$collapsed = $items->collapse();
$this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], $collapsed->toArray());

三次元以上は潰されないので、ご注意ください。


12. contains

bool contains(mixed $key, mixed $operator = null, mixed $value = null)

$operator に取り得る値は以下のとおりです。

=, ==, !=, <>: 緩い比較(型比較なし)

<, >, <=, >=: 大小

===, !==: 厳密な比較(型比較あり)


12.1. 引数が1つの値の場合

引数が1つの値のときだけ特別で、通常の配列の値を対象に判定します。それ以外のパターンでは、連想配列の配列が検査対象になります。

$entity = new Collection([1, 2, 3]);

$contains = $entity->contains(1);
$this->assertEquals(true, $contains);


12.2. 引数がクロージャの場合

$items = new Collection([

['id' => 1, 'score' => 1],
['id' => 2, 'score' => 2],
['id' => 3, 'score' => 3],
['id' => 4, 'score' => 4],
['id' => 5, 'score' => 5],
]);
$contains = $items->contains(function ($item) {
return $item['score'] >= 3;
}));
$this->assertEquals(true, $contains);


12.3. 引数をフルで指定した場合

$items = new Collection([

['id' => 1, 'score' => 1],
['id' => 2, 'score' => 2],
['id' => 3, 'score' => 3],
['id' => 4, 'score' => 4],
['id' => 5, 'score' => 5],
]);
$contains = $items->contains('score', '>=', 3);
$this->assertEquals(true, $contains);


12.4. オペレーターを省略できます。

比較演算子が = の場合は省略でき、以下のように書くと contains('score', '=', 3) と同じ振る舞いになります。

$items->contains('score', 3);


13. containsStrict

bool containsStrict(mixed $key, mixed $value = null)

contains の厳密比較 (===)バージョンです。ただし、クロージャを指定した場合は、contains と同じ挙動になります(なぜクロージャを取れるようにしたのか、意図が。

contains と挙動が異なるのは、以下のように、contains の例のパターン1 とパターン4(オペレーターは指定できず、常に === が使われます)だけです。


13.1. 引数が1つの値の場合

$entity = new Collection([1, 2, 3]);

// 1 === 1
$contains = $items->containsStrict(1);
$this->assertEquals(true, $contains);
// true !== 1
$contains = $items->containsStrict(true);
$this->assertEquals(false, $contains);


13.2. 引数が key, value の場合

$items = new Collection([

['id' => 1, 'score' => 1],
['id' => 2, 'score' => 2],
['id' => 3, 'score' => 3],
['id' => 4, 'score' => 4],
['id' => 5, 'score' => 5],
]);
$contains = $items->containsStrict('score', 3);
$this->assertEquals(true, $contains);

$contains = $items->containsStrict('score', '3');
$this->assertEquals(false, $contains);


14. crossJoin

Collection crossJoin(mixed $lists)

自身と $lists の交差集合を返します。

$items = new Collection([1, 2]);

$crossJoined = $items->crossJoin(['a', 'b']);
$this->assertEquals([
[1, 'a'],
[1, 'b'],
[2, 'a'],
[2, 'b'],
], $crossJoined->toArray());


15. dd (5.5)

void dd()

dd() ヘルパー関数のメソッド版です。

$items = new Collection([1, 2]);

$items->dd();


16. dump (5.5)

$this dump()

同じく、dump() のメソッド版です。

$items = new Collection([1, 2]);

$items->dump();


17. diff

Collection diff(mixed $items)

別のコレクション(または配列)を引数に取り、そちらに含まれない値を持つコレクションを返します。

$first = new Collection([1, 2, 3]);

$second = new Collection([3, 4, 5]); // or just an array
$diff = $first->diff($second);
$this->assertEquals([1, 2], $diff->toArray());


18. diffAssoc (5.5)

Collection diffAssoc(mixed $items)

diff() と同様ですが、キーと値両方とも検証します(内部で array_diff_assoc を呼んでます)。

$first = new Collection(['a' => 1, 'b' => 2, 'c' => 3]);

$second = new Collection(['a' => 3, 'b' => 2, 'd' => 3]);
$diff = $first->diffAssoc($second); // or just an array
$this->assertEquals(['a' => 1, 'c' => 3], $diff->toArray());


19. diffKeys

Collection diffKeys(mixed $items)

別のコレクション(または配列)を引数に取り、そちらに含まれないキーを持つコレクションを返します。

$first = new Collection(['id' => 1, 'name' => 'John Doe', 'birthday' => '2000-05-01']);

$second = new Collection(['id' => 2, 'name' => 'Jane Doe']); // or just an array
$diffKeys = $first->diffKeys($second);
$this->assertEquals(['birthday' => '2000-05-01'], $diffKeys->toArray());


20. each

$this each(callable $callback)

各要素に対してコールバック関数を適用します。

$values = [1, 2, 3];

Log::shouldReceive('info')->times(3)->andReturnUsing(
function ($message) {
$this->assertEquals(1, $message);
},
function ($message) {
$this->assertEquals(2, $message);
},
function ($message) {
$this->assertEquals(3, $message);
}
);
$items = new Collection($values);
$items->each(function ($item) {
Log::info($item);
});


21. eachSpread (5.5)

Collection eachSpread(callable $callback)

このあとに出てくる mapSpread() もそうですが、配列の中身を引数に展開してコールバック関数に渡すようになっています(連想配列だと実行時に Error: Cannot unpack array with string keys と怒られてしまいました。 $callback(...$params) の形式で引数に渡しているためです)。

以下の例で示すように、最後に配列のキーがくっつきます。

Log::shouldReceive('info')->times(2)->andReturnUsing(

function ($data) {
$this->assertEquals([1, 'John Doe', 10, 0], $data);
},
function ($data) {
$this->assertEquals([2, 'Jane Doe', 15, 1], $data);
}
);
$items = new Collection([
[1, 'John Doe', 10],
[2, 'Jane Doe', 15],
]);

$items->eachSpread(function ($id, $name, $score, $key) {
Log::info([$id, $name, $score, $key]);
});


22. every

bool every(string|callable $key, mixed $operator = null, mixed $value = null)

すべての要素が条件を満たしているかどうかを判定します。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);

$this->assertEquals(true, $items->every('score', '>=', 3));
$this->assertEquals(false, $items->every('score', '>', 3));
$this->assertEquals(true, $items->every(function ($item) {
return $item['score'] != 6;
}));


23. except

Collection except(mixed $keys)

指定されたキーを除外したコレクションを返します。

$entity = new Collection(['id' => 1, 'name' => 'John Doe', 'score' => 10]);

$excepted = $entity->except(['name']);
$this->assertEquals(['id' => 1, 'score' => 10], $excepted->toArray());

$excepted = $entity->except(['missing attribute']);
$this->assertEquals(['id' => 1, 'name' => 'John Doe', 'score' => 10], $excepted->toArray());


24. filter

Collection filter(callable $callback = null)

すべての要素をコールバック関数の条件でフィルターします。


24.1. 引数にコールバック関数を渡す場合

論理値を返すコールバック関数を渡すと true になった要素のみを返します。

// with closure

$items = new Collection([
['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
$filtered = $items->filter(function ($item) {
return $item['score'] > 3;
});
$this->assertEquals([2, 3], $filtered->pluck('id')->toArray());


24.2. 引数なしの場合

標準関数 array_filter と同じ挙動ですが、false になる要素を除外します。

// filter false values

$items = new Collection([1, false, null, 2]);
$filtered = $items->filter();
$this->assertEquals([1, 2], $filtered->values()->toArray());


25. when

mixed when(bool $value, callable $callback, callable $default = null)

$value が真ならひとつめのコールバック関数を、偽ならふたつめのコールバック関数を、コレクション全体に適用します。

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
]);

$over = 8;

$callbackWhenTrue = function (Collection $items) use ($over) {
return $items->filter(function ($item) use ($over) {
return $item['score'] > $over;
});
};
$callbackWhenFalse = function (Collection $items) use ($over) {
return $items->filter(function ($item) use ($over) {
return $item['score'] <= $over;
});
};
// true
$filtered = $items->when(true, $callbackWhenTrue, $callbackWhenFalse);
$this->assertEquals([1], $filtered->pluck('id')->toArray());

// false
$filtered = $items->when(false, $callbackWhenTrue, $callbackWhenFalse);
$this->assertEquals([2], $filtered->pluck('id')->toArray());


26. unless (5.5)

mixed unless(bool $value, callable $callback, callable $default = null)

when の逆バージョンです($value が偽ならひとつめのコールバック関数を、真ならふたつめのコールバック関数を、コレクション全体に適用します)。

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
]);

$over = 8;

$callbackWhenTrue = function (Collection $items) use ($over) {
return $items->filter(function ($item) use ($over) {
return $item['score'] > $over;
});
};
$callbackWhenFalse = function (Collection $items) use ($over) {
return $items->filter(function ($item) use ($over) {
return $item['score'] <= $over;
});
};
// true
$filtered = $items->unless(true, $callbackWhenTrue, $callbackWhenFalse);
$this->assertEquals([2], $filtered->pluck('id')->toArray());

// false
$filtered = $items->unless(false, $callbackWhenTrue, $callbackWhenFalse);
$this->assertEquals([1], $filtered->pluck('id')->toArray());


27. where

Collection where(string $key, mixed $operator, mixed $value = null)

与えられたキーバリューで要素をフィルターします。等価比較には == が使われます。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
// default operator '=='
$filtered = $items->where('score', 3);
$this->assertEquals([1], $filtered->pluck('id')->toArray());

// other operators
$filtered = $items->where('score', '>', 3);
$this->assertEquals([2, 3], $filtered->pluck('id')->toArray());


28. whereStrict

Collection whereStrict(string $key, mixed $value)

基本的な振る舞いは where と同じですが、比較演算子には === が使われます。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 3],
]);
// 3 === 3
$filtered = $items->whereStrict('score', 3);
$this->assertEquals([1, 3], $filtered->pluck('id')->toArray());

// 3 !== '3'
$filtered = $items->whereStrict('score', '3');
$this->assertEquals([], $filtered->pluck('id')->toArray());


29. whereIn

Collection whereIn(string $key, mixed $values, bool $strict = false)

与えられたキーバリューで要素をフィルターします。内部で in_array が使われており、等価比較には == が使われます。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
$filtered = $items->whereIn('score', [3, 4]);
$this->assertEquals([1, 2], $filtered->pluck('id')->toArray());


30. whereInStrict

Collection whereInStrict(string $key, mixed $values)

whereIn の厳格バージョンです。in_array を厳格モードで呼ぶだけで他は whereIn と同じ振る舞いをします(というか、whereIn の第三引数で厳格モードかどうかを指定できるので、以下のふたつの関数は等価です。

$items->whereIn('score', [3,4], true);

$items->whereInStrict('score', [3,4]);

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
// 3 === 3
$filtered = $items->whereInStrict('score', [3, 4]);
$this->assertEquals([1, 2], $filtered->pluck('id')->toArray());

// 3 !== '3'
$filtered = $items->whereInStrict('score', ['3', 4]);
$this->assertEquals([2], $filtered->pluck('id')->toArray());


31. whereNotIn

Collection whereNotIn(string $key, mixed $values, bool $strict = false)

与えられたキーバリューで要素をフィルターします。NotIn なので、含まれない要素のみを取り出します。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
$filtered = $items->whereNotIn('score', [3, 4]);
$this->assertEquals([3], $filtered->pluck('id')->toArray());


32. whereNotInStrict

Collection whereNotInStrict(string $key, mixed $values)

whereNotIn の厳格モードです。whereIn と同様、whereNotIn の第三引数を true にしたときと同じ振る舞いです。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
// 3 === 3
$filtered = $items->whereNotInStrict('score', [3, 4]);
$this->assertEquals([3], $filtered->pluck('id')->toArray());

// 3 !== '3'
$filtered = $items->whereNotInStrict('score', ['3', 4]);
$this->assertEquals([1, 3], $filtered->pluck('id')->toArray());


33. first

mixed first(callable $callback = null, mixed $default = null)

コールバック関数で与えられた条件に一致する最初の要素を取り出します。引数が省略された場合は無条件に最初の要素が返ります。

また、コールバック関数を適用するケースで、第二引数にデフォルト値を与えることができ、一致する要素がなかった場合に任意の値を割り当てることができるようになっています。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
// with closure
$first = $items->first(function ($item) {
return $item['score'] === 4;
});
$this->assertEquals(['id' => 2, 'score' => 4], $first);

// just first
$first = $items->first();
$this->assertEquals(['id' => 1, 'score' => 3], $first);

// with default
$first = $items->first(function ($item) {
return $item['score'] === 9;
}, ['id' => null, 'score' => null]);
$this->assertEquals(['id' => null, 'score' => null], $first);


34. firstWhere (5.5)

Collection firstWhere(string $key, mixed $operator, mixed $value)

与えられたキーバリューを用いた条件に一致する最初の要素を取得します。

下記の例では、 score4 の要素を取得します。

$items->where('score', '=', 4)->first() と同じ挙動であり、where() 同様、 $operator= のときは省略可能です。

$items = new Collection([

['id' => 1, 'score' => 3],
['id' => 2, 'score' => 4],
['id' => 3, 'score' => 5],
]);
$first = $items->firstWhere('score', 4);
$this->assertEquals(['id' => 2, 'score' => 4], $first);
$first = $items->firstWhere('score', '>', 4);
$this->assertEquals(['id' => 3, 'score' => 5], $first);


35. flatten

Collection flatten(int $depth = INF)

要素をフラット(一次元化)します。collapse と似ていますが、引数で階層を指定することができ、デフォルトの INF だと、再帰的にマージするので、複数次元の配列をシュッとすることができます。

$items = new Collection([

['John Doe', 'Johnny', ['tag-1', 'tag-2', 'tag-3']],
['Jane Doe', 'Janney', ['tag-2', 'tag-4']],
]);
$flatten = $items->flatten();
$this->assertEquals([
'John Doe', 'Johnny', 'tag-1', 'tag-2', 'tag-3',
'Jane Doe', 'Janney', 'tag-2', 'tag-4'
], $flatten->toArray());


36. flip

Collection flip()

キーと値を入れ替えます。

$columns = new Collection(['id', 'name', 'score']);

$flipped = $columns->flip();
$this->assertEquals(['id' => 0, 'name' => 1, 'score' => 2], $flipped->toArray());


37. forget

$this forget(string|array $keys)

指定されたキーの要素をコレクションから除去します。

単一のキーでもいいですし、キーの配列でもいいです。

$entity = new Collection(['id' => 1, 'name' => 'John Doe', 'score' => 10]);

// single
$forgotten = $entity->forget('name');
$this->assertEquals(['id' => 1, 'score' => 10], $forgotten->toArray());

// multiple
$forgotten = $entity->forget(['name', 'score']);
$this->assertEquals(['id' => 1], $forgotten->toArray());


38. get

mixed get(mixed $key, mixed $default = null)

指定されたキーを持つ要素を取り出します。デフォルト値を与えて、存在しなかったときの代替値を指定することもできます。

$entity = new Collection(['id' => 1, 'name' => 'John Doe', 'score' => 10]);

$name = $entity->get('score');
$this->assertEquals(10, $name);

$missingAttribute = $entity->get('missing attribute', 0);
$this->assertEquals(0, $missingAttribute);

以下次号