Edited at

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

More than 1 year has passed since last update.


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


はじめに

以下の記事の続編です。

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

では、第三弾として 77. search - 115. collect までをお送りします。


77. search

mixed search(mixed $value, bool $strict = false)

コレクションを与えられた条件で検索し、一致した値があれば、対応するキーを返します(最初にヒットした要素の、であって、すべての、ではないというところに注意が必要です)。


77.1. 値を渡すパターン

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

$foundKey = $items->search(2);
$this->assertEquals(1, $foundKey);


77.2. クロージャを渡すパターン

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
['id' => 3, 'name' => 'Adam Smith', 'score' => 10],
['id' => 4, 'name' => 'John Rock', 'score' => 8],
]);
$foundKey = $items->search(function ($item) {
return $item['score'] > 8;
});
$this->assertEquals(0, $foundKey);


78. shift

mixed shift()

コレクションの先頭の要素を削除して返します。

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

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


79. shuffle

Collection shuffle(int $seed = null)

要素をシャッフルします。

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

$shuffled = $items->shuffle();
$this->assertEquals([], $shuffled->diff($items)->toArray());


80. slice

Collection slice(int $offset, int $length = null)

要素をスライスします。

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

$sliced = $items->slice(2);
$this->assertEquals([2 => 3, 3 => 4, 4 => 5], $sliced->toArray());

$sliced = $items->slice(2, 2);
$this->assertEquals([2 => 3, 3 => 4], $sliced->toArray());


81. split

Collection split(int $numberOfGroups)

コレクションを指定された数のグループに分けます。配列の要素数が偶数なら同じ数ずつになりますが、奇数の場合は、ひとつめのグループの方が1つ多くなります。

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

$split = $items->split(2);
$this->assertEquals([
[1, 2, 3],
[3 => 4, 4 => 5]
], $split->toArray());


82. chunk

Collection chunk(int $size)

コレクションを指定されたサイズで分割します。split は「N 個に」、でしたが、こちらは「N 個ずつ」、になります。割り切れない場合は末尾の要素で調整します。

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

$split = $items->chunk(2);
$this->assertEquals([
[0 => 1, 1 => 2],
[2 => 3, 3 => 4],
[4 => 5]
], $split->toArray());


83. sort

Collection sort(callable $callback = null)

コールバック関数を用いて要素をソートします。

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
['id' => 3, 'name' => 'Adam Smith', 'score' => 10],
['id' => 4, 'name' => 'John Rock', 'score' => 8],
]);
$sorted = $items->sort(function ($a, $b) {
return $b['score'] <=> $a['score'];
});
$this->assertEquals([1, 3, 2, 4], $sorted->pluck('id')->toArray());


84. sortBy

Collection sortBy(callable|string $callback, int $options = SORT_REGULAR, bool $descending = false)

指定されたキー、またはコールバック関数で要素をソートします。

第三引数を true にすると、後述する sortByDesc と同じ振る舞いをします。


84.1. キーを指定するパターン

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
['id' => 3, 'name' => 'John Rock', 'score' => 10],
['id' => 4, 'name' => 'Adam Smith', 'score' => 8],
]);
$sorted = $items->sortBy('score');
$this->assertEquals([1, 3, 0, 2], $sorted->keys()->toArray());


84.2. クロージャを指定するパターン

以下のサンプルは、名前が空のユーザーがいた場合、先頭ではなく末尾に持ってくるようにするための関数を指定しています。

$items = new Collection([

['id' => 1, 'name' => '', 'score' => 10],
['id' => 2, 'name' => null, 'score' => 8],
['id' => 3, 'name' => 'John Rock', 'score' => 10],
['id' => 4, 'name' => 'Adam Smith', 'score' => 8],
]);
$sorted = $items->sortBy(function ($item) {
return (empty($item['name']) ? 1 : 0) . $item['name'];
});
$this->assertEquals([3, 2, 0, 1], $sorted->keys()->toArray());


85. sortByDesc

Collection sortByDesc(callable|string $callback, int $options = SORT_REGULAR)

指定されたキー、または、コールバック関数を用いて要素をソートします。

前述の通り、sortBy の第三引数で true を指定した場合と同じ振る舞いになります(なのでクロージャのパターンは省略)。

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
['id' => 3, 'name' => 'John Rock', 'score' => 10],
['id' => 4, 'name' => 'Adam Smith', 'score' => 8],
]);
$sorted = $items->sortByDesc('score');
$this->assertEquals([0, 2, 1, 3], $sorted->keys()->toArray());


86. splice

Collection splice(int $offset, int|null $length = null, mixed $replacement = array())

コレクションの一部を削除し、指定した要素で置換します($replacement が指定されなければ削除のみ)。削除された要素が戻り値になります。

// delete

$items = new Collection([1, 2, 3, 4, 5]);
$spliced = $items->splice(2);
$this->assertEquals([1, 2], $items->toArray());
$this->assertEquals([3, 4, 5], $spliced->toArray());
// replace
$items = new Collection([1, 2, 3, 4, 5]);
$spliced = $items->splice(1, 2, ['b', 'c']);
$this->assertEquals([1, 'b', 'c', 4, 5], $items->toArray());
$this->assertEquals([2, 3], $spliced->toArray());


87. sum

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

要素の値を合計します。


87.1. 引数なしのパターン

// values

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


87.2. キーを指定するパターン

// key

$items = new Collection([
['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
['id' => 3, 'name' => 'John Rock', 'score' => 10],
['id' => 4, 'name' => 'Adam Smith', 'score' => 8],
]);
$sum = $items->sum('score');
$this->assertEquals(36, $sum);


87.3. クロージャを指定するパターン

// closure

$methods = new Collection([
'Collection' => ['public' => 91, 'public static' => 3],
'Macroable' => ['public' => 1, 'public static' => 3],
]);
$sum = $methods->sum(function ($method) {
return Collection::make($method)->sum();
});
$this->assertEquals(98, $sum);


88. take

Collection take(int $limit)

指定された数の要素を先頭あるいは末尾から取得します(正の数の場合は先頭、負の数の場合は末尾)。

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

$taken = $items->take(3);
$this->assertEquals([1, 2, 3], $taken->toArray());


89. tap

$this tap(callable $callback)

指定されたコールバック関数に自身を渡し、それを戻します。

Ruby では基底の Object クラスに存在しているメソッドですが、tap = 傍受・盗聴という意味らしく、自身をクロージャの中で扱えるようにしつつ、スコープ外には影響を与えないようになっている不思議なメソッドです。なんのために使うのかというと、メソッドチェーンの間でログに出力するとか、ですかね。

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

$values = $items->tap(function ($tapped) use ($items) {
$this->assertEquals($items, $tapped);
})->all();
$this->assertEquals([1, 2, 3, 4, 5], $values);


90. transform

$this transform(callable $callback)

コールバック関数を用いて各要素を置き換えます(さらに言えば、$this を返します)。

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
]);
$transformed = $items->transform(function ($item) {
return $item['score'];
});
$this->assertEquals($items, $transformed);
$this->assertEquals([10, 8], $transformed->toArray());


91. unique

Collection unique(string|callable|null $key = null, bool $strict = false)

コレクションの要素からユニークな要素のみを返します。

後述する uniqueStrict で触れますが、 $strict = true にした場合、引数なしのパターンでは、意図した動作になりません。


91.1. 引数なしのパターン

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

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


91.2. キーを指定するパターン

// with key

$items = new Collection([
['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
['id' => 3, 'name' => 'John Rock', 'score' => 10],
['id' => 4, 'name' => 'Adam Smith', 'score' => 8],
]);
$unique = $items->unique('score');
$this->assertEquals([1, 2], $unique->pluck('id')->toArray());


91.3. クロージャを渡すパターン

// with closure

$unique = $items->unique(function ($item) {
return $item >= 8;
});
$this->assertEquals([1], $unique->pluck('id')->toArray());


92. uniqueStrict

Collection uniqueStrict(string|callable|null $key = null)

unique の厳密な比較バージョンです。

と言いたいところですが、引数なしのパターンでは、内部で array_unique を呼んでいるので、 (string)$a === (string)$b で比較されるため、0, false, null は同じとみなされます。

注意が必要です。


92.1. 引数なしのパターン

// values

$items = new Collection([0, 1, 2, null, 3, false]);
$unique = $items->uniqueStrict();
// $this->assertEquals([0, 1, 2, null, 3, false], $unique->toArray());
$this->assertEquals([0, 1, 2, 3], $unique->values()->toArray());


92.2. キーを指定するパターン

(省略)


92.3. クロージャを渡すパターン

$items = Collection::make([0, 1, 2, null, 3, false])->map(function ($item) {

return Collection::make(['value' => $item]);
});
$unique = $items->uniqueStrict('value');
$this->assertEquals([0, 1, 2, null, 3, false], $unique->pluck('value')->toArray());


93. values

Collection values()

配列のキーをリセットして(内部で array_values を呼んでいるので、キーは 0 から始まる添字になります)、返します。

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

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


94. zip

Collection zip(mixed $items)

ひとつあるいは複数の配列をジップします(ジップに馴染みのない方は分かりにくいと思いますが、各要素を順にまとめる処理です。下記のコードを見れば動きが分かると思います)。

要素の数が一致しない場合は少ない配列に合わせます(2番めの例をご覧ください)。

// with one arrays

$items = new Collection([1, 2, 3]);
$zipped = $items->zip(['a', 'b', 'c']);
$this->assertEquals([1, 'a'], $zipped->shift()->toArray());
$this->assertEquals([2, 'b'], $zipped->shift()->toArray());
$this->assertEquals([3, 'c'], $zipped->shift()->toArray());

// with two arrays
$items = new Collection([1, 2, 3, 4]);
$zipped = $items->zip(['a', 'b', 'c', 'd'], ['あ', 'い', 'う']);
$this->assertEquals([1, 'a', 'あ'], $zipped->shift()->toArray());
$this->assertEquals([2, 'b', 'い'], $zipped->shift()->toArray());
$this->assertEquals([3, 'c', 'う'], $zipped->shift()->toArray());


95. pad (5.5)

Collection pad(int $size, mixed $value)

$size 分、$value で要素を埋めます(array_pad のラッパーです)。

静的メソッドではないのでご注意ください。

$items = collect()->pad(5, 1);

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


96. toArray

array toArray()

各要素を配列として返します。前述の values メソッドとの違いは、再帰的に toArray を呼ぶことです。

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

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


97. jsonSerialize

array jsonSerialize()

各要素を再帰的に JSON にシリアライズ可能な形式に変換して返します(JsonSerializable インタフェースを実装したクラスであれば jsonSerialize メソッドの返す形式、Jsonable インタフェースを実装したクラスであれば toJson メソッドの返す形式、Arrayable を実装したクラスであれば toArray メソッドの返す形式、いずれでもなければ自分自身を返します。

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

$jsonSerializable = $entity->jsonSerialize();
$this->assertEquals(['id' => 1, 'name' => 'John Doe', 'score' => 10], $jsonSerializable);


98. toJson

string toJson(int $options)

要素を JSON 形式に変換して返します($options パラメータは、json_encode 関数に渡すオプションと同じです)。

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

$json = $entity->toJson();
$this->assertEquals('{"id":1,"name":"John Doe","score":10}', $json);


99. getIterator

ArrayIterator getIterator()

イテレータを返します。

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
]);
$iterator = $items->getIterator();

while ($iterator->valid()) {
$this->assertEquals($items->get($iterator->key()), $iterator->current());
$iterator->next();
}


100. getCachingIterator

CachingIterator getCachingIterator(int $flags = CachingIterator::CALL_TOSTRING)

CachingIterator のインスタンスを返します.

CachingIterator というのを初めて知ったんですが、イテレータをキャッシュしておいて、本体に影響を与えずに要素を操作できるようにするためのイテレータみたいです。

PHP: CachingIterator - Manual

下記のコードを見ると、$iterator->offsetUnset(1); をしているにもかかわらず、二回目のイテレーションではちゃんと要素がふたつあることが分かります。

$items = new Collection([

['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
]);
$iterator = $items->getCachingIterator(\CachingIterator::FULL_CACHE);

foreach ($iterator as $key => $item) {
$this->assertEquals($items->get($key), $item);
}
$iterator->offsetUnset(1);
$cachedIterator = $iterator->getCache();

$this->assertEquals(1, $iterator->count());
foreach ($cachedIterator as $key => $item) {
$this->assertEquals($items->get($key), $item);
}

$i = 0;
foreach ($iterator as $key => $item) {
$this->assertEquals($items->get($key), $item);
$i++;
}
$this->assertEquals(2, $i);


101. count

int count()

要素数を返します。

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

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


102. toBase

Collection toBase()

Collection クラスを継承したインスタンスに対して呼び出すと、基底クラスに変換して返します(内部では new self($this) しています)。

$items = new \Illuminate\Database\Eloquent\Collection([1, 2, 3]);

$base = $items->toBase();
$this->assertInstanceOf(Collection::class, $base);
$this->assertEquals($items->toArray(), $base->toArray());


103. offsetExists

bool offsetExists(mixed $key)

issetempty が呼ばれると自動的に呼ばれ、キーが存在するかどうかを判別します。

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

$this->assertTrue(isset($entity['id']));
$this->assertFalse(isset($entity['missing attribute']));


104. offsetGet

mixed offsetGet(mixed $key)

与えられたキーを持つ要素を返します($array[$key] でアクセスされた際に自動的に呼ばれます)。

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

$this->assertEquals(10, $entity['score']);


105. offsetSet

void offsetSet(mixed $key, mixed $value)

指定されたキーに値を割り当てます($array[$key] = $value の形でセットされた場合に自動的に呼ばれます)。

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

$entity['score'] = 12;
$this->assertEquals(12, $entity['score']);


106. offsetUnset

void offsetUnset(string $key)

指定されたキーを持つ要素を消去します(unset($array[$key]) の形で消去された際に自動的に呼ばれます)。

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

unset($entity['score']);
$this->assertFalse(isset($entity['score']));


107. __toString

string __toString()

コレクションを文字列に変換して返します(デフォルトの振る舞いでは JSON 形式に変換されます)。

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

$this->assertEquals('{"id":1,"name":"John Doe","score":10}', (string)$entity);


108. proxy

static void proxy(string $method)

プロクシメソッドにメソッドを追加します。

プロクシメソッドというのは、Collection クラスのメソッドを、高階関数をラップするための HigherOrderCollectionProxy というクラスに渡して、メソッドやプロパティへのアクセスを委譲するための機構です。

と言ってもなんのこっちゃってかんじだと思うので、サンプルコードをご覧ください。


108.1. キーを渡すパターン

下記のコードは、以下と等価です。疑似プロパティ(後述する __get マジックメソッドが呼ばれます)へのアクセスを委譲するパターンです。ここでは score プロパティを使っています。

$items->avg('score')

Collection::proxy('avg');

$items = new Collection([
['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8],
]);
$avgFunc = $items->avg;
$this->assertEquals(9, $avgFunc->score);
// $items->avg->score


108.2. メソッドを渡すパターン

下記のコードは、以下と等価です。疑似メソッド(後述する __get マジックメソッドが呼ばれます)へのアクセスを委譲するパターンです。ここでは、avg メソッドを使っています。

$items->max(function ($item) {

return $item->avg('score');

});

Collection::proxy('max');

$items = new Collection([
new Collection([
['id' => 1, 'name' => 'John Doe', 'score' => 10],
['id' => 2, 'name' => 'Jane Doe', 'score' => 8]
]),
new Collection([
['id' => 3, 'name' => 'Adam Smith', 'score' => 12],
['id' => 4, 'name' => 'John Rock', 'score' => 14],
]),
]);
$maxFunc = $items->max;
$this->assertEquals(13, $maxFunc->avg('score'));
// $items->max->avg('score')


109. __get

mixed __get(string $key)

プロクシメソッドにアクセスするときに呼ばれます。

下記の例で言うと、$key には 'every' という文字列が渡ってきます。

$entity = new Collection([

['id' => 1, 'name' => 'John Doe', 'is_premium' => true],
['id' => 2, 'name' => 'Jane Doe', 'is_premium' => true],
]);
$everyFunc = $entity->every;
$this->assertTrue($everyFunc->is_premium);
// $entity->every->is_preminum


110. macro

static void macro(string $name, callable $macro)

カスタムマクロを登録します。

// 雑な関数で申し訳ない…

Collection::macro('any', function ($value) {
foreach ($this->items as $item) {
if ($item == $value) {
return true;
}
}
return false;
});

$items = new Collection([1, 2, 3]);
$any = $items->any(1);
$this->assertTrue($any);
$any = $items->any(4);
$this->assertFalse($any);


111. mixin (5.5)

別のクラスを使ってマクロを登録します。

ちょっと直感的でないのが、ミックスインされるクラスのメソッドは、クロージャを返す必要がある、ということです。

前述の any マクロを、Anything というクラスに持たせます(クロージャを返すようにします)。

class Anything

{
public function any()
{
return function ($value) {
foreach ($this->items as $item) {
if ($item == $value) {
return true;
}
}
return false;
};
}
}

それを mixin を使ってマクロとして登録します。ミックスインされるクラスが複数のメソッドを持っていれば、それらがすべてマクロとして登録されます(public か protected のメソッド)。

Collection::mixin(new Anything());

$items = new Collection([1, 2, 3]);
$any = $items->any(1);
$this->assertTrue($any);
$any = $items->any(4);
$this->assertFalse($any);


112. hasMacro

static bool hasMacro(string $name)

マクロが登録済みか判別します。

// 雑な関数で(ry

Collection::macro('any', function ($value) {
foreach ($this->items as $item) {
if ($item == $value) {
return true;
}
}
return false;
});
$items = new Collection([1, 2, 3]);
$hasMacro = $items->hasMacro('any');
$this->assertTrue($hasMacro);
$hasMacro = $items->hasMacro('some');
$this->assertFalse($hasMacro);


113. __callStatic

static mixed __callStatic(string $method, array $parameters)

静的メソッドとしてマクロが呼ばれたときに呼ばれます。

// 雑な(ry

Collection::macro('times0', function ($n, $callback) {
$items = [];
for ($i = 0; $i < $n; $i++) {
$items[] = $callback($i);
}
return $items;
});
$items = Collection::times0(3, function ($n) {
return 'index-' . $n;
});
$this->assertEquals(['index-0', 'index-1', 'index-2'], $items);


114. __call

mixed __call(string $method, array $parameters)

インスタンスメソッドとしてマクロが呼ばれたときに呼ばれます。

// z(ry

Collection::macro('any', function ($value) {
foreach ($this->items as $item) {
if ($item == $value) {
return true;
}
}
return false;
});
$items = new Collection([1, 2, 3]);
$any = $items->any(1);
$this->assertTrue($any);


115. collect

mixed function collect($value = null)

new Collection, Collection::make と同様に、コレクションを構築するヘルパー関数です。

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

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


まとめ

世の中に、Collection クラスのメソッドを網羅したサンプルコードがないので記事にしようと思ったのがきっかけですが、存在しないのはそれなりの理由があって、有用性がそんなに高くないと、けっきょく使わないので情報が広まらないっていうことなんだと思いました。

5.4 ブランチではさらに増えていて、ついに 100 個を超えてしまいました。どこまで膨張するんだろうか…

2017-12-13 追記

5.5 で追加になったのは、16個でした(Macroable 含めて)。

200 くらいまで行くんだろうか、それともそろそろ打ち止めか...