最新XHPを使ってみよう


XHPをたくさん使ってみよう!

2年前のアドベントカレンダーで下記を紹介しました。
初めてのXHP Tutorial / XHPとReact

あれから2年、少し進化を遂げ独特なコロンを多用した記述方が通常のクラスの記述方法に近くなり、
今までクラスの名前空間がサポートされていませんでしたが、
ようやくサポートされました。

これまでは名前空間を使うためには class :foo:bar と書く必要がありましたが、
今回通常のクラスと同じ記法になりました。

以前のバージョンとの書き方は以下の様に異なります。

Version3まで

class :foo:bar extends :x:element {
  category %flow;
  children (:div, :code*);

  protected function render(): void {
    return <x:frag>{$this->getChildren());
  }
}

Version4

namespace foo;

use namespace Facebook\XHP\Core as x;
use namespace Facebook\XHP\ChildValidation as XHPChild;
use type Facebook\XHP\HTML\{div, code};

xhp class bar extends x\element
implements \Facebook\XHP\HTML\Category\Flow {
  use XHPChild\Validation;

  protected static function getChildrenDeclaration(): XHPChild\Constraint {
    return XHPChild\any_of(
      XHPChild\of_type<div>(),
      XHPChild\at_least_one_of(XHPChild\of_type<code>()),
    );
  }

  protected async function renderAsync(): Awaitable<x\node> {
    return <x:frag>{$this->getChildren()}</x:frag>;
  }
}

同じ様に見えて実は大きな変化があります。
まずはxhpのクラスは、xhp class と書く様に変更されています。
また、これまで利用していたrenderメソッドが廃止になり、renderAsyncのみへ変更されました。
これにより、レンダリングに関する処理はasyncとなります。
これは大きな変化!

他に名前空間対応をしたことで、:x:element などはすべて名前空間Facebook\XHP\Coreの配下となりました。
またここにはありませんが、htmlタグはほぼ全てFacebook\XHP\HTML配下となりました。

これまでの記述方法では全く動かなくなりましたが、修正範囲自体はすくなく、
またhhastのhhast-migrate --xhp-lib-v3-to-v4 コマンドで
自動でマイグレーションができますので、大きな問題はありません!

Hello World

XHPはReactの元になったもの、というのは多くの方が知っていると思いますが、
基本的にReactと同じ書き方なのは変わりません。
React同様にコンポーネントごとに実装していくスタイルとなっていますので、
前回の記事にあるRouterで実行されるアクションクラスを少しだけ変更して
簡単に実行してみましょう。

まずはasyncになったということで、アクションクラスのインターフェースは下記のものを利用します。

namespace Acme\Example\Action;

use type Ytake\Extended\HttpMessage\ServerRequestInterface;

<<__ConsistentConstruct>>
interface ActionInterface {

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

asyncは具象クラスで記述しますので、インターフェースで記述することはできません。

XHPを利用するクラスは下記の様に記述します。

namespace Acme\Example\Xhp;

use namespace Facebook\XHP\Core as x;
use type Facebook\XHP\HTML\div;
use type Facebook\XHP\Core\node;
use type Facebook\XHP\HTML\{strong, a};

final xhp class Arrived extends x\element {
  protected async function renderAsync(): Awaitable<node> {
    return <div><strong>Hello!</strong></div>;
  }
}

final xhp class ComponentLink extends x\element {
  protected async function renderAsync(): Awaitable<node> {
    return <a href="/">home</a>;
  }
}

final xhp class Hello extends x\element {
  protected async function renderAsync(): Awaitable<node> {
    return <div>
            <Arrived />
            <ComponentLink />
          </div>;
  }
}

例としてそれぞれのクラスでコンポーネントを用意し、
Helloクラスで組み合わせて利用する様になっています。
XHPはFacebook\XHP\Core\nodeオブジェクトとなりますので、
async利用ということで、Awaitable<Facebook\XHP\Core\node>が返却されます。

これを利用するアクションクラスは下記の通りです。

namespace Acme\Example\Action;

use type Ytake\Extended\HttpMessage\ServerRequestInterface;
use type Acme\Example\Xhp\Hello;

final class HomeAction implements ActionInterface {

  public async function process(
    ServerRequestInterface $request
  ): Awaitable<string> {
    $xhp = <Hello />;
    return await $xhp->toStringAsync();
  }
}

作成したXHPクラスはAcme\Example\Xhp\Helloとなりますので、
利用時は<Acme:Example:Xhp:Hello />となります。

クラスなどを利用しない場合はechoすることでそのままレンダリングされますが、
この場合は最後にエントリポイントでレンダリングするため、
Awaitable<string>を返却する必要があります。

最後にエントリポイントは次の様になります。

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 await (new $c())->process($request);
  exit();
}

ブラウザで上記のように表示されれば成功です!

attribute

コンポーネントに値を渡したい時は、これまで通りattributeを利用します。

final xhp class Arrived extends x\element {
  attribute string name @required;
  protected async function renderAsync(): Awaitable<node> {
    return <div><strong>Hello {$this->:name}!</strong></div>;
  }
}

final xhp class Hello extends x\element {
  attribute string name @required;
  protected async function renderAsync(): Awaitable<node> {
    return <div>
            <Arrived name={$this->:name}/>
            <ComponentLink />
          </div>;
  }
}

先ほどのクラスで、attribute string name @required;を記述することで、
値を取得することができます。
この例ではnameで文字列を必要とする、ということになります。
レンダリング時に動的に値を渡したい場合は、以下の様になるでしょう!

namespace Acme\Example\Action;

use type Ytake\Extended\HttpMessage\ServerRequestInterface;
use type Acme\Example\Xhp\Hello;

final class HomeAction implements ActionInterface {

  public async function process(
    ServerRequestInterface $request
  ): Awaitable<string> {
    $message = 'World';
    $xhp = <Hello name={$message} />;
    return await $xhp->toStringAsync();
  }
}

コンポーネントなどに慣れている方は簡単に利用できると思いますので、
基本的な使い方を覚えて、是非活用してみてください!