ミドルウェア

ミドルウェアは一般的にリクエストまたはレスポンスをインターセプトするために使用されます。たとえば、コントローラーを実行する前にユーザーの認証を一元的に検証したり、ユーザーが未ログインのときにログインページにリダイレクトしたり、レスポンスに特定のheaderを追加したりすることができます。また、特定のURIリクエストの割合を統計することもできます。

ミドルウェアのオニオンモデル


            ┌──────────────────────────────────────────────────────┐
            │                     middleware1                      │ 
            │     ┌──────────────────────────────────────────┐     │
            │     │               middleware2                │     │
            │     │     ┌──────────────────────────────┐     │     │
            │     │     │         middleware3          │     │     │        
            │     │     │     ┌──────────────────┐     │     │     │
            │     │     │     │                  │     │     │     │
   ── Reqeust ───────────────────> Controller ── Response ───────────────────────────> Client
            │     │     │     │                  │     │     │     │
            │     │     │     └──────────────────┘     │     │     │
            │     │     │                              │     │     │
            │     │     └──────────────────────────────┘     │     │
            │     │                                          │     │
            │     └──────────────────────────────────────────┘     │
            │                                                      │
            └──────────────────────────────────────────────────────┘

ミドルウェアとコントローラーは、クラシックなオニオンモデルを構成しています。ミドルウェアは、一層一層のオニオンの皮のようで、コントローラーはオニオンの芯のようです。図に示すように、リクエストは矢のようにミドルウェア1、2、3を通過してコントローラーに到達します。コントローラーはレスポンスを返し、レスポンスはまた3、2、1の順でミドルウェアを通過して最終的にクライアントに返されます。つまり、各ミドルウェアではリクエストを受け取ることも、レスポンスを取得することも可能です。

リクエストのインターセプト

時には、特定のリクエストがコントローラー層に到達しないようにしたい場合があります。たとえば、middleware2で現在のユーザーが未ログインであることを検出した場合、このリクエストを直接インターセプトし、ログインレスポンスを返すことができます。このプロセスは次のようになります。


            ┌────────────────────────────────────────────────────────────┐
            │                         middleware1                        │ 
            │     ┌────────────────────────────────────────────────┐     │
            │     │                   middleware2                  │     │
            │     │          ┌──────────────────────────────┐      │     │
            │     │          │        middleware3           │      │     │       
            │     │          │    ┌──────────────────┐      │      │     │
            │     │          │    │                  │      │      │     │
   ── Reqeust ─────────┐     │    │    Controller    │      │      │     │
            │     │ Response │    │                  │      │      │     │
   <───────────────────┘     │    └──────────────────┘      │      │     │
            │     │          │                              │      │     │
            │     │          └──────────────────────────────┘      │     │
            │     │                                                │     │
            │     └────────────────────────────────────────────────┘     │
            │                                                            │
            └────────────────────────────────────────────────────────────┘

図に示すように、リクエストがmiddleware2に到達した後、ログインレスポンスが生成され、レスポンスはmiddleware2からmiddleware1を通過してクライアントに返されます。

ミドルウェアインターフェース

ミドルウェアはWebman\MiddlewareInterfaceインターフェースを実装する必要があります。

interface MiddlewareInterface
{
    /**
     * 入ってくるサーバリクエストを処理します。
     *
     * 入ってくるサーバリクエストを処理してレスポンスを生成します。
     * もしレスポンスが自分自身で生成できない場合は、提供されたリクエストハンドラに委譲することができます。
     */
    public function process(Request $request, callable $handler): Response;
}

つまり、processメソッドを実装する必要があります。processメソッドは、support\Responseオブジェクトを返す必要があります。デフォルトでは、このオブジェクトは$handler($request)によって生成されます(リクエストはオニオンの芯を進み続けます)。または、response()json()xml()redirect()などのヘルパ関数から生成したレスポンスであっても構いません(リクエストはオニオンの芯を進むのを停止します)。

ミドルウェア内でのリクエストおよびレスポンスの取得

ミドルウェア内ではリクエストを取得することができ、コントローラー実行後のレスポンスも取得することができます。したがって、ミドルウェアは3つの部分に分かれます。

  1. リクエストが通過する段階、すなわちリクエスト処理前の段階
  2. コントローラーがリクエストを処理する段階、すなわちリクエスト処理段階
  3. レスポンスが出ていく段階、すなわちリクエスト処理後の段階

3つの段階はミドルウェアの中で次のように示されます。

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Test implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        echo 'ここはリクエストが通過する段階、すなわちリクエスト処理前';

        $response = $handler($request); // オニオンの芯を進み続け、コントローラーを実行してレスポンスを取得

        echo 'ここはレスポンスが出ていく段階、すなわちリクエスト処理後';

        return $response;
    }
}

例:認証ミドルウェア

app/middleware/AuthCheckTest.phpというファイルを作成します(ディレクトリが存在しない場合は自分で作成してください)。内容は以下の通りです:

<?php
namespace app\middleware;

use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AuthCheckTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        if (session('user')) {
            // 既にログインしているため、リクエストはオニオンの芯を進み続ける
            return $handler($request);
        }

        // 反射を使って、コントローラーのどのメソッドがログインを必要としないかを取得
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // アクセスするメソッドはログインを必要とする
        if (!in_array($request->action, $noNeedLogin)) {
            // リクエストをインターセプトし、リダイレクトレスポンスを返し、リクエストはオニオンの芯を進むのを停止
            return redirect('/user/login');
        }

        // ログインは必要なく、リクエストはオニオンの芯を進み続ける
        return $handler($request);
    }
}

新しいコントローラー app/controller/UserController.php を作成します。

<?php
namespace app\controller;
use support\Request;

class UserController
{
    /**
     * ログインを必要としないメソッド
     */
    protected $noNeedLogin = ['login'];

    public function login(Request $request)
    {
        $request->session()->set('user', ['id' => 10, 'name' => 'webman']);
        return json(['code' => 0, 'msg' => 'login ok']);
    }

    public function info()
    {
        return json(['code' => 0, 'msg' => 'ok', 'data' => session('user')]);
    }
}

注意
$noNeedLoginには、現在のコントローラーでログインせずにアクセスできるメソッドが記録されています。

config/middleware.phpに全体ミドルウェアを次のように追加します。

return [
    // 全体ミドルウェア
    '' => [
        // ... ここで他のミドルウェアは省略
        app\middleware\AuthCheckTest::class,
    ]
];

認証ミドルウェアのおかげで、コントローラー層ではビジネスコードに専念でき、ユーザーがログインしているかどうかを心配する必要がなくなります。

例:CORSミドルウェア

app/middleware/AccessControlTest.phpというファイルを作成します(ディレクトリが存在しない場合は自分で作成してください)。内容は以下の通りです:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AccessControlTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // リクエストがOPTIONSの場合は空のレスポンスを返し、そうでない場合はオニオンの芯を進みながらレスポンスを取得
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // レスポンスにCORSに関するHTTPヘッダーを追加
        $response->withHeaders([
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
        ]);

        return $response;
    }
}

ヒント
CORSはOPTIONSリクエストを生成する可能性があり、OPTIONSリクエストがコントローラーに到達しないようにするために、OPTIONSリクエストに対して直接空のレスポンス(response(''))を返してリクエストをインターセプトしています。
インターフェースの設定が必要な場合は、Route::any(..) または Route::add(['POST', 'OPTIONS'], ..) を使用して設定してください。

config/middleware.phpに全体ミドルウェアを次のように追加します。

return [
    // 全体ミドルウェア
    '' => [
        // ... ここで他のミドルウェアは省略
        app\middleware\AccessControlTest::class,
    ]
];

注意
Ajaxリクエストがカスタムヘッダーを定義している場合、ミドルウェアで Access-Control-Allow-Headers フィールドにそのカスタムヘッダーを追加する必要があります。そうしないと、Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response. というエラーが発生します。

説明

  • ミドルウェアは、グローバルミドルウェア、アプリケーションミドルウェア(アプリケーションミドルウェアは、多アプリケーションモードの下でのみ有効)、ルートミドルウェアに分けられます。
  • ミドルウェアの構成ファイルの場所は config/middleware.php です。
  • グローバルミドルウェアはキー '' の下に構成されます。
  • アプリケーションミドルウェアは特定のアプリケーション名の下で構成されます。例えば:
return [
    // グローバルミドルウェア
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // APIアプリケーションミドルウェア(アプリケーションミドルウェアは、多アプリケーションモードの下でのみ有効)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

コントローラーミドルウェアとメソッドミドルウェア

アノテーションを利用して、特定のコントローラーまたはコントローラーの特定のメソッドにミドルウェアを設定することができます。

<?php
namespace app\controller;
use app\middleware\Controller1Middleware;
use app\middleware\Controller2Middleware;
use app\middleware\Method1Middleware;
use app\middleware\Method2Middleware;
use support\annotation\Middleware;
use support\Request;

#[Middleware(Controller1Middleware::class, Controller2Middleware::class)]
class IndexController
{
    #[Middleware(Method1Middleware::class, Method2Middleware::class)]
    public function index(Request $request): string
    {
        return 'hello';
    }
}

ルートミドルウェア

特定のルートまたはルートのグループにミドルウェアを設定することができます。
たとえば、config/route.phpに次のような構成を追加します:

<?php
use support\Request;
use Webman\Route;

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Route::group('/blog', function () {
   Route::any('/create', function () {return response('create');});
   Route::any('/edit', function () {return response('edit');});
   Route::any('/view/{id}', function ($r, $id) {response("view $id");});
})->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

ミドルウェアコンストラクタのパラメータ

構成ファイルは、ミドルウェアまたは匿名関数を直接インスタンス化することをサポートしています。これにより、ミドルウェアにパラメータを渡すことが簡単になります。
たとえば、config/middleware.phpでも次のように構成できます。

return [
    // グローバルミドルウェア
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // APIアプリケーションミドルウェア(アプリケーションミドルウェアは、多アプリケーションモードの下でのみ有効)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

同様に、ルートミドルウェアもコンストラクタを使用してミドルウェアにパラメータを渡すことができます。たとえば、config/route.phpで:

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    new app\middleware\MiddlewareA($param1, $param2, ...),
    function(){
        return new app\middleware\MiddlewareB($param1, $param2, ...);
    },
]);

ミドルウェアでパラメータを使用する例

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class MiddlewareA implements MiddlewareInterface
{
    protected $param1;

    protected $param2;

    public function __construct($param1, $param2)
    {
        $this->param1 = $param1;
        $this->param2 = $param2;
    }

    public function process(Request $request, callable $handler) : Response
    {
        var_dump($this->param1, $this->param2);
        return $handler($request);
    }
}

ミドルウェアの実行順序

  • ミドルウェアの実行順序は グローバルミドルウェア -> アプリケーションミドルウェア -> コントローラーミドルウェア -> ルートミドルウェア -> メソッドミドルウェア です。
  • 同じレベルに複数のミドルウェアがある場合、実際の構成順に従って実行されます。
  • 404リクエストはデフォルトで任意のミドルウェアをトリガーしません(ただし、Route::fallback(function(){})->middleware()を使用してミドルウェアを追加することはできます)。

ルートからミドルウェアへのパラメータ渡し(route->setParams)

ルート構成 config/route.php

<?php
use support\Request;
use Webman\Route;

Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);

ミドルウェア(仮にグローバルミドルウェアとします)

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // デフォルトでは $request->route はnullなので、$request->routeが空であるかどうかを確認する必要があります
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

ミドルウェアからコントローラーへのパラメータ渡し

時には、コントローラーがミドルウェアで生成されたデータを使用する必要があります。その場合、$requestオブジェクトにプロパティを追加することで、コントローラーにパラメータを渡すことができます。たとえば:

ミドルウェア

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $request->data = 'some value';
        return $handler($request);
    }
}

コントローラー:

<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function index(Request $request)
    {
        return response($request->data);
    }
}

ミドルウェアで現在のリクエストルート情報を取得する

$request->routeを使用してルートオブジェクトを取得し、対応するメソッドを呼び出すことで情報を取得できます。

ルート構成

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', [app\controller\UserController::class, 'view']);

ミドルウェア

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $route = $request->route;
        // リクエストがいかなるルートにも一致しなかった場合(デフォルトルートを除く)、$request->route は nullになります
        // たとえば、ブラウザがアドレス /user/111 にアクセスすると、次の情報が表示されます
        if ($route) {
            var_export($route->getPath());       // /user/{uid}
            var_export($route->getMethods());    // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD','OPTIONS']
            var_export($route->getName());       // user_view
            var_export($route->getMiddleware()); // []
            var_export($route->getCallback());   // ['app\\controller\\UserController', 'view']
            var_export($route->param());         // ['uid'=>111]
            var_export($route->param('uid'));    // 111 
        }
        return $handler($request);
    }
}

注意

ミドルウェアで例外を取得する

ビジネス処理の過程で例外が発生することがあります。ミドルウェア内で $response->exception() を使用して例外を取得できます。

ルート構成

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', function (Request $request, $uid) {
    throw new \Exception('exception test');
});

ミドルウェア:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $response = $handler($request);
        $exception = $response->exception();
        if ($exception) {
            echo $exception->getMessage();
        }
        return $response;
    }
}

超グローバルミドルウェア

主プロジェクトのグローバルミドルウェアは主プロジェクトにのみ影響し、アプリケーションプラグインには影響しません。時には、すべてのプラグインを含むグローバルに影響を与えるミドルウェアを追加したい場合があります。その場合は、超グローバルミドルウェアを使用できます。

config/middleware.phpに次のように構成します:

return [
    '@' => [ // 主プロジェクト及びすべてのプラグインにグローバルミドルウェアを追加
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // 主プロジェクトにのみグローバルミドルウェアを追加
];

ヒント
@超グローバルミドルウェアは主プロジェクトの構成だけでなく、特定のプラグインでも構成できます。たとえば、plugin/ai/config/middleware.php@超グローバルミドルウェアを構成すると、主プロジェクト及びすべてのプラグインにも影響します。

特定のプラグインにミドルウェアを追加する

特定のアプリケーションプラグインにミドルウェアを追加したいが、プラグインのコードを変更したくない場合(アップグレードで上書きされる可能性があるため)、主プロジェクトでそのミドルウェアを構成できます。

config/middleware.phpに次のように構成します:

return [
    'plugin.ai' => [], // aiプラグインにミドルウェアを追加
    'plugin.ai.admin' => [], // aiプラグインのadminモジュール(plugin\ai\app\adminディレクトリ)にミドルウェアを追加
];

ヒント
他のプラグインに影響を与えるために、特定のプラグインに同様の構成を追加することもできます。たとえば、plugin/foo/config/middleware.phpに上記の構成を追加すると、aiプラグインにも影響を与えます。