Dancer + Xslate + Skinny によるMVC構成のベースを作ってみた

仕事でCatalystを使ったことはあるんですが、Perlのフレームワークも様変わりしてるみたいです。

例えば、Catalystだとこう書くところを、

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;
 
    $c->response->body( 'こんにちは!' );
}

Dancerだとこんな感じ。

get '/' => sub {
    'こんにちは!';
}

これ以上ありえないくらいにシンプルな記述です。rubyのsinarta風だそうです。

また、ログを出力したりパラメータにアクセスする時は、Catalystでは、下のようになるところを、

sub hello :Path :Args(1) {
    my ( $self, $c, $name ) = @_;
 
    $c->log->debug('logging.');
    $c->response->body( "こんにちは! $name" );
}

Dancerだとこんな感じ。

get '/hello/:name' => sub {
    debug 'logging.';
    'こんにちは!' . param 'name';
}

Catalystでは、何でもかんでも、$cからアクセスするところを、 Dancerでは、該当packageにログ出力やパラメータ取得の関数をぶちまけることで、debug とか param とかで、機能にアクセスできるようにしているようです。関数として呼び出せるということは、OOP的なインスタンス経由のアクセス程煩雑な要素を含めることなく記述をシンプルに保ちながら、get ‘/’ => sub {} のsubのようなルーティングの実処理の無名関数からもアクセスできるという、巧妙な方法だと思います。一昔前だと、packageに勝手に関数をimportしちゃうのは行儀が良くないと思われたかもしれないけど、「肉を斬らせて骨を断つ」的な見事な設計だと思いました。

Dancerは、小さいWebアプリを作るのが簡単なのは、すぐわかったんですが、Dancerである程度大きいアプリも作りたい。 そこで、シンプルを保ちながらMVCで書けなるように、テンプレートエンジン(Xslate)とORマッパ(Skinny)を繋げてみました。

├── I18N  多言語化リソース
├── bin   起動スクリプトなど
├── environments 環境別設定
├── lib
│   ├── DancerApp
│   │   ├── Controller
│   │   │   ├── Root.pm Root用コントローラ
│   │   │   └── User.pm Userディレクトリ用コントローラ
│   │   ├── Logic
│   │   │   ├── Base.pm ロジッククラスの基底クラス
│   │   │   └── User.pm ユーザロジック
│   │   ├── Model
│   │   │   └── Schema.pm Skinny用スキーマ
│   │   ├── Model.pm Skinnyモデルクラス
│   │   └── Plugin プラグイン
│   └── dancer_app.pm スタートアップ
├── public  静的ファイルディレクトリ
├── t       テストディレクトリ
├── validation 入力値検証用ルールディレクトリ
└── views Viewディレクトリ

Xslate

高速テンプレートエンジン。

Skinny

シンプルなORマッパー。リレーションはサポートされていない。 とてもバランスがいいと思う。SQL好きにとっては必要なことは全部できる気がする。

まとめたものの雰囲気

Controller lib/DancerApp/Controller/User.pm

コントローラ担当のファイル。logic という関数を使ってますが、ロジッククラスのインスタンスを取得する自作プラグインを用意して、それを使ってます。プラグインを作成するにも、他のプラグインのコードを見様見真似でできてしまった。 validationは、Dancer::Plugin::ValidateTinyを使いました。flashメッセージはDancer::Plugin::FlashMessage を使いました。(ただし、FlashMessageは、Xslateと相性がよくなかったので、ちょっと改造しました。

get '/add' => sub {
    my $User = logic('User');
    my $user_types = $User->user_types;
 
    template 'user/add', {user_types => $user_types};
};
 
post '/add' => sub {
    my $params = params;
    my $User = logic('User');
 
    # validation
    my $validation = validator $params, 'user.pl';
    unless ($validation->{valid}) {
        flash_errors $validation->{result};
        return forward '/user/add', {}, {method => 'GET'};
    }
 
    my $new_user = $User->register({
        name => $validation->{'result'}->{'name'},
        email => $validation->{'result'}->{'email'},
        user_type_id => $validation->{'result'}->{'user_type_id'},
    });
    die 'failed register user.', $@ unless $new_user;
 
    flash message => 'user created.';
    redirect 'user/';
};

Logic lib/DancerApp/Logic/User.pm

ロジッククラス。Skinnyを使ってビジネスロジックを書いてます。SQL使い放題です(涎 ある程度の規模にも耐えられるように、DBアクセスはロジッククラスを行なうのはだけにして、簡易DIのような形でコントローラとの密結合を防ぐのが目的です。

package DancerApp::Logic::User;
use strict;
use warnings;
use base qw( DancerApp::Logic::Base );
 
use Data::Dumper;
 
sub user_types {
    my ($self) = @_;
    $self->log->debug('enter user_types');
    my @rows = $self->schema->search_by_sql(q{select * from user_types order by id asc});
    return \@rows;
}
 
sub all_users {
    my ($self) = @_;
    $self->log->debug('enter all_users');
    my @rows = $self->schema->search_by_sql(q{select u.*, t.name as user_type from users as u inner join user_types as t on t.id = u.user_type_id});
    return \@rows;
}
 
sub register {
    my ($self, $user) = @_;
    $self->log->debug('enter register');
    return $self->schema->insert('users', $user);
}

View views/user/add

ビュー。Xslateを使ってる。fillinformを使いたかったので、Xslateプラグインをちょっと改造しました。 Xslateのデフォルトの記述方法(Kolon)はいいですね。Velocityと同じように、制御文の行では閉じタグを書く必要がなくなってる。

<div id="page">
  <h1>New User</h1>
  : block form | fillinform($flash.has_errors ? $params : $user || {}) -> {
  <form action="" method="post">
    <div>name</div>
    <input type="text" name="name" id="name" />
    <span class="error"><: $flash.err_name :></span>
    <div>email</div>
    <input type="email" name="email" id="email" />
    <span class="error"><: $flash.err_email :></span>
    <div>user type</div>
    <select name="user_type_id" id="user_type_id">
      <option value="">please select...</option>
      : for $user_types -> $type {
        <option value="<: $type.id :>"><: $type.name :></option>
      : }
    </select>
    <span class="error"><: $flash.err_user_type_id :></span>
    <div>
      : if $user {
        <input type="submit" value="update" />
      : } else {
        <input type="submit" value="create" />
      : }
    </div>
  </form>
  : }
  <a href="<: $request.base :>user">back</a>
</div>

githubに置いてあります。https://github.com/smeghead/dancer_app

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください