LaravelのPaginationエラーの原因と対処法

LaravelのPaginationエラーの原因と対処法

Laravelのページネーション周りのエラーは、「paginatorに渡している値が想定と違う」「URLやクエリ文字列の組み立てが崩れている」「DBクエリがページングに向いていない(GROUP BY/UNION/サブクエリ)」「Bladeの描画方法が合っていない」「API/SPAでレスポンス形式がズレている」といった原因で起きやすい。エラー文言は環境ごとにバラバラになりがちなので、まずはどの段階(クエリ生成・件数カウント・ページリンク生成・View描画・API返却)で落ちているかを切り分けると解決が速い。

よくあるPaginationエラーの例と発生条件

ページネーション関連で頻出する例。

Call to undefined method Illuminate\Support\Collection::links()
Method Illuminate\Database\Eloquent\Collection::paginate does not exist.
Too few arguments to function Illuminate\Pagination\LengthAwarePaginator::__construct()
Undefined variable: paginator
Property [links] does not exist on this collection instance.
SQLSTATE[...] (count用のSQLが失敗する:GROUP BY/UNION/サブクエリ絡み)

発生条件の典型:
・get() した「Collection」に対して links() を呼んでいる
・paginate() を「Collection」や「Modelインスタンス」に対して呼んでいる
・手動でPaginatorを作っているが引数が不足/型が違う
・APIで paginate() の結果をそのまま返しているのにフロント側が期待する形と違う
・GROUP BY / DISTINCT / UNION / サブクエリが入っていて、paginateが内部で作るcountクエリが落ちる
・ページリンクが /?page=2 にならず、検索条件が消える(クエリ文字列維持ミス)

原因1:get() してから links() を呼んでいる(Collectionにlinksは無い)

最も多いパターン。get() は「全部取る」なので、返るのは Collection。ページネーションではない。

// NG: get() した結果は Collection
$users = User::query()->orderBy('id')->get();
return view('users.index', compact('users'));

// Blade
{{ $users->links() }} // Call to undefined method ...Collection::links()

対処:get() ではなく paginate() を使う。

// OK
$users = User::query()->orderBy('id')->paginate(20);
return view('users.index', compact('users'));

// Blade
{{ $users->links() }}

「まず一覧を作って、途中で links を足したら落ちた」ケースはほぼこれ。

原因2:paginate() を呼ぶ場所が違う(Collection/Modelにpaginateは無い)

paginate() は Query Builder / Eloquent Builder のメソッド。CollectionやModelインスタンスには無い。

// NG: find() は Model(単体)を返す
$user = User::find(1);
$user->paginate(10); // Method ...::paginate does not exist

// NG: get() 後は Collection
$users = User::where('active', 1)->get();
$users->paginate(10); // Method ...Collection::paginate does not exist

対処:paginate() は get() の前、クエリ構築段階で呼ぶ。

// OK
$users = User::where('active', 1)->orderBy('id')->paginate(10);

原因3:検索条件やソートがページ移動で消える(クエリ文字列維持のミス)

検索フォーム(q=xxx)や並び替え(sort=created_at)を付けていると、ページ2へ行ったときに条件が消える事故が起きやすい。
発生条件:
・paginateのリンクにクエリが引き継がれていない
対処:withQueryString() または appends() を使う。

// OK: 現在のクエリ文字列を維持
$users = User::query()
    ->when(request('q'), fn($q) => $q->where('name', 'like', '%'.request('q').'%'))
    ->orderBy(request('sort', 'id'))
    ->paginate(20)
    ->withQueryString();

必要なパラメータだけ維持したい場合。

$users = User::query()
  ->paginate(20)
  ->appends([
    'q' => request('q'),
    'sort' => request('sort'),
  ]);

原因4:API/SPAでのページネーション形式のズレ(links/metaが無い等)

Laravelの paginate() は JSON で返すと、data と links/meta を含む構造になる。一方でフロントが「itemsだけ欲しい」設計だとズレる。

// 例: そのまま返す
return User::query()->paginate(20);

対処案:
・Laravel標準の形式(data, links, meta)にフロントを合わせる
・もしくは Resource で返却形式を固定する
Resource例。

use App\Http\Resources\UserResource;

return UserResource::collection(
    User::query()->orderBy('id')->paginate(20)
)->additional([
    'status' => 'ok',
]);

itemsだけ返す設計なら、paginatorの情報も併せて返す方がページUIを作りやすい。

$p = User::query()->orderBy('id')->paginate(20);

return response()->json([
  'items' => $p->items(),
  'current_page' => $p->currentPage(),
  'last_page' => $p->lastPage(),
  'total' => $p->total(),
  'per_page' => $p->perPage(),
]);

原因5:GROUP BY / DISTINCT / UNION / サブクエリでcountクエリが壊れる

paginate() は内部で「総件数を数えるcountクエリ」を作る。複雑なクエリだと、このcountがDB側でエラーになることがある(SQLSTATE系)。
発生条件の典型:
・selectRaw + groupBy の組み合わせ
・union を含む
・distinct の対象が複雑
対処案(目的に応じて選ぶ):
・集計結果をページングしたいなら、集計後の結果をサブクエリ化してからpaginateする
・総件数が不要なら simplePaginate() にしてcountを避ける

// OK: countを省略して軽量化(総件数・最終ページは出ない)
$rows = DB::table('logs')
  ->select(['id', 'level', 'created_at'])
  ->orderByDesc('id')
  ->simplePaginate(50);

集計をサブクエリ化する例(概念)。

$sub = DB::table('orders')
  ->selectRaw('user_id, COUNT(*) as cnt')
  ->groupBy('user_id');

$rows = DB::query()
  ->fromSub($sub, 't')
  ->orderByDesc('cnt')
  ->paginate(20);

原因6:ORDER BY が無く、ページごとに並び順が揺れる(重複/抜けが出る)

エラーとして落ちないが「ページ2に行ったら同じデータが出る」「ページ移動で順序が変わる」系の“Pagination不具合”の本命。
発生条件:
・orderByなし
・orderByが非一意(created_atだけ等)で、同時刻データが多い
対処:必ず安定ソートを入れる(主キーを最後に追加)。

// OK: 安定ソート
$users = User::query()
  ->orderByDesc('created_at')
  ->orderByDesc('id')
  ->paginate(50);

大量データや高頻度更新テーブルでは cursorPaginate() も選択肢。

$users = User::query()
  ->orderBy('id')
  ->cursorPaginate(50);

サンプル:検索+ソート+ページネーション(クエリ維持込み)

検索条件がページ移動で消えない、順序が揺れない、という“事故りにくい”形。

use App\Models\User;
use Illuminate\Http\Request;

public function index(Request $request)
{
    $q = $request->query('q');
    $sort = $request->query('sort', 'id');
    $dir = $request->query('dir', 'asc') === 'desc' ? 'desc' : 'asc';

    $allowedSorts = ['id', 'name', 'created_at'];
    if (!in_array($sort, $allowedSorts, true)) {
        $sort = 'id';
    }

    $users = User::query()
        ->when($q, fn($qb) => $qb->where('name', 'like', '%'.$q.'%'))
        ->orderBy($sort, $dir)
        ->orderBy('id', 'asc') // 安定ソート
        ->paginate(20)
        ->withQueryString();

    return view('users.index', compact('users', 'q', 'sort', 'dir'));
}

チェックリスト(上から順に確認する)

1) links() を呼んでいる変数が paginate()/simplePaginate()/cursorPaginate() の戻り値か(get()のCollectionではないか)
2) paginate() を get() の後に呼んでいないか(Builder段階で呼んでいるか)
3) ページ移動で検索条件が消えるなら withQueryString()/appends() を使っているか
4) APIなら返却形式(data/links/meta)とフロントの期待が一致しているか
5) GROUP BY/UNION等があるなら countクエリが壊れていないか(simplePaginateで回避できないか)
6) orderBy が入っていて順序が安定しているか(同値が多いなら id も追加しているか)