HHVM/Hack向けに作ったオレオレマイクロフレームワークにおける
HTTPリクエストのバリデーション実装方法を紹介したいと思います!
Hackならではの機能を使ってバリデーションの仕組みを用意しています。
残念ながらLaravelのような細かいバリデーションルール指定方法などは用意していません
アプリケーションに合わせてひたすら実装するべし!
HackではPHPのような動的なメソッドコールはstrictで利用するとtypecheckerに怒られます
回避方法はありますが、実装していくとstrictにしたくなるものです・・
それはさておき
このフレームワークでは、バリデーションはこうしてください、というルールは特に持っていませんが、
専用に用意したAttribute を記述することで
Laravelのフォームリクエスト のような挙動で、
バリデーションを実行することができます。
*Annotationだと思ってください
Validation対象のActionクラス
routeを '/contents/{content}'
として、このエンドポイントにアクセスした時にバリデーションを実行するようにします。
下記のようなクラスを作成します
ここに記述されているZend\Diactoros\Response\TextResponseクラスを利用してレスポンスを返却していますが、 Psr\Http\Message\ResponseInterface
を実装していればなんでも構いません。
<?hh // strict namespace App\Action\Document; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Diactoros\Response\TextResponse; final class ReadAction implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler, ): ResponseInterface { return new TextResponse('Hello world!'); } }
route登録は、 config/routes.global.php
ファイルに記述します。
<?hh return [ \Nazg\Foundation\Service::ROUTES => ImmMap { \Nazg\Http\HttpMethod::GET => ImmMap { '/contents/{content}' => ImmVector { App\Action\Document\ReadAction::class }, }, }, ];
忘れずにServiceModuleクラスにインスタンス生成方法を記述しましょう。
<?hh // strict namespace App\Module; use App\Action; use Ytake\HHContainer\Scope; use Ytake\HHContainer\ServiceModule; use Ytake\HHContainer\FactoryContainer; final class ActionServiceModule extends ServiceModule { <<__Override>> public function provide(FactoryContainer $container): void { $container->set( Action\Document\ReadAction::class, $container ==> new Action\Document\ReadAction(), Scope::PROTOTYPE, ); } }
これでrouteの準備ができました。
http://お好きなdomain/contents/aaaaa
などでアクセスできます。
Validationクラスを作る
ここではstringの値が送られているかどうか、というバリデーションを例にしますが、
せっかくなのでHackのshapeを利用して型チェックバリデーションとして実装します。
type-assert install
まずは hhvm/type-assert
をインストールします。
composer require hhvm/type-assert
PHPもインストールされている環境で、
上記コマンドでうまくインストールできない方は下記のようにするといいかもしれません
$ hhvm -d xdebug.enable=0 -d hhvm.jit=0 -d hhvm.hack.lang.auto_typecheck=0 $(which composer) require hhvm/type-assert
バリデーションクラスを、 App\Validation\ContentRequestValidator
クラスとして作成します。
Nazg\Foundation\Validation\Validator
クラスを継承して実装します。
<?hh // strict namespace App\Validation; use Facebook\TypeAssert; use Nazg\Foundation\Validation\Validator; use Psr\Http\Message\ServerRequestInterface; final class ContentRequestValidator extends Validator { const type ContentRequestShape = shape( 'content' => string, ); protected bool $shouldThrowException = true; protected Vector<string> $errors = Vector{}; <<__Override>> protected function assertStructure(): void { try { TypeAssert\matches_type_structure( type_structure(self::class, 'ContentRequestShape'), $this->request?->getAttributes(), ); } catch (TypeAssert\IncorrectTypeException $e) { $this->errors->add("type error"); } } protected function assertValidateResult(): Vector<string> { return $this->errors; } }
ContentRequestShape
ContentRequestShapeは、リクエストで受け取る値をshapeを使って型を記述しています。
shapeはGoのstructのようなものだと思っておくと理解しやすいかもしれません
shouldThrowException property
フレームワークで、バリデーションエラー時はExceptionを投げないようになっています。
trueにすることでExceptionHandlerクラスで自由にレスポンスを操作することができます。
Laravel/LumenのExceptionHandlerクラスの使い方とほぼ同じです。
assertStructure、assertValidateResultメソッド
フレームワークで用意しているバリデーションで、
型チェックと値自体のバリデーションの両方を実装することができるようになっています。
型チェックが先に実行され、assertValidateResultがバリデーション実行後の結果を返却します。
細かいバリデーションは、クラス内にそれぞれのバリデーションを行いたいメソッドを記述し、
assertValidateResultでそれらを実行し、結果を Vector<string>
に詰める、という具合です。
<?hh try { TypeAssert\matches_type_structure( type_structure(self::class, 'ContentRequestShape'), $this->request?->getAttributes(), ); } catch (TypeAssert\IncorrectTypeException $e) { $this->errors->add("type error"); }
Facebook\TypeAssert\matches_type_structure
でHTTPリクエストの値が期待している型かどうかをチェックし、
期待していない型の場合は、 Facebook\TypeAssert\IncorrectTypeException
がスローされるため、
Vectorに失敗したことを示す文字列を追加しています。
実装後このバリデーションクラスをServiceModuleクラスで登録します。
<?hh // strict namespace App\Module; use App\Validation; use Ytake\HHContainer\Scope; use Ytake\HHContainer\ServiceModule; use Ytake\HHContainer\FactoryContainer; final class ValidationServiceModule extends ServiceModule { <<__Override>> public function provide(FactoryContainer $container): void { $container->set( Validation\ContentRequestValidator::class, $container ==> new Validation\ContentRequestValidator(), Scope::SINGLETON, ); } }
ServiceModuleクラスはなんでも構いませんが、新たに作った場合はかならず config/modules.global.php
に記述してください。
<?hh return [ \Nazg\Foundation\Service::MODULES => ImmVector { \App\Module\ActionServiceModule::class, \App\Module\ExceptionServiceModule::class, \App\Module\MiddlewareServiceModule::class, \App\Module\LoggerServiceModule::class, \App\Module\ValidationServiceModule::class, }, ];
これでバリデーションの準備が整いました。
バリデーション実行
バリデーションを実行したいクラスのメソッドに Attribute
を記述します。
先ほど作成した App\Action\Document\ReadAction
クラスで実行するようにするには次の通りです。
<?hh namespace App\Action\Document; use App\Validation\ContentRequestValidator; 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\TextResponse; final class ReadAction implements MiddlewareInterface { <<RequestValidation(ContentRequestValidator::class)>> public function process( ServerRequestInterface $request, RequestHandlerInterface $handler, ): ResponseInterface { return new TextResponse('Hello world!'); } }
<<RequestValidation(ContentRequestValidator::class)>>
この部分がバリデーション指示になります。
指定方法は <<RequestValidation(実行したいバリデーションクラス)>>
となります。
最後にExceptionHandlerクラスで任意のレスポンスを返却するように記述すればOKです。
フレームワークのskeletonに継承した App\Exception\AppExceptionHandler
クラスが含まれていますので、
そのクラスを利用します。
<?hh namespace App\Exception; use Nazg\Http\StatusCode; use Nazg\Foundation\Validation\ValidationException; use Nazg\Types\ExceptionImmMap; use Nazg\Foundation\Exception\ExceptionHandler; use Psr\Http\Message\ResponseInterface; use Zend\Diactoros\Response\JsonResponse; class AppExceptionHandler extends ExceptionHandler { <<__Override>> protected function render( ExceptionImmMap $em, \Exception $e ): ResponseInterface { $message = $em->toArray(); if($e instanceof ValidationException) { $message = $e->errors(); } return new JsonResponse( $message, StatusCode::StatusInternalServerError, ); } }
Nazg\Foundation\Validation\ValidationException
クラスがスローされた場合に返却されるレスポンスを変更しました。
誤った型が送信されるとjsonで [type error!]
と返却されます。
バリデーションの実装は以上になりますが、
実は hack-routerで受け取ったリクエストの値は全てstringになるため、通常はこのバリデーションは絶対にエラーになりませんが、
アプリケーションで利用するものと異なるメソッドなどを指定した場合に発生しますので、
実装時に簡単なエラーなどを見つけることができるようになりますので、
色々チャレンジしてみてください。
以上、簡単なようでちょっと面倒臭いバリデーションの実装方法でした。