Skip to content

Rails Plugin::ResourceController

Posted on:2009年5月7日 at 01:38

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_urlcollection_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 にあります。

参照サイト