LaravelのMiddlewareでcookieがリクエストから取得できない場合にやること

requestのヘッダにはcookieが入っているのに、Laravelからcookieを取得しようとすると取得できないと言う現象にハマってしまいました…
`$_COOKIE['EXAMPLE']`で該当のcookieを取得できるのに、
`$request->cookie('EXAMPLE')` だと取得できない…

調査して解決したのでメモしときます。

## TL;DR

Laravelでcookieを扱うミドルウェアを追加する場合は、`EncryptCookies`ミドルウェアの後に定義しましょう。

## 環境

|名前|バージョン|
|---| -------- |
|PHP | 7.1.6    |
|Laravel| 5.5.14 |

## 現象

どのURLでもcookieが設定されていなければ付与し、設定されていればそのままにする、という処理を実装する必要がありました。
全てのURLが対象となるのでMiddlewareで実装することにしました。
以下のMiddlewareです。
リクエストヘッダにcookieがあるかチェックし、なければレスポンスにcookieを付与します。

```ExampleMiddleware.php
<?php

namespace App\Http\Middleware;

use Closure;

class ExampleMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        if (!$request->cookie('EXAMPLE')) {
            $response->cookie('EXAMPLE', rand(), 60 * 24);  // 1日
        }

        return $response;
    }
}
```

Kernel.phpを修正して作成したミドルウェアを読み込ませるようにします。
作成したMiddlewareをKernel.phpに登録します。

```Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\UserTrackCookie::class, // ★追加
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    ];
}
```

これで動作するかと思いましたが期待した動作になりません。
cookieがあれば同じ値を使用し続けてほしいのですが、ExampleMiddlewareで毎回rand()で新しい値がセットされてしまいます。
`$_COOKIE['EXAMPLE']` でvar_dumpすると値が入っていますが、`$request->cookie('EXAMPLE')`をvar_dumpしても値が入っていません。

## やったこと

[Laravelのマニュアル](https://readouble.com/laravel/5.5/ja/requests.html) を読むと

> Laravelフレームワークが作成するクッキーは全て暗号化され、認証コードで著名されています。つまりクライアントにより変更されると、無効なクッキーとして取り扱います。

とありました。
もしかして無効にされているんじゃないか？と思って、EncryptCookiesミドルウェアの`$except`にcookie名を追加して暗号化対象外にしました。
そうすると、 `$request->cookie('EXAMPLE')` で値が取得できるようになりました。

変更されると無効になるということは、登録したMiddlewareの順序によっては値が変わってしまって取得できないこともあるのでは？と検討をつけました。

EncryptCookiesミドルウェアはLaravelプロジェクトを作成すると自動で生成されるファイルです。
実態は`Illuminate\Cookie\Middleware\EncryptCookies` にあります。
このクラスのhandleメソッドを見ると

```Illuminate\Cookie\Middleware\EncryptCookies.php
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $this->encrypt($next($this->decrypt($request)));
    }
```

とあり、リクエストを復号化したのち次のミドルウェアに処理を委譲しその戻り値を暗号化しています。
ミドルウェアの実行順序ですが、

1. `$middleware`に登録された順でリクエストが処理される
2. `$middlewareGroups`に登録された順でリクエストが処理される
3. `$routeMiddleware`のうちのミドルウェアの利用を宣言した順でリクエストが処理される
4. `$routeMiddleware`のうちのミドルウェアの利用を宣言した逆順でレスポンスが処理される
5. `$middlewareGroups`に登録された逆順でレスポンスが処理される
6. `$middleware`に登録された逆順でレスポンスが処理される

なので、失敗したケースでは
１ExampleMiddleware -> ２EncryptCookies(request復号化) -> ３EncryptCookies(response暗号化) -> ４ExampleMiddleware
の順で処理されています。

> Laravelフレームワークが作成するクッキーは全て暗号化され、認証コードで著名されています。つまりクライアントにより変更されると、無効なクッキーとして取り扱います。

復号化された値と暗号化された値で比較するために、値が変わって無効扱いにされてしまって取得できないものと思われます。

順序を以下に入れ替えます。

１EncryptCookies -> ２ExampleMieddleware -> ３ExampleMieddleware -> ４EncryptCookies

この順序だと暗号化対象外の`$except`に入れなくても`$request->cookie('EXAMPLE')`で値を取得することができました！
復号化された値と復号化された値で比較するため、同じものと認識されるので取得できるものと思われます。

`$middleware`での登録をやめ、`$middlewareGroups`で登録するよう変更しました。

```Kernel.php
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \App\Http\Middleware\UserTrackCookie::class, // ★必ずEncryptCookiesより順序は後ろにすること
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];
```

Laravelのミドルウェアでcookieを読み込んだりセットする場合は`EncriptCookies`ミドルウェアより前に定義してしまうと、cookieが取得できない場合があるので注意！

Laravelでcookieを扱うミドルウェアを追加する場合は、`EncryptCookies`より前に定義せず、**後に定義しましょう。**

## 参考

[Laravel 5.5 リクエストの取得](https://readouble.com/laravel/5.5/ja/requests.html)