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を反映)