PHPと分離し始めたHHVM/Hackですが、
折角なので多くの方が やってみた で終わらないように、
シンプルで薄いマイクロなフレームワーク、というか、
Web Applicationのボイラープレートと言ってもいいくらいの簡単なものを公開しました。
*名前は 指輪物語 より
機能を備えたフレームワークを作るよりも、
最近はコンポーネントなどを組み合わせる開発者も多いため、
最低限のリクエスト・レスポンス以外の機能を付け加える予定はありませんが、
とっかかりには小さくチャレンジできるのではと思います。
簡単な使い方などを紹介します。
HHVM環境構築
Ubuntu16.04などのサーバが手元にある方は、簡単にHHVMの環境が構築できます
または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を利用しています。
クラス追加時は、以下のコマンドを必ず実行して、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が混在し、
AndroidやiOSの開発者に注意されることなどもあるのではないかと思いますが、
そう云うケースや、バリデーションに利用することができます。
ここではレスポンスに対して、期待通りのレスポンスを返しているか
チェックするミドルウェアを追加してみましょう
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によるアプリケーション開発を紹介しました