[Perl]PSGI入門第四章 Text::XslateでViewを分離

# 本日のお題 [Text::Xslate](http://search.cpan.org/~syohex/Text-Xslate-3.4.0/lib/Text/Xslate.pm)でViewを分離

## おさらい

1. [PSGI入門第一章 環境変数の一覧を出す](http://qiita.com/worthmine/items/96f36332e620aef34001)
2. [PSGI入門第二章 CGI::SessionをPSGIで無理やり使う](http://qiita.com/worthmine/items/987ac1e422348b9f0ea5)
3. [PSGI入門第三章 動的ルーティングを実装する](http://qiita.com/worthmine/items/9662ac8f30a30b167123)

## 前提
- 本記事では[HTML5](https://www.w3.org/TR/html5/)の生成に限って記述[^1]します。(Text::Xslate自体は汎用のテンプレートエンジンですので、HTML5の生成に限らず様々な範囲で利用できます)
- [CGI.pm](http://search.cpan.org/~leejo/CGI-4.35/lib/CGI.pod)の中にText::Xslateを理論上盛り込めます[^2]ので、HTML5で動くCGIを作ることを否定しません。
- [Plack](http://search.cpan.org/~miyagawa/Plack-1.0042/lib/Plack.pm)とText::Xslateは相互に独立したモジュールですので、別にPSGIを書くにあたってText::Xslateを利用ことは必須ではありません。
- デフォルトのテンプレートである`Kolon`で記述[^3]します。
- HTML5自体の解説は省略します。
- 私も入門したてです。ツッコミ大歓迎

[^1]: HTML4で書きたいというニーズが見当たらないし、それならCGI.pmで生成したHTMLをPSGIで表示したって構わない。
[^2]: 私がたどり着いた順番がPlack->Xslateだったので、この可能性は未テストです
[^3]: おそらくそれが一番文献が多いので

## Xslateを利用するメリット
- Model及びControllerからほぼ完全にViewを分離できる。プログラマとデザイナーが同一のリポジトリで作業するときなどの分業に最適
- XS製なので、下手な自作テンプレートより早い
- [TT](http://www.template-toolkit.org/)などの従来のテンプレートからの移行も容易い[^4]
- [Markdown](http://www.markdown.jp/what-is-markdown/)などのモダンなマークアップを取り込むことが容易
- Hello, [Perl6](https://perl6.org/)! 文法がPerl6ライクなのでゆくゆくPerl6に移行するときのコストが下がる

[^4]: [TTを使いたい場合はこちら](http://search.cpan.org/~syohex/Text-Xslate-3.4.0/lib/Text/Xslate/Syntax/TTerse.pm)を参照してください

いくつかのメリットは、そのままデメリットかもしれません。
## Xslateを利用するデメリット
- XS製なので、インストールできない環境（レンタルサーバーなど）がある
- 文法がPerl6ライクなので、導入時に戸惑うことがある。
- HTML5に関して何か特別なメソッドを有している訳ではない[^5]ので、HTML5に関する知識は必要

[^5]: builderモジュールを探せば良い気はする

## ま、やってみましょう。
```perl:Myapp.pl
use strict;
use warnings;
use Carp;

use Plack::App::Path::Router::PSGI;
use Path::Router;
use Plack::Request;

use Encode;                 # Text::Xslateはデコード済みUTF-8が前提なのでEncodeの出番です。

use Text::Xslate;
use Text::Xslate::Bridge::MultiMarkdown;
# ↑宣言しなくても下記の記述で読み込んでくれるが、plackupの時点でインストールを検知したいので明示する
my $tx = Text::Xslate->new(
    module => ['Text::Xslate::Bridge::MultiMarkdown'],
    syntax => 'Kolon',
    cache => 0,
    verbose => 1,
);

use CGI::Session;
my $session = CGI::Session->new( undef, undef, { Directory => './sessions' } ); # ダミーのセッション
my $sid = $session->param('CGISESSID') || undef;
$session->delete(); # ダミーはすぐ消す
$session = CGI::Session->new( undef, $sid, { Directory => './sessions' } ); # 本セッション
if ( $session->is_expired() ) { # 期限切れを消して再発行
    $session->delete();
    $session = CGI::Session->new( undef, $sid, { Directory => './sessions' } );
}else{
    $session->expire('+1d');
}

my $router = Path::Router->new;
$router->add_route( '/'         => target => \&root );
$router->add_route( '/env'      => target => \&env );
$router->add_route( '/session'  => target => \&session );
$router->add_route( '/:action/:id' =>
    validations => {
        action  => qr/^\w[\w\-]{0,9}$/,
        id      => qr/^\d{1,10}$/,
    },
    target => \&action
);

# now create the Plack app
my $app = Plack::App::Path::Router::PSGI->new( router => $router );
$app->to_app();

# 以下routerで呼ばれる要素
sub root {
    my $env = shift;
    my $title = 'Welcome to ' . $env->{'HTTP_HOST'};
    my $render = $tx->render( 'Templates/root.tx', {
        title => $title,
        description => decode_utf8('ここはルートです。'),
    } );
    return response( $env, $render );
}

sub action {
    my $env = shift;
    my ( $action, $id ) = @{ $env->{'plack.router.match.args'} };
    my $req = Plack::Request->new($env);
    my $method = $req->method;
    if( $method =~ /^(:?GET|HEAD)$/s ) {
        my $render = $tx->render( 'Templates/action.tx', {
            title   => decode_utf8('動的ルーティングしてみる'),
            action  => $action,
            id      => $id,
            method  => $method,
        } );
        return response( $env, $render );
    }elsif( $method eq 'POST' ){
        $action = $req->body_parameters->{'action'};
        $id = $req->body_parameters->{'id'};
        my $render = $tx->render( 'Templates/action.tx', {
            title   => decode_utf8('動的ルーティングしてみる'),
            action  => $action,
            id      => $id,
            method  => $method,
        } );
        return response( $env, $render );
    }
    my $render = $tx->render( 'Templates/405.tx', {
        title   => decode_utf8('HTTPメソッドエラー'),
        action  => $action,
        id      => $id,
        method  => $method,
    } );
    return response( $env, $render, -status => 405 );
}

sub session {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my %param = %{ $req->body_parameters };
    if( exists $param{'action'} and $param{'action'} eq 'forget' ) {
        $session->param( 'str', '' );
    }elsif( exists $param{'str'} ){
        $session->param( 'str', $param{'str'} );
    }

    my $render = $tx->render( 'Templates/session.tx', {
        title   => decode_utf8('セッション管理してみる'),
        str     => $session->param('str') || '',
    } );
    return response( $env, $render );
}

sub env {
    my $env = shift;
    my @str;
    while ( my ( $key, $value ) = each %$env ) {
        push @str, "$key = $value" unless ref $value;
    }
    my $render = $tx->render( 'Templates/env.tx', {
        title   => decode_utf8('環境変数一覧'),
        list    => [sort @str],
    } );
    return response( $env, $render );
}

# サブルーチン
sub response {
    my $env = shift;
    my $body = encode_utf8(shift) || croak 'empty body!';
    my %ARG = @_ if @_;
    my $status = $ARG{'-status'} || 200;
    croak "unvalid status: $status" if $status !~ /^\d{3}$/s;
    my $mime = $ARG{'-MIME'} || 'text/html; charset=utf-8';
    my $headers = $ARG{'-headers'} || {};
    my $req = Plack::Request->new($env);
    my $res = $req->new_response($status);
    $res->content_length( length $body );
    $res->content_type($mime);
    $res->header(
        'Set-Cookie'    => $session->cookie,
        %$headers
    );
    $res->body($body) unless $req->method eq 'HEAD';
    $res->finalize;
}
```
スッキリしましたね！以下が必要なテンプレートファイルです。`Templates/`に保存してください。

```html:Templates/base.tx
<!DOCTYPE html>
<html lang="ja-jp">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
: if $description {
    <meta name="description" content="<: $description :>">
: }
: if $noindex {
    <meta name="robots" content="noindex,nofollow,noarchive,noodp,noydir">
: }
    <title><:$title:></title>
</head>

<body>

<div class="container">
<h1><: $title :></h1>

<hr>

: block main -> {}

<hr>

<footer class="col-sm-12">
<ul class="list-inline">
    <li><a href="/">トップへ戻る</a></li>
    <li><a href="/env">環境変数一覧へ</a></li>
    <li><a href="/session">セッションのテストへ</a></li>
    <li><a href="/action/1234567890">動的ルーティングのテストへ</a></li>
</ul>
</footer>
</div>

</body></html>
```
本記事の範疇を超えるので解説しませんが、cssやjsの追加、及び[OGP](https://en.wikipedia.org/wiki/Facebook_Platform#Open_Graph_protocol)などの記載をしたいときはbaseをいじる必要がありますが、基本的にbaseは不変です。footerを別ファイルにしてコンテンツに追加があった場合もいじらなくていいようにすることはできます。

```html:Templates/405.tx
: cascade Templates::base
: around main -> {
<p>指定されたメソッドが不正です。method:<: $method :></p>
: }
```
基本的な継承は上記のように行います。

```html:Templates/root.tx
: cascade Templates::base
: around main -> {
<: markdown('
- [環境変数一覧へ](/env)
- [セッションのテストへ](/session)
- [動的ルーティングのテストへ](/action/1234567890)
') | mark_raw :>
: }
```
普通こういう書き方はしません[^6]が、他に入れるところが思い当たらなかったのでmarkdownをここに入れました。
単にmarkdownを挿入するとXslateはhtmlエスケープをしてしまうので、パイプ演算子で`markdown('*some text*')　| mark_raw`とする必要があるところだけが味噌。

[^6]: 最もアクセスが多いであろうルートは軽い方がいいので、動的記述は少ない方が良い。

```html:Templates/env.tx
: cascade Templates::base
: around main -> {
<p>
: for $list -> $item {
    <: $item :><br>
:}
</p>
: }
```
配列に基づく繰り返し構文などは上記です。

```html:Templates/action.tx
: cascade Templates::base
: around main -> {
: if ( $method == 'POST' ) {
<p><a href="/<: $action~'/'~$id :>">アクセスしてみる</a></p>
: }
<form method="POST" action="/<: $action~'/'~$id :>" enctype="multipart/form-data"><p>
    指定されたmethod:<: $method :><br>
    指定されたaction:<input type="text" name="action" value="<: $action :>">任意の英数字<br>
    指定されたID:<input type="text" name="id" value="<: $id :>">10桁までの整数<br>
    <button type="submit">変更する</button>
</p></form>
: }
```
Perl6での文字列の連結は`.`ではなく`~`で行います。

以下、特に補足すべき注意事項はありませんが、デモを走らせるために必要です。

```html:Templates/session.tx
: cascade Templates::base
: around main -> {
<form method="POST" action="/session" enctype="multipart/form-data"><p>
    <input type="text" name="str" value="<: $str :>">
    <button type="submit" name="action" value="remember">記憶</button>
    <button type="submit" name="action" value="forget">消去</button>
</p></form>
: }
```

##　Kolonの文法
- 関数で始まる行は`:`で始めます。
- HTML中に関数などを埋め込みたい場合は`<: function(some) :>`を利用します。
- コメントは行頭を`:#`とします。

##　テンプレートへの値の渡し方
```
my $render = $tx->render( 'Templates/*.tx', {
    title   => decode_utf8('UTF-8な文字列はデコード必須'),
    list    => $arrayref || \@array,
    hash    => $hashref || \%hash,
    str     => $str,
} );
```
- ここにきて`decode_utf8()`が必須[^7]になります。
- 返り値はデコード済みUTF-8ですので`encode_utf8()`処理が表示までに必要です。
- 配列やハッシュなどは参照渡しをする必要があります。

[^7]: `use utf8`している場合を除く。この場合の検証はしてません。

**ね、簡単でしょう？**あなたも始めよう！PSGI！

## 次章
[PSGI入門第五章 Log::Dispatch::Configでデバッグログを取る](http://qiita.com/worthmine/items/c1434a7b8b63d53b7caf)

# 宣伝
本記事は自前で有料WebサービスをPerlで書いてリリースするに辺り、つまづいた点や気づいたことを共有する目的で書き始めました。

![worthmine-qiita.png](https://qiita-image-store.s3.amazonaws.com/0/10766/370df067-1108-c7b8-2ea3-59ab11155660.png)こちらのQRコードをスマホで読み取り、
興味を持った方は<https://qrown.me>までぜひお立ち寄りください！