LoginSignup
0
0

More than 5 years have passed since last update.

Hackで作るMiddleware Dispatcher

Posted at

前回のエントリで扱った、
hhvm/hack-http-request-response-interfacesを実装したライブラリを使って、
HackのMiddleware Dispatcherを作ってみましょう。

PSR-15 For Hack

最近のPHPのミドルウェアはPSR-15を実装したものが多くあります。
PSR-15: HTTP Server Request Handlers
必ずしもこのインターフェースに準拠する必要はありませんが、
せっかくなので置き換えてみます。
hhvm/hack-http-request-response-interfaces依存へ変更します。
それぞれ下記の様にするとそのまま利用できます。

Acme\HackHttpServer\MiddlewareInterface
<?hh // strict

namespace Acme\HackHttpServer;

use type Facebook\Experimental\Http\Message\ResponseInterface;
use type Facebook\Experimental\Http\Message\ServerRequestInterface;

/**
 * An HTTP middleware component participates in processing an HTTP message,
 * either by acting on the request or the response. This interface defines the
 * methods required to use the middleware.
 */
interface MiddlewareInterface {
  /**
   * Process an incoming server request and return a response, optionally delegating
   * response creation to a handler.
   *
   * @param ServerRequestInterface $request
   * @param RequestHandlerInterface $handler
   * @return ResponseInterface
   */
  public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
  ): ResponseInterface;
}
Acme\HackHttpServer\RequestHandlerInterface
<?hh // strict
namespace Acme\HackHttpServer;

use type Facebook\Experimental\Http\Message\ResponseInterface;
use type Facebook\Experimental\Http\Message\ServerRequestInterface;

/**
 * An HTTP request handler process a HTTP request and produces an HTTP response.
 * This interface defines the methods require to use the request handler.
 */
interface RequestHandlerInterface {
  /**
   * Handle the request and return a response.
   * @param ServerRequestInterface $request
   *
   * @return ResponseInterface
   */
  public function handle(ServerRequestInterface $request): ResponseInterface;

Hack用に変更したものはこちら ytake/hack-http-server-request-handlers-interfaces
実装前の準備はこれで完了です。

Middleware For Hack

それでは実際に実装してみましょう。

依存ライブラリ

例によってhh_autoload.jsonへの記述などを行いましょう。

hh_autoload.json
{
  "roots": [
    "src/"
  ],
  "devRoots": [
    "tests/"
  ]
}

依存ライブラリなどは下記のもので十分です。
ytake/hack-http-server-request-handlers-interfacesを使わない場合は、
下記から除外してください。

composer.json
  "require": {
    "hhvm": ">=3.30",
    "hhvm/hhvm-autoload": "^1.7",
    "hhvm/hsl": "^3.30.0",
    "facebook/hack-http-request-response-interfaces": "^0.1",
    "ytake/hack-http-server-request-handlers-interfaces": "^1.0.0"
  },
  "require-dev": {
    "hhvm/hacktest": "^1.3",
    "facebook/fbexpect": "^2.2.0",
    "hhvm/hhast": "^3.30.0",
    "ytake/hungrr": "^0.2.0"
  }

hhvmにおけるcomposerを介したライブラリのインストールは、これまで紹介した様に

hhvm $(which composer) install

で行いましょう。

インスタンス生成のためのインターフェース

HTTPの処理におけるミドルウェアで、スタック時にインスタンスを生成を行うか、
ミドルウェア実行時までインスタンス生成を待つか、
様々なパターンが考えられます。

今回は実行時にインスタンス生成を行うパターンに対応するために、
下記のインターフェースを作成しておきます。

<?hh // strict

namespace Acme\Middleware;

use type Acme\HackHttpServer\MiddlewareInterface;

interface Resolvable {

  public function resolve(
    classname<MiddlewareInterface> $middleware
  ): MiddlewareInterface;
}

MiddlewareInterfaceを実装したクラス名であればなんでも構わない、という指定です。
インスタンス生成方法は、リフレクションで行うか、またはDIコンテナなどを用いることができます。
今回はリフレクションで行う様にしてみましょう。
このインターフェースを実装したクラスは下記の様に実装します。

<?hh //strict

namespace Acme\Middleware;

use type ReflectionClass;
use type Acme\HackHttpServer\MiddlewareInterface;

class InstanceResolver implements Resolvable {

  public function resolve(
    classname<MiddlewareInterface> $middleware
  ): MiddlewareInterface {
    $ref = new ReflectionClass($middleware);
    return $ref->newInstance();
  }
}

これでミドルウェアの処理が行える準備が整いました。

MiddlewareStack

上記のインターフェースを実装したミドルウェアクラスを配列などでスタックさせ、
リクエストを受け取ってからレスポンスを返却するまでの処理をミドルウェアとして実行する直前の部分までを実装します。
ミドルウェアクラスのスタックは、Vectorなどを用いて表現できます。
このクラスを利用するときにお好みで指定できる様に、
ここでは Traversable<classname<MiddlewareInterface>> を利用します。
利用イメージとしては下記のものです。

    $stack = new MiddlewareStack(
      [MockMiddleware::class, FakeMiddleware::class, FakeMiddleware::class],
    );

コンストラクタは下記のもので十分でしょう。

class MiddlewareStack {

  protected Resolvable $resolver;
  protected Vector<classname<MiddlewareInterface>> $queue = Vector {};

  public function __construct(
    Traversable<classname<MiddlewareInterface>> $queue,
    ?Resolvable $resolver = null,
  ) {
    $this->queue = new Vector($queue);
    $this->resolver = (is_null($resolver)) ? new InstanceResolver() : $resolver;
  }
}

ミドルウェアは記述した順番に実行しますが、
処理を玉ねぎの様に挟み込んで一つずつ実行する必要があります。
一番最初に実行されるミドルウェアは、一番最初と最後に実行されなければならないため、
配列を逆転させる必要が出てきます。
Vectorを利用する場合は、反転はreverseメソッドで行えますのでこれを利用しましょう。
ミドルウェアの取り出しはVectorのshiftメソッドを利用します。

<?hh // strict
namespace Acme\Middleware;

class MiddlewareStack {

  protected Resolvable $resolver;
  protected Vector<classname<MiddlewareInterface>> $queue = Vector {};

  public function __construct(
    Traversable<classname<MiddlewareInterface>> $queue,
    ?Resolvable $resolver = null,
  ) {
    $this->queue = new Vector($queue);
    $this->resolver = (is_null($resolver)) ? new InstanceResolver() : $resolver;
  }

  <<__Rx>>
  public function isEmpty(): bool {
    return $this->queue->isEmpty();
  }

  public function reverse(): void {
    $this->queue->reverse();
  }

  public function shift(): MiddlewareInterface {
    return $this->resolver->resolve(
      $this->queue->pop()
    );
  }
}

これでMiddlewareStackのクラスは実装できました。

MiddlewareDispatcher

最後にリクエストを扱いながらミドルウェアを実行するクラスです。
これもシンプルなクラスです。
hhvm/hack-http-request-response-interfacesや、PSR-15を置き換えたインターフェースを利用するだけで、
実装ができます。
いくつかのエラーをスローできる様にExceptionクラスを用意しておきます。
実装コードは下記のものになります。

Acme\Middleware\Dispatcher
<?hh //strict

namespace Acme\Middleware;

use type Acme\HackHttpServer\RequestHandlerInterface;
use type Acme\HackHttpServer\MiddlewareInterface;
use type Facebook\Experimental\Http\Message\ServerRequestInterface;
use type Facebook\Experimental\Http\Message\ResponseInterface;

<<__ConsistentConstruct>>
class Dispatcher implements RequestHandlerInterface {

  public function __construct(
    protected MiddlewareStack $stack,
    protected ?RequestHandlerInterface $handler = null
  ) {
    $stack->reverse();
  }

  public function handle(ServerRequestInterface $request): ResponseInterface {
    if ($this->stack->isEmpty()) {
      if ($this->handler is RequestHandlerInterface) {
        return $this->handler->handle($request);
      }
      throw new Exception\MiddlewareNotFoundException('Middleware Class Not Found.');
    }
    return $this->processor($this->stack->shift(), $request);
  }

  protected function processor(
    MiddlewareInterface $middleware,
    ServerRequestInterface $request
  ): ResponseInterface {
    return $middleware->process($request, $this);
  }
}

ミドルウェアのスタックを渡し、反転させて順番に処理を行う様になります。
これでミドルウェアの実装は完了です。
実際にテストコードなどで動かしてみましょう。

テスト

動作確認用のミドルウェアクラスと、リクエストハンドラクラスを作成して、
テストを実行します。

動作確認用のミドルウェアクラス

動作が確認できるシンプルなもので十分ですのでミドルウェア通過時に、
何らかの状態を操作するものを用意します。
ヘッダーを操作するものにしてみましょう。

<?hh // strict

use type Facebook\Experimental\Http\Message\ResponseInterface;
use type Facebook\Experimental\Http\Message\ServerRequestInterface;
use type Acme\HackHttpServer\MiddlewareInterface;
use type Acme\HackHttpServer\RequestHandlerInterface;

final class MockMiddleware implements MiddlewareInterface {

  const string MOCK_HEADER = 'x-testing-call-count';

  public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler,
  ): ResponseInterface {
    $request = $request->withAddedHeader(self::MOCK_HEADER, vec["1"]);
    return $handler->handle($request);
  }
}

リクエストハンドラクラス

上記のミドルウェアクラスの値をそのままレスポンスに書き込むクラスです。
これもシンプルな実装で十分です。
facebook/hack-http-request-response-interfacesを実装したクラスを用意するには少しばかり大変なため、
これを実装したライブラリを利用してレスポンスを返却する様にします。
ここでは拙作のytake/hungrrを用いて記述します。

<?hh // strict

use type Acme\HackHttpServer\RequestHandlerInterface;
use type Facebook\Experimental\Http\Message\ServerRequestInterface;
use type Facebook\Experimental\Http\Message\ResponseInterface;
use type Ytake\Hungrr\Response;
use type Ytake\Hungrr\StatusCode;
use type NazgHeredityTest\Middleware\MockMiddleware;
use namespace HH\Lib\Experimental\IO;
use function json_encode;

final class SimpleRequestHandler implements RequestHandlerInterface {

  public function __construct(
    private IO\WriteHandle $handle
  ) {}

  public function handle(ServerRequestInterface $request): ResponseInterface {
    $header = $request->getHeader(MockMiddleware::MOCK_HEADER);
    if (count($header)) {
      $this->handle->rawWriteBlocking(json_encode($header));
      return new Response($this->handle, StatusCode::OK);
    }
    $this->handle->rawWriteBlocking(json_encode([]));
    return new Response($this->handle, StatusCode::OK);
  }
}

ヘッダが操作されているかどうかを判定してレスポンスを変更する処理です。

ミドルウェアディスパッチャのテスト

最後に実装したAcme\Middleware\Dispatcherクラスを用いた機能テストを記述して実行してみましょう。
これまでに紹介したHackTestを利用します。


<?hh // strict

use type Acme\Middleware\Dispatcher;
use type Acme\Middleware\MiddlewareStack;
use type Ytake\Hungrr\ServerRequestFactory;
use type Ytake\Hungrr\Response;
use type Facebook\HackTest\HackTest;
use namespace HH\Lib\Experimental\IO;
use function Facebook\FBExpect\expect;

final class DispatcherTest extends HackTest {

  public function testFunctionalMiddlewareRunner(): void {
    list($read, $write) = IO\pipe_non_disposable();
    $heredity = new Dispatcher(
      new MiddlewareStack([]),
      new SimpleRequestHandler($write)
    );
    $response = $heredity->handle(
      ServerRequestFactory::fromGlobals($read),
    );
    $content = $read->rawReadBlocking();
    $decode = json_decode($content);
    expect($decode)->toBeSame([]);
  }

  public function testFunctionalMiddlewareStackRunner(): void {
    list($read, $write) = IO\pipe_non_disposable();
    $heredity = new Dispatcher(
      new MiddlewareStack([MockMiddleware::class, MockMiddleware::class]),
      new SimpleRequestHandler($write)
    );
    $response = $heredity->handle(
      ServerRequestFactory::fromGlobals(),
    );
    $decode = json_decode($read->rawReadBlocking());
    expect(count($decode))->toBeSame(2);
  }
}

testFunctionalMiddlewareRunnerメソッドでは、
ミドルウェアを使わずに、リクエストハンドラを実行するテストです。
ヘッダがない場合は、空のJSONが返却されます。
testFunctionalMiddlewareStackRunnerメソッドは、
ミドルウェアで追加されたHeaderの個数をレスポンスで返却するテストです。

簡単な実装例とテストを紹介しましたが、
PHPのライブラリ開発とそこまで大き変わらないことがわかると思います。
実際に取り入れて利用してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0