第3章 仕様案を参考に実装

ここでは実際に手を動かしながら具体的な実装方法を説明してゆきます。各項目ごとに完成形のサンプルコード github に存在しますので完成形のコードはそちらを参照ください。

3.1 最低限度の画面を作成

前回までで環境構築と開発用のアプリケーションサーバーの立ち上げまで進みました、今回は bulletin.pl ファイルに実際にコードを書き込みながら画面を作ってゆきます。

順番は「トップ画面」 -> 「一覧画面」 -> 「書き込み入力画面」で作業します。全体のイメージやURLについては「第1章やることを整理しておく」を参照ください。

実際のソースコードはこちらの github からみれます。

https://github.com/Becom-Developer/beginning-mojo/blob/master/chapter3/section1/bulletin.pl

3.1.1 掲示板の紹介画面

実装をするまえに画面のイメージ画像をのせておきますので参考にしてみてください。

掲示板の紹介画面

今回は Mojolicious::Lite で作成なのですが、Lite の場合は__DATA__配下がテンプレートとして読み込まれており、共通テンプレートは layouts/default.html.ep 記載の部分にあたります。下記のように修正してみましょう。

...
@@ layouts/default.html.ep
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title><%= title %></title>
  </head>
  <body><%= content %></body>
</html>
...

次に「掲示板の紹介画面」にあたる index.html.ep を下記のように修正してみましょう。

...
@@ index.html.ep
% layout 'default';
% title '掲示板の紹介画面';
<h1>だれでも書込みできる掲示板</h1>
<p><a href="/list">掲示板の一覧表示へ</a></p>
...

修正後webブラウザをリロードすると掲示板の紹介画面ができていることが確認できます。この手順で「一覧画面」と「書き込み入力画面」を作成してみます、具体的な手順は下記を参考にしてみてください。

3.1.2 掲示板の一覧画面

掲示板の一覧画面

「掲示板の紹介画面」に該当する index.html.ep の下に下記のように追記

...
@@ list.html.ep
% layout 'default';
% title '掲示板の一覧表示';
<h1>一覧表示</h1>
<p><a href="/">掲示板の紹介画面へ</a></p>
<p><a href="/create">掲示板への書き込み入力画面へ</a></p>
...

コントローラー get '/' 配下に下記のように追記

...
get '/list' => sub ($c) {
    $c->render( template => 'list' );
};
...

3.1.3 掲示板の書き込み入力画面

掲示板の書き込み入力画面

「掲示板の一覧画面」に該当する list.html.ep の下に下記のように追記

...
@@ create.html.ep
% layout 'default';
% title '掲示板への書き込み入力画面';
<h1>書き込み入力画面</h1>

<form method="post" action="/store">
<p>コメント:<input type="text" name="comment"></p>
<p><input type="submit" value="送信する"></p>
</form>

<p><a href="/list">掲示板の一覧表示へ</a></p>
...

コントローラー get '/list' 配下に下記のように追記

...
get '/create' => sub ($c) {
    $c->render( template => 'create' );
};
...

3.2 画面遷移を確認するための固定値を設定

一通りの画面はつくりましたが、一覧表示画面に掲示板のコメントが表示された状態の様子がまだわからないので、データベースと掲示板のロジックをつくるまえに仮の値を画面に表示しておきます。暫定的な画面の仕上がりを確認したいために固定の値を割り当てて画面を再現するのはよく使われるやり方です。

参考: https://github.com/Becom-Developer/beginning-mojo/blob/master/chapter3/section2/bulletin.pl

3.2.1 掲示板コメントを表示した一覧画面

暫定の値を入れた一覧画面


list.html.ep の部分を下記のように修正

...
@@ list.html.ep
% layout 'default';
% title '掲示板の一覧表示';
<h1>一覧表示</h1>
<table border="1">
  <caption>掲示板</caption>
  <thead>
    <tr>
      <th>コメント</th>
      <th>投稿時刻</th>
    </tr>
  </thead>
  <tbody>
  % for my $bulletin (@{$c->stash->{bulletin_list}}) {
    <tr>
      <td><%= $bulletin->{comment} %></td>
      <td><%= $bulletin->{created_ts} %></td>
    </tr>
  % }
  </tbody>
  <tfoot>
    <tr>
      <th>投稿数の合計</th>
      <td><%= $c->stash->{total} %></td>
    </tr>
  </tfoot>
</table>
<p><a href="/">掲示板の紹介画面へ</a></p>
<p><a href="/create">掲示板への書き込み入力画面へ</a></p>
...

get '/list' を下記のように修正

get '/list' => sub ($c) {
    my $dummy_list = [
        +{
            id          => 1,
            comment     => 'はじめての投稿',
            deleted     => 0,
            created_ts  => '2021-03-10 16:20:25',
            modified_ts => '2021-03-10 16:20:25',
        },
        +{
            id          => 2,
            comment     => '今日は晴れでした',
            deleted     => 0,
            created_ts  => '2021-03-11 16:20:25',
            modified_ts => '2021-03-11 16:20:25',
        },
        +{
            id          => 3,
            comment     => 'アイスがうまかった',
            deleted     => 0,
            created_ts  => '2021-03-12 16:20:25',
            modified_ts => '2021-03-12 16:20:25',
        },
        +{
            id          => 4,
            comment     => 'あいつはうまいこと言うなぁ',
            deleted     => 0,
            created_ts  => '2021-03-13 16:20:25',
            modified_ts => '2021-03-13 16:20:25',
        },
    ];
    $c->stash->{bulletin_list} = $dummy_list;
    my $total = scalar @{$dummy_list};
    $c->stash->{total} = $total;
    $c->render( template => 'list' );
};

3.2.2 書き込み入力画面から一覧画面への画面遷移をつなげる

入力フォームから送信される /store のコントローラーが未実装だったので、仮のコントローラーを作成しておきます。今回画面遷移をみたいだけなのでリクエストされたらそのまま /list へリダイレクトするように作っておきます。写真の赤枠、送信するボタンのところです。

送信するボタンを押す時の遷移

get '/create' の下に下記のように追記

post '/store' => sub ($c) {
    $c->redirect_to('/list');
};

3.3 データーベースと接続

今回はデーターベースに sqlite3 アプリケーションとのつなぎ方は Teng というモジュールを活用します。Teng というモジュールはORM(オーアールマッピング)と言われる手法を使った接続の仕方になります。

参考: https://github.com/Becom-Developer/beginning-mojo/blob/master/chapter3/section3/bulletin.pl

3.3.1 モジュールのインストール

Teng モジュールをインストールするにはもし docker を起動中であれば一旦ストップしていただいて、そのあと cpanfile に追加するモジュールを記載し、そのあと改めて docker-compose up するとモジュールのインストールが始まりその後アプリケーションが起動します。

echo "requires 'Teng', '0.32';" >> cpanfile
(sqlite3 と接続するためにはこちらのモジュールも必要)
echo "requires 'DBD::SQLite', '1.66';" >> cpanfile
(もう一度 docker を立ち上げると自動的にインストールが始まる)
docker-compose up

3.3.2 Teng モジュールを使える状態にする

Teng モジュールについての詳しい情報は metacpan サイトから検索するとみれます

今回は Teng という名前で関数をつくりこの中で Teng のオブジェクトを用意しています。

...
use Teng;
use Teng::Schema::Loader;
...

sub teng {
    my $dsn_str = 'dbi:SQLite:./db/bulletin.db';
    my $user    = '';
    my $pass    = '';
    my $option  = +{
        RaiseError     => 1,
        PrintError     => 0,
        AutoCommit     => 1,
        sqlite_unicode => 1,
    };

    my $dbh  = DBI->connect( $dsn_str, $user, $pass, $option );
    my $teng = Teng::Schema::Loader->load(
        dbh       => $dbh,
        namespace => 'DB::Teng',
    );
    return $teng;
}
...

3.3.3 list コントローラーのダミーの値をデータベースに切り替える

Teng オブジェクトのなかから search メソッドを呼び出してデータを取得しています。今回はSQLの実行速度は考慮していないので、 Schema::Loader 呼び出しからの実行をしています。
get '/list' => sub ($c) {
    my $teng = teng();

    my @bulletin_rows = $teng->search( 'bulletin', +{ deleted => 0 } );
    my $bulletin_list = [];
    for my $row (@bulletin_rows) {
        push @{$bulletin_list}, $row->get_columns;
    }
    $c->stash->{bulletin_list} = $bulletin_list;
    my $total = scalar @{$bulletin_list};
    $c->stash->{total} = $total;
    $c->render( template => 'list' );
};

3.3.4 store コントローラーの書き込みのロジックを追加する

store メソッドに書き込みのロジックを追加の時に日付を取得してタイムスタンプをつくります。タイムスタンプの作り方としてデーターベース側で自動的に記録するやり方もありますが、今回は Time::Piece モジュールで日付を取得しています。Time::Piece モジュールについては現在はPerlの標準モジュールになっています。

...
use Teng;
use Teng::Schema::Loader;
use Time::Piece;
...

post '/store' => sub ($c) {
    my $params = $c->req->params->to_hash;
    my $teng   = teng();
    my $t      = localtime;
    my $date   = $t->date;
    my $time   = $t->time;
    $params = +{
        %{$params},
        deleted     => 0,
        created_ts  => "$date $time",
        modified_ts => "$date $time",
    };
    $teng->fast_insert( 'bulletin', $params );
    $c->redirect_to('/list');
};

3.4 全体の調整

現状で最低限度の掲示板としては機能していますが、様々な問題があります。もっとも問題というのをどこからどこまでを許容してどこまでを調整するのかという事がありますので、きちんとアプリを作りたい場合はきちんと要件を定義したほうがよかったりします。

参考: https://github.com/Becom-Developer/beginning-mojo/blob/master/chapter3/section4/bulletin.pl

3.4.1 値が空でも登録ができてしまう問題

文字を入力するフォームでありがちなのは文字を入力していないのにそのまま登録されてしまう問題です。よほど特殊な作り込みでもない限り文字が入力されていないときは登録をさせないようにする処置が必要です。こういう条件の場合は入力実行をさせないことを「バリデーションする」という言い方をします。

3.4.2 バリデーションの手法

今回はトリムというやり方で対応してみます。トリムというのは文字の両端に存在する空白文字を削除するやりかたです。もし入力フォームに空白文字だけを入力された場合、それは何も入力していないとみなし、両端の空白文字を全て削って何も残っていない場合は入力されていないことと判定することにします。

3.4.3 バリデーションが実行された時の画面

バリデーションが実行された時の画面になぜ入力が実行されなかったかがわかるようにしておく必要があります。今回は下記のような画面表示にしたいとおもいます。

バリデーション実行時

3.4.4 コントローラーとテンプレを修正

トリムのロジックについては Mojo の機能に Mojo::Util というものがありその中で trim 関数というものが用意されていますのでそちらを活用することにしましょう。

参考: https://metacpan.org/pod/Mojo::Util

...
use Mojo::Util qw(trim);
...

post '/store' => sub ($c) {
    my $params = $c->req->params->to_hash;

    # 値が空の場合は登録しない
    my $comment = $params->{comment};
    my $trimmed = trim($comment);
    if ( !$trimmed ) {
        $c->flash( msg => "正しく値を入力してください" );
        $c->redirect_to('/create');
        return;
    }

    my $teng = teng();
    my $t    = localtime;
    my $date = $t->date;
    my $time = $t->time;
    $params = +{
        %{$params},
        deleted     => 0,
        created_ts  => "$date $time",
        modified_ts => "$date $time",
    };
    $teng->fast_insert( 'bulletin', $params );
    $c->redirect_to('/list');
};

正しく入力されない場合は入力画面にリダイレクトしたメッセージを表示

...
@@ create.html.ep
% layout 'default';
% title '掲示板への書き込み入力画面';
<h1>書き込み入力画面</h1>
% if ( my $msg = flash('msg') ) {
  <p style="color:red"><%= $msg %></p>
% }
<form method="post" action="/store">
<p>コメント:<input type="text" name="comment"></p>
<p><input type="submit" value="送信する"></p>
</form>

<p><a href="/list">掲示板の一覧表示へ</a></p>
...

3.5 まとめ

3.6 次回

今回実際に Perl のソースコードを書いてみていったいこれはどうなっているか、いろいろと疑問がわいてきたと思います。とくに Mojo のバーション9 からは -signatures オプションが有効になっており、旧来のPerlのサブルーチンの書き方とはことなります。

疑問に思ったところを一つ一つ調べなが学習してゆくのが結局は早道だったりします。自力で調べても難しいと感じた場合、このブログのコメント枠に質問するのもいいかもしれませんね。

次回は完成したアプリに対しての課題について考えてみます。