Laravel 配列のグループ化とページ分け問題
Laravel Eloquentを使う時に、groupByメソッドもついているので、簡単に使えます。
$orders = DB::table('products')
->select('name', 'price', 'category')
->groupBy('category')
->get();
SQLでは次に相当:
SELECT name, price, category FROM products GROUP BY category;
ほとんどの場面で、クエリーを作る時にgroupByをチェインすることで問題は解決できます。
特別な場面には配列で処理
とあるプロジェクトの話ですが、上記の方法ではうまく対応できません。理由は多少複雑ですが、おおまかに言えば、1)二つ以上のテーブルのデータを合併する必要がある(コラム数が多いためselect+joinが非常に煩雑になる)、2)とある特定のコラムでグループ化するではなく、そのコラムのデータの一部でグループ化する必要がある(例えば9桁の番号だが、上位8桁でグループ化する)。
そういった場面を対処するために、一般化できるPHP配列での処理を試してみました。
まずはコレクションデータまたは配列を、特定のコラムでグループ化すること。こちらはコレクションのgroupBy処理の配列版とも言えます。
function groupItemsByColumn(Collection|array $items, String $column) : array
{
$items = is_array($items) ? $items : $items->toArray();
if (empty($items)) return [];
return array_reduce($items, function (array $acc, array $el) use ($column) {
$group = $el[$column];
$acc[$group][] = $el;
return $acc;
}, []);
}
groupItemsByColumn
関数で、次のような構造の変換が可能となります。
$languages = [
['id'=>'111', 'type' => 'interpreted', 'name'=>'PHP'],
['id'=>'112', 'type' => 'interpreted', 'name'=>'JavaScript'],
['id'=>'113', 'type' => 'interpreted', 'name'=>'Python'],
['id'=>'001', 'type' => 'compiled', 'name'=>'C'],
['id'=>'002', 'type' => 'compiled', 'name'=>'Go'],
['id'=>'003', 'type' => 'compiled', 'name'=>'Rust'],
['id'=>'221', 'type' => 'mixed', 'name'=>'Java'],
['id'=>'222', 'type' => 'mixed', 'name'=>'Kotlin'],
]
$grouped = groupItemsByColumn($lanugages, 'type');
$grouped = [
'interpreted' => [
['id'=>'111', 'type' => 'interpreted', 'name'=>'PHP'],
['id'=>'112', 'type' => 'interpreted', 'name'=>'JavaScript'],
['id'=>'113', 'type' => 'interpreted', 'name'=>'Python'],
],
'compiled' => [
['id'=>'001', 'type' => 'compiled', 'name'=>'C'],
['id'=>'002', 'type' => 'compiled', 'name'=>'Go'],
['id'=>'003', 'type' => 'compiled', 'name'=>'Rust'],
],
'mixed' => [
['id'=>'221', 'type' => 'mixed', 'name'=>'Java'],
['id'=>'222', 'type' => 'mixed', 'name'=>'Kotlin'],
]
];
もし上記の例に、type
コラムがない、けどグループ化しないといけない時は、id
の上位2桁でグループ化するのも可能だが、$group
変数に少し条件判断を加える:
$group = $column == 'id' ? substr($el['id'], 0, -1)) : $el[$column];
もちろん、グループ名(キー)が00
,11
とかになってしまいますが、一応同じグループ分けが可能となります。
配列データをブレードビューに渡す時のページ分け問題
上記の処理で、グループ化された配列データを取得することができました。
これで終わりと思ったら、次にまた現実的な問題が出てきて、項目数が多いため、pagination/ページ分けが必要になってくると。
Laravelでは、Eloquentを使う時にpaginate
メソッドをチェインすることで、簡単にpaginatorオブジェクトを作ることが可能:
$users = DB::table('users')->paginate(5);
$items = Item::where('stock', '>', 0)->paginate(); //デフォルトは15
しかし、前節の処理では、すでに配列に変換しているため、当然paginate
メソッドは使えません。この問題を一般的に考えると、いわば配列データをいかにブレードビューに渡し、ページ分けすることです。さらにいうと、配列データのままではページ分けが若干難しくなるため、いかに配列データをpaginatorオブジェクトに変換するか、が肝になるかと。
ここでLengthAwarePaginator
を使うことで解決できます:
use Illuminate\Pagination\LengthAwarePaginator;
//...とあるコントローラで
$grouped = groupItemsByColumn($items, 'type');
$current_page = $request->current_page ?: 1;
$per_page = $request->per_page ?: 10;
$data = new LengthAwarePaginator(
collect($grouped)->forPage($current_page, $per_page),
count($grouped),
$per_page,
$current_page,
['path' => $request->url()]
);
return view('Item.list', compact('data'));
ここでいくつかのポイントとして:
- 配列データを
collect
関数でコレクションタイプに変換 - コレクションに
forPage
メソッドでページ情報を渡す -
url
は現在リクエストのurlを渡す - 2番目のパラメーターは配列の合計項目数を渡すが、ここはグループ化後の数を渡している
もしいくつかのコントローラで同じことをするならば、traitとして抽象化することも考えられるでしょう。
<?php
namespace App\Http\Controllers\Traits;
use Illuminate\Pagination\LengthAwarePaginator;
trait HasPaginator
{
protected function createPaginator(array $data, ?int $current_page, ?int $per_page, String $url)
{
$current_page = $current_page ?: 1;
$per_page = $per_page ?: ITEM_PER_PAGE; // 定数を定義するなど
return new LengthAwarePaginator(
collect($data)->forPage($current_page, $per_page),
count($data),
$per_page,
$current_page,
['path' => $url]
);
}
}
// とあるコントローラで
use HasPaginator;
// ...
$data = $this->createPaginator($grouped, $request->page, $request->per_page, $request->url());
これでブレードビューで、Eloquentを使う時と同じくページ分けを表示できます。URLのquery stringを保持したい場合はappends
を付けましょう。
<div class="d-flex justify-content-center mb-5">
{{ $data->links() }}
{{-- query stringをつける --}}
{{ $data->appends(request()->except('page'))->links() }}
</div>
ちなみにEloquentを使う場合はwithQueryString
メソッドをpaginate
メソッドにチェインすることで可能となります。
$items = Item::where('stock', '>', 0)->paginate(15)->withQueryString();
おそらく多くの場合はEloquentのメソッドで対処はできると思いますが、今回は若干特別な場面、配列データのグループ化とページ分けについてまとめました。
以上です!
Discussion