バリデーションの実装方法について調べる
公開日: 2024/04/14
概要
WebAPI、Web画面にしろWebアプリでバリデーションを実装することは多い。実装にはWebアプリケーションフレームワーク(以下FW)の機能に頼ることもできるし、規模が小さければ入力データをもとに直書きしても良い。 OSSのソースコードを元にしてバリデーションをどのように実装しているかを調査し自身の実装に役立てたい。
バリデーションの流れ
HTTPリクエストには自由なデータが入力されうるため、バリデーションを通してエラーメッセージのリストを作成して入力エラーとして例外、またはエラー返却する。入力エラーとならない場合は以降は有効なデータとして扱える。
flowchart LR 生のHTTPリクエスト --> p((バリデーション)) p((バリデーション)) --成功--> バリデーション済データ p((バリデーション)) --失敗--> エラーリスト
パターン
シンプルパターン
FWのコントローラに直書きする。項目が増えたり制約が複雑になると辛い。
モデルとして定義してバリデーション実施する
FormクラスやModelクラスと呼ばれるオブジェクトのコンストラクタとしてHTTPリクエストを渡してインスタンス化する。PydanticのModelやDjangoのFormがある。
言語標準の型やFW提供のクラス等で項目ごとの制約を定義し、足りない分はクラス内のメソッドとして定義する。メソッドの定義方法としてclean_xxのように命名規則による方法とデコレータで指定する方法がある。
- Django
- https://github.com/django/django/blob/main/django/forms/forms.py#L329
- フィールド名でforを回して命名規則clean_を元に関数を呼んでいる
- https://github.com/django/django/blob/main/django/forms/forms.py#L329
ルールとして定義してバリデーション実施する
データをロジックを分割する。FuelPHPのValidationやCakePHPのValidatiorがある。 データとは別にルールをインスタンス化してルールを追加していく。ルールにデータを引数として渡すことでエラーを判定する。
- CakePHP
- https://github.com/cakephp/cakephp/blob/47da9e07c564bf199ab99134165c0248d6510722/src/Validation/Validator.php#L224
- データを渡してルール側にキーがあれば_processRulesでルール実行している
- https://github.com/cakephp/cakephp/blob/47da9e07c564bf199ab99134165c0248d6510722/src/Validation/Validator.php#L224
自前でバリデーションするなら
どこでチェックすればいいのだろうかをクラス図にまとめてみる。
FieldXをドメイン層の値オブジェクトとして考えて条件を集約しつつも、あくまでチェックするのはFormXのメソッドにするとドメイン層は使い回せそう。
相関チェックはどこにおけばいいのだろう、FieldX#isValidに引数として渡せばいいのか?FormXのメソッドとして持たせてしまう方法もあるがロジックが分散されてしまうか。
validateAllでvalidateXXを全部実行できるようデコレータなりアノテーションを入れておけば修正に強くなれる。
しかし、validateXXがFormX毎に作成されてしまう。FieldXを呼ぶだけの薄いメソッドなので重複はしょうがないか。FieldXがそこまで多くなければvalidateAllに集約してしまっても良さそう。
classDiagram class Controller { register() } class Form { <<interface>> cleanedData() validateAll() } class FormA { fieldA: FieldA fieldB: FieldB cleanedData() validateAll() validateFieldA() validateFieldB() } class FormB { fieldA: FieldA fieldC: FieldC cleanedData() validateAll() validateFieldA() validateFieldC() } class FormCreator { +createForm(formType: FormType) Form } class FormType { <<enumeration>> FormA FormB } class FieldA { value isValid() } class FieldB { value isValid() } class FieldC { value isValid(fieldA:FieldA) } Controller --> FormCreator FormCreator --> FormType Controller o--> FormA Controller o--> FormB Form <|-- FormA Form <|-- FormB FormA o--> FieldA FormA o--> FieldB FormB o--> FieldA FormB o--> FieldC