PHP のコミュニティでは FIG (Framework Interop Group) が PSR-7 (HTTP メッセージインターフェース) を採択されるとともに、ミドルウェアがより注目されるようになってきています。今後のフレームワーク選びのために、PHP だけでなく、Node.js や Go のミドルウェアを調べました。
背景
マイクロサービスへの関心の高まり
ミドルウェアを採用するプロジェクトが台頭してきた背景にはマイクロサービスへの関心が高まっていることが推測されます。Lumen はマイクロサービスの開発のためのソリューションであり、PHP で書かれた競合のマイクロフレームワークよりも速度が速いことをアピールしています。
PHP フレームワークの速度比較は PHP Framework Benchmark のプロジェクトを、
さまざまな言語のフレームワークの速度比較は (Web Framework Benchmarks) をご参照ください。
マイクロサービスの前提条件
マーチン・ファウラー氏は マイクロサービスの前提条件 (MicroservicePrerequisites) の記事のなかで次の条件を挙げています。
- 迅速なプロビジョニング
- 基本的なモニタリング
- アプリケーションの迅速なデプロイ
マイクロサービスのトレードオフ
マーチン・ファウラー氏はマイクロサービスのトレードオフ (Microservice Trade-Offs) の記事のなかで、利点として「強固なモジュールの境界」、「個別にデプロイ」、「技術の多様性」、欠点は「分散」、「最終的な整合性」、「運用上の複雑さ」を挙げています。
マイクロサービスの事例
モノリシックなRubyからGoによるマイクロサービスへ が参考になります。pdf 生成を go の並行処理に切り替えることで、速度が12倍に増加、メモリ消費量が30%減少。成績表の計算処理を go に切り替えることで10倍スピードアップしたとのことです。
Go のマイクロサービスを採用した理由はデプロイがかんたんで、AWS CodeDeploy のセットアップは2時間程度しかかからなかったことが述べられています。
マイクロサービスに関する資料のまとめ にも事例が掲載されています。
ミドルウェアのユースケース
StackPHP の開発者である Igor 氏は「HttpKernel middlewares」のなかでよいユースケースとして次の機能を上げています。
- 認証
- デバッグツールバー
- インジェクトされるルート - たとえば管理パネル。
- 署名つき Cookie
- アセットマネジメント
- SSL の強制
- エラーハンドリング
- セッション
ルーター
ミドルウェアのメリットがわかりやすい具体例としてルーター(ルーティング)を挙げることができます。ミドルウェアを採用していれば、プロジェクトやサービスごとの優先順位に合わせてルーターを切り替えることができます。たとえば、速度を重視してシンプルなパターンに徹するか、複雑なパターンに対応する代わりに速度が遅くなることを受け入れるということが考えられます。
ミドルウェアの制約
スタックやフィルターチェーンの限界
事例を調べると、登録した順に実行するスタックやフィルタチェーン (Chain of Responsibility)が使われているケースを多く見られます。
フレームワークやアプリケーションがより複雑な問題に対処するために、イベントバス (Event Bus) やイベントディスパッチャ (Event Dispatcher) など、イベントや優先順位を指定できるシステムを採用している場合、使い勝手がわるいものになります。
API の統一は困難
API が統一されれば、さまざまなプロジェクトで共有できる反面、たとえば、Expressive と StackPHP の違いのように、どのプロジェクトでも採用できる統一的な API は困難と考えられます。
Ruby の Rack のコミュニティでは次のメジャーバージョンを巡り、the_metal や Rack-Next で議論が繰り広げられていますが、なかなか収拾がつかないようです。
PSR-7 が普及するのはこれから
PHP に関して、ミドルウェアの前提となる PSR-7 は2015年に採択されたばかりで、普及したかどうかを判断するには少なくとも数年待たなければなりません。PSR-7 を採用する新しいプロジェクトが台頭しなかったり、既存の大きなコミュニティを抱えるプロジェクトが PSR-7 に移行するメリットを見いだせなかったり、PSR-7 が普及しない可能性もあります。
ミドルウェアの分類
独立したコンポーネントである
プラグインやエクステンションとほとんど同じ意味で使われます。これらとの違いは、関心の中心が HTTP であることや、複数のフレームワークやアプリケーションで利用可能であることです。
ミドルウェアの登録と実行に徹しており、DI コンテナやルーターなどの最小限の機能をもたない傾向にあります。以前は、フレームワークの機能の一部であったものが、洗練されて、独立したプロジェクトになった事例が見られます。
node.js では Koa や connect が挙げられます。PHP では PSR-7 対応の Relay や Zend Stratigility が挙げられます。ほかに Symfony 2 の HttpKernel をミドルウェアとして評価して使う取り組みとして StackPHP が挙げられます。Golang では Negroni や Alice が挙げられます。
PHP (PSR-7 対応)
Zend Stratigility/Expressive
Zend Stratigility/Expressive の場合、Express と同じ引数をとります。
$app->pipe('/foo', function ($req, $res, $next) {
return $res->end('Foo');
});
Relay
function (
Request $request, // the request
Response $response, // the response
callable $next // the next middleware
)
PHP (PSR-7 未対応)
StackPHP
StackPHP の場合、Symfony の HttpKernelInterface を実装するオブジェクトを Stack/Builder の push メソッドで登録します。HttpKernelInterface の handle メソッドは Request を引数とし、Response を戻り値とします。
function handle(Request $request)
$stack = (new Stack\Builder())
->push('Stack\Session')
node.js
Koa
function *(next)
go
Negroni
Martini と同じ開発者が開発している Negroni は特定のルート限定でミドルウェアを登録できるようになっています。
router := mux.NewRouter()
adminRoutes := mux.NewRouter()
// add admin routes here
// Create a new negroni for the admin middleware
router.Handle("/admin", negroni.New(
Middleware1,
Middleware2,
negroni.Wrap(adminRoutes),
))
Alice
Alice は複数のミドルウェアを連結しやすくなっています。
func (h http.Handler) http.Handler
alice.New(Middleware1, Middleware2, Middleware3).Then(App)
フレームワークの機能拡張の中心的な手段である
プラグインやエクステンションと同じ意味で使われます。
node.js では Express 4 のミドルウェアが挙げられます。PHP では PSR-7 対応の Slim 3 が挙げられます。Go であれば Martini が挙げられます。
PHP (PSR-7 対応)
Slim 3
function ($request, $response, $next)
Node.js
Express 4
function (req, res, next)
Go
Martini
Martini は Express と Sinatra にインスパイアされたとのことです。
m.Use(func(res http.ResponseWriter, req *http.Request) {
})
m.Use(func(c martini.Context, log *log.Logger) {
})
m.Handlers(
Middleware1,
Middleware2,
Middleware3,
)
フレームワークの機能拡張の補助的な手段である
プラグインやエクステンションといった別の拡張機能が用意されています。2000年代から存在するフレームワークは before
、after
などのフィルターメソッドが備わっていました。
PHP (PSR-7 未対応)
Laravel
Laravel の場合、$request と $next に加えて、追加のパラメーターを指定できるようになっています。グローバルおよび特定のルートに登録できます。
handle($request, Closure $next)
handle($request, Closure $next, $additonal_parameters)
Silex
Silex の場合、特定のフェーズごとに登録メソッドを用意しており、引数の種類が異なります。特定のルートに限定することもできます。実行の優先順位を指定することもできます。これらの機能を実現するために内部では Event Dispatcher とリフレクションが利用されています。
$app->before(function (Request $request, Application $app)
$app->after(function (Request $request, Response $response)
$app->finish(function (Request $request, Response $response)
$app->get('/somewhere', function () {
// ...
})
->before($before1)
->before($before2)
->after($after1)
->after($after2)
$app->before(function (Request $request) {
// ...
}, 32);
$app->before(function (Request $request) {
// ...
}, Application::EARLY_EVENT);
Phalcon
Phalcon のマイクロフレームワークは Silex と同じような登録メソッドが用意されています。
$app->before(function () use ($app) {
});
$app->map('/api/robots', function () {
});
$app->after(function () use ($app) {
});
$app->finish(function () use ($app) {
});