MVCアーキテクチャ
CGI::Applicationに限らずWebベースのアプリケーションは図3のようにさまざまな情報を組み合わせて、出力画面となるHTMLを生成しなければなりません。

CGI::ApplicationではデフォルトでHTML::TemplateとCGIモジュールを利用可能です。モードごとの画面はHTMLテンプレートファイルとして作成します。クエリ文字列はCGIモジュールによって解析することができます。データベース部分もCGI::Application::Plugin::DBHを用いることで、DBIを通じてSQLiteなどさまざまなRDBMSを利用できます。MVC(Model-View-Controller)アーキテクチャに当てはめると、データベースがModel、HTMLテンプレートファイルがView、そして「Cashbook.pm」内のモードに対応するサブルーチンがコントローラに相当します。クエリ文字列はコントローラに与えられる引数です。MVCアーキテクチャに沿うことで、CGIアプリケーション全体の構成が見通しの良いものになります。
モードの実装
それぞれのモードの中身を見ていきます。
まず、"view"モードのサブルーチンを見ます。モードを処理するサブルーチンはCashbookパッケージ内のメソッドであるため、第1引数に自身のインスタンス$selfが渡されます。さらに、プラグインによって、第2引数にはハッシュへのリファレンスが渡される場合があります。57行目でこれらの引数を受け取っています。
59~68行目はDBIを通じてデータベースへのアクセスを行っている部分です。素のDBIでは事前にconnectメソッドを呼んで、データベースサーバへのコネクションを張る必要がありますが、CGI::Application::Plugin::DBHを用いるとデータベースへのアクセスが必要となった時点で自動的にコネクションが張られます。また、素のDBIではコネクション成功時にデータベースハンドルが返され、以降のデータベースアクセスの際に用いますが、CGI::Application::Plugin::DBH下では$self->dbhがそれに相当します。データベースから得た情報は一旦、ローカル変数に格納します。
70~72行目がテンプレートから出力画面を生成しているところです。70行目でload_tmplメソッドを呼び出し、引数に指定したHTMLテンプレートファイルを読み込んで、戻り値からテンプレートオブジェクトを得ています。このオブジェクトにパラメータを設定しているのが71行目です。72行目のoutputメソッドを実行すると、テンプレート内の<TMPL_VAR ...>や<TMPL_LOOP ...>などの内容がここで指定した値に置き換わったものが返ります。これが出力画面のHTMLとなります。
53 ### 54 ### 一覧表示 55 ### 56 sub view { 57 my($self, $err) = @_; 58 59 my $sth = $self->dbh->prepare('SELECT * FROM cashbook') || die($DBI::errstr); 60 $sth->execute() || die($DBI::errstr); 61 my @rows = (); 62 while(my $hash_ref = $sth->fetchrow_hashref()){ 63 push(@rows, $hash_ref); 64 } 65 $sth->finish; 66 my $hash_ref = $self->dbh->selectrow_hashref( 67 'SELECT SUM(income) AS total_income, SUM(expense) AS total_expense, SUM(income) - SUM(expense) AS total FROM cashbook' 68 ) || die($DBI::errstr); 69 70 my $tmpl = $self->load_tmpl('view.tmpl'); 71 $tmpl->param(rows => \@rows, %$hash_ref, %$err); 72 return $tmpl->output; 73 }
次に、"insert_input"モードのサブルーチンを見ます。ここでも"view"モードの場合と同じように79行目で引数を受け取り、81~84行目でテンプレートから出力画面を作成しています。ただし、「入力」画面なのでデータベースへのアクセスは行っていません。ここで使用しているHTMLテンプレートファイル「insert_input.tmpl」には、次の「実行」モードとなる、"insert_finish"へ遷移するようにrmパラメータを設定するフォームを書いています。
75 ### 76 ### 項目の追加(入力) 77 ### 78 sub insert_input { 79 my($self, $err) = @_; 80 81 my $tmpl = $self->load_tmpl('insert_input.tmpl'); 82 my($d, $m, $y) = (localtime($^T))[3 .. 5]; 83 $tmpl->param(date => sprintf('%04d%02d%02d', $y + 1900, $m + 1, $d)); 84 return $tmpl->output; 85 }
「実行」を担う"insert_finish"モードを見ると、93~101行目でデータベースアクセスを行い、新たな項目を追加しています。その後、forwardメソッドを呼び出しています。forwardメソッドはCGI::Application::Plugin::Forwardによって供給されるもので、呼び出すと他のモードに強制遷移します。HTTPリダイレクトとは異なり、あくまで他のモードを呼び出して返り値を得るだけです。この返り値を自らの返り値とすることで、あたかも遷移したかのように見せています。ここでは"view"モードを呼び出しています。第2引数に無名ハッシュへのリファレンスを渡していますが、これが"view"サブルーチンの第2引数にそのまま渡されます。
87 ### 88 ### 項目の追加(実行) 89 ### 90 sub insert_finish { 91 my $self = shift; 92 93 $self->dbh->do( 94 'INSERT INTO cashbook (id, date, synopsis, income, expense, remarks) VALUES(NULL, ?, ?, ?, ?, ?)', 95 undef, 96 $self->query->param('date'), 97 $self->query->param('synopsis'), 98 $self->query->param('income'), 99 $self->query->param('expense'), 100 $self->query->param('remarks') 101 ) || die($DBI::errstr); 102 return $self->forward('view', { 'info' => '追加したよ' }); 103 }
他のモードも同様の構成です。
まとめ
以上の解説でCGI::Applicationを活用したアプリケーションの構造を理解していただけたでしょうか。特にサンプルのようなCRUD(Create, Read, Update, Delete)型のアプリケーションはCGI::Applicationのフレームワークを用いて記述すると、自然とMVCアーキテクチャに則った形となり、コードの見通しがよくなります。今後作成されるCGIアプリケーションでは、ぜひCGI::Applicationを活用してください。
さて、既にお気づきの方もおられるかと思いますが、サンプルではクエリ文字列で渡されたパラメータのチェックを一切行っていません。このままでは不正なパラメータを正規の処理に渡すことが可能で、悪意ある利用者にセキュリティホールを突かれてしまいます。CGI::Application::Plugin::VaridateRMプラグインなどで適切な処理を入れてみてください。これは読者への課題とします。
