【初Laravel】ワイ、POSTでページネーションを実装する

#はじめに
この記事はやめ太郎氏(@Yametaro )のワイ記法を参考に書かせていただいています。
せっかくなので、僕の故郷である長崎の方言、長崎弁で綴ろうと思います。
長崎弁では自分のことを「オイ」、相手のことを「ワイ」と呼びますが、一応ワイ記法はワイを一人称としているので、例外的に本記事でもワイは自分のことを指すこととします。

#ある日、大学の研究室にて
SJ教授「おい、ひろすぐ、頼みたいことがあるんだけど」

ワイ「お、ゴミ出しならやらんですよ？」

SJ教授「ゴミ出しならお前らがやらないから俺が朝やったよ。ちょっと作ってほしい**webアプリ**があるの」

ワイ「webアプリ？ついに我が研究室も**研究費でビジネス**始めるとですか？」

SJ教授「（それはダメだろ・・・）」
SJ教授「うちの研究室の歴代の**論文をデータベース化**して、簡単に**アブストラクトをリストで検索できる**ようなwebアプリを作ってほしいの、できる？」

ワイ「ほーん、**余裕**ですばい！まっちょってください！」

SJ教授「ただし、すでにある研究室のwebサイトにマージしたいから、バックエンドは**Laravel**で作ってね。」

ワイ「ら・・・ららべる・・・？そんなん知らんとですけど・・・」

SJ教授「お前、**Twitterで学生エンジニアぶってる**くせに、Laravel知らないんかい」
SJ教授「とりあえず、そういうことなので、頑張って調べながらやってくれや。」
SJ教授「仕様書送っとくから、よろしく〜」

ワイ「（雑すぎやろ・・・）」
ワイ「とりあえず仕様書見ながら頑張るしかなかね・・・」

Slack「ｽｺｺｺｯ」

#仕様書を見てみると
ワイ「お、**仕様書**が送られて来とるな、どれどれ・・・」
![スクリーンショット 2020-06-15 4.40.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333871/b6ed7ed1-f0c5-8705-a290-2d6f161b8119.png)


ワイ「**論文探し太郎**・・・」
ワイ「勝手にこんな名前つけてたら、**やめ太郎さん**に怒られるばい・・・」

ワイ「ていうか、**URLを綺麗にしておきたい**って**どういうこだわり**持っとらすとかね・・・、めちゃめちゃ強調しとらすやん・・・」

ワイ「まあありがちなサイトたいね〜。これなら簡単にできそうばい」
ワイ「とりあえず、Laravelの基本的な使い方ば勉強してから、試しに作ってみよかね。」

#頑張ってある程度形はできたものの
ワイ「とりあえず見た目はできた！」
ワイ「あれれ、**ページネーション**がうまくいかんね、なんでやろ・・・」
ワイ「２ページ目以降のリンクをクリックすると、**検索キーワードがリセット**されてしまう・・・」
ワイ「Laravelには、`paginate()`という便利なメソッドがあって、**クエリにくっつけてアイテム数を指定するだけ**で簡単にページネーションが実現できるってQiitaに書いてあったとけどなぁ」

マトラボ先輩「**POSTで投げて**るからだろうね〜」

ワイ「わ！びっくりした！**マトラボパイセン**じゃなかですか！」
ワイ「も、もしかして、**マル秘の仕様書**を盗み見してたとですか・・・？」

マトラボ先輩「せっかく助けてあげようとしてたのに・・・」
マトラボ先輩「ていうか、**同じ研究室**なんだから別にいいでしょ」

ワイ「確かに・・・」
ワイ「師匠、助けてください・・・」

マトラボ先輩「良かろう。」
マトラボ先輩「２ページ目以降で検索条件がリセットされちゃうのは、**POSTでキーワードを投げている**ので、**URLでその値が保持できてない**んだね」
マトラボ先輩「そもそも、最初の検索するときの通信は**POST**なのに、ページリンク押したら**GET**で通信するんだから、それなんとかしないとね」

ワイ「POSTだと**無理**とですか？」

マトラボ先輩「うーん、`pagenate()`単体だと**難しい**かも。」
マトラボ先輩「ほら、GETでのリクエストだと、URLに`?hoge=fuga`みたいな感じで、検索キーワードを持っておけるでしょ？」
マトラボ先輩「こうしておけば、今の**ページ番号**と**検索キーワード**をパラメータとして持たせておいて、**ページネーション**が実現できるんだけど・・・」
マトラボ先輩「POSTでのリクエストは、URLには何もつかないからね」
マトラボ先輩「調べる限りLaravelの`paginate()`も**GETでのリクエストにしか対応していない**みたいだし」

ワイ「そ、そんなぁ・・・」
ワイ「教授に**仕様変更願**を出さんばですね、これはワイには不可能ですばい」
ワイ「わ、ワイのせいじゃなかですよ！**Laravelが悪か**とです！バーカバーカ！」

マトラボ先輩「（こいつ、ガキかよ・・・）」
マトラボ先輩「せっかくだし、**ページネーション実装**してみなよ。POSTでもできるはずだよ。」

ワイ「じ、自分で実装・・・？」

マトラボ先輩「何事も勉強！ページネーションなんて、**webエンジニアの９割は10分で実装できる**と言われているんだから、ひろすぐ君もできるようにならなきゃ！」

ワイ「９割が10分で実装できる・・・？この業界は**天才しかおらん**とですか・・・？」
ワイ「パイセン、手取り足取り教えてください！」

マトラボ先輩「あ、いや、僕は**研究**があるから！自分で頑張ってね〜、バイバイ」

ワイ「えええええええ！」
ワイ「助けてくれんとですか・・・、トホホ」

#ページネーションの実装と言われても
ワイ「そもそもページネーションってどんな仕組みで動いとるとかね」
ワイ「とりあえず**ググって**まとめてみるばい！」

>
ページング機能を実装する上で必要な情報は以下の通り
1. １ページあたり何件表示するか（limit）
2. 今何ページ目にいるのか
3. そのページはデータの何番目から始まるのか（offset）
4. 全部で何件あるのか
5. 全部で何ページあるのか
>
SQLのクエリでは、offsetとlimitを使った検索方法があって、部分的に結果を返すことが可能

#ワイ、完全に理解する

ワイ「ふむふむ、なんとなく分かってきたばい」
ワイ「つまり、こういうことたい」
ワイ「部分的な結果を表示させたい時は、**検索キーワード、offset、limit**で検索をかける。」
ワイ「ページリンクを押した時に、offset、limitに加えて検索キーワードを再送信することで、欲しいページの内容をリクエストして、再描画すればよかったい！」
ワイ「今日は冴えとる！！」

#実際にコードを書いてみる
ワイ「取りあえず、**テンプレート側**から書こうかね。」
ワイ「まずは、**値を持たせておく機構**を作るばい」

```php:ronbun.blade.php
<form action="{{ url('/')}}" method="POST" name="search_form" id="search-form">
  @csrf
  <input type="hidden" id="in-offset" name="offset" value="{{$offset}}"> //オフセット
  <input type="hidden" id="in-limit" name="limit" value="{{$limit}}"> //リミット
  <input type="hidden" id="in-key" name="key" value="{{$keyword}}"> //検索キーワード
						
  <input type="text" name="keyword" placeholder="キーワード" value="{{$keyword}}">
  <button type="submit" id="post">検索</button>
</form>
```
ワイ「こんな感じで、`hidden`にそれぞれの値を持たせておけば、リンクを押した時に`value`を書き換えて送信できるばい」
ワイ「よし、次は、リンクの部分を作っていくばい」
ワイ「デザインはめんどくさいから**Bootstrapくん**になんとかしてもらおう・・・」

```php:ronbun.blade.php
<nav aria-label="Page navitation" class="but-phone">
  <ul class="pagination justify-content-center">
  @if($count == 0) 
    <div>そんな研究ありませ〜ん</div>
  @else
    //「最初に戻る」、「前へ」ボタン
    <li class="page-item nonactive toFirst"><a class="page-link first" href="#">«</a></li>
    <li class="page-item nonactive toPrev"><a class="page-link prev" href="#">‹</a></li>

    //ページの数分ページリンクを生成							
    @for($i = 0; $i < pageNum; $i ++)
      //カレントページならリンクに色をつける
      @if($i == $pageNumber)
        <li class="page-item active page" data-offset="{{$i*10}}"><a class="page-link link-num" href="#">{{$i+1}}</a></li>
      @else
        <li class="page-item nonactive page" data-offset="{{$i*10}}"><a class="page-link link-num" href="#">{{$i+1}}</a></li>
      @endif
    @endfor

    //「最後に飛ぶ」、「次へ」ボタン
    <li class="page-item nonactive toNext"><a class="page-link next" href="#">›</a></li>
    <li class="page-item nonactive toEnd"><a class="page-link end" href="#">»</a></li>
  @endif
  </ul>
</nav>
```
ワイ「まずは**”前へ”ボタン**と、**”最初に戻る”ボタン**を作って・・・」
ワイ「そこからページリンクを**ページ数分**回して作る」
ワイ「**カレント**のページリンクには**色をつけたい**から、`class`ば`active`にして、そうでないものは`nonactive`にしちょる」
ワイ「最後に、**”次へ”ボタン**と、**”最後へ飛ぶ”ボタン**を作って完成やね」
ワイ「`data-offset`に**自分のページの時のoffsetを持たせておけば**、クリックされた時にその値を送信すればよかけん、楽やね」
ワイ「さて、そしたら次は**送信する機構**を作るばい」
ワイ「リンクを押したら**`hidden`を書き換えて送信**したいから、**JavaScript**で書けばよかね」
ワイ「**jQuery**、苦手やけど、練習やと思って頑張るばい」

```javascript:script.js
$(function () { 
  //必要な値の取得
  const offset = $('#in-offset').val();
  const limit = $('#in-limit').val();
  const pages = ???
  
  $(document).on('click', '#post', function () {
    //検索ボタンを押した時
    $('#in-offset').val(0);
  }).on('click', '.page', function () {
    //ページリンクを押した時
    const pageoffset = $(this).attr('data-offset');
    $('#in-offset').val(pageoffset);
    $('#search-form').submit();
  }).on('click', '.toFirst', function () {
    //「最初に飛ぶ」ボタンを押した時
    $('#in-offset').val(0);
    $('#search-form').submit();
  }).on('click', '.toPrev', function () {
    //「前へ」ボタンを押した時
    $('#in-offset').val(Number(offset) - Number(limit));
    $('#search-form').submit();
  }).on('click', '.toNext', function () {
    //「次へ」ボタンを押した時
    $('#in-offset').val(Number(offset) + Number(limit));
    $('#search-form').submit();
  }).on('click', '.toEnd', function () {
    //「最後に飛ぶ」ボタンを押した時
    $('#in-offset').val((Number(pages)-1)*Number(limit));
    $('#search-form').submit();
  });
});
```
ワイ「作りよって思ったけど・・・」
ワイ「これ、**ページの総数**がないと**最後に飛ぶ**の計算ができんたい」
ワイ「JSで取ってきたいけん、`hidden`に**追加する**ばい」
ワイ「なんか、あとで**ヒット件数も使う**って**ワイの勘**が言っとる・・・」
ワイ「今は使わないけど、**とりあえず追加**しとこ・・・」

```php:ronbun.blade.php
<form action="{{ url('/')}}" method="POST" name="search_form" id="search-form">
  @csrf
  <input type="hidden" id="in-offset" name="offset" value="{{$offset}}"> //オフセット
  <input type="hidden" id="in-limit" name="limit" value="{{$limit}}"> //リミット
  <input type="hidden" id="in-key" name="key" value="{{$keyword}}"> //検索キーワード
  <input type="hidden" id="in-pages" name="pages" value="{{$pages}}"> //ページ数
  <input type="hidden" id="in-count" name="count" value="{{$count}}"> //ヒット件数
						
  <input type="text" name="keyword" placeholder="キーワード" value="{{$keyword}}">
  <button type="submit" id="post">検索</button>
</form>
```
ワイ「これでJSは完成ばい」

```javascript:script.js
$(function () { 
  //必要な値の取得
  const offset = $('#in-offset').val();
  const limit = $('#in-limit').val();
  const pages = $('#in-pages').val();
  const count = $('#in-count').val(); //あとで使う予感がする
  
  $(document).on('click', '#post', function () {
    //検索ボタンを押した時
    $('#in-offset').val(0);
  }).on('click', '.page', function () {
    //ページリンクを押した時
    const pageoffset = $(this).attr('data-offset');
    $('#in-offset').val(pageoffset);
    $('#search-form').submit();
  }).on('click', '.toFirst', function () {
    //「最初に飛ぶ」ボタンを押した時
    $('#in-offset').val(0);
    $('#search-form').submit();
  }).on('click', '.toPrev', function () {
    //「前へ」ボタンを押した時
    $('#in-offset').val(Number(offset) - Number(limit));
    $('#search-form').submit();
  }).on('click', '.toNext', function () {
    //「次へ」ボタンを押した時
    $('#in-offset').val(Number(offset) + Number(limit));
    $('#search-form').submit();
  }).on('click', '.toEnd', function () {
    //「最後に飛ぶ」ボタンを押した時
    $('#in-offset').val((Number(pages)-1)*Number(limit));
    $('#search-form').submit();
  });
});
```
ワイ「`#post`のクリックは、**検索ボタン**のことやね」
ワイ「これは、どのページからでも**新しいキーワードで正しく検索できる**うにしとるとばい」
ワイ「こればせんと、そのまま**カレントのoffsetが送信されて**しまって、１ページ目から表示できない可能性があるけんね」
ワイ「そのあとからは、ページリンクや、矢印リンクを押した時に、**正しいoffsetをセット**して、**サーバーに送信**しとるったいね」

ワイ「さてさて、最後は**検索する機構**ば作らんばね」
ワイ「これは、**コントローラー**に書けばよかね」

```php:RonbunController.php
//１ページあたりに表示する件数
const LIMIT = 10;

  public function search(Request $request){
    //キーワード取得
    $keyword = $request->input('keyword');

    //ヒット件数取得
    $count = Ronbun::where('title', 'LIKE', "%$keyword%")
    ->orWhere('author', 'LIKE', "%$keyword%")
    ->orWhere('predation', 'LIKE', "%$keyword%")
    ->count();

    //オフセット取得
    $offset = (int)$request->input('offset');
    if(!isset($offset) || ($offset < 0 && $offset > $count )){
      $offset = (int)0;
    }
		
    //ページ数計算
    $pages = ceil($count / self::LIMIT);

    //現ページ番号計算
    $pageNumber = $offset / self::LIMIT;
	 
    //検索
    $sql = Ronbun::where('title', 'LIKE', "%$keyword%")
    ->orWhere('author', 'LIKE', "%$keyword%")
    ->orWhere('predation', 'LIKE', "%$keyword%")
    ->offset($offset)
    ->limit(self::LIMIT)
    ->get();

    //テンプレートにデータを渡す
    return view('plist', ['data'=> $sql, 'keyword'=>$keyword, 'offset'=>$offset, 'pages'=>$pages, 'pageNumber'=>$pageNumber, 'count'=>$count, 'limit'=>self::LIMIT]);
  }
```
ワイ「よっしゃ！書けたばい！」
ワイ「動くかな？？」

# 動かしてみると
ワイ「あれれ、リンクを押すと、**ページの先頭にスクロールする**だけやん」
ワイ「なんで・・・？」
ワイ「うーん、リンクの遷移先が`#`やけんかなあ」
ワイ「でも、そうしておかんばなあ」
ワイ「そもそも**カーソル当ててもポインタにならん**たいね」
ワイ「CSSで**なんとかなる**やろ」

```css:style.css
/*リンクの無効化*/
.page-link{
  pointer-events: none;
}

/*ホバー時にポインタにする*/
.page-item{
  cursor : pointer;
}

/*ホバー時に色を変える*/
.nonactive:hover  .page-link{
  background-color: #dee2e6;
  border-color: #dee2e6;
}
```
ワイ「おっし、これで**リンクは無効**にできたばい」
ワイ「ちゃんとカーソル当てたらポインタになるし、ついてに**ホバーした時色変わる**お洒落仕様にしたったけんね」

#２度目の挑戦
ワイ「ちゃんとできるかな・・・（ドキドキ）」
ワイ「ポチっとな」

ワイ「**できた！！！**」
ワイ「ついにできたで！！」
ワイ「あれれ」
ワイ「これ、**１ページ目の時**でも**"戻る"**ボタンが押せてしまうばい」
ワイ「もしかして・・・」
ワイ「やっぱり！**最後のページの時**にも**”次へ”ボタン**が押せてしまうたい！」
ワイ「確か、**Bootstrapくん**は、`class`に`disable`を追加してやれば押せなくできたはず・・・」
ワイ「ページ読み込み時に**カレントのページ番号を確認**して、`class`ばいじるか・・・」

#ここで勘が的中
ワイ「はっっっ・・・！」
ワイ「ここでさっきの`count`ば使うったい！」
ワイ「**なんちゅう勘**・・・」

```javascript:script.js
//矢印リンクの非活性化
//カレントが１ページ目の時
if (Number(offset) == 0) {
  $('.toFirst').addClass('disabled');
  $('.toPrev').addClass('disabled');
} else {
  $('.toFirst').removeClass('disabled');
  $('.toPrev').removeClass('disabled');
}
//カレントが最後のページの時
if (Number(offset) == (Math.ceil(count / limit) - 1)* Number(limit)){
  $('.toEnd').addClass('disabled');
  $('.toNext').addClass('disabled');
} else {
  $('.toEnd').removeClass('disabled');
  $('.toNext').removeClass('disabled');
}
```

```css:style.css
/*矢印リンクの非活性化*/
.disabled{
  pointer-events: none;
}
```
ワイ「**もうちょいいい書き方**あったかもしれんけど・・・汗」
ワイ「とりあえず、**カレントページのoffsetを確認**して、最初のページと最後のページの時は**矢印リンクを使えんく**してみたばい」

#何はともあれ完成
ワイ「ついに**完成**したばい・・・涙」

SJ教授「お、ひろすぐ、もうできたのか」
SJ教授「**Laravelの勉強**にはなったかい」

ワイ「めちゃめちゃ疲れましたよ・・・」
ワイ「後半はLaravelの勉強というより**ページネーションの勉強**でしたし・・・」

SJ教授「ページネーション？そんなの**メソッドに渡して終わり**でしょ？」

ワイ「ふふん、教授、知らんとですか？」
ワイ「Laravelの`paginate()`メソッドでは、**POSTリクエストでのページネーションはできん**とですばい！」
ワイ「**全てワイが独自に実装した**とですばい！」

SJ教授「あー・・・、**そのこと**なんだけどさ・・・」

ワイ「え・・・？」

SJ教授「POSTのページネーション、**去年マトラボくんが実装**してくれてるから、どっか探せばメソッド出てくるはずだよ」

ワイ「へ？」
ワイ「**すでにあった**・・・だと・・・？」
ワイ「ワイの努力・・・」

#マトラボ先輩は隣の部屋で
マトラボ先輩「（ふふっ、**後輩を育てるのが先輩の仕事**だからね・・・！）」

**〜おわり〜**