2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Hack and HHVMAdvent Calendar 2020

Day 3

Hackのルーターを触ってみよう

Posted at

Hack-Router

今回は、Hack-Routerを使って、
簡単にHTTPに関する処理を実装してみましょう。

Hackはライブラリが多くなく(むしろ少ない)、
ルーターはこのライブラリくらいしかありません。
ライブラリを作りたい方はすでにあるHackのライブラリを触って、
コードを読んでから作ってみると良いでしょう。
今回はそんな定番とも言えるルーターライブラリを触ります。

PHPと異なり、Generics、Enumsを利用していますので、
PHPしか利用していない方は少しばかり難しいですが、
大したことはありません。

開発環境の用意

手元にHHVMなんてないよ!という方は、
HHVM(proxygen) 環境を構築してみようにありますので、
事前に準備しておきましょう。

インストール

まずは前日の[2020] HHVM/Hackの始め方 導入編にある通り、
hhvm/hhvm-autoloadをインストールします。

$ composer require hhvm/hhvm-autoload

続いてfacebook/hack-routerをインストールします。

$ composer install facebook/hack-router

HTTPリクエストをそのまま使うこともできますが、
初めてHackを触る方で手抜をしたい方はHTTP Message implementationライブラリを使った方が楽でしょう。
下記のコマンドでライブラリをインストールします。

$ composer require ytake/hungrr

続いて src、tests(今回は使いませんが!)ディレクトリを作成し、
composer.jsonと同じルートディレクトリにhh_autoload.jsonを作成して下記の通りに記述します。

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

そして最後に.hhconfigを作成して以下の通りに記述しておきましょう。

.hh_config
assume_php=false
ignored_paths = [ "vendor/.+/tests/.+" ]
disallow_elvis_space=true
disallow_non_arraykey_keys=true
disallow_unsafe_comparisons=true
decl_override_require_hint=true
enable_experimental_tc_features=shape_field_check,sealed_classes
disable_primitive_refinement=true
disable_static_local_variables = true
disallow_array_literal = true
allowed_decl_fixme_codes=1002,2053,4045,4047
allowed_fixme_codes_strict=1002,2011,2049,2050,2053,4007,4027,4045,4047,4053,4104,4106,4107,4108,4110,4128,4135,4188,4240,4323

allowed_fixme_codes_strictはライブラリでエラーになっているものもいくつかありますので、
その中で代表的なエラーは警告しないように指定しています。

composer.jsonで任意のautoloadの設定を記述しておきましょう。

composer.json
{
    "name": "acme/sample",
    "require": {
        "facebook/hack-router": "^0.19.7",
        "hhvm/hhvm-autoload": "^3.1",
        "ytake/hungrr": "^0.13.3"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Sample\\": "src/"
        }
    }
}

これでルーターを使うまでの準備が整いました。
$ composer dump-autoloadなどは実行しておきましょう

Hack Routerを理解する

まずはルーターの定義を記述します。

src/Router/ExampleRouter.hack
namespace Acme\Example\Router;

use type Facebook\HackRouter\{BaseRouter, HttpMethod};

type TResponder = (function(dict<string, string>):string);

final class ExampleRouter extends BaseRouter<TResponder> {
  
  <<__Override>>
  protected function getRoutes(
  ): ImmMap<HttpMethod, ImmMap<string, TResponder>> {
    return ImmMap {
      HttpMethod::GET => ImmMap {
        '/' => ($_) ==> 'Hello, world',
      },
    };
  }
}

Facebook\HackRouter\Routerは、<+TResponder>を要求する規定クラスになっています。
この<+TResponder>はGenericsになっているので、
利用者が必要とする形を指定することができます。

この例では type TResponder = (function(dict<string, string>):string);としており、
これはルーティングが見つかれば、コールバックで該当する処理を返却し、
引数としてdict<string, string>を渡す、という指定です。

具体的なルーティングは、下記の部分となります。

  <<__Override>>
  protected function getRoutes(
  ): ImmMap<HttpMethod, ImmMap<string, TResponder>> {
    return ImmMap {
      HttpMethod::GET => ImmMap {
        '/' => ($_) ==> 'Hello, world',
      },
    };
  }

HTTPメソッドは、Facebook\HackRouter\HttpMethod enumsを利用する様に
Facebook\HackRouter\Routerの抽象メソッドで指定されているものです。

  abstract protected function getRoutes(
  ): KeyedContainer<HttpMethod, KeyedContainer<string, TResponder>>;

KeyedContainerはHackで用意されている型で、HH\KeyedContainerを指します。
これはディクショナリであれば利用できる様になっていますので、
次の様にdictを利用しても問題ありません。

  <<__Override>>
  protected function getRoutes(
  ): dict<HttpMethod, dict<string, TResponder>> {
    return dict [
      HttpMethod::GET => dict[
        '/' => ($_) ==> 'Hello, world',
      ],
    ];
  }

ですが、通常ルーティングを動的に操作することはまずないと思いますので、
ImmMapを使うのが適切です。
*ImmMapはイミュータブルなMapなのでそれ以上変更はできません。

Facebook\HackRouter\HttpMethod enumsでは下記のものが用意されています。

namespace Facebook\HackRouter;

enum HttpMethod: string {
  HEAD = 'HEAD';
  GET = 'GET';
  POST = 'POST';
  PUT = 'PUT';
  PATCH = 'PATCH';
  DELETE = 'DELETE';
  OPTIONS = 'OPTIONS';
  PURGE = 'PURGE';
  TRACE = 'TRACE';
  CONNECT = 'CONNECT';
  REPORT = 'REPORT';
  LOCK = 'LOCK';
  UNLOCK = 'UNLOCK';
  COPY = 'COPY';
  MOVE = 'MOVE';
  MERGE = 'MERGE';
  NOTIFY = 'NOTIFY';
  SUBSCRIBE = 'SUBSCRIBE';
  UNSUBSCRIBE = 'UNSUBSCRIBE';
}

十分ですね!

Routerを起動する

それでは実際にHTTPリクエストを受けた時にルーティングが起動する様にしてみましょう。

public/index.hack
use type Acme\Example\Router\ExampleRouter;
use type Facebook\HackRouter\{BaseRouter, HttpMethod};
use type Ytake\Hungrr\ServerRequestFactory;

<<__EntryPoint>>
async function mainAsync(): Awaitable<void> {
  require_once __DIR__.'/../vendor/autoload.hack';
  \Facebook\AutoloadMap\initialize();

  $router = new ExampleRouter();
  $request = ServerRequestFactory::fromGlobals();
  $vec = $router->routeRequest($request);
  $c = $vec[0];
  echo $c($request->getQueryParams());
}

先ほど記述したルータークラスのインスタンスを生成し、
ServerRequestFactory::fromGlobals()でHTTPリクエストを取得し、
リクエスト情報をルーターオブジェクトに渡すことで、
マッチするルートがあるかを調べて返却します。

存在しないルートのリクエストであれば
Facebook\HackRouter\NotFoundExceptionがスローされます。

マッチするものがあれば、TResponderで指定した(function(dict<string, string>):string)
が返却されます。
これをコールすることで、
'Hello, world',が返却される、という仕組みです。
callbackされるならPHPでよく使う__invokeがあるのに、と多くの方が思うと思いますが、
Hackでは_invokeはcallableとみなされませんので利用することができません。

実際に利用する場合はこのままだとさすがに少し厳しいので、
以下の様にクラスを用意するといいでしょう。

src/Action/ActionInterface.hack
namespace Acme\Example\Action;

use type Ytake\Extended\HttpMessage\ServerRequestInterface;

<<__ConsistentConstruct>>
interface ActionInterface {

  public function process(
    ServerRequestInterface $request
  ): string;
}

<<__ConsistentConstruct>>Attributeは、コンスタラクタに対するものです。
このAttributeが記述されたクラスは、
継承する場合にかならずコンストラクタの引数などは同一のものとならなければなりません。
記述することでインスタンス生成でコンストラクタに必要な引数が保証されます。
インターフェースの記述すると、コンストラクタで必要な引数はない、ということになります。
*例のため簡単にできる様に記述していますが、実際にはこうしません!注意!

このインターフェースを実装したアクションクラスを用意します。

namespace Acme\Example\Action;

use type Ytake\Extended\HttpMessage\ServerRequestInterface;
use function json_encode;

final class HomeAction implements ActionInterface {

  public function process(
    ServerRequestInterface $request
  ): string {
    return 'Sample ' . json_encode($request->getQueryParams());
  }
}

続いてルータークラスを修正します。

src/Router/ExampleRouter.hack
namespace Acme\Example\Router;

use type Ytake\Extended\HttpMessage\ServerRequestInterface;
use type Facebook\HackRouter\{BaseRouter, HttpMethod};
use type Acme\Example\Action\{HomeAction, ActionInterface};

type TResponder = classname<ActionInterface>;

final class ExampleRouter extends BaseRouter<TResponder> {
  <<__Override>>
  protected function getRoutes(
  ): ImmMap<HttpMethod, ImmMap<string, classname<ActionInterface>>> {
    return ImmMap {
      HttpMethod::GET => ImmMap {
        '/' => HomeAction::class,
      },
    };
  }
}

classname<>はHackで用意された特殊な型で、
className::classで記述することを指示するもので、
Interfaceを指定することでそのインターフェースを実装したクラス名のみ記述ができるようになります。
こうすることでtypecheckerがImmMapの中身を認識して、
型安全かどうかがわかる、という仕組みです。

最後にエントリポイントを修正します。

src/Router/ExampleRouter.hack
use type Acme\Example\Router\ExampleRouter;
use type Facebook\HackRouter\{BaseRouter, HttpMethod};
use type Ytake\Hungrr\ServerRequestFactory;

<<__EntryPoint>>
async function mainAsync(): Awaitable<void> {
  require_once __DIR__.'/../vendor/autoload.hack';
  \Facebook\AutoloadMap\initialize();

  $router = new ExampleRouter();
  $request = ServerRequestFactory::fromGlobals();
  $vec = $router->routeRequest($request);
  $c = $vec[0];
  echo (new $c())->process($request);
}

これで / にアクセスするとActionクラスのprocessメソッドで指定した文字列が返却されます。
リクエストパラメーターをつけるとjson_encodeされて表示されます。
ここでポイントは先ほどの<<__ConsistentConstruct>>です。
TResponderで指定した型をtypecheckerが認識しているため、
<<__ConsistentConstruct>>がなければ不安定なコンストラクタとしてエラーで実行できません。
そのためここでは<<__ConsistentConstruct>>を記述し、
動的にインスタンス生成をできる様にしています。

今回はHackのルーターを通してHackの型にも触れる内容をお届けしました。
次回はXHPを使ってHTMLを安全に返却してみましょう!

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?