フォーム

Ray.WebFormModuleは、Aura.InputRay.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]を付与します。AuraInputInterceptorapply()実行前に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