spree のソースを見ていて遅らばせながら初めて知った Rails の Plugin Resource Controller。
RESTful な設計を行うと、コントローラに書くことの大半は同じような内容となる。 アクションに index, show, new, edit, create, update, destroy を持ち、 モデルを使用して必要な情報を集め、テンプレートを使ってレンダリングする。
Rails のポリシーである DRY に従って、本来書きたいビジネスロジックだけに集中することを可能にしてくれるのが ResourceController だ。
以下は Rails v.2.3.2 の scaffold にて生成した PostsController
になる。
class PostsController < ApplicationController
# GET /posts
# GET /posts.xml
def index
@posts = Post.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @posts }
end
end
# GET /posts/1
# GET /posts/1.xml
def show
@post = Post.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @post }
end
end
# GET /posts/new
# GET /posts/new.xml
def new
@post = Post.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @post }
end
end
# GET /posts/1/edit
def edit
@post = Post.find(params[:id])
end
# POST /posts
# POST /posts.xml
def create
@post = Post.new(params[:post])
respond_to do |format|
if @post.save
flash[:notice] = 'Post was successfully created.'
format.html { redirect_to(@post) }
format.xml { render :xml => @post, :status => :created, :location => @post }
else
format.html { render :action => "new" }
format.xml { render :xml => @post.errors, :status => :unprocessable_entity }
end
end
end
# PUT /posts/1
# PUT /posts/1.xml
def update
@post = Post.find(params[:id])
respond_to do |format|
if @post.update_attributes(params[:post])
flash[:notice] = 'Post was successfully updated.'
format.html { redirect_to(@post) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @post.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /posts/1
# DELETE /posts/1.xml
def destroy
@post = Post.find(params[:id])
@post.destroy
respond_to do |format|
format.html { redirect_to(posts_url) }
format.xml { head :ok }
end
end
end
これが、ResourceController を使うと、
class PostsController < ApplicationController
resource_controller
end
だけで済む。 それだけではなく、用意されている扱い易い API、ヘルパーを使うことで、よくある要件に対する記述を非常に簡潔に書けるようになる。また、ネストされたリソースも扱い易くなる。
Table of Contents
Open Table of Contents
作者である James さんのサイトの超訳
で Resource Controller の作者である James さんによる ResourceController の紹介があるので、 以下に超訳したものを載せておく。誤訳が多分にあるはずなので、正確な内容は上記のページで確認して欲しい。
2007/10/19 の記事なので内容的に古くなっているが、ResourceController のコンセプトを理解するのに役立つと思う。 最新の内容は、
で確認するのがよいだろう。
resource_controller の紹介::コントローラで本来やりたいことにフォーカスする
あなたが Rais を使っていて、今コミュニティーやフレームワークでどういうことが起きているのか絶えず追っかけているのなら、 おそらく RESTful なアプリケーションを書いているでしょう。 そして、あなたは私同様、既に気づいているかもしれません。大半のコントローラが同じ基本パターンに従っているということに。 その一例として、scaffold_resource が生成するコントローラの雛形があります。
私はその基本パターンを隠して、ドメイン固有の API を使ったユニークなフィーチャーを書くグレートな方法を欲しい思っていました。 そしてまた、私はネストされたり、ポリモフィックなリソースをよく書く傾向にあったので、そのコードを全て DRY にし、 より直感的に url を生成できるようにしたいとも思っていました。 最終的には、そのコードのテストを充分にテストしたものにしたいと思いました。 その結果として、何も壊すことなしに、新しい機能が必要となった時にそれをうまくやり遂げることができるでしょうから。
そう、それを作成しました。これがそれです。
resource_controller
ResourceController::Base
を継承するだけでその機能を利用できます。
class PostsController < ResourceController::Base
end
[追記]
継承の他に、以下のようなデコレータメソッドによる追加も可能。
class PostsController < ApplicationController
resource_controller
end
API
Before/after コールバック、responses、そして flash メッセージは、とてもフレキシブルな API を通して管理されます。 使用する人たちの要求が簡潔でるときはシンタックスを簡潔に、 また API の呼び出しを直感的な方法で操作できるように可能な限りしたつもりです。 私が思い付いたのは、scoping の方式でした。そこでは、様々な API メソッドの呼び出しがチェーンされ、 ブロックを使用してスコープされています。
例えば、create アクションを例に挙げてみます。Post モデルの author 属性に current user を割り合てるには、before ブロックを追加してみてください。
class PostsController < ResourceController::Base
create.before do
@post.author << current_user
end
end
あなたのアプケーションが拡張していくのに合わせて、Ajax な機能を追加するために、 create メソッドに RJS レスポンスを追加してみてください。簡単です。
class PostsController < ResourceController::Base
create.before do
@post.author << current_user
response do |wants|
wants.html
wants.js
end
end
end
実際のところ、レスポンスの追加にはショートカットがあります。
既に resource_controller
で提供されているレスポンス(HTML)、もしくは、他で追加されているレスポンスに対して、
単に追加だけしたいのならば、response ブロックは省略できます。
class PostsController < ResourceController::Base
create do
before do
@post.author << current_user
end
wants.js
end
end
私は一般的に幾つかのアクションで同じ RJS テンプレートを使用するので、 このショートカットは本当に扱い易いと思いました。 なぜかと言うと、幾つかのアクションに同じレスポンスを追加するのにワンライナーで書くことができるからです。
class PostsController < ResourceController::Base
[create, update].each { |action| action.wants.js {render :template => 'post.rjs'} }
end
やったね。そして、flash にも同様に適用できます。
class PostsController < ResourceController::Base
create.flash "Wow! RESTful controllers are so easy with resource_controller!"
end
より詳細な API、サンプルについては、RDocs を見て下さい。
Helpers
Helpers はオジェクトの管理、url の生成、親のリソースの関連を管理を行うために内部的に利用されます。
member オブジェクトのような、あるコントローラの振る舞い、また、取得してきたコレクションをカスタマイズしたいときには、 ヘルパーメソッドをオーバーライドするだけですみます。
Note:
For certain resource_controller
functionality to work properly, user-defined helpers must make use of the other r_c
helpers. The following examples do not follow that convention for clarity purposes - see the docs for more details.
例えば、投稿記事(post)にパーマネントリンクを使いたかったとします。
その場合には、Post オブジェクトを取得する方法を変更する必要があるでしょう。
object
メソッドをオーバーライドするだけです。
class PostsController < ResourceController::Base
private
def object
@object ||= Post.find_by_permalink(param[:id])
end
end
また、おそらく index
メソッドにページネーション(pagination)を追加したいでしょう。
その時にも、member オブジェクトの取得を変更した時と同様に、
collection の取得の振る舞いを変更することができます。
class PostsController < ResourceController::Base
private
def collection
@collection ||= Post.find(:all, :page => {:size => 10, :current => params[:page]})
end
end
詳細、サンプルは、RDocs にあります。
[追記]
現在は、関連を考慮し、end_of_association_chain
ヘルパーメソッドを使用して取得するようだ。
class PostsController < ResourceController::Base
private
def collection
@collection ||= end_of_association_chain.find(:all, :page => {:size => 10, :current => params[:page]})
end
end
Namespaced Resources
[原文のまま]
…are handled automatically, and any namespaces are available, symbolized, in array form from the namespaces helper method.
ネストされたリソース
これまた自動的にハンドルされます。
これはかなり痛々しいものに違いありません。r_c (ResourceController) がそれを和らげてくれます。
belongs_to :model
のような ActiveRecored チックなシンタックスで、
あなたのために resource_controller
は関連を扱います。
class CommentsController < ResourceController::Base
belongs_to :post
end
ポリモフィックリソース
これは私自身のアプリケーションで多く見られる概念です。
resource_controller
ができる前までは、様々なところでこれは悩みの種でした。
私は、幾つかの問題を urligence を使って解決し、
resource_controller
を使って残りの部分を扱いました。
これはほとんど全てのことをしてくれます。
単に、コントローラの中で belongs_to
シンタックスを使用すれば、
r_c はアクションが呼び出される時にどの関連が存在するのかを推測します。
belongs_to
に渡される引数は、1 階層の親のみになります。
深くネストされたリソースはサポートされていません。
(ですが、私はもし要望があれば追加することに反対する訳ではありません。)
class CommentsController < ResourceController::Base
belongs_to :post, :product
end
上記のサンプルにおいて、コントローラは自動的に Post と Product 何れかの親が存在することを推測し、
全ての comments
をその親のスコープの中に入れます。
コントローラはまた、ルーティングに定義されていれば、親無しにレスポンスを返します。
urligence
のお陰で、ポリモフィックコントローラのビューの中での URL の生成は本当に容易です。
object_url
、collection_url
ヘルパーは自動的に親リソースのスコープを維持します。
# /posts/1/comments
object_url # => /posts/1/comments/#{@comment.to_param}
object_url(comment) # => /posts/1/comments/#{comment.to_param}
edit_object_url # => /posts/1/comments/#{@comment.to_param}/edit
collection_url # => /posts/1/comments
# /products/1/comments
object_url # => /products/1/comments/#{@comment.to_param}
object_url(comment) # => /products/1/comments/#{comment.to_param}
edit_object_url # => /products/1/comments/#{@comment.to_param}/edit
collection_url # => /products/1/comments
# /comments
object_url # => /comments/#{@comment.to_param}
object_url(comment) # => /comments/#{comment.to_param}
edit_object_url # => /comments/#{@comment.to_param}/edit
collection_url # => /comments
詳細は、RDocs にあります。