ミドルウェア
ミドルウェアは通常、リクエストまたはレスポンスをインターセプトするために使用されます。たとえば、コントローラーを実行する前にユーザーの身元を統一的に検証する場合、ユーザーがログインしていない場合はログインページにリダイレクトするなど、レスポンスに特定のヘッダーを追加する場合、特定のURIリクエストの割合を集計する場合などがあります。
ミドルウェアオニオンモデル
┌──────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── リクエスト ──────────────────> コントローラー ─── レスポンス ───────────────────────────> クライアント
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
ミドルウェアとコントローラーは、クラシックなオニオンモデルを構成しており、ミドルウェアは一層一層のオニオンの皮のように、コントローラーはオニオンの中心部のようです。この図に示すように、リクエストは矢印のようにミドルウェア1、2、3を通ってコントローラーに到達し、コントローラーがレスポンスを返し、その後、レスポンスは3、2、1の順番でミドルウェアを通って最終的にクライアントに返されます。つまり、各ミドルウェア内でリクエストを取得し、またレスポンスを取得することができます。
リクエストのインターセプト
時には、特定のリクエストをコントローラーレイヤーに到達させたくない場合があります。たとえば、middleware2で現在のユーザーがログインしていないことが分かった場合、直接リクエストをインターセプトし、ログインレスポンスを返すことができます。このようなフローは次のようになります。
┌────────────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌────────────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── リクエスト ────────┐ │ │ コントローラー │ │ │ │
│ │ レスポンス │ │ │ │ │ │
<───────────────────┘ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
図のように、リクエストはmiddleware2に到達した後、ログインレスポンスが生成され、そのレスポンスがmiddleware2を通ってmiddleware1に返され、クライアントに返されます。
ミドルウェアインターフェース
ミドルウェアはWebman\MiddlewareInterface インターフェースを実装する必要があります。
interface MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(Request $request, callable $handler): Response;
}
つまり、processメソッドを実装する必要があり、processメソッドはsupport\Responseオブジェクトを返さなければならず、デフォルトではこのオブジェクトは$handler($request)によって生成されます(リクエストはオニオンの中心部を通り抜け続けます)、またはresponse() json() xml() redirect()などのヘルパー関数によって生成されたレスポンス(リクエストはオニオンの中心部を通り抜けます)でも構いません。
中間層でリクエストとレスポンスを取得する
中間層では、リクエストを取得し、またはコントローラーの実行後のレスポンスを取得することができます。したがって、中間層には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')]);
}
}
config/middleware.phpに以下のようにグローバルミドルウェアを追加します:
return [
// グローバルミドルウェア
'' => [
// ... 他のミドルウェアを省略
app\middleware\AuthCheckTest::class,
]
];
認証ミドルウェアがあることで、コントローラーレベルでのログインの確認について心配する必要なく、コントローラーレイヤーでビジネスコードを書くことができます。
サンプル:クロスドメインリクエストミドルウェア
ファイル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
{
// オプションリクエストであれば空のレスポンスを返し、それ以外は次の段階へ進めてレスポンスを得る
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// レスポンスにクロスドメイン関連の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;
}
}
ヒント
クロスオリジンリクエストではOPTIONSプリフライトリクエストが発生することがあります。OPTIONSリクエストをコントローラーに到達させたくないため、空のレスポンス(response(''))を直接返してリクエストをインターセプトします。
APIにルーティングが必要な場合は、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';
}
}
ルートミドルウェア
特定の1つまたは複数のルートにミドルウェアを設定することができます。
例えば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, ...),
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, ...),
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プラグインに影響を与えます。