天然パーマです。

MojoliciousとCPANモジュールで作る「Nopaste」チュートリアル

僕はWebアプリの開発言語にPerlを使っていますが、Perlで書くためのWeb Application Frameworkとして、 Mojoliciousを最近では利用しています。

Web Application Framework(WAF)とは、 Webアプリケーションの開発を効率的に行うためのライブラリ群(つまりフレームワーク)で、これがなければ少しでも大きめのアプリになると大変な思いをします。以下Mojoliciousについてとりあえずの、参考リンクです。 ちなみに昨日かな?Mojoliciousはバージョン3.0が出ました。

スクリーンショット 2012-06-27 18.12.24.png

基本的には上記の記事で書いてる通り僕は「WAFはMojolicious推し」なのですが、イマイチ浸透していない感があるので、 ちょっと実用的なチュートリアルを載せてみます。Mojoliciousの他にもCPANモジュールをいくつか使っています。 作るアプリは「Nopaste」アプリと言って、タイトルとコメントを入力するとユニークなURLでページを生成して表示させるという代物です。よく、プログラミングのコードの一部などをコメント欄に入力して、出来たページを他の人に見せるなどという使われ方をされますね。

スクリーンショット 2012-06-27 18.15.20.png

では、駆け足になるかもしれませんが、行ってみましょうー!



インストール&ひな形を生成する

MojoliciousはCPANモジュールなので、cpanmコマンドなどからインストールできます。

$ cpanm Mojolicious

もしくはシステム全体にインストールするならば、

$ cpanm --sudo Mojolicious

としてください。正常にインストールされれば、「mojo」コマンドが使えるようになりますので、プロジェクトのひな形を作ります。本当に小さいアプリならば、Mojolicious::LiteというRubyで言うSinatraチックな記述の仕方が出来ますが、今回は通常の「アプリ」としてひな形を生成します。

$ mojo generate app Nopaste::Web

ここで、ネームスペースをNopaste「::Web」としているのは、個人的な趣向で、Webのライブラリを置く領域とその他DBなどをしっかりと分けたいからです。作られる「nopaste_web」ディレクトリに潜ると以下のファイルがあると思います。これらがWebアプリを作るためのひな形となります。

./
├── lib
│   └── Nopaste
│       ├── Web
│       │   └── Example.pm
│       └── Web.pm
├── log
├── public
│   └── index.html
├── script
│   └── nopaste_web
├── t
│   └── basic.t
└── templates
    ├── example
    │   └── welcome.html.ep
    └── layouts
        └── default.html.ep

アプリを立ち上げる

ひな形の状態でテストとしてアプリを立ち上げてみましょう。Mojoliciousには「morbo」というサーバコマンドが付属しているのでそれを使います。

$ morbo script/nopaste_web

すると、デフォルトだと起動したホストの「3000」番ポートでアプリが立ち上がります。お使いのブラウザで「http://localhost:3000/」にアクセスしてみると、「Welcome to the Mojolicious real-time web framework!」と書かれたWelcomeページが見えると思います。 ちなみに「plackup」でもアプリを立ち上げることが出来ます。

$ plackup -p 3000 script/nopaste_web

トップページを作る

トップページではタイトルとコメントを受け付けるフォームを表示します。では、トップページを作ってみましょう。 「lib/Nopaste/Web.pm」を編集します。このファイルはアプリのセットアップをする部分ですが、その中のルーティングと呼ばれる部分を記述するのです。アクセスされるURLとそれに対応するコントローラの対応付けをします。 いらない部分を消して以下のようにします。

package Nopaste::Web;
use Mojo::Base 'Mojolicious';

sub startup {
     my $self = shift;
     my $r = $self->routes;
     $r->get('/')->to('root#index');
}

1;

「$r->get('/')->to('root#index')」の部分が「/」というパス、つまりトップページにGETでアクセスされた時に、「Nopaste::Web::Root」というコントローラの「index」メソッドを呼び出すという意味になります。

次にNopaste::Web::Rootコントローラを作りましょう。とはいっても、現段階ではindexメソッドがHTMLを描画するだけなので、こんな感じになります。「lib/Nopaste/Web/Root.pm」というファイルです。

package Nopaste::Web::Root;
use Mojo::Base 'Mojolicious::Controller';

sub index {
    my $self = shift;
    $self->render();
}

1;

さて肝心のHTML、正確にはHTMLを出力するためのテンプレートファイルをどこに、どう書くかですが、 「templates/root/index.html.ep」をいじります。そう、Mojoliciousのテンプレートファイルの拡張子は「.html.ep」なのです。

% layout 'default';

<form action="/" method="post">
  Title <input type="text" name="title" size="40" />
  Source <textarea name="body" rows="15" cols="40"></textarea>
  <input type="submit" value="paste" />
</form>

HTMLでは見慣れない「% layout 'default';」という記述がありますね。Mojoliciousのテンプレートでは「%」などを駆使して、変数を展開したり、Perlコードを書いたり、レイアウトに使用するテンプレートを選択したりします。ここでのlayoutコマンドは「templates/layouts/default.html.ep 」というテンプレートをレイアウトに使うという意味です。 余裕があったらレイアウトファイルをいじってみてください。

入力値チェックを行う

「lib/Nopaste/Web.pm」を再びいじり、トップページからのPOSTを受け付けるようにしましょう。

my $r    = $self->routes;
    $r->get('/')->to('root#index');
    $r->post('/')->to('root#post'); # ここを追加する

次にRootコントローラに追加します。先ほど作った「lib/Nopaste/Web/Root.pm」というファイルですね。 何を追加するかと言うと、入力値のチェック(=ヴァリデーション)をするロジックです。例えばタイトルが空だったり、コメントが空だったりしたら、エラーを表示させます。「ベタ」で書いてもいいのですが、今回は拡張性も考えてFormValidator::Liteというモジュールを使ってみましょう(これから紹介する足りないモジュールについてはcpanmなどを利用してインストールしてください)。

また、エラーメッセージを表示する時に、入力された値が消えないようにHTML::FilInForm::Liteというモジュールを使って工夫をします。ちょっと読み解くのに時間がかかるかもしれませんがRootコントローラは以下のメソッドを追加します。

package Nopaste::Web::Root;
use Mojo::Base 'Mojolicious::Controller';
use FormValidator::Lite; # 追加
use HTML::FillInForm::Lite; #追加

...;

sub post {
     my $self = shift;
     my $validator = FormValidator::Lite->new($self->req);

     # エラーメッセージを設定
     $validator->set_message(
        'title.not_null' => 'Title is Empty',
        'body.not_null' => 'Body is Empty',
    );
    # 入力値チェック
    my $res = $validator->check(
        title => [qw/NOT_NULL/],
        body => [qw/NOT_NULL/],
    );
    # もし入力値が正しくなかったら
    if ($validator->has_error) {
        my @messages = $validator->get_error_messages;
        $self->stash->{error_messages} = \@messages;
        # 入力された値を充填しながら、描画
        my $html = $self->render_partial('root/index')->to_string;
        return $self->render_text(
            HTML::FillInForm::Lite->fill(\$html, $self->req->params),
            format => 'html',
        );
    }
    # 入力値の妥当性が保証された
    # 続きは後ほど
}

1;

先ほど作った「templates/root/index.html.ep」テンプレートをエラーの時にも利用しています。エラーメッセージを表示させるためにも、以下のように編集します。

% layout 'default';

% if ($self->stash->{error_messages}) {
<ul id="caution">
% for my $message ( @{$self->stash->{error_messages}} ) {
<li><%= $message %></li>
% }
</ul>
% }

<!-- 以下は先ほどの通り -->

テンプレートの機能「%」と「<%= 変数 %>」を利用してエラーメッセージを描画しています。 この状態では、ヴァリデーションがうまく行かない場合、つまりタイトルとコメントが空の時、指定したエラーメッセージが表示されます。

スクリーンショット 2012-06-27 18.27.07.png

逆にどちらも内容が入っていて、ヴァリデーションを通ったケースには何も記述していません(分かりにくい感じになっちゃいましたがその場合「Mojolicious」のエラーが出るかもです)。

DBを扱う

入力値が妥当であればそれをDBに格納しないといけません。RailsのActiveRecordのようなDBに関わる便利なものはMojoliciousにはついていません!そこで外部のO/R Mapperと呼ばれるモジュールなどを使うとよいでしょう。 今回は詳しい記述はしませんが、十分枯れているDBIx::Skinnyを使います。 と、その前にSQLのテーブル定義です。MySQLを一応想定しています。

CREATE TABLE entry (
    `id` VARCHAR(36) NOT NULL,
    `title` VARCHAR(255) NOT NULL,
    `body` TEXT NOT NULL,
    `created_on` DATETIME NOT NULL,
    PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

仮として、「nopaste」という名前のデータベースを作って上記のSQLを実行したとしておきます。 次にDBIx::Skinnyのところ。「lib/Nopaste/DB.pm」を作ります。

package Nopaste::DB;
use DBIx::Skinny;
1;

これだけ。ただ、「lib/Nopaste/DB/Schema.pm」というファイルも作らなくてはいけません。

package Nopaste::DB::Schema;
 use DBIx::Skinny::Schema;
 use DateTime;
 use DateTime::Format::Strptime;
 use DateTime::Format::MySQL;
 sub pre_insert_hook {
     my ( $class, $args ) = @_;
     $args->{created_on} = DateTime->now( time_zone => 'Asia/Tokyo' );
}

install_inflate_rule '^.+_on$' => callback {
     inflate {
         my $value = shift;
         my $dt = DateTime::Format::Strptime->new(
             pattern => '%Y-%m-%d %H:%M:%S',
             time_zone => container('timezone'),
         )->parse_datetime($value);
         return DateTime->from_object( object => $dt );
     };
     deflate {
         my $value = shift;
         return DateTime::Format::MySQL->format_datetime($value);
     };
 };

install_table entry => schema {
     pk 'id';
     columns qw/id title body created_on/;
     trigger pre_insert => \&pre_insert_hook;
};

 install_utf8_columns qw/title body/;

1;

ふぅ。今度はこのデータベースを操作するクラスのインスタンスをMojoliciousのコントローラから利用できるようにします。と、その前に... データベースの設定を読み込ますための設定ファイルを書きましょう。これはMojoliciousの機能で後ほど読み込むことが出来ます。アプリケーションのディレクトリの直下に「nopaste.conf」というファイルを作り、各自の設定にあわせて記述します。

+{
    db => {
        dsn => 'dbi:mysql:nopaste',
        username => 'root',
        password => undef,
    }
};

そして、「lib/Nopaste/Web.pm」に以下を追加です。

sub startup {
    my $self = shift;
    my $config = $self->plugin('Config', { file => 'nopaste.conf' }); # 追加
    $self->attr( db => sub { Nopaste::DB->new( $config->{db} ) } ); # 追加
    ...;

これで、Rootコントローラからデータベースを操作できる(コントローラでデータベースを操作するかどうか議論がありますが、今回はより簡便化するためにコントローラでDB操作をしています)! タイトルとコメント欄の値をユニークなIDと共に「insert」しましょう。「lib/Nopaste/Web/Root.pm」のpostメソッドに以下を追加します。

package Nopaste::Web::Root;
use Mojo::Base 'Mojolicious::Controller';
...;
use Data::GUID::URLSafe; # 追加
...;

sub post {
...;
# 入力値の妥当性をチェックしたら...

    my $entry = $self->app->db->insert('entry',{
        id => Data::GUID->new->as_base64_urlsafe,
        title => $self->req->param('title'),
        body => $self->req->param('body'),
    });

    $self->redirect_to('/paste/' . $entry->id );
}

お分かりの通りDBIx::Skinnyを継承して作った「Nopaste::DB」のインスタンスはコントローラから「$self->app->db」でアクセス出来るのですね。postメソッドの最後には、生成されたページへリダレイクトさせています。

仕上げ

タイトル、コメントを含むエントリーを投稿、つまりDBにinsert出来たら、次はページの表示です。 上記で「生成されたページへリダレイクト」させてますが、今のところNot Foundな状態なので描画するようにしましょう。ルーティングの記述、コントローラへのメソッドの追加、テンプレートを書く、この順序で行きましょう。

まずは「lib/Nopaste/Web.pm」。最後に一行追加します。

...;
    $r->route('/paste/:id')->to('root#entry');
}

1;

次に「lib/Nopaste/Web/Root.pm」に「entry」メソッドのコードを書きます。 無駄にシャレてコメント欄(bodyフィールドの値)をText::VimColor使ってシンタックスハイライトさせてます。

package Nopaste::Web::Root;
use Mojo::Base 'Mojolicious::Controller';
...;
use Text::VimColor; #追加
use Encode; #追加

...;

sub entry {
    my $self = shift;
    my $entry = $self->app->db->single('entry',{ id => $self->stash->{id} });
    unless( $entry ){
        return $self->render_not_found;
    }
    my $syntax = Text::VimColor->new(
        filetype => 'perl',
        string   => encode_utf8( $entry->body )
    );
    $self->stash->{code} = decode_utf8($syntax->html);
    $self->stash->{entry} = $entry;
}

1;

次に「templates/root/entry.html.ep」を作ります。

% layout 'default';
 % title $entry->title;

<div id="entry">
<h2><%= $entry->title %></h2>
<pre><%== $code %></pre>
</div>

これで、一通り完成です。トップページに行き、タイトルとコメントを埋めて投稿して見てください。 ページが生成され入力した値が見れるはずです。 また、CSSを調整すればコメント欄に入れたPerlコードがシンタックスハイライトされてかっこよく表示されると思います。

スクリーンショット 2012-06-27 18.17.02.png



まとめ

PerlのWeb Application Framework「Mojolicious」やその他CPANモジュールを使って、入力値の妥当性チェックやDB操作を行う機能を持ったWebアプリのチュートリアル?を解説しました。サンプルとなるNopasteアプリは以下のgithubにあげてありますのでご参考にしてください。

なんか駆け足というか舌足らずというかやってることのボリュームがありすぎる感じだったりとかで、うまく伝わらないかもしれませんが、気になる人はMojoliciousをチェックしてみるといいんじゃないでしょうか。

ちなみに、メルマガでPerl初心者向けコンテンツを開始しました、という通りなんで有料で申し訳ないのですが、興味のある方はお試し購読してみてください。