Posted at

HHVM/Hack : Building your own Micro Framework

今回はこれまでの記事の内容を踏まえて、

Hackのライブラリを組み合わせて簡単なWebマイクロフレームワークを作ります。

この記事を初めて読む場合は、Hack and HHVM Advent Calendar 2018のエントリを事前に目を通しておくことをおすすめします。

フレームワークには欠かせないDependency Injectionを担当する依存解決ライブラリの作り方は

Hackで作る簡単サービスロケーター

Hackで作る簡単サービスロケーター / Generics

Hackにおけるリクエストレスポンスを担当するライブラリについては、

HackにおけるHTTP Request and Response Interfaces

リクエストハンドラ・ミドルウェアについては

Hackで作るMiddleware Dispatcher

を参照してください。

本エントリに対応したソースコードはこちらです


composer

今回利用するcomposer.jsonは以下の通りです。

Hackで作るMiddleware Dispatcherの元になっている nazg/heredityを利用していますが、

前回のエントリの通り、自分で実装したものをそのまま利用もできますので、

お好きな方を選んでください。


composer.json

{

"name": "acme/sample",
"minimum-stability": "stable",
"require": {
"hhvm": "^3.30.0",
"hhvm/hhvm-autoload": "^1.7",
"hhvm/hsl": "^3.30.0",
"hhvm/hsl-experimental": "^3.30.0",
"ytake/hungrr": "^0.2.0",
"nazg/heredity": "^1.1.0",
"facebook/hack-router": "^0.17"
},
"require-dev": {
"hhvm/hacktest": "^1.3",
"facebook/fbexpect": "^2.3"
},
"autoload": {
"psr-4": {
"Acme\\Sample\\": "src/"
}
}
}


hh-autoload

hh_autoload.jsonはいつもどおりのもので構いません。


hh_autoload.json

{

"roots": [
"src/"
],
"devRoots": [
"tests/"
]
}


.hhconfig

PHPライブラリは一切利用しませんので下記のものにしましょう。

assume_php = false

ignored_paths = [ "vendor/.+/tests/.+" ]
safe_array = true
safe_vector_array = true


サービスコンテナとミドルウェアを組み合わせる

Hackで作るMiddleware Dispatcher#インスタンス生成のためのインターフェース

で用意したインターフェースを実装し、

以前のエントリで実装したコンテナを組み合わせます。

インスタンス生成自体をコンテナに依頼するだけで実装は完了です。

<?hh // strict

namespace Acme\Sample\Middleweare;

use type Acme\Sample\Container;
use type Nazg\Heredity\Resolvable;
use type Ytake\HackHttpServer\MiddlewareInterface;

class ContainerResolver implements Resolvable {

public function __construct(
protected Container $container
) {}

public function resolve(
classname<MiddlewareInterface> $middleware
): MiddlewareInterface {
return $this->container->get($middleware);
}
}

コンテナのgetメソッドは。前回実装で下記のGenericsを利用しているので、

Ytake\HackHttpServer\MiddlewareInterface(Acme\HackHttpServer\MiddlewareInterface)インターフェースを実装しているクラス名が渡されれば、

どんなものがきても問題ありません。

Typecheckerでもその様に判断されますので、警告が発生することはありません。

  <<__Rx>>

public function get<T>(classname<T> $t): T {
// 省略
}


Actionクラスを作成

ただ単に文字列を返却するだけのクラスを作成します。

HTTPリクエストに対応するクラスをActionクラスとして作成します。

実態はリクエスト・レスポンス間におけるミドルウェアの一部という位置付けで実装しましょう。

<?hh // strict

namespace Acme\Sample\Middleware;

use type Facebook\Experimental\Http\Message\ResponseInterface;
use type Facebook\Experimental\Http\Message\ServerRequestInterface;
use type Ytake\HackHttpServer\MiddlewareInterface;
use type Ytake\HackHttpServer\RequestHandlerInterface;
use type Ytake\Hungrr\Response;
use type Ytake\Hungrr\StatusCode;

use namespace HH\Lib\Experimental\IO;

final class IndexAction implements MiddlewareInterface {

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

public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
$this->handle->rawWriteBlocking('hello!');
return new Response($this->handle, StatusCode::OK);
}
}

ミドルウェアのテストとして作成したクラスと同じものを利用してもいいです。


Router実装

Routerにはfacebook/hack-routerを利用します。

BaseRouterクラスを継承して、どんなレスポンスを対応させるかはGenericsを使うことで

アプリケーションに合わせて用意できます。

今回は、リクエストに対応する先ほどのアクションクラスを返却できる様に下記の様にしましょう。

use namespace Facebook\HackRouter;

type ImmRouteMap = ImmMap<HackRouter\HttpMethod, ImmMap<string, TResponder>>;
type MiddlewareVector = ImmVector<classname<MiddlewareInterface>>;
type TResponder = shape(
'middleware' => MiddlewareVector
);

突然こう書かれてもよくわかりませんね。

routerの定義は下記の様に記述できれば良い、ということになります。

use namespace Facebook\HackRouter;

ImmMap {
HackRouter\HttpMethod::GET => ImmMap {
'/' => shape(
'middleware' => ImmVector {
Middleware\IndexAction::class,
},
)
}
}

HTTPメソッドごとにURIを記述し、

URIはミドルウェアとして動作させるものを記述します。

上から順番に実行されるものと考えておけば良いでしょう。

getRoutesメソッドの処理を記述するだけでルーターとして利用できますので、

今回は下記の通りに実装します。

<?hh // strict

namespace Acme\Sample;

use namespace Facebook\HackRouter;
use type Facebook\Experimental\Http\Message\HTTPMethod;
use type Ytake\HackHttpServer\MiddlewareInterface;

type ImmRouteMap = ImmMap<HackRouter\HttpMethod, ImmMap<string, TResponder>>;
type MiddlewareVector = ImmVector<classname<MiddlewareInterface>>;
type TResponder = shape(
'middleware' => MiddlewareVector
);

final class Router extends HackRouter\BaseRouter<TResponder> {

public function __construct(
private ImmRouteMap $routeMap
) {}

<<__Override>>
protected function getRoutes(
): ImmMap<HackRouter\HttpMethod, ImmMap<string, TResponder>> {
return $this->routeMap;
}
}


アプリケーションクラス

最後にここまでに実装したクラスを組み合わせてアプリケーションとして動作する様に実装します。

以前のエントリで実装したコンテナライブラリに先ほどのRouterクラス、

そしてミドルウェアとして動作させたいアクションクラスのインスタンス生成方法を指定してみましょう。

ここではサンプルで作りますので一つのクラスに記述しますが、

実際にさらに作り込んでいく場合は、責務に合わせて分割していくと良いでしょう。

インスタンス生成方法とルート定義を合わせて記述します。

  private function registerDependencies(): void {

$routes = ImmMap {
HackRouter\HttpMethod::GET => ImmMap {
'/' => shape(
'middleware' => ImmVector {
Middleware\IndexAction::class,
},
)
}
};
$this->container->set(
HackRouter\BaseRouter::class,
($container) ==> new Router($routes),
Scope::SINGLETON
);
$this->container->set(
Middleware\IndexAction::class,
($container) ==> new Middleware\IndexAction($this->writeHandle),
);
$this->container->lock();
}

実際にはルート定義は設定ファイルなどにしても良いでしょう。

ここでは以前のエントリで実装したコンテナライブラリをそのまま利用しています。

Routerクラスのインスタンスでルート定義のImmMapを必要としていましたので、

インスタンス生成時にImmMapインスタンスを利用する様に記述します。

コンテナに登録したインスタンス生成方法について、不必要な変更を防ぐために用意したlockメソッドを実行し、

これ以上の変更が行われない様にしています。

次にこのクラス自体が依存するインスタンスと、

HTTPリクエストを受けてからミドルウェアに処理実行を依頼し、

レスポンスを描画するところまで一気に実装します。

といっても実装内容は簡単です。

  public function __construct(

protected Container $container,
private IO\ReadHandle $readHandle,
private IO\WriteHandle $writeHandle
) {}

public function run(ServerRequestInterface $request): void {
$this->registerDependencies();
$router = $this->container->get(HackRouter\BaseRouter::class);
list($responder, $map) = $router->routeRequest($request);
$stack = new MiddlewareStack(
$responder['middleware'],
new Middleware\ContainerResolver($this->container),
);
$queue = new Heredity($stack);
$queue->handle($request);
echo $this->readHandle->rawReadBlocking();
}

コンストラクタはハンドルクラスのみ依存させ、

runメソッドで依存解決を行なってから、

コンテナから必要なクラスのインスタンスを取得し、

ルーターライブラリにリクエストインスタンスを渡し。ルーターにマッチしたものを取得して、

ミドルウェアライブラリに処理を実行してもらい、最後に受けとるレスポンスクラスを描画で扱う流れになっています。

このクラスの全体は下記の通りです

<?hh // strict

namespace Acme\Sample;

use type Acme\Sample\Router;
use type Acme\Sample\Container;
use type Facebook\Experimental\Http\Message\ServerRequestInterface;
use type Nazg\Heredity\Heredity;
use type Nazg\Heredity\MiddlewareStack;

use namespace Facebook\HackRouter;
use namespace HH\Lib\Experimental\IO;
use namespace Acme\Sample\Middleware;

class Application {

public function __construct(
protected Container $container,
private IO\ReadHandle $readHandle,
private IO\WriteHandle $writeHandle
) {}

public function run(ServerRequestInterface $request): void {
$this->registerDependencies();
$router = $this->container->get(HackRouter\BaseRouter::class);
list($responder, $map) = $router->routeRequest($request);
$stack = new MiddlewareStack(
$responder['middleware'],
new Middleware\ContainerResolver($this->container),
);
$queue = new Heredity($stack);
$queue->handle($request);
echo $this->readHandle->rawReadBlocking();
}

private function registerDependencies(): void {
// routes
$routes = ImmMap {
HackRouter\HttpMethod::GET => ImmMap {
'/' => shape(
'middleware' => ImmVector {
Middleware\IndexAction::class,
},
)
}
};
$this->container->set(
HackRouter\BaseRouter::class,
($container) ==> new Router($routes),
Scope::SINGLETON
);
$this->container->set(
Middleware\IndexAction::class,
($container) ==> new Middleware\IndexAction($this->writeHandle),
);
$this->container->lock();
}
}


エントリポイント

リクエスト受診時に上記のアプリケーションクラスを実行するための処理です。

一般的にpublic/index.phpなどとして利用されるコードです。

このサンプルでは下記の通りに記述することで動作します。

<?hh // strict

require_once __DIR__ . '/../vendor/hh_autoload.hh';

use type Acme\Sample\Application;
use type Acme\Sample\Container;
use type Ytake\Hungrr\ServerRequestFactory;
use namespace HH\Lib\Experimental\IO;

<<__EntryPoint>>
function main(): noreturn {

list($read, $write) = IO\pipe_non_disposable();
$app = new Application(new Container(), $read, $write);
$app->run(ServerRequestFactory::fromGlobals(IO\request_input()));
exit(0);
}

ここまでできたら実際に動かしてみましょう。

これまでのエントリで紹介したproxygenで動かします。

hhvm -m server -p 8080 -d hhvm.server.source_root=/Path/To/Project/public -d hhvm.server.type=proxygen

ブラウザなどからアクセス(127.0.0.1:8080など)して画面上に hello! と表示されているのを確認できれば、

簡単なマイクロフレームワークの完成です。

ここから必要な機能やライブラリを追加することで、PHPのフレームワークと変わらずWebアプリケーションが開発できる様になります。

HTMLなどの出力は初めてのXHP

初めてのXHP Tutorial / XHPとReact

などにあるようにこのマイクロフレームワークにXHPも利用できます。

ここまでHackのみで簡単なアプリケーション/マイクロフレームワークを作ることができました。

足りないライブラリなどを開発して、どんどん公開していきましょう!