フォーム
Ray.WebFormModuleは、Aura.InputとRay.Diを使ってアスペクト指向でWebフォームをバリデーションするモジュールです。フォームフィールド、バリデーションルール、送信値、レンダリングヘルパーを1つのフォームクラスに集約できるため、テストや変更が容易です。
インストール
Composerでray/web-form-moduleをインストールします。
composer require ray/web-form-module
アプリケーションモジュールにWebFormModuleをインストールします。
use Ray\Di\AbstractModule;
use Ray\WebFormModule\WebFormModule;
class AppModule extends AbstractModule
{
protected function configure()
{
$this->install(new WebFormModule());
}
}
互換性のためRay\WebFormModule\AuraInputModuleクラスもWebFormModuleの薄いサブクラスとして残されています。新規コードではWebFormModuleを使ってください。
フォームクラス
自己初期化フォームクラスでは、init()メソッドでフィールドとバリデーションルールを定義します。フォームがsubmit()を実装している場合、その戻り値が送信データとして使われます。基礎となるフォームAPIについてはAura.Input self-initializing formsを参照してください。
use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\SetAntiCsrfTrait;
class MyForm extends AbstractForm
{
use SetAntiCsrfTrait;
public function init()
{
$this->setField('name', 'text')
->setAttribs([
'id' => 'name'
]);
$this->filter->validate('name')->is('alnum');
$this->filter->useFieldMessage('name', 'Name must be alphanumeric only.');
}
public function submit()
{
return $_POST;
}
public function __toString()
{
$form = $this->form();
$form .= $this->helper->tag('div', ['class' => 'form-group']);
$form .= $this->helper->tag('label', ['for' => 'name']);
$form .= 'Name:';
$form .= $this->helper->tag('/label') . PHP_EOL;
$form .= $this->input('name');
$form .= $this->error('name');
$form .= $this->helper->tag('/div') . PHP_EOL;
$form .= $this->input('submit');
$form .= $this->helper->tag('/form');
return $form;
}
}
コントローラー
フォームのバリデーションが必要なメソッドに#[FormValidation]を付けます。formにはコントローラー上のフォームプロパティ名を、onFailureにはバリデーション失敗時に呼び出すメソッド名を指定します。
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use Ray\WebFormModule\Annotation\FormValidation;
use Ray\WebFormModule\FormInterface;
class MyController
{
/** @var FormInterface */
protected $contactForm;
#[Inject]
public function setForm(#[Named("contact_form")] FormInterface $form)
{
$this->contactForm = $form;
}
#[FormValidation(form: "contactForm", onFailure: "badRequestAction")]
public function createAction()
{
// validation success
// vnd.error+json の詳細は #[VndError] で追加できます。
}
public function badRequestAction()
{
// validation failed
}
}
ビュー
フォームが文字列表現を提供している場合、フォームをechoするとフォームHTML全体がレンダリングされます。
echo $form;
個別の入力要素やエラーメッセージもレンダリングできます。
echo $form->input('name'); // <input id="name" type="text" name="name" size="20" maxlength="20" />
echo $form->error('name'); // "Name must be alphanumeric only." または空文字
CSRF Protections
CSRF(クロスサイトリクエストフォージェリ)保護はopt-inで、独立した2つの経路のいずれかで有効化できます。
- フォーム単位: フォームに
use SetAntiCsrfTrait;を追加します。DIでAntiCsrfInterfaceが注入され、postConstruct()でトークンフィールドが追加され、apply()の呼び出しごとにトークンが検証されます。 - アクション単位: バリデーション対象のメソッドに
#[CsrfProtection]を付与します。AuraInputInterceptorがapply()実行前にAntiCsrfInterfaceをフォームへ注入します。
どちらの経路でも、トークン不一致時にはAbstractForm::apply()がCsrfViolationExceptionをthrowします。どちらも使わない場合はCSRF検証は行われません。
use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\Annotation\CsrfProtection;
use Ray\WebFormModule\Annotation\FormValidation;
use Ray\WebFormModule\SetAntiCsrfTrait;
class MyForm extends AbstractForm
{
use SetAntiCsrfTrait;
}
class MyController
{
#[FormValidation(form: "contactForm")]
#[CsrfProtection]
public function createAction()
{
}
}
独自のAntiCsrfクラスを提供することもできます。詳しくはAura.InputのApplying CSRF Protectionsを参照してください。
0.xからのマイグレーション
1.0ではDoctrine Annotationsを廃止し、PHP 8のネイティブ属性に移行しました。型宣言も強化されています。主な書き換えは次の通りです。
| Before (0.x) | After (1.0) |
|---|---|
@FormValidation(form="f", onFailure="badRequest") |
#[FormValidation(form: 'f', onFailure: 'badRequest')] |
@FormValidation(form="f", antiCsrf=true) |
#[FormValidation(form: 'f')] + #[CsrfProtection] |
@InputValidation(form="f") |
#[InputValidation(form: 'f')] |
@VndError(message="...", logref="...") |
#[VndError(message: '...', logref: '...')] |
new AuraInputInterceptor($injector, $reader) |
new AuraInputInterceptor($injector) |
public function input($input) / public function error($input) |
input(string $input): string / error(string $input): string |
破壊的変更の完全なリストはCHANGELOG.mdを参照してください。
Claude Codeによる自動マイグレーション
Ray.WebFormModuleにはClaude Code skillの.claude/skills/migrate-to-1.0/SKILL.mdが同梱されています。このskillは、アノテーションから属性への変更、antiCsrf=trueから#[CsrfProtection]への分離、Reader引数の削除、FormInterface署名更新をAIアシスタントに案内します。利用側プロジェクトの.claude/skills/にディレクトリをコピーし、/migrate-to-1.0で起動してください。
Validation Exception
#[InputValidation]を使うと、バリデーション失敗時にRay\WebFormModule\Exception\ValidationExceptionが投げられます。HTML表現を使わないAPIアプリケーションに便利です。
use Ray\WebFormModule\Annotation\InputValidation;
class Foo
{
#[InputValidation(form: "form1")]
public function createAction($name)
{
// ...
}
}
Ray\WebFormModule\FormVndErrorModuleをインストールすると、#[FormValidation]を付けたメソッドもバリデーション失敗時に同じ例外を投げます。
use Ray\Di\AbstractModule;
use Ray\WebFormModule\FormVndErrorModule;
use Ray\WebFormModule\WebFormModule;
class FakeVndErrorModule extends AbstractModule
{
protected function configure()
{
$this->install(new WebFormModule());
$this->override(new FormVndErrorModule());
}
}
キャッチした例外のerrorプロパティをechoすると、application/vnd.error+jsonメディアタイプの表現が出力されます。
echo $e->error;
//{
// "message": "Validation failed",
// "path": "/path/to/error",
// "validation_messages": {
// "name": [
// "Name must be alphanumeric only."
// ]
// }
//}
#[VndError]属性でvnd.error+jsonに詳細情報を追加できます。
#[FormValidation(form: "contactForm")]
#[VndError(message: "foo validation failed", logref: "a1000", path: "/path/to/error", href: ["_self" => "/path/to/error", "help" => "/path/to/help"])]
public function createAction()
{
}
デモ
Ray.WebFormModuleリポジトリでデモアプリケーションを起動できます。
php -S docs/demo/1.csrf/web.php