LaravelのFormRequestをPSR-15対応で再現しようとした
こんにちは、ナカエです。
LaravelのFormRequestは便利ですよね。
コントローラのアクションの処理を変更することなく、事前にHTTPリクエスト入力のバリデーションを行うことができます。
PSR-15対応のフレームワークにおいて、このFormRequestの機能の一部を再現するライブラリを試作したので、本日はその紹介記事です。
要件
- RequestHandlerにユーザが定義したFormRequestを注入してバリデーションと認可の処理を行える
- FormRequestのバリデーションと認可の記述をLaravelのように手軽に行える
- FormRequestの継承階層は抽象クラス1段までで抑える
- MiddlewareディスパッチャやDIコンテナの実装に依らない
2はLaravelのFormRequestの継承階層が深すぎるという不満があったための制約です。 Laravelフレームワークのユーザが作成する独自のFormRequestクラスは、Symfony HTTP FoundationのRequest、Laravelの Illuminate HTTP Request、FormRequestを継承する事になっており、お世辞にも美しい設計とは言えません。
3はPSR-15という共通のインターフェースを無駄にしないための制約です。Webフレームワークの主要機能の一つであるMiddlewareやRequestHandler(またはController, Action)のディスパッチにPSR-15以外の制約が入るようでは、あまり使い勝手がいいとは言えません。
成果物
FormRequestはPSR-15 Middlewareとして実装し、先日の記事のようにRequestHandler内でFormRequestのmiddlewareをディスパッチするという構成にしました。バリデーション機構にはilluminateのvalidationパッケージをそのまま使っています。
使い方
<?php
use Psr\Http\Message\ServerRequestInterface;
// 1. FormRequestMiddlewareInterfaceを実装したクラスを作成。抽象クラスを使うとLaravelと似た感じで書ける。
class YourFormRequest extends \N1215\PSR15FormRequest\FormRequest
{
public function authorize(ServerRequestInterface $request): bool
{
return true;
}
protected function rules(): array
{
return [
'id' => ['required', 'integer'],
];
}
protected function messages(): array
{
return [];
}
protected function attributes(): array
{
return [
'id' => 'ID',
];
}
}
// 2. FormRequestHandlerを作成。handle()の呼び出し時にFormRequestMiddlewareを通してからinnerHandle()を呼び出す処理になっている。
use Psr\Http\Message\ResponseInterface;
class YourRequestHandler extends \N1215\PSR15FormRequest\FormRequestHandler
{
/**
* @param YourFormRequest $formRequest
*/
public function __construct(YourFormRequest $formRequest)
{
parent::__construct($formRequest);
}
protected function innerHandle(ServerRequestInterface $request): ResponseInterface
{
return new \Zend\Diactoros\Response\JsonResponse(['message' => 'handled.']);
}
}
// 3. FormRequestMiddlewareをインスタンス化。illuminate/validationのパッケージとPSR-17 ResponseFactory, StreamFactoryに依存
use Illuminate\Filesystem\Filesystem;
use Illuminate\Translation\Translator;
use Illuminate\Translation\FileLoader;
use Illuminate\Validation\Factory;
use Zend\Diactoros\ResponseFactory;
use Zend\Diactoros\StreamFactory;
$langDirPath = __DIR__ . '/../resources/lang';
$validationFactory = new Factory(new Translator(new FileLoader(new Filesystem(), $langDirPath), 'en'));
$errorResponder = new \N1215\PSR15FormRequest\FormRequestErrorResponder(new ResponseFactory(), new StreamFactory());
$formRequest = new YourFormRequest($validationFactory, $errorResponder);
// 4. FormRequestHandlerをインスタンス化
$handler = new YourRequestHandler($formRequest);
// 5. PSR-15のRequestHandlerとして使う
use Zend\Diactoros\ServerRequestFactory;
use Zend\HttpHandlerRunner\Emitter\SapiEmitter;
$request = ServerRequestFactory::fromGlobals();
$response = $handler->handle($request);
(new SapiEmitter())->emit($response);
感想
LaravelのFormRequestの利便性はさすがですね。DIコンテナやディスパッチャ、HTTPリクエストと密結合気味なものの、その代わりにユーザ側の便利さを実現しているということでしょう。今回は利用しませんでしたが、AOPのライブラリを使うなどしないと、疎結合を保ったままここまでの利便性を再現するのはなかなか難しそうです。