気軽にHackチャレンジ マイクロフレームワーク公開

PHPと分離し始めたHHVM/Hackですが、
折角なので多くの方が やってみた で終わらないように、
シンプルで薄いマイクロなフレームワーク、というか、
Web Applicationのボイラープレートと言ってもいいくらいの簡単なものを公開しました。

github.com

*名前は 指輪物語 より

機能を備えたフレームワークを作るよりも、
最近はコンポーネントなどを組み合わせる開発者も多いため、
最低限のリクエスト・レスポンス以外の機能を付け加える予定はありませんが、
とっかかりには小さくチャレンジできるのではと思います。

簡単な使い方などを紹介します。

HHVM環境構築

Ubuntu16.04などのサーバが手元にある方は、簡単にHHVMの環境が構築できます

Installation: Introduction

またはVagrantで簡単に構築することもできます。
公開しているytake/gardening-hhvmを利用すると、3.23.4の環境が起動します。

$ vagrant box add ytake/gardening-hhvm

Vagrantの詳細については gardening-hhvm#install-gardening-box

Dockerについては後日

install

composer を使ってcreate-projectをする場合は、次のコマンドを実行します。

$ hhvm -d xdebug.enable=0 -d hhvm.jit=0 -d hhvm.php7.all=1 -d hhvm.hack.lang.auto_typecheck=0 \
 $(which composer) create-project nazg/skeleton [アプリケーション名] --prefer-dist

依存しているPSR-15インターフェースがphp7以上となっているため、
PHP7モードで実行する必要があります。

簡単にAPIを作ってみよう

環境構築後は実際に実装するだけです。
簡単なAPIを実装します。

処理フロー

このフレームワークAPIやそこまで大きくない規模のアプリケーション利用を想定して、
ADRを採用しています。

まずはAction、Routerです。

Action

Zend Expressiveのようにこのフレームワークにおいても、
アクションはあくまで一つのミドルウェアにすぎず、PSR-15(現在ドラフト)を採用しています。

これにならって、まずはActionを用意します。
src/Action/ReadAction.php として下記のものを記述します。

<?hh

namespace App\Action;

use App\Responder\IndexResponder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\JsonResponse;

final class ReadAction implements MiddlewareInterface {

  public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler,
  ): ResponseInterface {
    return new JsonResponse([]);
  }
}

Hackのコードだけであれば、 <?hh // strict で厳格モードで実装ができますが、
PHPのライブラリが含まれる場合は、厳格モードにせずに実装します
PSR-7に準拠したライブラリで、HHVM/Hackで動作するものであればなにを利用しても構いません。
デフォルトでは zendframework/zend-diactoros になります。

このActionをRouterに登録します。

Router

デフォルトでは config/routes.global.php に記述するだけです
GETリクエストで作用するActionとして下記の通りに記述します。

<?hh

return [
  \Nazg\Foundation\Service::ROUTES => ImmMap {
    \Nazg\Http\HttpMethod::GET => ImmMap {
      '/' => ImmVector{ App\Action\IndexAction::class },
      '/sample' => ImmVector{ App\Action\ReadAction::class },
    },
  },
];

Register Container

利用準備はこれで整いますが、
このフレームワークでは簡単なDependency Injectionをサポートしており、
インスタンスの生成方法を指定する必要があります。
LaravelのようなAuto Wiringはないため、記述していないクラスは生成することができません。

デフォルトでは src/Module/ActionServiceModule.php が用意されていますので、
そちらに追記します。

<?hh // strict

namespace App\Module;

use App\Action\IndexAction;
use App\Action\ReadAction;
use App\Responder\IndexResponder;
use Ytake\HHContainer\Scope;
use Ytake\HHContainer\ServiceModule;
use Ytake\HHContainer\FactoryContainer;

final class ActionServiceModule extends ServiceModule {

  public function provide(FactoryContainer $container): void {
    $container->set(
      IndexAction::class,
      $container ==> new IndexAction(new IndexResponder()),
      Scope::PROTOTYPE,
    );
    // 追加したActionのインスタンス生成方法を記述
    $container->set(
      ReadAction::class,
      $container ==> new ReadAction(),
      Scope::PROTOTYPE,
    );
  }
}

Scopeは指定しない場合は都度インスタンスを生成するPrototypeになりますが、
Singletonを望む場合は、 Scope::SINGLETON を指定してください。

Containerの詳細な使い方については、
ytake/hh-container を参照ください。

クラス追加時に忘れずにdump-autoload

このフレームワークはHackに最適化されたcomposerプラグインのhhvm-autoloadを利用しています。

github.com

クラス追加時は、以下のコマンドを必ず実行して、hh_autoload.phpに反映してください。

hhvm -d xdebug.enable=0 -d hhvm.jit=0 -d hhvm.php7.all=1 -d hhvm.hack.lang.auto_typecheck=0 $(which composer) dump-autoload

実行後は追加した /sample にアクセスしてみてください。
空のJson配列が返却されているはずです。

Hackならではの機能を使ってみよう

HackにはShapeという配列に対しての型をチェックするものがあり、
配列に対しても厳格さを要求することができます。

APIを開発する際に、あるカラムにstringやintが混在し、
AndroidiOSの開発者に注意されることなどもあるのではないかと思いますが、
そう云うケースや、バリデーションに利用することができます。

ここではレスポンスに対して、期待通りのレスポンスを返しているか
チェックするミドルウェアを追加してみましょう

TypeAssert

hhvm/type-assert を使って厳格に調べるように実装し、
configファイルで特定のrouteにのみ作用するように記述します。

<?hh // strict

namespace App\Middleware;

use Facebook\TypeAssert;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class TypeAssertMiddleware implements MiddlewareInterface {

  const type ReadStructure = shape('name' => string,);

  public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler,
  ): ResponseInterface {
    $response = $handler->handle($request);
    $decode = json_decode($response->getBody()->getContents(), true);
    TypeAssert\matches_type_structure(
      type_structure(self::class, 'ReadStructure'),
      $decode,
    );
    return $response;
  }
}

配列に期待する型をshapeで記述しています。
この例では、配列の中の name はstringであることとなります。

const type ReadStructure = shape('name' => string,);

先ほどの例と同様に、config/routes.global.phpや、
MiddlewareServiceModule.phpを作成し、アプリケーションに登録します。

MiddlewareServiceModule

デフォルトのActionServiceModule.phpと同様のクラスを作成します

<?hh // strict

namespace App\Module;

use App\Middleware\TypeAssertMiddleware;
use App\Responder\IndexResponder;
use Ytake\HHContainer\Scope;
use Ytake\HHContainer\ServiceModule;
use Ytake\HHContainer\FactoryContainer;

final class MiddlewareServiceModule extends ServiceModule {

  public function provide(FactoryContainer $container): void {
    $container->set(
      TypeAssertMiddleware::class,
      $container ==> new TypeAssertMiddleware(),
    );
  }
}

各configに追記します。

config/module.global.php

依存解決方法を記載したServiceModuleクラスを追加します

return [
  \Nazg\Foundation\Service::MODULES => [
    \App\Module\ActionServiceModule::class,
    \App\Module\MiddlewareServiceModule::class,    
  ],
];

config/routes.global.php

特定のrouteで作用するように、ImmVectorに追記します。
Actionクラスを挟み込むように作用させるには、Actionクラスよりも前に指定します。
Action, Middlewareはここで指定した順番で実行されます。

<?hh

return [
  \Nazg\Foundation\Service::ROUTES => ImmMap {
    \Nazg\Http\HttpMethod::GET => ImmMap {
      '/' => ImmVector{ 
        \App\Middleware\TypeAssertMiddleware::class, 
        \App\Action\IndexAction::class,
      },
      '/sample' => ImmVector{ App\Action\ReadAction::class },
    },
  },
];

これで /sample にアクセスすると 配列に期待している型とは異なっているため、
Facebook\TypeAssert\IncorrectTypeException がスローされます。

これを回避するため、src/Action/ReadAction.php で返却されるレスポンスを変更します。

<?hh // strict

// 省略
final class ReadAction implements MiddlewareInterface {

  public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler,
  ): ResponseInterface {
    return new JsonResponse(['name' => 'ytake']);
  }
}

これで /sample にアクセスすると期待通りの型になったことにより、
通常のjsonのレスポンスが返却されます。

今回は簡単なHackによるアプリケーション開発を紹介しました