Mojolicious::Guides::Rendering - コンテンツのレンダリング
説明
本書は、Mojoliciousレンダラを使ったコンテンツ生成について説明します。
概念
すべてのMojolicious開発者が知るべき本質
レンダラ
レンダラは、複数のテンプレートシステムとデータエンコードモジュールを利用して、スタッシュデータを実際のレスポンスに変換する小さなブラックボックスです。
{text => 'Hello.'} -> 200 OK, text/html, 'Hello.'
{json => {x => 3}} -> 200 OK, application/json, '{"x":3}'
{text => 'Oops.', status => '410'} -> 410 Gone, text/html, 'Oops.'
開発者またはルーティングから十分な情報が提供されれば、テンプレートは自動的に検出されます。テンプレート名はtemplate.format.handler命名規則に従うことが期待されています。templateはデフォルトではcontroller/actionまたはルート名です。デフォルトのformatはhtml、handlerはep です。
{controller => 'users', action => 'list'} -> 'users/list.html.ep'
{template => 'foo', format => 'txt'} -> 'foo.txt.ep'
{template => 'foo', handler => 'epl'} -> 'foo.html.epl'
controller値は、Mojo::UtilのdecamelizeによってCamelCaseからsnake_caseに変換されます。-文字は/ に置き換えられます。
{controller => 'My::Users', action => 'add'} -> 'my/users/add.html.ep'
{controller => 'my-users', action => 'show'} -> 'my/users/show.html.ep'
テンプレートはすべてアプリケーションのtemplateディレクトリに置いてください。このパスはMojolicious::Rendererのpathsで変更できます。または、Mojolicious::RendererのclassesのうちいずれかのDATAセクションに記述してください。
__DATA__; @@ time.html.ep % use Time::Piece; % my $now = localtime; <!DOCTYPE html> <html> <head><title>Time</title></head> <body>The time is <%= $now->hms %>.</body> </html> @@ hello.txt.ep ...
レンダラは、プラグインを使用して追加のテンプレートシステムをサポートするように簡単に拡張できますが、それについては後で詳しく説明します。
埋め込みPerl
Mojoliciousには埋め込みPerlまたはepと呼ばれる、すぐに使える最小限でありながら非常に強力なテンプレートシステムが含まれています。それはMojo::Templateに基づいています。そして、少数の特別なタグと改行文字を使ってPerlコードを実際のコンテンツに埋め込むことができます。すべてのテンプレートに対してstrict、warnings、utf8とPerl 5.10 features|featureが自動的に有効になります。
<% Perlコード %> <%= Perl式、XML文字がエスケープされた結果が返されます %> <%== Perl式、評価結果が返されます %> <%# デバッグに役立つコメント %> <%% "<%"に置換されます。テンプレートの生成に使えます %> % Perlコード行、"<% line =%>"(説明は後ほど)として扱われます %= Perl式行、"<%= line %>"として扱われます %= Perl式行、"<%= line %>"として扱われます %# コメント行、デバッグに役立ちます %% "%"に置き換えられ、テンプレートを生成するのに便利です
タグと行はほとんど同じように機能しますが、コンテキストによって使い分けると見た目が少し良くなるでしょう。セミコロンは、すべての式に自動で追加されます。
<% my $i = 10; %>
<ul>
<% for my $j (1 .. $i) { %>
<li>
<%= $j %>
</li>
<% } %>
</ul>
% my $i = 10;
<ul>
% for my $j (1 .. $i) {
<li>
%= $j
</li>
% }
</ul>
空白文字の扱いが異なることを別にすれば、両方のサンプルは似たPerlのコードを生成します。そのまま変換すると次のようになるでしょう。
my $output = '';
my $i = 10;
$output .= '<ul>';
for my $j (1 .. $i) {
$output .= '<li>';
$output .= xml_escape scalar + $j;
$output .= '</li>';
}
$output .= '</ul>';
return $output;
イコールサインを追加すると、Perl式の結果に含まれる文字列< 、>、&、'、"のエスケープを無効にできます。エスケープは、アプリケーションに対するXSS攻撃を防ぐためにデフォルトでおこなわれます。
<%= 'I ♥ Mojolicious!' %> <%== '<p>I ♥ Mojolicious!</p>' %>
Mojo::ByteStreamオブジェクトだけは自動エスケープの対象外です。
<%= b('<p>I ♥ Mojolicious!</p>') %>
タグの前後にある文字列は、追加のイコール記号をタグの最後につけることによって、除去できます。
<% for (1 .. 3) { %>
<%= 'この式の前後にある空白文字はトリムされます' =%>
<% } %>
改行はバックスラッシュでエスケープできます。
これは <%= 1 + 1 %> \ 1行になります
改行の直前のバックスラッシュはもう一つのバックスラッシュ によってエスケープできます。
これは <%= 1 + 1 %> \\ 複数行に \\ なります
最後の文字がバックスラッシュでない限り、改行文字はすべてのテンプレートに自動的に追加されます。また、テンプレートの末尾の空行は無視されます。
末尾に改行文字が付きません <%= 1 + 1 %> \
テンプレートの最初で、名前に無効な文字を含まないスタッシュ値は通常の変数として自動的に初期化され、コントローラオブジェクトは$selfと$cの両方が自動的に初期化されます。
$c->stash(name => 'tester'); Hello <%= $name %> from <%= $self->tx->remote_address %>.
myapp.*のようなプレフィックスは、通常はテンプレートの中で見せたくないスタッシュ値に使います。
$c->stash('myapp.name' => 'tester');
たくさんのヘルパー関数が利用可能ですが、後ほど紹介します。
<%= dumper {foo => 'bar'} %>
基礎
すべてのMojolicious開発者が知っておくべきもっとも一般的に利用される機能
自動的な描画
Mojolicious::Controllerのrenderメソッドを呼び出すことで、レンダラを手動で起動できます。しかし、通常は必要ありません。なぜならルータの処理が終わったとき、何もレンダリングされていない場合はレンダラが自動的に呼び出されるからです。これは、何もアクションを実行しない、テンプレートだけを指し示すルーティングを作成できるということです。
$c->render;
ただし、大きな違いがひとつあります。renderを手動で呼ぶことによって、テンプレートが現在のコントローラオブジェクトを使用し、アプリケーションクラスのMojoliciousのcontroller_class属性で指定されたデフォルトコントローラを使用しないことを保証できます。
$c->render_later;
Mojolicious::Controllerのrender_laterメソッドを使って自動レンダリングを無効にすることもできます。 これは、ノンブロッキング操作を先に実行したい場合にレンダリングを遅らせるのに非常に便利です。
テンプレートの描画
レンダラはいつも正しいテンプレートを検知しようとしますが、スタッシュのtemplateの値を使って描画するテンプレートを指定することもできます。最後のスラッシュより前の部分は、テンプレートを探すサブディレクトリとして解釈されます。
# foo/bar/baz.*.* $c->render(template => 'foo/bar/baz');
特定のformatやhandlerを選択することも同様に簡単です。
# foo/bar/baz.txt.epl $c->render(template => 'foo/bar/baz', format => 'txt', handler => 'epl');
特定のテンプレートの描画は、とてもよくあるタスクなのでショートカットが用意されています。
$c->render('foo/bar/baz')
テンプレートが実際に存在するかどうかわからない場合には、複数ある代わりのテンプレートを試すために Mojolicious::Controllerのrender_maybeメソッドを使うこともできます。
$c->render_maybe('localized/bar') or $self->render('foo/bar');
文字列への描画
描画の結果をレスポンスとして生成するのではなく、直接使いたい場合もあることでしょう。たとえば、Eメールを送る場合などです。Mojolicious::Controllerのrender_to_string を使って行えます。
my $html = $c->render_to_string('mail');
エンコーディングは実行されません。結果を他のテンプレートで再利用したり、バイナリデータを生成することが簡単になります。
my $pdf = $c->render_to_string('invoice', format => 'pdf');
$self->render(data => $pdf, format => 'pdf');
すべての渡された引数は、自動的にローカライズされ、 描画処理の間だけ利用できます。
テンプレートバリアント
異なったデバイス間でアプリケーションの見た目をよくするためには、複数あるテンプレートからいずれかを選択するために、variantスタッシュ値が利用できます。
# foo/bar/baz.html+phone.ep
# foo/bar/baz.html.ep
$c->render('foo/bar/baz', variant => 'phone');
このスタッシュ値はとても自由に使えます。なぜなら、その名前のテンプレートが実際に存在したときだけ適用され、存在しなければ一般的なテンプレートにフォールバックするからです。
インラインテンプレートの描画
epのようないくつかのレンダラは、テンプレートをインラインで渡すことができます。
$c->render(inline => 'The result is <%= 1 + 1%>!');
自動検知はパスに依存するため、handlerを与える必要があるかもしれません。
$c->render(inline => "<%= shift->param('foo') %>", handler => 'epl');
テキストの描画
文字列はtextスタッシュ値を使ってバイトに変換できます。与えた値はMojolicious::Rendererのencodingで自動的にエンコードされます。
$c->render(text => 'I ♥ Mojolicious!');
データの描画
生のバイトはスタッシュのdataの値によって描画できます。エンコーディングは実行されません。
$c->render(data => $bytes);
JSONの描画
スタッシュのjson の値を使用すると、Perlデータ構造がレンダラに渡され、Mojo::JSONを使用してJSONに直接エンコードされます。
$c->render(json => {foo => [1, 'test', 3]});
ステータスコード
レスポンスのステータスコードを、スタッシュのstatusの値によって変更できます。
$c->render(text => 'Oops.', status => 500);
コンテンツタイプ
レスポンスのContent-Typeヘッダは、実際のスタッシュのformatの値に対応するMIMEタイプが元になっています。
# Content-Type: text/plain $c->render(text => 'Hello.', format => 'txt'); # Content-Type: image/png $c->render(data => $bytes, format => 'png');
これらの対応は、Mojoliciousのtypesを使って、簡単に拡張したり変更したりできます。
# 新しいMIMEタイプの追加 $app->types->type(md => 'text/markdown');
スタッシュデータ
データは、どのようなPerlのネイティブなデータ型のものであれ、stashを通してテンプレートにレファレンスとして渡せます。
$c->stash(description => 'web framework');
$c->stash(frameworks => ['Catalyst', 'Mojolicious']);
$c->stash(spinoffs => {minion => 'job queue'});
%= $frameworks->[1]
%= $spinoffs->{minion}
以下はすべてPerlの通常の制御構造なので、どれもうまく動きます。
% for my $framework (@$frameworks) {
<%= $framework %> is a <%= $description %>.
% }
% if (my $description = $spinoffs->{minion}) {
Minion is a <%= $description %>.
% }
さまざまな方法でレンダリングされる可能性があり、スタッシュ値が実際に設定されるかどうかわからない場合は、単にMojolicious::Plugin::DefaultHelpersのstash を使用します。
% if (my $spinoffs = stash 'spinoffs') {
Minion is a <%= $spinoffs->{minion} %>.
% }
ヘルパー
ヘルパーは、アプリケーション、コントローラー、テンプレートのどこででも使える小さな関数の集まりです。
%= dumper [1, 2, 3] # アプリケーション my $serialized = $app->dumper([1, 2, 3]); # コントローラー my $serialized = $c->dumper([1, 2, 3]);
デフォルトヘルパーとタグヘルパーは区別されます。デフォルトヘルパーは、Mojolicious::Plugin::DefaultHelpersのdumperのようにより汎用的なものです。一方、タグヘルパーはMojolicious::Plugin::TagHelpersのlink_toのようにテンプレート専用であり、主にHTMLタグの生成に使用されます。
%= link_to Mojolicious => 'https://mojolicious.org'
コントローラーでは、Mojolicious::Controllerのhelpersメソッドを使ってヘルパーだけが呼び出されるように制限し、既存のメソッドとの衝突を防げます。
my $serialized = $c->helpers->dumper([1, 2, 3]);
すべての組み込みのヘルパーのリストは、Mojolicious::Plugin::DefaultHelpers とMojolicious::Plugin::TagHelpersにあります。
コンテンツネゴシエーション
リソースの表現方法が複数あったり、RESTに忠実なコンテンツネゴーシエーションを行う必要があるときは、Mojolicious::Controllerのrenderの代わりにMojolicious::Plugin::DefaultHelpersのrespond_toも使用できます。
# /hello (Accept: application/json) -> "json"
# /hello (Accept: text/xml) -> "xml"
# /hello.json -> "json"
# /hello.xml -> "xml"
# /hello?format=json -> "json"
# /hello?format=xml -> "xml"
$c->respond_to(
json => {json => {hello => 'world'}},
xml => {text => '<hello>world</hello>'}
);
もっとも適切な表現がformat、GET/POSTパラメーター、スタッシュのformatの値またはAcceptリクエストヘッダから自動的に選択され、スタッシュのformatの値に格納されます。AcceptリクエストヘッダまたはContent-TypeレスポンスヘッダのMIMEタイプマッピングを変更するには、Mojoliciousのtypesを使用します。
$c->respond_to(
json => {json => {hello => 'world'}},
html => sub {
$self->content_for(head => '<meta name="author" content="sri">');
$self->render(template => 'hello', message => 'world')
}
);
ひとつのrenderコールに含めるには表現が複雑すぎる場合は、コールバックを使用できます。
# /hello (Accept: application/json) -> "json"
# /hello (Accept: text/html) -> "html"
# /hello (Accept: image/png) -> "any"
# /hello.json -> "json"
# /hello.html -> "html"
# /hello.png -> "any"
# /hello?format=json -> "json"
# /hello?format=html -> "html"
# /hello?format=png -> "any"
$c->respond_to(
json => {json => {hello => 'world'}},
html => {template => 'hello', message => 'world'},
any => {text => '', status => 204}
);
実行可能な表現が見つからない場合は、anyによるフォールバックが行われるか、 空の204レスポンスが自動的に描画されます。
# /hello -> "html"
# /hello (Accept: text/html) -> "html"
# /hello (Accept: text/xml) -> "xml"
# /hello (Accept: text/plain) -> undef
# /hello.html -> "html"
# /hello.xml -> "xml"
# /hello.txt -> undef
# /hello?format=html -> "html"
# /hello?format=xml -> "xml"
# /hello?format=txt -> undef
if (my $format = $self->accepts('html', 'xml')) {
...
}
より高度なネゴシエーションのロジックには、Mojolicious::Plugin::DefaultHelpersのacceptsヘルパーを使うこともできます。
例外とnot_foundページの描画
これまでにすでに組み込みの404(Not Found)や500(Server Error)ページを見たことがあるでしょう。間違いがあるとき自動的に描画されます。これはあなた自身が書いた例外ハンドリングが失敗したときのためのフォールバックです。開発中にとくに役に立つことでしょう。例外ページは、 Mojolicious::Plugin::DefaultHelpersのreply->exceptionやMojolicious::Plugin::DefaultHelpersのreply->not_foundヘルパーを使って手動で描画することもできます。
use Mojolicious::Lite;
use Scalar::Util 'looks_like_number';
get '/divide/:dividend/by/:divisor' => sub {
my $c = shift;
my $dividend = $c->param('dividend');
my $divisor = $c->param('divisor');
# 404
return $self->reply->not_found
unless looks_like_number $dividend && looks_like_number $divisor;
# 500
return $self->reply->exception('Division by zero!') if $divisor == 0;
# 200
$self->render(text => $dividend / $divisor);
};
app->start;
例外ページのテンプレートは自由に変更できます。本番環境では、にアプリケーションにより関連した内容をユーザー表示したいことが多いですからね。レンダラは、組み込みのデフォルトテンプレートにフォールバックする前にexception.$mode.$format.*またはnot_found.$mode.$format.*を毎回探します。
use Mojolicious::Lite;
get '/dies' => sub { die 'Intentional error' };
app->start;
__DATA__;
@@ exception.production.html.ep
<!DOCTYPE html>
<html>
<head><title>Server error</title></head>
<body>
<h1>Exception</h1>
<p><%= $exception->message %></p>
=head2 スタッシュ
<pre><%= dumper $snapshot %></pre>
</body>
</html>
Mojoliciousのbefore_renderフックを使えば、 レンダラに渡された引数を処理に割り入って変更できるようになり、より高度なカスタマイズが可能になります。
use Mojolicious::Lite;
hook before_render => sub {
my ($c, $args) = @_;
# 例外テンプレートが確実に描画されるようにする
return unless my $template = $args->{template};
return unless $template eq 'exception';
# コンテンツネゴシエーションで許可されている場合にJSONレンダリングに切り替える
$args->{json} = {exception => $self->stash('exception')}if $self->accepts('json');
};
get '/' => sub { die "This sho...ALL GLORY TO THE HYPNOTOAD!\n" };
app->start;
レイアウト
epテンプレートを使うほとんどの場合で、生成したコンテンツをHTMLの雛形の中にラップしたいのではないでしょうか。レイアウト機能のおかげでこれはとても簡単にできます。
use Mojolicious::Lite;
get '/' => {template => 'foo/bar'};
app->start;
__DATA__;
@@ foo/bar.html.ep
% layout 'mylayout';
Hello World!
@@ layouts/mylayout.html.ep
<!DOCTYPE html>
<html>
<head><title>MyApp</title></head>
<body><%= content %></body>
</html>
Mojolicious::Plugin::DefaultHelpersのlayoutヘルパーを使って適切なレイアウトテンプレートを選択し、現在のテンプレートの結果をMojolicious::Plugin::DefaultHelpersのcontentヘルパーで配置できます。ふつうのスタッシュ値をlayoutヘルパーに渡すこともできます。
use Mojolicious::Lite;
get '/' => {template => 'foo/bar'};
app->start;
__DATA__;
@@ foo/bar.html.ep
% layout 'mylayout', title => 'Hi there!';
Hello World!
@@ layouts/mylayout.html.ep
<!DOCTYPE html>
<html>
<head><title><%= $title %></title></head>
<body><%= content %></body>
</html>
layoutヘルパーを使う代わりに、スタッシュのlayoutの値、または、layout引数を渡してrenderメソッドを呼び出すこともできます。
$c->render(template => 'mytemplate', layout => 'mylayout');
layoutのスタッシュ値をアプリケーション全体で利用できるようにするには、Mojoliciousのdefaultsが使えます。
$app->defaults(layout => 'mylayout');
レイアウトはMojolicious::Controllerのrender_to_stringでも利用可能ですが、 layoutの値はレンダラの引数(スタッシュの値ではなく)として渡される必要があります。
my $html = $c->render_to_string('reminder', layout => 'mail');
部分テンプレート
大きなテンプレートは、より小さくて管理しやすいかたまりに分割できます。こうしてできた部分テンプレートは、その他のテンプレートと共有できます。Mojolicious::Plugin::DefaultHelpersのincludeヘルパーを使えば、あるテンプレートを別のテンプレートにインクルードできます。
use Mojolicious::Lite;
get '/' => {template => 'foo/bar'};
app->start;
__DATA__;
@@ foo/bar.html.ep
<!DOCTYPE html>
<html>
%= include '_header', title => 'Howdy'
<body>Bar</body>
</html>
@@ _header.html.ep
<head><title><%= $title %></title></head>
部分テンプレートにはなんでも好きな名前が付けられます。でも、名前の先頭にアンダースコアを付けるのが通例になっています。
再利用可能なテンプレートブロック
同じことを繰り返すのは楽しくありません。だから、通常のPerlサブルーチンのように動く再利用可能なテンプレートブロックがepに書けるようになっています。beginとendキーワードを使います。両方のキーワードは囲いタグであって、本物のPerlコードではないことに気を付けてください。そのため、beginとendの後に置けるのは空白文字だけです。
use Mojolicious::Lite;
get '/' => 'welcome';
app->start;
__DATA__;
@@ welcome.html.ep
<% my $block = begin %>
% my $name = shift;
Hello <%= $name %>.
<% end %>
<%= $block->('Wolfgang') %>
<%= $block->('Baerbel') %>
単純にPerlコードに変換すると次のようになるでしょう。
my $output = '';
my $block = sub {
my $name = shift;
my $output = '';
$output .= 'Hello ';
$output .= xml_escape scalar + $name;
$output .= '.';
return Mojo::ByteStream->new($output);
};
$output .= xml_escape scalar + $block->('Wolfgang');
$output .= xml_escape scalar + $block->('Baerbel');
return $output;
テンプレートブロックはテンプレート間で共有できませんが、多くの場合、テンプレートの部品をヘルパーに渡すために使われます。
ヘルパーの追加
アクションは小さく、なるべく多くのコードが再利用できるようにしましょう。ヘルパーはこれをとても簡単にします。ヘルパーは現在のコントローラーオブジェクトを第一引数として渡すので、これを使ってアクションでできるたくさんのことが行えます。
use Mojolicious::Lite;
helper debug => sub {
my ($c, $str) = @_;
$c->app->log->debug($str);
};
get '/' => sub {
my $c = shift;
$c->debug('アクションからのHello!');
} => 'index';
app->start;
__DATA__;
@@ index.html.ep
% debug 'テンプレートからのHello!';
ヘルパーは最後の引数にテンプレートブロックを受け取ることもできます。たとえば、タグヘルパーやフィルターを使うのに最適です。ヘルパーの結果をMojo::ByteStreamオブジェクトにラップすることで、間違えて二重エスケープをすることを防げます。
use Mojolicious::Lite;
use Mojo::ByteStream;
helper trim_newline => sub {
my ($c, $block) = @_;
my $result = $block->();
$result =~ s/\n//g;
return Mojo::ByteStream->new($result);
};
get '/' => 'index';
app->start;
__DATA__;
@@ index.html.ep
%= trim_newline begin
Some text.
%= 1 + 1
More text.
% end
スタッシュ値と同様に、myapp.*のようなプレフィックスを使用することで、テンプレートのなかにむき出しの関数としてヘルパーを書かなくて済みます。また、アプリケーションが大きくなるにつれて、ネームスペース中に整理できるようになります。すべてのプレフィックスは自動的に、 現在のコントローラーオブジェクトを含んだプロキシオブジェクトを返却する ヘルパーになります。また、そこからはネストされたヘルパーを呼び出せます。
use Mojolicious::Lite;
helper 'cache_control.no_caching' => sub {
my $c = shift;
$c->res->headers->cache_control('private, max-age=0, no-cache');
};
helper 'cache_control.five_minutes' => sub {
my $c = shift;
$c->res->headers->cache_control('public, max-age=300');
};
get '/news' => sub {
my $c = shift;
$c->cache_control->no_caching;
$c->render(text => 'Always up to date.');
};
get '/some_older_story' => sub {
my $c = shift;
$c->cache_control->five_minutes;
$c->render(text => 'This one can be cached for a bit.');
};
app->start;
ヘルパーは再定義可能ですが、衝突を避けるために、よく注意してください。
コンテンツブロック
Mojolicious::Plugin::DefaultHelpersのcontent_forヘルパーを使うと、コンテンツのブロック全体をあるテンプレートから別のテンプレートへと渡せます。レイアウトがサイドバー等の独立したセクションをテンプレートに挿入する場合にとても便利です。
use Mojolicious::Lite; get '/' => 'foo'; app->start; __DATA__; @@ foo.html.ep % layout 'mylayout'; % content_for header => begin <meta http-equiv="Content-Type" content="text/html"> % end <div>Hello World!</div> % content_for header => begin <meta http-equiv="Pragma" content="no-cache"> % end @@ layouts/mylayout.html.ep <!DOCTYPE html> <html> <head><%= content 'header' %></head> <body><%= content %></body> </html>
フォーム
HTMLフォームをより効率的に構築するために、Mojolicious::Plugin::TagHelpersのform_for のようなタグヘルパーを使うことができます。ルート名が与えられていれば、リクエストメソッドが自動的に選択されます。ほとんどのブラウザは、フォームをサブミットするメソッドとしてGETとPOSTだけを許可していて、PUTやDELETEには対応していないので、_methodクエリパラメーターで偽装できます。
use Mojolicious::Lite;
get '/' => 'form';
# PUT /nothing
# POST /nothing?_method=PUT
put '/nothing' => sub {
my $c = shift;
# リダイレクトで二重フォーム送信を防ぐ
my $value = $c->param('whatever');
$c->flash(confirmation => "We did nothing with your value ($value).");
$c->redirect_to('form');
};
app->start;
__DATA__;
@@ form.html.ep
<!DOCTYPE html>
<html>
<body>
% if (my $confirmation = flash 'confirmation') {
<p><%= $confirmation %></p>
% }
%= form_for nothing => begin
%= text_field whatever => 'I ♥ Mojolicious!'
%= submit_button
% end
</body>
</html>
二重フォーム送信を防ぐためにMojolicious::Plugin::DefaultHelpersのflashとMojolicious::Plugin::DefaultHelpersのredirect_toは一緒に使用されることが多く、リダイレクトされたページをリロードすると消える確認メッセージをユーザーが受信できるようになります。
フォーム検証
アプリケーションに送信するGETとPOSTパラメーターはMojolicious::Plugin::DefaultHelpersのvalidationメソッドを使って検証できます。すべての未知のフィールドはデフォルトで無視されるので、どれをrequiredのrequiredにして、どれをoptionalのoptionalにするのかを、値のチェックを実行する前に決める必要があります。すべてのチェックはすぐに実行されます。Mojolicious::Validator::Validationのis_validのようなメソッドからすぐに結果を得て、より高度な検証ロジックを構築できます。
use Mojolicious::Lite;
get '/' => sub {
my $c = shift;
# パラメーターが送信されたかをチェック
my $v = $c->validation;
return $c->render('index') unless $v->has_data;
# パラメーターを検証する(「pass」は「pass_again」に依存)
$v->required('user')->size(1, 20)->like(qr/^[a-z0-9]+$/);
$v->required('pass_again')->equal_to('pass')
if $v->optional('pass')->size(7, 500)->is_valid;
# 検証が失敗したかをチェック
return $c->render('index') if $v->has_error;
# 結果を描画する
$c->render('thanks');
};
app->start;
__DATA__;
@@ index.html.ep
<!DOCTYPE html>
<html>
<head>
<style>
label.field-with-error { color: #dd7e5e }
input.field-with-error { background-color: #fd9e7e }
</style>
</head>
<body>
%= form_for index => begin
%= label_for user => 'Username (required, 1-20 characters, a-z/0-9)'
<br>
%= text_field 'user', id => 'user'
%= submit_button
<br>
%= label_for pass => 'Password (optional, 7-500 characters)'
<br>
%= password_field 'pass', id => 'pass'
<br>
%= label_for pass_again => 'Password again (equal to the value above)'
<br>
%= password_field 'pass_again', id => 'pass_again'
% end
</body>
</html>
@@ thanks.html.ep
<!DOCTYPE html>
<html><body>Thank you <%= validation->param('user') %>.</body></html>
Mojolicious::Plugin::TagHelpersのタグヘルパーによって生成されたフォーム要素は 自動的に以前の値を復元し、検証が失敗したフィールドのclass属性にfield-with-errorを 追加します。CSSによるスタイリングが簡単になります。
<label class="field-with-error" for="user"> Username (required, only characters e-t) </label> <input class="field-with-error" type="text" name="user" value="sri">
利用可能なチェックの完全なリストは、 Mojolicious::ValidatorのCHECKSを参照してください。
フォーム検証チェックの追加
検証チェックはMojolicious::Validatorのadd_checkを使って登録できます。成功した場合はfalse値を返します。true値を使用して追加情報を渡すことができます。追加情報はMojolicious::Validator::Validationのerrorで取得できます。
use Mojolicious::Lite;
# 「range(範囲)」チェックを追加する
app->validator->add_check(range => sub {
my ($v, $name, $value, $min, $max) = @_;
return $value < $min || $value > $max;
});
get '/' => 'form';
post '/test' => sub {
my $c = shift;
# カスタムチェックでパラメーターを検証する
my $v = $c->validation;
$v->required('number')->range(3, 23);
# 検証が失敗したときフォームを再描画する
return $c->render('form') if $v->has_error;
# リダイレクトで二重フォーム送信を防ぐ
$c->flash(number => $v->param('number'));
$c->redirect_to('form');
};
app->start;
__DATA__;
@@ form.html.ep
<!DOCTYPE html>
<html>
<body>
% if (my $number = flash 'number') {
<p>ありがとう。数値 <%= $number %> は有効です。</p>
% }
%= form_for test => begin
% if (my $err = validation->error('number')) {
<p>
%= '値が必要です。' if $err->[0] eq 'required'
%= '値は3から23の範囲で入力してください。' if $err->[0] eq 'range'
</p>
% }
%= text_field 'number'
%= submit_button
% end
</body>
</html>
クロスサイトリクエストフォージェリー(CSRF)
CSRFはWebアプリケーションに対する非常に一般的な攻撃であり、普通のリンクなどを介して、ログインしているユーザーが送信するつもりのないフォームを送信するように仕掛けます。この攻撃からユーザー守るためにすべきことは、Mojolicious::Plugin::TagHelpersのcsrf_fieldを使って隠しフィールドをフォームに追加し、Mojolicious::Validator::Validationのcsrf_protectで 検証することだけです。
use Mojolicious::Lite;
get '/' => {template => 'target'};
post '/' => sub {
my $c = shift;
# CSRFトークンのチェック
my $v = $c->validation;
return $c->render(text => '不正なCSRFトークンです!', status => 403)
if $v->csrf_protect->has_error('csrf_token');
my $city = $v->required('city')->param('city');
$c->render(text => "低軌道イオン砲が$cityに向けられている!")
unless $v->has_error;
} => 'target';
app->start;
__DATA__;
@@ target.html.ep
<!DOCTYPE html>
<html>
<body>
%= form_for target => begin
%= csrf_field
%= label_for city => 'どの街に低軌道イオン砲を向ける?'
%= text_field 'city', id => 'city'
%= submit_button
%= end
</body>
</html>
Ajaxリクエストなどの場合、Mojolicious::Plugin::DefaultHelpersのcsrf_tokenヘルパーを使用してトークンを直接生成することもできます。それから、トークンをX-CSRF-Tokenリクエストヘッダーと一緒に渡します。
発展
一般的ではないが、より強力な機能。
テンプレートの継承
継承はレイアウトの概念を一歩進めます。Mojolicious::Plugin::DefaultHelpersのcontentヘルパーと Mojolicious::Plugin::DefaultHelpersのextendsヘルパーを使って、名前付きブロックを含むテンプレートスケルトンを構築できます。スケルトンテンプレートは、子テンプレートによってオーバーライドできます。
use Mojolicious::Lite;
# first > mylayout
get '/first' => {template => 'first', layout => 'mylayout'};
# third > second > first > mylayout
get '/third' => {template => 'third', layout => 'mylayout'};
app->start;
__DATA__;
@@ layouts/mylayout.html.ep
<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body><%= content %></body>
</html>
@@ first.html.ep
%= content header => begin
デフォルトヘッダ―
% end
<div>Hello World!</div>
%= content footer => begin
デフォルトフッター
% end
@@ second.html.ep
% extends 'first';
% content header => begin
新しいヘッダー
% end
@@ third.html.ep
% extends 'second';
% content footer => begin
新しいフッター
% end
この連鎖を進めれば、とてもハイレベルなテンプレートの再利用が可能になります。
静的ファイルのサーブ
静的ファイルは、アプリケーションのpublicディレクトリから自動的にサーブされます。サーブ対象のディレクトリはMojolicious::StaticのpathsまたはMojolicious::StaticのclassesにおけるDATAセクションによってカスタマイズできます。これで十分でない場合、Mojolicious::Plugin::DefaultHelpersのreply->static や Mojolicious::Plugin::DefaultHelpersのreply->fileを使って手動でサーブすることもできます。
use Mojolicious::Lite;
get '/' => sub {
my $c = shift;
$c->reply->static('index.html');
};
get '/some_download' => sub {
my $c = shift;
$c->res->headers->content_disposition('attachment; filename=bar.png;');
$c->reply->static('foo/bar.png');
};
get '/leak' => sub {
my $c = shift;
$c->reply->file('/etc/passwd');
};
app->start;
カスタムレスポンス
多くのレスポンス内容は、静的であれ動的であれ、Mojo::Asset::File と Mojo::Asset::Memory オブジェクトによってサーブされます。キャッシュされたJSONデータや一時ファイルなどの静的コンテンツ のために、Mojolicious::Plugin::DefaultHelpersのreply->asset を使って、コンテンツネゴシエーションをRange、If-Modified-Since、If-None-Matchヘッダーで行いつつコンテンツをサーブできます。
use Mojolicious::Lite;
use Mojo::Asset::File;
get '/leak' => sub {
my $c = shift;
$c->res->headers->content_type('text/plain');
$c->reply->asset(Mojo::Asset::File->new(path => '/etc/passwd'));
};
app->start;
さらに強力なコントロールを得るために、ヘルパーを無視して Mojolicious::Controllerのrenderedメソッドを使い、レスポンスの生成が完了した時点をレンダラに知らせることもできます。
use Mojolicious::Lite;
use Mojo::Asset::File;
get '/leak' => sub {
my $c = shift;
$c->res->headers->content_type('text/plain');
$c->res->content->asset(Mojo::Asset::File->new(path => '/etc/passwd'));
$c->rendered(200);
};
app->start;
ヘルパープラグイン
便利なヘルパーは、複数のアプリケーション間で共有したいこともあることでしょう。プラグインを使えば簡単です。
package Mojolicious::Plugin::DebugHelper;
use Mojo::Base 'Mojolicious::Plugin';
sub register {
my ($self, $app) = @_;
$app->helper(debug => sub {
my ($c, $str) = @_;
$c->app->log->debug($str);
});
}
1;
register メソッドがプラグインを読み込んだ時点でコールされます。そして、アプリケーションにヘルパーを追加するためには、 Mojoliciousのhelperを使います。
use Mojolicious::Lite;
plugin 'DebugHelper';
get '/' => sub {
my $c = shift;
$c->debug('It works!');
$c->render(text => 'Hello!');
};
app->start;
CPAN完全互換の配布用プラグインのためのスケルトンを自動的に生成できます。
$ mojo generate plugin DebugHelper
もしPAUSEアカウントを持っていれば(http://pause.perl.orgでリクエストできます)、わずか数コマンドでCPANにリリースできます。
$ perl Makefile.PL $ make test $ make manifest $ make dist $ mojo cpanify -u USER -p PASS Mojolicious-Plugin-DebugHelper-0.01.tar.gz
プラグインへのコンテンツのバンドル
テンプレートや静的ファイルなどのアセットは、プラグインにバンドルできます。CPANにリリースする場合でも大丈夫です。
$ mojo generate plugin AlertAssets
$ mkdir Mojolicious-Plugin-AlertAssets/lib/Mojolicious/Plugin/AlertAssets
$ cd Mojolicious-Plugin-AlertAssets/lib/Mojolicious/Plugin/AlertAssets
$ mkdir public
$ echo 'alert("Hello World!");' > public/alertassets.js
$ mkdir templates
$ echo '%= javascript "/alertassets.js"' > templates/alertassets.html.ep
プラグインの名前に基づいた 合理的なユニークな名前を付けましょう。そして、registerが呼ばれるとき、対応するディレクトリを検索パスの一覧に追加します。
package Mojolicious::Plugin::AlertAssets;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::File 'path';
sub register {
my ($self, $app) = @_;
# "templates"と"public"ディレクトリを追加する
my $base = path(__FILE__)->sibling('AlertAssets');
push @{$app->renderer->paths}, $base->child('templates')->to_string;
push @{$app->static->paths}, $base->child('public')->to_string;
}
1;
プラグインをいったんインストールし、読み込めば、両方とも通常のtemplatesとpublicディレクトリのように機能します。優先順位は少し低くなります。
use Mojolicious::Lite;
plugin 'AlertAssets';
get '/alert_me';
app->start;
__DATA__;
@@ alert_me.html.ep
<!DOCTYPE html>
<html>
<head>
<title>Alert me!</title>
%= include 'alertassets'
</head>
<body>You've been alerted.</body>
</html>
すると、バンドルしたプラグインのDATAセクションにあるアセットと同じように使用できます。
package Mojolicious::Plugin::AlertAssets;
use Mojo::Base 'Mojolicious::Plugin';
sub register {
my ($self, $app) = @_;
# クラスを追加する
push @{$app->renderer->classes}, __PACKAGE__;
push @{$app->static->classes}, __PACKAGE__;
}
1;
__DATA__;
@@ alertassets.js
alert("Hello World!");
@@ alertassets.html.ep
%= javascript "/alertassets.js"
動的コンテンツの後処理
一般的にMojoliciousのafter_dispatchフックによる後処理はとても簡単ですが、レンダラによって生成されたコンテンツのためには、Mojoliciousのafter_renderを使うのがより効率的です。
use Mojolicious::Lite;
use IO::Compress::Gzip 'gzip';
hook after_render => sub {
my ($c, $output, $format) = @_;
# "gzip => 1" がスタッシュにセットされているかを確認する
return unless $c->stash->{gzip};
# ユーザーエージェントがgzip圧縮を許可するかどうかを確認する
return unless ($c->req->headers->accept_encoding // '') =~ /gzip/i;
$c->res->headers->append(Vary => 'Accept-Encoding');
# gzipでコンテンツを圧縮する
$c->res->headers->content_encoding('gzip');
gzip $output, \my $compressed;
$$output = $compressed;
};
get '/' => {template => 'hello', title => 'Hello', gzip => 1};
app->start;
__DATA__;
@@ hello.html.ep
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>Compressed content.</body>
</html>
動的に生成されたコンテンツのすべてを圧縮したい場合は、Mojolicious::Rendererのcompressを有効にすることもできます。
ストリーミング
すべてのコンテンツを一度に描画する必要はありません。Mojolicious::Controllerのwriteを使って小さなチャンクを連続で流すこともできます。
use Mojolicious::Lite;
get '/' => sub {
my $c = shift;
# ボディを準備する
my $body = 'Hello World!';
$c->res->headers->content_length(length $body);
# 排出コールバックに直接書き込みを開始する
my $drain;
$drain = sub {
my $c = shift;
my $chunk = substr $body, 0, 1, '';
$drain = undef unless length $body;
$c->write($chunk, $drain);
};
$c->$drain;
};
app->start;
前のデータチャンク全部が実際に書き込まれるたびに、排出コールバックが実行されます。
HTTP/1.1 200 OK Date: Sat, 13 Sep 2014 16:48:29 GMT Content-Length: 12 Server: Mojolicious (Perl) Hello World!
Content-Lengthヘッダーを提供する代わりに、Mojoliciousのfinishを使用して、完了したときに接続を手動で閉じることもできます。
use Mojolicious::Lite;
get '/' => sub {
my $c = shift;
# ボディを準備する
my $body = 'Hello World!';
# 排出コールバックに直接書き込みを開始する
my $drain;
$drain = sub {
my $c = shift;
my $chunk = substr $body, 0, 1, '';
length $chunk ?$c->write($chunk, $drain) : $c->finish;
};
$c->$drain;
};
app->start;
Keep-aliveを妨げるため、この方法はかなり非効率的ですが、EventSourceおよび同様のアプリケーションのために必要な場合があります。
HTTP/1.1 200 OK Date: Sat, 13 Sep 2014 16:48:29 GMT Connection: close Server: Mojolicious (Perl) Hello World!
チャンク化されたトランスファーエンコーディング
コンテンツがとても動的な場合は、レスポンスコンテンツのContent-Lengthが前もってわからないかもしれません。そのような場合は、チャンク化されたトランスファーエンコーディングや Mojolicious::Controllerのwrite_chunkが便利です。一般的な用途として、HTMLドキュメントのheadセクションを事前にブラウザに送信し、参照する画像やスタイルシートの事前ロードを高速化できます。
use Mojolicious::Lite;
get '/' => sub {
my $c = shift;
$c->write_chunk('<html><head><title>Example</title></head>' => sub {
my $c = shift;
$c->finish('<body>Example</body></html>');
});
};
app->start;
オプションの排出コールバックは、処理を継続する前に、すべての以前のチャンクが書き込まれることを保障します。ストリームを終了するためには、Mojolicious::Controllerのfinish を呼び出すか、空データのチャンクを書き込みます。
HTTP/1.1 200 OK Date: Sat, 13 Sep 2014 16:48:29 GMT Transfer-Encoding: chunked Server: Mojolicious (Perl) 29 <html><head><title>Example</title></head> 1b <body>Example</body></html> 0
とくに、長時間応答がないときのタイムアウトと組み合わせるとき、Comet(ロングポーリング)をするのに便利でしょう。Webサーバーによっては制限のために、完璧には動作しないデプロイ環境があるかもしれません。
エンコーディング
ファイルに保存されたテンプレートは、デフォルトではUTF-8であると期待されますが、 Mojolicious::Rendererのencodingを使って簡単に変更できます。
$app->renderer->encoding('koi8-r');
DATAセクションからのすべてのテンプレートは、必ずPerlスクリプトのエンコーディングになります。
use Mojolicious::Lite; get '/heart'; app->start; __DATA__; @@ heart.html.ep I ♥ Mojolicious!
Base64エンコードDATAファイル
画像などのBase64でエンコードされた静的ファイルは、テンプレートと同じように、簡単にアプリケーションのDATAセクションに保存できます。
use Mojolicious::Lite;
get '/' => {text => 'I ♥ Mojolicious!'};
app->start;
__DATA__;
@@ favicon.ico (base64)
...base64 encoded image...
DATA テンプレートのインフレ―ト
ファイルとして保存されているテンプレートは、DATAセクションのファイルよりも優先されます。ファイルテンプレートはアプリケーションのデフォルトセットとして含めることができ、ユーザーは後にこれをカスタマイズできます。Mojolicious::Command::Author::inflateコマンドでDATAセクションにあるすべてのテンプレートと静的ファイルを、実際のファイルとしてtemplatesおよびpublic ディレクトリに書き込みます。
$ ./myapp.pl inflate...
テンプレート文法のカスタマイズ
EPRendererプラグインをカスタム設定と一緒にロードすることによって、簡単にテンプレートの文法全体を変更できます。
use Mojolicious::Lite;
plugin EPRenderer => {
name => 'mustache',
template => {
tag_start => '{{',
tag_end => '}}'
}
};
get '/' => 'index';
app->start;
__DATA__;
@@ index.html.mustache
Hello {{= $name }}.
Mojo::Templateは利用できるすべてのオプションを含んでいます。
お気に入りテンプレートシステムの追加
Mojolicious::Plugin::EPRendererが提供するepではないテンプレートシステムがお好みのこともあるでしょう。また、CPANに登録されているプラグインのなかにお気に入りが見つからないかもしれません。そんなときは、新しいhandler を registerが呼び出されるとき、Mojolicious::Rendererのadd_handlerで追加すれば大丈夫です。
package Mojolicious::Plugin::MyRenderer;
use Mojo::Base 'Mojolicious::Plugin';
sub register {
my ($self, $app) = @_;
# "mine"ハンドラーの追加
$app->renderer->add_handler(mine => sub {
my ($renderer, $c, $output, $options) = @_;
# ワンタイム使用インラインテンプレートのチェック
my $inline_template = $options->{inline};
# "templates"ディレクトリに適切なテンプレートがあるかをチェック
my $template_path = $renderer->template_path($options);
# DATAセクションに適切なテンプレートがあるかをチェック
my $data_template = $renderer->get_data_template($options);
# この部分はあなたのテンプレートシステムに応じて変わります :)
...
# 描画された結果をレンダラに渡す
$$output = 'Hello World!';
# またはエラーが起きたら終了する
die 'Something went wrong with the template';
});
}
1;
inlineテンプレートは、ユーザーによって提供されると、オプションとともに渡されます。アプリケーションのtemplatesディレクトリを検索するにはMojolicious::Rendererのtemplate_pathが、DATAセクションを検索するにはMojolicious::Rendererのget_data_templateが使えます。
use Mojolicious::Lite;
plugin 'MyRenderer';
# インラインテンプレートの描画
get '/inline' => {inline => '...', handler => 'mine'};
# DATAセクションのテンプレートを描画
get '/data' => {template => 'test'};
app->start;
__DATA__;
@@ test.html.mine
...
バイナリデータを生成するためのハンドラを追加する
デフォルトでは、レンダラはすべてのhandlerが自動的にエンコードされる必要がある文字を生成しますが、代わりにバイトを生成することで簡単に無効にできます。
use Mojolicious::Lite;
use Storable 'nfreeze';
# "storable"ハンドラを追加
app->renderer->add_handler(storable => sub {
my ($renderer, $c, $output, $options) = @_;
# 自動的なエンコーディングを削除
delete $options->{encoding};
# スタッシュのデータをエンコード
$$output = nfreeze delete $c->stash->{storable};
});
# "storable" 値がすでにセットされている場合に"ハンドラ" 値を自動的にセットする
app->hook(before_render => sub {
my ($c, $args) = @_;
$args->{handler} = 'storable'
if exists $args->{storable} || exists $c->stash->{storable};
});
get '/' => {storable => {i => '♥ mojolicious'}};
app->start;
Mojoliciousのbefore_renderフックは、storableのようなスタッシュ値のためにhandler値を明示的にセットしなくて済むようにするなど、個別に対応するために使用できます。
# 明示的な"handler" 値
$c->render(storable => {i => '♥ mojolicious'}, handler => 'storable');
# 暗黙的な "handler" 値("before_render" フックを使用)
$c->render(storable => {i => '♥ mojolicious'});
もっと学ぶには
さあ、Mojolicious::Guides を続けるか、Mojolicious wikiを見てみましょう。多くの著者がドキュメントやサンプルをたくさん書いています。
サポート
このドキュメントでわからない部分があれば、 mailing list かirc.freenode.net (chat now!)の公式IRCチャンネル #mojo まで気軽に質問してください。 (2019/08/16 Mojolicious 8.12を反映)
Mojoliciousドキュメント日本語訳