ActiveModelLike::ErrorsというPerlモジュールを書いた
通常Model内のエラーをControllerに渡す方法はWAFに組み込まれている。が、薄いWAFだとControllerとViewだけあって、Modelは好きに実装して下さいということが多い。得にPerlは軽量なWAFが多く、Mojoliciousのように薄くはないけどModel層がないものもある。ちょっとしたものならメソッドの戻り値で返してもいいと思うが、それが増えていくとController側でメソッド毎の戻り値のパターンを意識しないといけなくなるので、すぐに辛くなる。で、どうするかというと、大体Model内で$self->{errors}->{}
のようなハッシュやエラー用のクラスを作って、そこへエラーを格納してControllerに戻すというケースが多いと思う。その際、エラーの有無を確認するメソッドや、メッセージを表示するメソッドが必要になるが、それがアプリケーション毎に違うとそれもそれで辛い。
ということを考えていたときに、ふと「Railsと同じI/Fだったら、(個人的に使う分には)わかりやすくていいかも」とActiveModel::Errorsのソースを読んでみると、基本的な機能は簡単に移植できそうだったので興味本位でPerlに移植してみた。
https://github.com/nkwhr/ActiveModelLike-Errors
ActiveModel::Errorsの4.2.xをベースにしている。[]
や[]=
は同等のことを実現する方法がなさそうだったので(あったら教えてください)省いたのと、疑問符付きのメソッド名は一部変更している。また、本家のAM::ErrorsはAM::ValidationsやAM::Translationsと連携することで、SymbolからHuman Readableなメッセージに変換したりI18n対応を行っているが、その辺まで移植しようとするとキリがないので、割り切ってエラーを格納して返すだけのものにした。なのでActiveModelLike。(言い訳)
対応表
AcitveModel::Errors | ActiveModelLike::Errors |
---|---|
[ ], [ ]= | N/A |
add | add |
add_on_blank | N/A |
add_on_empty | N/A |
added? | is_added |
as_json | N/A |
blank? | is_blank |
clear | clear |
count | count |
delete | delete |
each | each |
empty? | is_empty |
full_message | full_message |
full_messages | full_messages |
full_messages_for | full_messages_for |
generate_message | N/A |
get | get |
has_key? | has_key |
include? | include |
key? | N/A |
keys | keys |
new | new |
set | set |
size | size |
to_a | to_array |
to_hash | to_hash |
to_xml | N/A |
values | values |
使い方
各メソッドはREADMEかActiveModel::Errorsのドキュメントを見てもらった方が早いので省略。
Mojoliciousの場合、以下のようにModelに組み込めばRailsっぽく使える。
package MyApp::Model::User; use Mojo::Base -base; use ActiveModelLike::Errors; has 'errors' => sub { ActiveModelLike::Errors->new; }; has 'name'; has 'age'; sub is_valid { my $self = shift; $self->errors->add(name => 'is invalid') if $self->name ne 'nkwhr'; $self->errors->add(age => 'is invalid') if $self->age != 32; return $self->errors->is_empty; } 1;
package MyApp::Controller::Example; use Mojo::Base 'Mojolicious::Controller'; use MyApp::Model::User; sub foo { my $self = shift; my $user = MyApp::Model::User->new({ name => $self->param('user'), age => $self->param('age'), }); unless ($user->is_valid) { my $error_messages = join(', ', @{$user->errors->full_messages}); $self->flash(error => $error_messages); return $self->redirect_to('index'); } $self->render('foo'); } 1;