$30 off During Our Annual Pro Sale. View Details »

PSR-15 Request Handlerから理解するMiddlewareの仕組み

n1215
PRO
October 30, 2018

PSR-15 Request Handlerから理解するMiddlewareの仕組み

PHP Meetup Osaka 2018.10の発表資料です。
PSR-15 Server Request HandlersとServer Middlewareについての解説。

PSR-15: https://www.php-fig.org/psr/psr-15/

n1215
PRO

October 30, 2018
Tweet

More Decks by n1215

Other Decks in Programming

Transcript

  1. For PHP Meetup Osaka 2018.10
    PSR‑15 Request Handlerから理解するMiddlewareの仕組み
    2018/10/30 株式会社 Nextat 中榮健二

    View Slide

  2. 自己紹介
    中榮健二 (なかえけんじ)
    株式会社Nextat 取締役
    twitter: @n_1215
    普段はLaravelを使ったソシャゲ開発などをやっています
    baserCMS コアコミッター
    最近ext‑asyncが気になる

    View Slide

  3. 今日は
    PSR‑15 HTTP Server Request Handlers
    の話をします。
    内容
    1. PSR の話
    2. PSR‑7 HTTP Message の話
    3. PSR‑15 HTTP Server Request Handlers の話
    4. PSR‑15 HTTP Server Middleware の話
    5. PSR‑15の議論の歴史 の話

    View Slide

  4. 1. PSRの話

    View Slide

  5. PSR
    PSR = PHP Standards Recommendations
    PHPを使う上でオススメの規約やインターフェースの集まり
    "Standards"とあるがPHPの公式ではない
    PHP‑FIG
    PHP‑FIG = PHP Framework Interoperability Group
    PSRを作っているグループ
    PHP製のフレームワーク・CMS・ツールの開発者が集まっている
    共通の規約や仕様を定めて相互運用性を高める目的

    View Slide

  6. View Slide

  7. PSRのメリット(個人の感想です)
    プロジェクトのコーディング規約を作るための雛形として有用
    今後PSRに準拠するライブラリやFWが増えそうなので、ユーザーとして知っておいて損はない
    自分で似たような仕組みを作るときにも参考になる
    PHP‑FIGのGoogle Groupの議論が興味深い

    View Slide

  8. PSRの分類
    (1) オートローディング
    PSR‑4: Autoloader
    名前空間とディレクトリ構造を対応させる規約
    Composerを使っていれば恩恵に預かっていることも多いはず
    Deprecated
    PSR‑0: Autoloading Standard (Deprecated)
    PSR‑4の前身

    View Slide

  9. (2) コーディングスタイル
    PSR‑1: Basic Coding Standard
    PSR‑2: Coding Style Guide
    Draft
    PSR‑5: PHPDoc Standard
    PHPDocコメントの書き方の標準
    PSR‑12: Extended Coding Style Guide
    PSR‑1/PSR‑2でカバーしきれない最近のPHPの機能に対応
    PSR‑19: PHPDoc tags
    PSR‑5の補完。タグの一覧を提供

    View Slide

  10. (3) インターフェース (今回は触れないもの)
    PSR‑3: Logger Interface
    PSR‑6: Caching Interface
    PSR‑11: Container Interface
    PSR‑13: Link definition interfaces
    PSR‑16: Common Interface for Caching Libraries
    PSR‑18: HTTP Client Interfaces
    Draft
    PSR‑14: Event Dispatcher Interafaces

    View Slide

  11. (3) インターフェース (HTTP系/今回のお題)
    PSR‑7: HTTP message Interfaces
    PSR‑15: HTTP Server Request Handlers
    PSR‑17: HTTP Factories
    他のインターフェースは個々で完結しているものが多いが、この3つはシリーズ物

    View Slide

  12. 2. PSR‑7 HTTP messageの話

    View Slide

  13. HTTPサーバアプリケーション

    View Slide

  14. HTTPサーバアプリケーション
    入力:HTTPリクエスト
    POST /path HTTP/1.1
    Host: example.com
    this=is&request=body
    出力:HTTPレスポンス
    HTTP/1.1 200 OK
    Content-Type: text/plain
    This is the response body

    View Slide

  15. 最近のブラウザは簡単にHTTPメッセージの中身が見られる

    View Slide

  16. PHPでのHTTPメッセージの処理
    echo 'Hello ' . htmlspecialchars($_GET["name"]) . '!';
    スーパーグローバル($_GET, $_POST...)に入った値からHTTPリクエストの値が取れる
    echoやheader()関数でHTTPレスポンスを組み立てる
    HTTPに詳しくなくても使えてとてもお手軽
    グローバル変数……

    View Slide

  17. HTTPメッセージ実装乱立の時代
    HTTPリクエストとHTTPレスポンスのクラスを作って抽象化したい
    HTTPメッセージの実装がFW・プロジェクトごとに乱立していた

    View Slide

  18. PSR‑7: HTTPメッセージの標準化

    View Slide

  19. PSR‑7の特徴
    イミュータブルなオブジェクト (≒
    状態を変更するsetterを持たない)
    $request = $request
    ->withMethod('GET')
    ->withUri(new Uri('https://example.com/'));
    bodyがStreamとして表現される
    public function withBody(StreamInterface $body);
    HTTPクライアント側とHTTPサーバ側に両対応
    interface ServerRequestInterface extends RequestInterface

    View Slide

  20. PSR‑7の採用例
    Guzzle
    Slim3
    CakePHP3
    zend‑expressive
    ReactPHP

    View Slide

  21. Symfony HTTP Foundation
    一番勢力の強い[要出典] HTTPメッセージ実装
    Symfonyのコンポーネント
    Laravelなどで利用(illuminate/http)
    今後はPSR‑7と二分する感じになりそう?
    PSR‑7互換のためのブリッジもある
    https://symfony.com/doc/current/components/psr7.html
    以降の話は、HTTP Foundationや他の実装に置き換えても事情は同じ

    View Slide

  22. 3. PSR‑15 HTTP Server Request Handlers の話

    View Slide

  23. HTTPサーバアプリケーション
    入力:HTTPリクエスト
    出力:HTTPレスポンス

    View Slide

  24. HTTPサーバアプリケーションの抽象化
    interface RequestHandlerInterface
    {
    public function handle(ServerRequestInterface $request): ResponseInterface;
    }
    利用
    $request = new PSR7Request();
    $handler = new MyRequestHandler();
    $response = $handler->handle($request);

    View Slide

  25. なるほどいまいちわからん

    View Slide

  26. 身近な例
    Server Request Handler ≒
    MVC FWのControllerのアクション
    class MyController
    {
    public function hello(ServerRequestInterface $request) : ResponseInterface
    {
    return new JsonResponse(['hello' => 'world']);
    }
    }

    View Slide

  27. 実際はControllerのアクションは利便性重視な場合が多い
    $router->get('/hello/{name}', 'MyController::hello');
    class MyController
    {
    public function hello(string $name): array
    {
    return ['hello' => $name];
    }
    }
    (リクエスト →) ルートパラメータ → 配列 (→ JSONレスポンス)
    暗黙的な変換でRequest Handler相当の処理が導出されている

    View Slide

  28. 4. PSR‑15 HTTP Server Middleware の話

    View Slide

  29. HTTP Server Middleware
    インターフェース
    interface MiddlewareInterface
    {
    public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
    ): ResponseInterface;
    }
    Request Handlerを横断する共通処理を行う

    View Slide

  30. PSR‑15より前の実装はこういうシグネチャが多かった
    class MyMiddleware
    {
    public function handle(
    Request $request,
    callable $next
    ): Response;
    }

    View Slide

  31. なるほど全然わからん

    View Slide

  32. 具体例をそれっぽいコードで

    View Slide

  33. 1)メンテナンスモード
    public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
    ): ResponseInterface {
    //
    前処理
    if ($this->isMaintenanceMode()) {
    return new Response('
    メンテナンス中です', 503);
    }
    return $handler->handle($request);
    }

    View Slide

  34. 2) レスポンスに共通ヘッダ付与
    public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
    ): ResponseInterface {
    $response = $handler->handle($request);
    //
    後処理
    $modifiedResponse = $response
    ->withHeader('X-Powered-By: OreoreFramework/0.1')
    ->withHeader('X-XSS-Protection', '1; mode=block');
    return $modifiedResponse;
    }

    View Slide

  35. 3) ベンチマーカー
    RequestHandlerの処理時間をログに書き込む
    public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
    ): ResponseInterface {
    //
    前処理
    $start = microtime(true);
    $response = $handler->handle($request);
    //
    後処理
    $time = microtime(true) - $start;
    $ms = number_format($time * 1000, 1);
    $message = "processed in {$ms} ms.";
    $this->logger->info($message);
    return $response;
    }

    View Slide

  36. 他にもいろいろ
    認証
    Cookie
    CSRFチェック
    参考: middlwares/psr15‑middleares

    View Slide

  37. class MyMiddleware implements MiddlewareInterface
    {
    public function process(
    SeverRequestInterface $request,
    RequestHandlerInterface $handler
    ): ResponseInterface {
    //
    前処理
    // ...
    //
    ハンドラがリクエストを処理
    $response = $handler->handle($request);
    //
    後処理
    // ...
    return $response;
    }
    }

    View Slide

  38. イメージ図

    View Slide

  39. 早期リターンの場合

    View Slide

  40. Interceptorパターン
    処理が呼び出される前後に割り込み、制御を奪って別の処理を行うパターン
    https://en.wikipedia.org/wiki/Interceptor_pattern
    Aspected Oriented Programming
    MiddlewareはServer Request Handlerの処理に対して限定的なAOPの機能を提供

    View Slide

  41. Interceptorの実装例
    一般的なAOPで扱われる処理はより汎用的なもの
    参考:Ray.Aop のメソッドインターセプター
    class MyInterceptor implements MethodInterceptor
    {
    public function invoke(MethodInvocation $invocation)
    {
    // Before method invocation
    // ...
    // Method invocation
    $result = invocation->proceed();
    // After method invocation
    // ...
    return $result;
    }
    }

    View Slide

  42. よく似ている
    class MyMiddleware implements MiddlewareInterface
    {
    public function process(
    SeverRequestInterface $request,
    RequestHandlerInterface $handler
    ): ResponseInterface {
    //
    前処理
    // ...
    //
    ハンドラがリクエストを処理
    $response = $handler->handle($request);
    //
    後処理
    // ...
    return $response;
    }
    }

    View Slide

  43. 入出力に注目する

    View Slide

  44. コード
    Server Request Hadler + Middleware → Server Request Handler
    class WrappedHandler implements RequestHandlerInterface
    {
    public function __construct(
    MiddlewareInterface $middleware,
    RequestHandlerInterface $handler
    ) {
    $this->middleware = $middleware;
    $this->handler = $handler;
    }
    public function handle(
    ServerRequestInterface $request
    ): ResponseInterface {
    return $middleware->process($request, $this->handler);
    }
    }
    $newHandler = new WrappedHandler($middleware, $handler);
    $response = $newHandler->handle($request);

    View Slide

  45. 合成が容易
    Server Request Hadler + Middleware * N → Server Request Handler
    Middlewareを複数追加できる玉ねぎ型の構造

    View Slide

  46. 注: クリーンアーキテクチャとは別です
    図は玉ねぎ型ですが

    View Slide

  47. ここまでのまとめ
    HTTPサーバアプリケーションやControllerアクションの抽象がServer Request Handler
    MiddlewareはServer Request Handler限定のInterceptor
    Server Request Hadler と Middleware を組み合わせてServer Request Handlerを再構成できる
    Request Handlerに影響を与えずに複数の処理を追加できる

    View Slide

  48. 5. PSR‑15の議論の歴史の話

    View Slide

  49. Middlewareありき
    ここまでの説明と、PSR‑15の議論の経緯は逆になる
    もともとMiddlewareの互換性を目指してPSR‑15が立ち上がった
    各プロジェクトのMiddleware実装が先行していた
    Request Handlerという概念はまだ見出されていなかった

    View Slide

  50. interface SinglePassMiddleware
    {
    public function handle(
    Request $request,
    callable $next
    ): Response;
    }
    interface DoublePassMiddleware
    {
    public function handle(
    Request $request,
    Response $response,
    callable $next
    ): Response;
    }
    HTTPレスポンスを引数に取るシグネチャもある=ダブルパス方式
    PSR‑15が採用したのはシングルパス方式
    PSR‑15では、ファクトリを使うレスポンスの新規生成を推奨
    → PSR‑17 HTTP Factories

    View Slide

  51. Server Request Handlersという名付け
    第二引数のcallableや\Closureの入出力の型がわかりづらい
    callable $next → DelegateInterface $delegate
    Delegateは処理を任せるものという意味合い
    Middlewareを第一に考えた命名
    そもそもDelegateInterfaceの命名が悪いのでは?
    MiddlewareがなくてもDelegateだけを扱える
    単体でも存在できるのにDelegateという名前はふさわしくない
    DelegateがなければMiddlewareは存在しない
    主従が逆では?
    → \Psr\Http\Server\RequestHandlerInterface $handler

    View Slide

  52. パッケージが2つに分割された
    psr/http‑server‑handler
    psr/http‑server‑middleware
    依存関係
    psr/http‑server‑handlerはpsr/http‑messageに依存
    psr/http‑server‑middlewareはpsr/http‑server‑handlerに依存

    View Slide

  53. タイトルも変更
    PSR‑15 HTTP Middleware

    PSR‑15 Server Request Handlers

    View Slide

  54. おまけ: PSR‑15 フレームワークを作ろう
    Middleware / Request Handler Dispatcher
    MiddlewareとRequest Handlerを組み合わせてRequest Handlerを作る
    Router
    Requestの内容に応じたRequest Handlerを呼び出す
    DI Container
    Request Handlerの組み立て補助
    どれも既存の実装があるので、組み合わせるだけでも可
    特にMiddleware Dispatcherを一度作ればPSR‑15が理解できる

    View Slide

  55. View Slide

  56. → 簡単なの作った。
    https://github.com/n1215/tsukuyomi
    https://github.com/n1215/jugoya (Middlewareディスパッチャ)

    View Slide

  57. まとめ
    PSR‑15の歴史はMiddlewareの話から始まった
    PSR‑15の真の主役 Server Request Handlerは遅れて登場した
    新規向けにはServer Request Handlerからのほうが理解しやすい
    実用上はMiddlewareとServer Request Handlerの合成がキモ
    PSR‑15フレームワークを作るのは楽しいですよ

    View Slide

  58. ご静聴ありがとうございました
    ご質問があればどうぞ
    PR
    毎週水曜1830~ 京都でもくもく会を開催中
    https://nextat.connpass.com/

    View Slide