Laravelアプリケーション学習⑫（ミドルウェア）

本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。

#目次
1.Laravelのアーキテクチャ
2.アプリケーションのアーキテクチャ
3.<font color="LimeGreen">HTTPリクエストとレスポンス</font>
4.データベース
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践

#3．HTTPリクエストとレスポンス

#3.4 ミドルウェア
Laravelのミドルウェアはコントローラクラスの処理前後に位置し、主にHTTPリクエストのフィルタリングやHTTPレスポンスの変更を担う。
Laravelで提供されているミドルウェアは以下の３種類

１．システム全体で使用するミドルウェア***（グローバルミドルウェア）***
２．特定のルートに適用されるミドルウェア***（ルートミドルウェア）***
３．コントローラクラスのコンストラクタで指定するミドルウェア***（コンストラクタ内ミドルウェア）***


HTTPリクエストがコントローラのアクションメソッドに到達するまでに、グローバルミドルウェア、ルートミドルウェア、コンストラクタ内ミドルウェアの順に通過する。
コントラクタのアクションメソッドからレスポンスが返されるとき、再びミドルウェアを通るため、必要に応じてレスポンス内容の変更や新たなレスポンスの生成が可能である。
これらの処理はIlluminate\Pipeline\Pipelineクラスがそれぞれのミドルウェアを実行する

## デフォルトで用意されているミドルウェア
laravelでは標準で多数のミドルウェアが用意されており、app/Http/kernel.phpのapp\Http\Kernerクラスで定義されている。ミドルウェアは指定された順番で実行されるので注意が必要。

### グローバルミドルウェア
グローバルミドルウェアは、ルーターに登録されたコントローラクラスが動作する前に実行される。
***→グローバルミドルウェアではルート情報を取り扱う処理ができない***

下記にグローバルミドルウェアを示す。

| 1 | 2 |
|:-:|:--|
| ***ミドルウェア***  | ***概要***  |
| App\Http\Middleware\PreventRequestsDuringMaintenance  |  メンテナンスの場合のふるまいを変更することができ、すべてアクセスに対してメンテナンス画面を表示、または特定のURIをメンテナンスから除外 |
| Illuminate\Foundation\Http\Middleware\ValidatePostSize  |リクエストボディのサイズをチェックし、この値が不正な場合はIlluminate\Http\Exception\PostTooLargeExceptionをスローする。   |
| Illuminate\Foundation\Http\Middleware\ConvertEmptyStringToNull  | リクエストの中で空文字をすべてNULLに変更  |
| App\Http\Middleware\TrimStrings  | リクエスト文字列に対してtrim処理を行い空文字を削除する。exceptプロパティにこの処理を除外したいリクエストパラメータを指定できる  |
| App\Http\Middleware\TrustProxies  | リバースプロキシーやロードバランサを使用している環境で、アプリケーション内でHTTPSのリンクなどが生成されない場合、信頼できるipなどproxiesプロパティの配列で追記することで作用する。CDNなどを利用する場合はこのミドルウェアに記述する必要がある  |
| Fritcake\Cors\HandleCors  | LaravelでAPIを提供する際、異なるドメインからJavascriptなどを介したコールでHTTPリクエストを制限する。Apache HTTP ServierやNginxなどでCORSを設定している場合は不要 |
|  App\Http\Middleware\TrustHosts |  信頼できるホストを記述し、それ以外のホストからのアクセスを制限する。単一のWebサーバで単一サイトをホストしている場合は不要だが、１つのアプリケーション内でサブドメインなどで分割しているケースなどで利用できる | （）

### ルートミドルウェア
デフォルトではwebミドルウェアグループに記述されている。下記にルートミドルウェアを示す。

| 1 | 2 |
|:-:|:--|
| ***ミドルウェア***  |*** 概要***  |
| App\Http\Middleware\EncryptCookies  |  クッキーの暗号化及び復号を行う。他のアプリケーションで発行されたクッキーは復号できないため、復号対象から除外したいクッキーをexceptプロパティの配列で指定する |
| App\Http\Middleware\VertifyCsrfToken |CSRF対策のXSRF-TOKEN発行やトークンのチェックを行う。HEADリクエスト、GETリクエスト、OPTIONSリクエスト以外を対象に動作する。対象から外したい場合はURIをexceptプロパティで指定する。  |
| Illuminate\Coockie\Middleware\AddQueuedCookiesToResponse | Cookie::queueで登録した値をレスポンスにクッキーとして追加して返却する。  |
| Illuminate\Session\Middleware\StartSession  | セッションを有効にし、レスポンス返却時にセッションに書き込む。 |
| Illuminate\Session\Middleware\AuthenticateSession  | パスワード変更時に他のデバイスでログインしている、対象のユーザーをログアウトさせる  |
| Illuminate\View\Middleware\ShareErrorsFromSession  | Bladeテンプレートのerrors変数に、セッションのerrorsキーから取得したエラー内容を埋め込む |
| Illuminate\Routing\Middleware\SubstituteBindings |  Eloquentモデルと結合させて、ルートに利用されるidなどからデータベース検索を行い、コントローラクラスやルートで利用可能にする。この機能はRoute Model Bindingと呼ばれる | （）


### 名前付きミドルウェア
ルーターへの登録またはコントローラクラスのコンストラクタなどで任意の名前で指定して利用できる
下記に名前付きミドルウェアを示す。

| 1 | 2 | 3 |
|:-:|:-:|:--|
| ***ミドルウェア名***  |  ***ミドルウェアクラス*** | ***概要*** |
|  auth | Illuminate\Auth\Middleware\Authenticate  | 認証済みユーザーかどうかを判定し、認証済みユーザーでない場合、Illuminate\Auth\AuthenticationExceptionがスローされる |
| auth.basic|   Illumiante\Auth\Middleware\AuthenticateWithBasicAuth|Basic認証を行う
|bindings| Illuminate\Routing\Middleware\SubstituteBindings  | グローバルミドルウェアで記述されているものと同一のミドルウェア  |
| cache.headers  | Illuminate\Http\Middleware\Authorize  | ETag（エンティティタグ）を利用しコンテンツのキャッシュを制御します。  |
|  can | Illuminate\Auth\Middleware\Authorize  | 特定のモデルやリソースへのアクションに対して認可を行う。この機能は認可機能と組み合わせて使う  |
|  throttle |  Illuminate\Routing\Middleware\hrottleRequests | 同一ユーザーが単位時間内に規定回数以上のアクセスを行ったかどうか判定する。規定回数以上アクセスした場合はIlluminate\Http\Exceptions\ThrottleRequestsExceptionがスローされる  |
| signed  | Illuminate\Routing\Middleware\ValidateSignature  | 署名付きのアクセスで有効な署名かどうか、制限時間内のアクセスかどうかを判定する  |
| guest  | App\Http\Middleware\RedirectlfAuthenticated  | 認証済みかどうかを判定し、認証済みの場合は/homeにリダイレクトする。リダイレクト先が固定されているので、アプリケーションに合わせたミドルウェアを用意する必要がある。  |
|  verified |  Illumiante\Auth\Middleware\EnsureEmailsVerified | メールアドレス認証済みユーザーのみにアクセスを許可をするルートミドルウェア。利用にはIlluminate\Contracts\Auth\MustVerifyEmailインターフェースを定義したクラスをauthドライバ経由で利用しなればならない。  | （）


このようにlaravelでは標準で様々なミドルウェアが準備されているが、不要なミドルウェアは除外しておくと、パフォーマンスが向上するケースがあるので、利用するミドルウェアを事前に調査しておいた方がよい。

## 独自のミドルウェアの実装
本記事では、リクエストヘッダとレスポンスヘッダをログに書き出す独自のミドルウェアを実装していく。なお、グローバルミドルウェアやルートミドルウェアに実装の違いはなく、どのタイミングで実行させるかを指定するだけである。

###　①ミドルウェアクラスの生成
リクエストヘッダとレスポンスヘッダをログに書き出すミドルウェアとして、HeaderDumperクラスを作成する。

```
php artisan make:middleware HeaderDumper
```
app/Http/Middlewaresディレクトリ配下にHeaderDumperクラスが生成される。

### ②リクエストヘッダログの出力
作成したクラスにリクエストヘッダのログを出力するように変更する。

```php
<?php
declare(strict_types=1)

namespace App\Http\Middleware;

use Closure;
use Illumiate\Http\Request;
use Psr\Log\LoggerInterface;

use function info;
use function strval; // 文字列に変換する関数

final class HeaderDumper
{
   private $logger;
   
   public function __construct(LoggerInterface $logger)
   {
      $this->logger = $logger;
   }

   public function handle(Request $request, Closure $next)
   {
      $this->logger->info('request', ['header' => strval($request->headers)]);

      // ヘルパー関数を利用する場合は以下の通り
      // info('request', ['header' => strval($request->headers)]);
     return $next($request);
   }
}
```
***＜コード解説＞***
上記コードに示す通り、リクエストインスタンスがhandleメソッドに渡された場合に、headersプロパティにアクセスしてリクエストヘッダを取得する。このプロパティはSymfony\Component\HttpFoundation\HeaderBagインスタンスとなるので、文字列にキャストするか、__toStringメソッドを用いてことで、リクエストヘッダを文字列で取得する。
取得した文字列をログに書き込むために、コンストラクタインジェクションでPsr\Log\LoggerInterfaceを読み込んでいる。（infoヘルパー関数でも可能。その場合Psr\Log\LoggerInterfaceは不要）

### ③レスポンスヘッダのログ出力
レスポンスヘッダを取得するには、ミドルウェアクラス内のhandleメソッドに記述されている`$next($request)`の戻り値を取得する。
ミドルウェアクラス以降に順次ミドルウェアとコントローラクラスを実行し、その戻り値が返却される。
レスポンスヘッダはリクエストヘッダと同様にheadersプロパティにアクセスし、インスタンスを文字列にキャストすることで取得する。

```php
public function (Request $request, Closure $next)
{
   $this->logger->info('requet', ['header' => strval($request->headers)]);

   $response = $next($request);
   $this->logger->info('response', ['header' => strval($response->headers)]);

   return $response;
}
```

### ④ミドルウェアの登録
ミドルウェアの登録は`App\Http\kernel`クラスに追記する。
グローバルミドルウェアで最初に動作するようにすることで（先頭に記載）、最後に出力されるレスpん巣を取得可能にしている。
下記にKernel.phpを示す。

```php
<?php
declare(strict_types=1);

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as Httpkernel;

class Kernel extends HttpKernel
{
   protected $middleware = [  // ①
      \App\Http\Middleware\HeaderDumper::class, // 作成したミドルウェアを追記
      \Illuminate\Foundation\Http\Middleware\CheckFormaintenanceMode::class,
      --[略]--
   ];

   protected $middlewareGroups = [ // ②
      --[略]--
   ];

   protected $routeMiddleware = [ // ③
      --[略]--
   ];
}
```
***＜コード解説＞***
①グローバルミドルウェアの定義はmiddlewareプロパティの配列に文字列でクラス名を定義する。全アクセスに対して適用したいミドルウェアはここに定義する。
②複数のミドルウェアをまとめて扱いたい場合は、middlewareGroupsプロパティにグループ名とともに文字列でクラス名を定義する。
③特定のルートやコントローラに対してミドルウェアを適用する場合は、routeMiddlewareプロパティにキー名とともにクラス名を定義する。

なお、標準で登録されている「web」グループと「api」グループは、ルートの定義のroute/web.phpを通る処理に対しては、必ず「web」グループのミドルウェアが適用され、route/api.phを通る処理に対しては、「api」グループが適用される。
これは、App\Providers\RouteServiceProviderクラスに記述されており、アプリケーションに合わせてミドルウェアが属数グループを変更したい場合は、RouteServiceProviderクラス内の指定を変更すればよい。