概要

WebAPI、Web画面にしろWebアプリでバリデーションを実装することは多い。実装にはWebアプリケーションフレームワーク(以下FW)の機能に頼ることもできるし、規模が小さければ入力データをもとに直書きしても良い。 OSSのソースコードを元にしてバリデーションをどのように実装しているかを調査し自身の実装に役立てたい。

バリデーションの流れ

HTTPリクエストには自由なデータが入力されうるため、バリデーションを通してエラーメッセージのリストを作成して入力エラーとして例外、またはエラー返却する。入力エラーとならない場合は以降は有効なデータとして扱える。

flowchart LR

生のHTTPリクエスト --> p((バリデーション))
p((バリデーション)) --成功--> バリデーション済データ
p((バリデーション)) --失敗--> エラーリスト

パターン

シンプルパターン

FWのコントローラに直書きする。項目が増えたり制約が複雑になると辛い。

モデルとして定義してバリデーション実施する

FormクラスやModelクラスと呼ばれるオブジェクトのコンストラクタとしてHTTPリクエストを渡してインスタンス化する。PydanticのModelやDjangoのFormがある。

言語標準の型やFW提供のクラス等で項目ごとの制約を定義し、足りない分はクラス内のメソッドとして定義する。メソッドの定義方法としてclean_xxのように命名規則による方法とデコレータで指定する方法がある。

ルールとして定義してバリデーション実施する

データをロジックを分割する。FuelPHPのValidationやCakePHPのValidatiorがある。 データとは別にルールをインスタンス化してルールを追加していく。ルールにデータを引数として渡すことでエラーを判定する。

自前でバリデーションするなら

どこでチェックすればいいのだろうかをクラス図にまとめてみる。

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