CGI::Application 練習

CGI::Applicationの練習として、簡単な投票CGIを作ってみることに。http://kk01234.hp.infoseek.co.jp/cgi-bin/sample01/vote.cgi
(refereあるとダメっぽいので、アドレスコピペしてください)
所要時間は2時間ほど。使い方に慣れてないことも考えると、これくらいの時間で結構きれいに作れるのはなかなかいいかも。

ファイル構成

vote.cgi
CGI本体
Post.pm
CGI::Applicationを継承したフレームワーク
ReadData.pm
データ読み書き用モジュール
view.html
テンプレート(メインビュー)
addview.html
テンプレート(追加用ビュー)
fate.dat
データ

vote.cgi

#!/usr/local/bin/perl
use Post;

my $post = new Post(PARAMS =>{filename => 'fate.dat'});
$post->run();

メインのCGIでは、CGI::Applicationを継承したPostというモジュールのインスタンスを生成して、run()するだけ。

Post.pm

package Post;
use base 'CGI::Application';
use ReadData;
use strict;

sub setup{
    my $self = shift;
    $self->start_mode('view');
    $self->run_modes([qw(view post addview add)]);
    $self->header_props(-type=>'text/html', -charset=>'EUC-JP');
    $self->param('filename', 'vote.dat') unless defined $self->param('filename');
    $self->param('data', new ReadData($self->param('filename')));
}

sub view{
    my $self = shift;
    return $self->__viewhtml();
}

sub post{
    my $self = shift;
    $self->param('data')->post($self->query()->param('key'));
    return $self->__viewhtml();
}

sub addview{
    my $self = shift;
    return $self->__addviewhtml();
}

sub add{
    my $self = shift;
    $self->param('data')->add($self->query()->param('name'));
    return $self->__viewhtml();
}

# private function
sub __viewhtml{
    my $self = shift;
    my $tmpl = $self->load_tmpl('view.html');
    my $data = $self->param('data')->get();
    my @loop = ();
    foreach(sort {$data->{$b}->{count} <=> $data->{$a}->{count}} keys %{$data}){
        push(@loop, {key => $_, name => $data->{$_}->{name}, 
                     count => $data->{$_}->{count}});
    }
    $tmpl->param(DATA => \@loop);
    return $tmpl->output();
}

sub __addviewhtml{
    my $self = shift;
    return $self->load_tmpl()->output();
}

1;

run_modes()で、CGIに使うrun modeを設定する。CGIからは、"rm=xxxx"のパラメーターで、xxxxに対応したサブルーチンを呼び出す。

今回はrun_modes()にarray referenceを渡しているので、run modeの名前と同じサブルーチンが呼び出されます。

vote.cgiでnew Post()に渡したPARAMSは、こっちのparam()で取り出すことができます。

後は何かあるかな・・・load_tmpl()は、引数を省略すると、「現在のrun mode+.html」が読み込まれます。

まあ簡単なコードですね。ループ1つ、分岐0だし。

ReadData.pm

package ReadData;
use Digest::MD5;
use strict;

sub new {
    my ($class, $f) = @_;
    my $self = {};
    $self->{filename} = (defined($f) && length($f)) ? $f : 'data.dat';
    return bless($self, $class);
}

sub get{
    my $self = shift;
    return $self->__read();
}

sub add{
    my $self = shift;
    my $name = shift;
    my $ctx  = new Digest::MD5();
    my $key  = $ctx->add($name)->hexdigest();
    my $data = $self->__read();
    unless(exists($data->{$key})){
        $data->{$key}->{name}  = $name;
        $data->{$key}->{count} = 0;
    }
    $self->__write($data);
}

sub post{
    my $self = shift;
    my $key  = shift;
    my $data = $self->__read();
    $data->{$key}->{count}++;
    $self->__write($data);
}

#private method
sub __read{
    my $self = shift;
    my $data = {};
    open(F, $self->{filename});
    while(<F>){
        tr/\x0D\x0A//d;
        my($key, $name, $count) = split(/\t+/);
        $data->{$key}->{name}  = $name;
        $data->{$key}->{count} = $count;
    }
    close(F);
    return $data;
}

sub __write{
    my $self = shift;
    my $data = shift;
    open(F, ">$self->{filename}");
    foreach(keys %{$data}){
        print F join("\t", $_, $data->{$_}->{name}, $data->{$_}->{count}), "\n";
    }
    close(F);
}

1;

タブ区切りデータの読み書きをするモジュール。ReadDataじゃなくなってしまった。ネーミング失敗・・・。

データファイルの中身は

key				name	count
e5f8e3bdcb8ea3aba6d7796f8b497b29	セイバー	11
5fb62c4809b0fa8f81260c065d0a1ca0	士郎	3

みたいな。

view.html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-jp">
</head>
<body>
<h1>人気投票</h1>
<table border=1>
    <tr><th>名前</th><th>票</th><th></th></tr>
    <TMPL_LOOP name=DATA>
    <tr>
         <td><TMPL_VAR NAME=name></td><td><TMPL_VAR NAME=count></td>
         <td>
              <form action="vote.cgi" method=post>
              <input type=hidden name=rm value=post>
              <input type=hidden name=key value=<TMPL_VAR NAME=key>>
              <input type=submit value=投票></form>
         </td>
    </tr>
    </TMPL_LOOP>
</table>
<p><a href="vote.cgi?rm=addview">追加する</a></p>
</body>
</html>

HTML::Templateのテンプレート形式。"rm"がrun modeを設定してるというのが分かるかと思います。

addview.html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-jp">
</head>
<body>
<h1>追加</h1>
<form action="vote.cgi" method=post>
<input type=hidden name=rm value=add>
<input type=text name=name>
<input type=submit value=追加>
</form>
</body>
</html>