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;

参考