Scaffold処理は何をやっている?

 前回でbookmarkアプリができたわけだが、今はすべての動作が

scaffold :item

で動いているわけで、この中で何が起きているかはよくわからない。今までのRubyの生半可な知識から、「scaffold」はメソッドで、「:item」はシンボルということはわかっている。で、scaffoldはRubyのDSLっぽい機能(動的にクラス定義やメソッド定義を追加できる機能)を使い、リクエストを処理するメソッドを動的に生成しているはずだ。

scaffoldメソッドはどこにある?
 ItemControllerはApplicationControllerのサブクラスなので、scaffoldはApplicationControllerのメソッドなのだろう。…と思ったら、ApplicationControllerも

class ApplicationController < session_key =""> '_bookmark_session_id'
end

こんな感じなので、ActionController::Baseにあるのではないかと思われる。このクラス自体は空だが、アプリケーション全体のフィルタ的処理を書く場所として生成されているようだ。詳細は不明だが、セッションの設定もされている模様。で、ActionController::Baseはというと…

$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))

unless defined?(ActiveSupport)
begin
$:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
require 'active_support'
rescue LoadError
require 'rubygems'
gem 'activesupport'
end
end

require 'action_controller/base'
require 'action_controller/deprecated_redirects'
require 'action_controller/request'
require 'action_controller/deprecated_request_methods'
require 'action_controller/rescue'
require 'action_controller/benchmarking'
require 'action_controller/flash'
require 'action_controller/filters'
require 'action_controller/layout'
require 'action_controller/deprecated_dependencies'
require 'action_controller/mime_responds'
require 'action_controller/pagination'
require 'action_controller/scaffolding'
require 'action_controller/helpers'
require 'action_controller/cookies'
require 'action_controller/cgi_process'
require 'action_controller/caching'
require 'action_controller/verification'
require 'action_controller/streaming'
require 'action_controller/session_management'
require 'action_controller/components'
require 'action_controller/macros/auto_complete'
require 'action_controller/macros/in_place_editing'

require 'action_view'
ActionController::Base.template_class = ActionView::Base

ActionController::Base.class_eval do
include ActionController::Flash
include ActionController::Filters
include ActionController::Layout
include ActionController::Benchmarking
include ActionController::Rescue
include ActionController::Dependencies
include ActionController::MimeResponds
include ActionController::Pagination
include ActionController::Scaffolding
include ActionController::Helpers
include ActionController::Cookies
include ActionController::Caching
include ActionController::Verification
include ActionController::Streaming
include ActionController::SessionManagement
include ActionController::Components
include ActionController::Macros::AutoComplete
include ActionController::Macros::InPlaceEditing
end

こんな感じになっており、大量のModuleをincludeしているだけ。ということは、scaffoldメソッドはこれらモジュールのどれかで定義されているはず、だとすると…

require 'action_controller/scaffolding'
include ActionController::Scaffolding

ここにあるとしか考えられない。

scaffoldメソッドの処理内容
 「action_controller\scaffolding.rb」を見てみると、予想通りにscaffoldメソッドを発見。RDocを見てみると、こんな風にメソッドが追加されるらしい。この、Entryの部分がModelのクラスになるのだろう。

class WeblogController < method =""> :post, :only => [ :destroy, :create, :update ],
:redirect_to => { :action => :list }

def index
list
end

def list
@entries = Entry.find(:all)
render_scaffold "list"
end

def show
@entry = Entry.find(params[:id])
render_scaffold
end

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

def new
@entry = Entry.new
render_scaffold
end

def create
@entry = Entry.new(params[:entry])
if @entry.save
flash[:notice] = "Entry was successfully created"
redirect_to :action => "list"
else
render_scaffold('new')
end
end

def edit
@entry = Entry.find(params[:id])
render_scaffold
end

def update
@entry = Entry.find(params[:id])
@entry.attributes = params[:entry]

if @entry.save
flash[:notice] = "Entry was successfully updated"
redirect_to :action => "show", :id => @entry
else
render_scaffold('edit')
end
end
end

ソースを見てみると、最終的にActionController::BaseはClassMethodsモジュールをextendすることになっているようだ。

module ActionController
module Scaffolding
def self.included(base)
base.extend(ClassMethods)
end

module ClassMethods

def scaffold(model_id, options = {})

scaffoldメソッドの第一引数model_idは、modelの名前だ(bookmarkアプリの場合は、item)。で、こんな風に変数をいくつか初期化して…

singular_name = model_id.to_s
class_name = options[:class_name] || singular_name.camelize
plural_name = singular_name.pluralize
suffix = options[:suffix] ? "_#{singular_name}" : ""

その後、module_evalでメソッド定義を追加しまくっている。追加されているメソッドは、RDocに書かれているものと同じだ。メソッドを動的に変更するため、ところどころにさきほど初期化した変数が埋め込まれているのがわかる。

module_eval <<-"end_eval", __FILE__, __LINE__ verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
:redirect_to => { :action => :list#{suffix} }


def list#{suffix}
@#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10
render#{suffix}_scaffold "list#{suffix}"
end

def show#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end

def destroy#{suffix}
#{class_name}.find(params[:id]).destroy
redirect_to :action => "list#{suffix}"
end

def new#{suffix}
@#{singular_name} = #{class_name}.new
render#{suffix}_scaffold
end

def create#{suffix}
@#{singular_name} = #{class_name}.new(params[:#{singular_name}])
if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully created"
redirect_to :action => "list#{suffix}"
else
render#{suffix}_scaffold('new')
end
end

def edit#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end

def update#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
@#{singular_name}.attributes = params[:#{singular_name}]

if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully updated"
redirect_to :action => "show#{suffix}", :id => @#{singular_name}
else
render#{suffix}_scaffold('edit')
end
end

private
def render#{suffix}_scaffold(action=nil)
action ||= caller_method_name(caller)
# logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger

if template_exists?("\#{self.class.controller_path}/\#{action}")
render :action => action
else
@scaffold_class = #{class_name}
@scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
@scaffold_suffix = "#{suffix}"
add_instance_variables_to_assigns

@template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false))

if !active_layout.nil?
render :file => active_layout, :use_full_path => true
else
render :file => scaffold_path('layout')
end
end
end

def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end

def caller_method_name(caller)
caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
end
end_eval
end

というわけで、RDocの完成版ソースと比べてみてみれば、scaffoldメソッドがやっていることは一目瞭然。少し複雑な部分があるとすれば、ビュー部分だろう。この部分はさすがにActionControllerの中では完結しないため、(自前で作成したrhtmlがない場合には、)事前に用意されたテンプレートファイルが使われるようになっている。細かい点でわからないところはあるが、生成されるメソッドの内容自体については、実際にScaffold処理を静的に生成してから見ていくことにする。

クラス名からのソースファイルの探し方
 Javaの場合は、基本的に(内部クラスやpublicでないクラスを除いて)1クラス->1ソースファイルになるため、ソースファイルを探すのは容易だ。
Rubyの場合は、1つのクラスが複数のモジュールから成っていることが多く、結果として1クラスの定義が複数のソースファイルにまたがっている場合が多くなり、ソースを追うのが多少面倒になってくる。
RDocをインストールしているのであれば、RDocのクラスページに

Class ActionController::Base
In: lib/action_controller/base.rb
lib/action_controller/cgi_process.rb
lib/action_controller/deprecated_redirects.rb
lib/action_controller/test_process.rb
Parent: Object

このような形で表示されているため、参考にしていきたいところ。Web上にもRDocは存在するが、ファイルの種類は参考になるが、ファイルの位置はローカル環境とは異なっているため、参考にならない。
Gemでインストールした場合、RDocのファイル表示のトップディレクトリは「%RUBY_HOME%\lib\ruby\gems\1.8\gems\ライブラリ名(actionpack-1.13.5など)\」になる。

次のステップ
 次は、scaffoldメソッドで動的に生成されていた各種の処理を、script\generateを使って実際のRubyスクリプトとして生成し、その中を見ていく。

コメント