[laravel]ミドルウェアでpaginate系アクションの前処理・後処理を行う

laravelにはミドルウェアという便利な機能があります。

デフォルトで用意してくれている「auth」とかは多くの方が使われているんじゃないでしょうか？

このミドルウェア、デフォルトではコントローラのアクションのロジック前処理をするのですが、
設定次第では**アクションを実行した後**に処理を行ったり、引数をとって**処理を動的に**することもできます。
(ちなみに、アクション後に実行するタイプを**[afterミドルウェア](https://readouble.com/laravel/5.2/ja/middleware.html)**といいます)

具体的にどんな使い方ができるのか、
実装した機能を例にとって解説していきたいと思います。
##やること
ページネーションのあるアクションで、
・「page:0,page:1」のリクエストが送られてきた時はパラメータを外し、
・ラストページ以降を参照した時は404エラーを返す。

##ミドルウェアの実装

###artisanコマンド
```terminal

php artisan make:middleware ValidatePage
```
app/Http/middlewareにミドルウェアが作成されます。
次に、app/Http/kernel.phpに登録。仕様する場面は限られているため「$routeMiddleWare」に登録します。

```php:kernel.php
protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    // 追加
    'page' => \App\Http\Middleware\ValidatePage::class,
];
```
で、登録したミドルウェアを使ってみましょう。

```php:route.php
Route::get('/book/page:{page}', "BookController@Index")->middleware('page');
```
これで、/bookにアクセスすればpageミドルウェアが動作してくれるようになりました。
###「page:0」「page:1」の時の処理
まずはbeforeミドルウェアです。先ほど作ったValidatePage.phpを下記のように追記します。

```php:ValidatePage.php
<?php

namespace App\Http\Middleware\Top;

use Closure;

class PageUrlValidate
{
    /**
     * Handle an incoming request.
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  $paginate_array ページネーション情報を含む配列の名前
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $page = (int) $request->route()->parameter('page');
        // pageパラメータが0,1の時はパラメータを取り除いたページへリダイレクトする
        if ($page === 0 || $page === 1) {
            $redirect_to = preg_replace('/\/page:[0-1]/', '', $request->fullurl());
            return redirect($redirect_to, 301);
        }
        return $next($request); // ここより前に書くと前処理してくれる
    }
}
```
これで`https://web-page.com/book/page/0`みたいなURLでリクエストが飛んできた時には0を取ってリダイレクトしてくれます。

###afterミドルウェア
続いて、アクション実行後の処理を書いていきます。

```
        $response = $next($request);
        // ここから下に書くと後処理になる

        $paginator = $response->original->$paginate_array ?? null;

        if ($this->isPaginator($paginator)) {
            // 引数を元にページネーション情報を含んだレコード情報を参照,最終ページより後を参照していたら404エラーにする
            if ($paginator->currentPage() > $paginator->lastPage()) {
                abort(404);
            }
        } else {
            abort(404);
        }

        return $response;
    }

    // ページネーションを持っているかを判定
    public function isPaginator($paginator): bool
    {
        return $paginator instanceof LengthAwarePaginator;
    }
}
```
最後のページが何番なのかは、実際にレコードを取得してこないと分かりません。
そのため、afterミドルウェアの実行タイミングはまさにうってつけといえますね！


###メリット
####①クローラビリティの向上
paginate()を使った場合、**最初の1ページ目のURLはページ情報のパラメータ「page」がありません**。
ユーザーが次のページへのリンクを押して以降pageパラメータが付きます。

・・・なのですが、実際には手打ちで0や1を指定しても表示できてしまいます。

```
hogehoge.co.jp/book         // 通常表示した場合の1ページ目 1~10件を表示 
hogehoge.co.jp/book/page:0  // 1~10件目を表示
hogehoge.co.jp/book/page:1  // 1~10件目を表示
```

ユーザーがURLバーで直接0,1を打つ可能性は低いかもしれませんが、
クローラーがサイト内を巡回する場合、ページ型のコンテンツの0,1へのリクエストを送ることがあります。

異なるリクエストが来た時にも同じレスポンス(1ページ目の情報)を返してしまう = 重複コンテンツ
重複コンテンツ = 質の低い情報 と評価されかねません。
同じ情報を表示するならば、同じURLへリダイレクト処理をしてあげる方が適切な対応といえるでしょう。

####②Httpステータスの適正化
仮に全100件ある情報について、１ページ10件の情報を表示するようなviewを作る場合、
ラストページは10になります。

ですが、URLを弄って「page:20」とかを入力すると、
「表示する情報は0件だけど、Httpステータスは200」を返します。[Httpステータスの種類と意味](https://developer.mozilla.org/ja/docs/Web/HTTP/Status)

存在しないページにアクセスしているのに正常なステータスコードを返すのはおかしいですよね？


####注意点は？
やることは極力シンプルにした方がいいです。

例で挙げたようなafterミドルウェアは、「アクション実行後にページネーションを持ったレスポンスがある」という前提で作られています。

しかし、実際にはアクションの途中でページ情報を持たないレスポンスを返す可能性もありえます。
findOrFailを使った場面なんてそうですね。

似て非なるシチュエーションの処理を一つのミドルウェアに実行させるとバグをうむ可能性があるので、
ミドルウェアを２つに分割するといった対応を検討するのがいいかもしれませんね。