ユーザーからの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を実装している方がいたら教えて欲しいです!