2007年11月18日日曜日

scaffoldが生成するコード

scaffoldでは、Controller、Helper、View(RHTML)が生成される。

Controller
itemモデルから生成した、ItemsController。ApplicationControllerを継承。Railsでは、URIに対応したメソッドが呼ばれる、ということは知っている。たとえば…
  • items/list -> ItemsControllerのlistメソッドが呼ばれる
  • hoge/fuga -> HogeControllerのfugaメソッドが呼ばれる
こんな感じ。で、scaffoldで生成されたメソッドは、以下の8つ。
  • index
  • list
  • show
  • new
  • create
  • edit
  • update
  • destroy
上から順に、一つひとつみていく。

indexメソッド

def index
list
render :action => 'list'
end

コントローラー名のみを指定された場合(items/)に呼び出される。一応、items/index でも呼び出された。
処理内容はというと、listメソッドとrenderメソッドを順番に呼んでいるだけ。listは後で片づけるとして、renderが何をやっているのかがよくわからないので調べてみる。メソッドが定義されているActionController::Baseのコメントを読んでみると、どうやら :action => 'xxx' で指定したactionに対応したテンプレートを使ってレスポンスをレンダリングするらしい。今回の例では、list.rhtmlを使ってレンダリングする。
listメソッドを呼んだら、勝手にlistのテンプレートを使ってレンダリングされるようになってるのかも、と漫然と考えていたが、どうもそうではないらしい。基本的には呼び出されたアクションに対応したテンプレートが使われ、を変更するためにはrenderメソッドを使った明示的な切り替えが必要、ということらしい。
indexメソッドでrenderメソッドを呼び出さなければ、index.rhtmlが使われる、というわけだ。generate scaffoldはindex.rhtmlを生成しないので、動かないが。

verifyメソッド呼び出し

# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :create, :update ],
:redirect_to => { :action => :list }

このverifyメソッド、ActionController::Verification::ClassMethodsにあって、リクエストヘッダレベルの検証をやっているらしい。この例では、destroy, create, updateメソッドはPOSTで呼び出されないといけないらしい。GETで呼び出された場合には、listアクションにリダイレクトされる。

やっていることはわかったが、なぜ呼び出せるのかがわからない。コメントには「すべてのControllerのクラスメソッドになる」と書いてあるので、どこかでevalしてるんだろうけど、いつ、どこでやってるのか現時点ではわからない。このあたりは、別途Rails起動時のシーケンスを追う中で明らかにしたい。

listメソッド

def list
@item_pages, @items = paginate :items, :per_page => 10
end

paginateメソッドを呼び出し、インスタンス変数itemsとitem_pagesをセットする。戻り値が配列になっているため、微妙にわかりづらい。paginateメソッドもverifyメソッドと同じく、すべてのControllerのクラスメソッドになっている。
paginateメソッドは、指定されたモデルの今回のページ分のコレクションとPaginatorオブジェクトを返している。Paginatorオブジェクトは、ページング用のプロパティを持っている。

showメソッド

def show
@item = Item.find(params[:id])
end

モデルのfindクラスメソッドにidを渡してモデルオブジェクトを一件取得し、インスタンス変数に保存するだけのシンプルな処理。「show/x」みたいな形で渡されたものがidとみなされるのは、特別扱いだろうか。このあたりも、正常形のシーケンスを追う中で確かめたい。

newメソッド

def new
@item = Item.new
end

モデルオブジェクトのインスタンスを新規作成し、インスタンス変数に保存して終了。

createメソッド

def create
@item = Item.new(params[:item])
if @item.save
flash[:notice] = 'Item was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end

渡されたパラメータをもとにItemオブジェクトを新規作成して、保存。保存に成功したらリダイレクトでlistアクションを呼び出す。失敗したら、newアクションのテンプレートを呼び出す。ここでちょっと驚いたのが、何もしていないのにリクエストパラメータが「params[:item]」という風にまとめられていること。どういうルールでまとまっているのかわからなかったので、createアクションを呼び出すHTMLをみてみた。

<p><label for="item_url">Url</label>

<input id="item_url" name="item[url]" size="30" value="" type="text"></p>

<p><label for="item_title">Title</label>

<input id="item_title" name="item[title]" size="30" value="" type="text"></p>

<p><label for="item_description">Description</label>

<textarea cols="40" id="item_description" name="item[description]" rows="20"></textarea></p>

name属性値がすべて、item[xxxxxx]という形になっている。railsはおそらく、こういう形でかかれたリクエストをモデルの属性として扱う仕様なのだろう。

editメソッド

def edit
@item = Item.find(params[:id])
end

IDをもとにモデルオブジェクトを一件検索し、インスタンス変数に保存して終了。showと同じだ。

updateメソッド

def update
@item = Item.find(params[:id])
if @item.update_attributes(params[:item])
flash[:notice] = 'Item was successfully updated.'
redirect_to :action => 'show', :id => @item
else
render :action => 'edit'
end
end

idをもとにモデルオブジェクトを取得し、リクエストパラメータで更新。更新したモデルオブジェクトの保存が成功すれば、リダイレクトでshowアクションを呼び出す。その際、idとしてモデルのインスタンス変数を渡している。保存が失敗した場合は、editアクションのテンプレートを呼び出す。
ここで目をひかれたのは、:idに@itemを入れてるところ。redirect_toは、文字通りリダイレクトするはずなので、次のアクションにオンメモリでItemオブジェクトを渡すことはできない。こうやって渡されたオブジェクトは、次のアクションにどういう形で引き渡されるんだろうか?そもそも、showで必要なのはItemオブジェクト全体じゃなく、idだけのはずだけど。

destroyメソッド

def destroy
Item.find(params[:id]).destroy
redirect_to :action => 'list'
end

idをもとにモデルオブジェクトを取得し、destroyで破棄する。その後、リダイレクトでlistアクションを呼び出す。

Helper

空。空です。まわりの説明をみていると、どうやらこのクラスはViewHelperの置き場所として用意されているようで、Viewから呼びたいメソッドを定義したくなったらここに書くらしい。

rhtml
rhtmlで生成されたものは、以下の6つ。
  • items.rhtml
  • list.rhtml
  • show.rhtml
  • new.rhtml
  • _form.rhtml
  • edit.rhtml
こちらも順にみていく。

items.rhtml
すべてのテンプレートを内部にレンダリングする、親テンプレート。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" lang="en" lang="en">
<head>
<meta equiv="content-type" content="text/html;charset=UTF-8">
<title>Items: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
</head>
<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield %>

</body>
</html>

中身以外の部分が全部書いてある。中身はyieldで差し込まれるようだ。あとは、Controllerで何回か出てきた「flash[:notice]」がある。Controllerでは、保存が成功した際にメッセージを格納していたが、ここに出力されるわけだ。

list.rhtml
listアクションで使われるテンプレート。

<h1>Listing items</h1>

<table>
<tbody><tr>
<% for column in Item.content_columns %>
<th><%= column.human_name %></th>
<% end %>
</tr>

<% for item in @items %>
<tr>
<% for column in Item.content_columns %>
<td><%=h item.send(column.name) %></td>
<% end %>
<td><%= link_to 'Show', :action => 'show', :id => item %></td>
<td><%= link_to 'Edit', :action => 'edit', :id => item %></td>
<td><%= link_to 'Destroy', { :action => 'destroy', :id => item }, :confirm => 'Are you sure?', :method => :post %></td>
</tr>
<% end %>
</tbody></table>

<%= link_to 'Previous page', { :page => @item_pages.current.previous } if @item_pages.current.previous %>
<%= link_to 'Next page', { :page => @item_pages.current.next } if @item_pages.current.next %>


<%= link_to 'New item', :action => 'new' %>


まず、この部分でヘッダを作っている。

<% for column in Item.content_columns %>
<%= column.human_name %>
<% end %>

モデルクラスには、content_columnsなんてものがあるらしい。そして、columnにはfuman_nameという、おそらくは表示用の名前が格納されているようだ。このあたりはActiveRecordをみていく中で確認したい。

次に、テーブル本体。

<% for item in @items %>

<% for column in Item.content_columns %>
<%=h item.send(column.name) %>
<% end %>
<%= link_to 'Show', :action => 'show', :id => item %>
<%= link_to 'Edit', :action => 'edit', :id => item %>
<%= link_to 'Destroy', { :action => 'destroy', :id => item }, :confirm => 'Are you sure?', :method => :post %>

<% end %>

@itemsを順番に取り出した後に、content_columnごとにitemオブジェクトの中身を表示している模様。ここらあたりも、sendメソッドの仕様を確認しないと細かいところはわからない。後で確認する。
最後には、link_toというメソッドで各actionを呼び出すリンクを作成している。

ラスト、ページング。

<%= link_to 'Previous page', { :page => @item_pages.current.previous } if @item_pages.current.previous %>
<%= link_to 'Next page', { :page => @item_pages.current.next } if @item_pages.current.next %>

前ページと次ページのリンクを作っているだけだが、pageパラメータとしてPaginatorオブジェクトのcurrent.previousやcurrent.nextを渡している。Paginate処理はControllerの開始時にフィルタとして処理されており、pageパラメータはPaginatorオブジェクトを復元する際に使われている。
ちなみに、previousやnextプロパティは、それぞれ前ページ・次ページが存在しない場合には存在しないため、リンクも表示されなくなる。
link_toにはactionもURLも書かれていないが、listアクションが呼び出される。action_view -> url_helper.rbのActionView::Helpers::UrlHelperのlink_toをみてみると、実際の変換はActionController::Baseのurl_forがやっているらしい。こちらのコメントによると、actionもurlも指定されていない場合は今と同じURLが使われるらしい。

show.rhtml

<% for column in Item.content_columns %>
<p>
<b><%= column.human_name %>:</b> <%=h @item.send(column.name) %>
</p>
<% end %>

<%= link_to 'Edit', :action => 'edit', :id => @item %> |
<%= link_to 'Back', :action => 'list' %>

モデルのカラムごとに中身を表示している。listとほぼ同じ。

new.rhtml

<h1>New item</h1>

<% form_tag :action => 'create' do %>
<%= render :partial => 'form' %>
<%= submit_tag "Create" %>
<% end %>

<%= link_to 'Back', :action => 'list' %>

form_tagメソッドは、ActionView::Helpers::FormTagHelperで定義されていて、その名のとおりformタグを定義するためのメソッド。
render :partialは、最終的にはActionView::Partials::render_partialが呼ばれており、このメソッドは :partialオプションの値の先頭に「_(アンダースコア)」をつけたテンプレート(rhtml)を使う仕様になっている。

_form.rhtml
new.rhtmlからrender :partial で呼び出されていたテンプレート。

<%= error_messages_for 'item' %>

<!--[form:item]-->
<p><label for="item_url">Url</label>

<%= text_field 'item', 'url' %></p>

<p><label for="item_title">Title</label>

<%= text_field 'item', 'title' %></p>

<p><label for="item_description">Description</label>

<%= text_area 'item', 'description' %></p>
<!--[eoform:item]-->

いくつか知らないメソッドがあるが、みてわかる通りな感じ。text_fieldはテキストフィールドを作成するのだろう。引数の書き方は見慣れない形だが、前から順番にプロパティがたどられるようだ。

edit.rhtml

<h1>Editing item</h1>

<% form_tag :action => 'update', :id => @item do %>
<%= render :partial => 'form' %>
<%= submit_tag 'Edit' %>
<% end %>

<%= link_to 'Show', :action => 'show', :id => @item %> |
<%= link_to 'Back', :action => 'list' %>

new.rhtmlとほとんど同じだ。違うのは、formタグのアクションが「update」んあっており、idとしてitemインスタンスが指定されている点。パラメータにモデルインスタンスを指定すると、HTML上ではいったいどうなるのだろう?実際に表示させてHTMLファイルをみてみると…

<form action="/items/update/1" method="post">

なるほど、という感じだ。リクエストURIとして出力されている。

というわけで、scaffoldが生成した処理はほぼ把握した。

次のステップ
次は、リクエスト発生からレスポンス作成までの流れを一通り追い、Railsの正常形の流れをつかみたい。

0 件のコメント: