ユーザーからのPOST等された入力値の妥当性をチェックする Validation をどこでやるか問題が個人的にありまして〜、DBを使わないケースならばいわゆるFomrValidator::*を使ってControllerでやればいいのですが、Modelを経由するようなアプリだとControllerだけじゃ不安よねぇ〜、Modelだけ使う時もあるし、Model単体のテストで再現出来ないよね〜なんて思ってます。で、実際の実装をControllerではFormValidator::Lite、Modelの一部にData::Validatorを使っているのですが、なんかコレも効率悪い感じしてたんで、ちょいと実験的に理想の一つを実装してみました。
こんな条件です。
- エラーメッセージを簡単に設定したいのでValidationモジュールにはFormValidator::Liteを使う
- 色々錯誤していたらORMの段階でValidationしてResultオブジェクトを返すってのがいいのではないか
- Resultオブジェクトではhas_error/error_messagesメソッドをはやしてControllerで扱いやすくする
- Validationが通ればentryメソッドで生成されたORMのオブジェクトを取得出来る
- WAFはMojolicious、ORMにはTengを使う前提で書いてみる
するとController側はこんな風に書ける。
sub post { my $self = shift; my $user = $self->stash->{user}; return $self->render_not_found unless $user; my $result = $self->model('Entry')->create({ user_id => $user->id, title => $self->req->param('title') || '', body => $self->req->param('body') || '', }); if($result->has_error){ $self->stash->{error_messages} = $result->error_messages; return $self->render('/entry/create'); } $self->redirect_to('/entry/' . $result->entry->id); }
$self->model('Entry')ってのはMyApp::Model::Entryを呼び出しすショートカットなんだけど、createメソッドの返り値が例のResultオブジェクトになっている。
Model側はもちろん他の処理も入るけど最小限これでイケる。
sub create { my ($self, $args) = @_; my $result = $self->db->insert('entry', { title => $args->{title}, body => $args->{body}, user_id => $args->{user_id} }); return $result; }
肝心なのは通常「use parent 'Teng'」するMyApp::DBモジュール。これをちょいと拡張する。
package MyApp::DB; use Mouse; use String::CamelCase qw//; use Module::Load qw//; use MyApp::DB::Result; extends 'Teng'; sub insert { my ($self, $table_name, $args, $prefix) = @_; my $class = "MyApp::Form::" . String::CamelCase::camelize($table_name); Module::Load::load($class); my $form = $class->new; my $validator = $form->check($args); if($validator->has_error) { my $result = MyApp::DB::Result->new( has_error => 1, error_messages => [$validator->get_error_messages()] ); return $result; } my $entry = $self->SUPER::insert( $table_name, $args, $prefix ); my $result = MyApp::DB::Result->new( entry => $entry ); return $result; }; __PACKAGE__->meta->make_immutable(); 1;
MyApp::DB::Resultはこんなん。
package MyApp::DB::Result; use Mouse; has error_messages => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } ); has has_error => ( is => 'rw', isa => 'Bool', default => 0 ); has entry => ( is => 'rw', isa => 'Object'); __PACKAGE__->meta->make_immutable(); 1;
その他にFormValidator::Liteを呼び出すためのMyApp::Formと個別のルールが書かれたMyApp::Form::Entryなどが存在する。以下がMyApp::Formで親クラス。FormValidator::Liteに渡す際、パラメータ系の互換にするめにMojo::Parametersを暫定的に使ってます。
package MyApp::Form; use Mouse; use Mojo::Parameters; use FormValidator::Lite; FormValidator::Lite->load_constraints(qw/Japanese/); sub validator { my ($self, $args) = @_; my $params = Mojo::Parameters->new(%$args); my $validator = FormValidator::Lite->new($params); $validator->load_function_message('ja'); return $validator; } __PACKAGE__->meta->make_immutable(); 1;
子にあたるMyApp::Form::Entryにはルールが存在している。モジュールにべた書きしてます。
package MyApp::Form::Entry; use Mouse; extends 'MyApp::Form'; use utf8; sub check { my ($self, $args) = @_; my $v = $self->validator($args); $v->set_param_message( title => 'タイトル', body => '本文', user_id => 'ユーザーID' ); my $res = $v->check( title => ['NOT_NULL', [qw/LENGTH 1 100/]], body => [qw/NOT_NULL/], user_id => [qw/INT/] ); return $v; } __PACKAGE__->meta->make_immutable(); 1;
ORMべったりでそもそもメソッド上書きしているけど、insertじゃない名前にしたりルールが存在しない場合は通常動作させるとか... も含めてこれはアリな気がするぞ... もしくはORMの層じゃなくてModelのとこで書くのもいいし。
他にモデルやDB層でいい感じのValidationを実装している方がいたら教えて欲しいです!