例えばRESTfulなディスパッチテーブルとして、以下のようなものがあるとします。
- GET /avatar
- 全サーバーのプレイヤーキャラクターの一覧表示。avatarsの方がLISTっぽいですが。
- POST /avatar
- プレイヤーキャラクターの新規作成。
$c->req->paramsがなければ新規作成画面で、あれば処理終了後、/avatar/{world}/{forename}に転送。 - GET /avatar/{world}
- {world}サーバーのプレイヤーキャラクターの一覧表示。これもavatarsの方がLISTっぽいですが。
- GET /avatar/{world}/{forename}
- プレイヤーキャラクターの単独表示(兼、編集&新規作成完了画面)。
- PUT /avatar/{world}/{forename}
- プレイヤーキャラクターの編集。
$c->req->paramsがなければ編集画面で、あれば処理実行後、/avatar/{world}/{forename}に転送。 - DELETE /avatar/{world}/{forename}
- プレイヤーキャラクターの削除。
$c->req->paramsがなければ削除確認画面で、あれば処理実行後、/avatarに転送。
なるほど、綺麗ですね。ところが、肝心のPOST, PUT, DELETEメソッドを、どのように発行してもらいましょうか。
一つの考えはこうです(Data::Localizeによるローカライズ後の出力結果の例です)。
<ul>
<li><form method="put" action="http://localhost:4423/avatar/bahamut/foo/">
<p><input type="submit" value="編集" /></p>
</form></li>
<li><form method="delete" action="http://localhost:4423/avatar/bahamut/foo/">
<p><input type="submit" value="削除" /></p>
</form></li>
<li><form method="post" action="http://localhost:4423/avatar/">
<p><input type="submit" value="新規作成" /></p>
</form></li>
</ul>
こんなところでしょうか。しかし、HTML4やXHTML1ではform要素のmethod属性にはgetかpostしか使えないんですねえ。今更ながらですが。
また、formはまどろっこしいという悩みもあります。普通のa要素(アンカー。いわゆるリンク)はインライン要素ですが、formはインライン要素とはいえ子としてブロック要素しか持てないので、記述が増えてしまって面倒です。
例えば目下開発中のこのアプリケーションでは、言語を英語・日本語・独語・仏語・エスペラントから選べるようにしていますが、PUTで出した編集画面のままで言語を切り替える際にも、単純なa要素(アンカー)による切り替えが出来ないんですね。つまり、GETだと編集画面の言語を切り替えるのではなく、言語切り替え後には個別表示画面が出ているという案配です。_methodをfill-inするなど、formが増えれば増えるだけ面倒になります。
ではどうするか。やっぱりRailsのようにするしかないと思いました。
似非REST
Railsなどでは、methodは一律にpostとしつつ、hidden属性指定した_methodという名前のinput要素のvalue属性にputやdeleteを指定しています。
つまり、以下のように簡単にHTTPメソッドを見るのではなく......
sub fork_in_fetched_avatar
:Chained('fetch_avatar')
:PathPart('')
:Args(0)
{
my ($self, $c) = @_;
given ($c->req->method) {
when ('GET' ) { $c->detach('read_avatar') }
when ('PUT' ) { $c->detach('edit_avatar') }
when ('DELETE') { $c->detach('delete_avatar') }
default { $c->detach('invalid_method') }
}
}
HTTPメソッドを一旦stash辺りに保存しておく必要があるんですね。
# どこか、root辺りで仕込む
$c->stash->{method} = $c->req->param('_method');
$c->stash->{method} = $c->stash->{method} ? uc $c->stash->{method}
: $c->req->method;
# ちょっと乱暴(笑)
# $c->stash->{method} = uc( $c->req->param('_method') // $c->req->method );
# ...
sub fork_in_fetched_avatar
:Chained('fetch_avatar')
:PathPart('')
:Args(0)
{
my ($self, $c) = @_;
given ($c->stash->{method}) {
when ('GET' ) { $c->detach('read_avatar') }
when ('PUT' ) { $c->detach('edit_avatar') }
when ('DELETE') { $c->detach('delete_avatar') }
default { $c->detach('invalid_method') }
}
}
これを実現するためには、テンプレートは以下のようになります(Text::MicroTemplate::Extendedの場合で、$lは$stashにあるData::LocalizeオブジェクトのエイリアスとするようにMyApp::Web::Viewで定義しています)。
<ul>
<li><form method="post" action="[%= $s->{uri} %]">
<p>
<input type="submit" value="[%= $l->localize('edit') %]" />
<input type="hidden" name="_method" value="put" />
</p>
</form></li>
<li><form method="post" action="[%= $s->{uri} %]">
<p>
<input type="submit" value="[%= $l->localize('delete') %]" />
<input type="hidden" name="_method" value="delete" />
</p>
</form>
<li><form method="post" action="[%= $c->uri_for('/avatar/') %]">
<p>
<input type="submit" value="[%= $l->localize('create') %]" />
</p>
</form></li>
</ul>
編集画面を表示する(送信されたのではない、と理解する)ためにHTML::Shakan側で必要な手当は、前の記事の通りです。$c->req->paramsがなければ......という判断時に、_methodを無視するという手当てですね。
その他の代替策(JavaScriptやGET)
formを使いたくなければ、JavaScriptで強引に処理する手がありますが、JavaScriptがいつも有効とは限りませんし、この手のユーザー対象層を考えるとJavaScriptが使えないi-modeを無視する訳にもいきません。
GETメソッドである普通のアンカー(リンク)のURIのお尻に?_method=deleteなどと書くのは論外です。見苦しいURIは、単に美的感覚云々の問題以前に、自己説明的なURIであることを放棄しているからです。それなら/deleteなどのURIにしてアクションを作った方が遙かにましでしょう。
HTML5が出ても多分このまま似非REST
XHTML2はお蔵入りなので、HTML5に期待するとしても、2010年という先の話でもありますし、勧告と(それに先立つ)実装がいくらか出ても、まさか「HTML5対応版UAの未対応」(「IE9以降限定」など)とするわけにはいきません。PS3のウェブブラウザは多分対応してくれるでしょうが、なまじそのような更新を強制的に敷く仕組みがないPCの方が面倒ですし、バグ対応ではなく機能強化を目的とする大規模な更新が大変難しい携帯電話になるともはや絶望的です。
RESTのことをろくすっぽ理解していないので、間違いなく事実誤認などがあると思いますが......RESTは技術仕様や対応UAが揃わないと辛いような感触を覚えました。
ということで、当分はこの似非RESTのまま、数年間は冒険しない方が良さそうです。
コメントする