On Jan 5, 2008 8:21 AM, George B. [email protected] wrote:
Clearly this is a book asking to be written. That it hasn’t yet
appeared tells me that not many people (or at least not the right
ones) have a strong enough and solid grasp of it to write that book,
even if they personally know that it’s a useful idea.
Surely such a thing would be a tale of epic proportions, equivalent to
the
Lord of the Rings trilogy and the Chronicles of Narnia combined. Well,
that’s how complicated some people think it is.
In truth, restful routing is plain and simple. It’s like those books you
wrote when you were a kid in kindergarten that if a book critic were to
read
them he would jab his eyes out with a pen.
For this example I’m going to use a personal favourite: a forum system.
It’s
small enough to not be overwhelming, yet large enough to explain how
restful
routing should work (and generally why you should use it).
It all starts with the good 'ol config/routes.rb file. In here is where
all
the nice little routes live, from map.root all the way down to the
map.connect ‘:controller/service.wsdl’, :action => ‘wsdl’ that many
people
still leave in their routes file, thinking that if they remove it the
entire
world would collapse upon itself into one small quantum singularity. In
here
you’d place something similar to:
map.resources :forums, :has_many => :topics
map.resources :topics, :has_many => :posts
map.resources :posts
Not running on Rails 2.0? Then this is the code you want:
map.resources :forums do |forum|
forum.resources :topics, :name_prefix => “forum_”
end
map.resources :topics do |topic|
topic.resources :posts, :name_prefix => “topic_”
end
map.resources :posts
This defines routes like:
/forums/1 ← Show a forum
/forums/1/topics ← Index action for a single forum
/forums/1/topics/1 ← showing a single topic
/topics/1 ← Same thing
/topics/1/posts/ ← I would imagine this would do a similar thing to
/topics/1
/topics/1/posts/1/edit ← Allows you to edit a single post
/posts/1/edit ← Same thing
Now to define something like this without the magic of restful routing,
one
would have to be clinically insane:
map.connect “/forums/:id”, :controller => “forums”, :action => “show”
map.connect “/forums/:forum_id/topics/”, :controller => “topics”,
:action =>
“index”
map.connect “/forums/:forum_id/topics/:id”, :controller => “topics”,
:action
=> “show”
map.connect “/topics/:id”, :controller => “topics”, :action => “show” ←
Does this look familar to /forums/:id?
map.connect “/topics/:topic_id/posts”, :controller => “posts”, :action
=>
“index”
map.connect “/topics/:topic_id/posts/:id/edit”, :controller => “posts”,
:action => “edit”
map.connect “/posts/:id/edit”, :controller => “posts”, :action => “edit”
And this is only the tip of the iceberg!
Seeing a pattern here? Restful routing gives you a whole heap of cool
stuff,
namely the 7 core methods that I’ll cover right after the models.
A forum system has the following tables: forums, topics, posts and
users,
and the models would look something like the following:
class Forum < ActiveRecord::Base
has_many :topics
has_many :posts, :through => :topics
end
class Topic < ActiveRecord::Base
has_many :posts
belongs_to :forum
belongs_to :user
end
class Post < ActiveRecord::Base
belongs_to :topic
belongs_to :user
end
class User < ActiveRecord::Base
has_many :topics
has_many :posts
def to_s
login
end
end
The fields don’t matter, but throughout the tutorial I make reference to
@
forum.name or something similar, so we’ll assume forums has at least a
name
field. We’ll assume post has a text field and users has a login field.
That’ll give you some idea of how the system works: Forum → Topics →
Posts.
In restful routing there are seven “core” methods (actions) that you’re
given for the controllers: index, show, new, create, edit, update,
destroy.
Each of these have a set request method on them, for example you can’t
GET
to the create, update and destroy actions and you can’t post to the
index,
new or edit actions. These actions work with these request methods:
GET: index, show, new, edit
POST: create
PUT: update
DELETE: destroy
“What the?! PUT & DELETE, where did they come from?”, I hear you cry!
These
are hacked into the calls for the appropriate action using javascript,
it
passes in one more parameter (_method) which is then handled by the
rails
code and depending on what method you called you will get the page you
were
looking for, or a routing error.
The forums controller could look like this:
class ForumsController < ApplicationController
def index
@forums = Forum.find(:all)
end
def show
@forum = Forum.find(params[:id])
end
def new
@forum = Forum.new
end
def create
@forum = Forum.create(params[:forum])
flash[:notice] = “You have created a forum!”
redirect_to forums_path
end
def edit
@forum = Forum.find (params[:id])
end
def update
@forum = Forum.find(params[:id])
@forum.update_attributes(params[:forum])
flash[:notice] = “You have updated #{@forum.name }”
redirect_to forum_path
end
def destroy
@forum = Forum.find(params[:id])
@forum.destroy
flash[:notice] = “You have deleted #{@forum.id}”
redirect_to forums_path
end
You’ll see here that I’ve twice made a call to redirect_to using the
argument of forums_path. Because we’ve defined map.resources :forums in
our
config/routes.rb file, it knows that we want to go to { :controller =>
“forums”, :action => “index” } and the best part is that we don’t have
to
keep trying { :controller => “forums”, :action => “index” } every time
we
want to go to that specific action, but instead we type forums_path.
I’ve also made a single call to forum_path, and I haven’t specified an
argument for it, so how does Rails know that I want to go to the forum
that
I just updated?
Rails will see that there’s an argument mission from the forum_path and
will
go looking for the @forum instance variable you’ve defined in your
controller. If you never defined one or defined it as something other
than
@forum, it will mention something about ambiguous routes and you’ll have
to
specify the variable.
Now what if you wanted to go to the new or edit action? Simple:
new_forum_path and edit_forum_path(@forum) will take you to the
corresponding actions. Remember that you don’t need to specify an
argument
for the edit_forum_path if @forum is defined. Inside these actions
you’ll
want to go further, you’ll want to create a new forum and update a
forum.
For the create action you could specify this for your form:
Rails 2.0:
form_for @forum do |f|
Rails 2.0 will see that @forum is a new record and link you the create
action.
Pre Rails 2.0:
form_for :forum, @forum, :url => forums_path do |f|
Prior to Rails 2.0 that checking wasn’t in, you’ll have to define your
own
link.
“B-b-b-ut”, you stammer, “you’ve linked to the forums index, right?
Isn’t
that what forums_path is?”
Well, yes and no. This has everything to do with the four request
methods
mentioned previously, because the form’s method attribute is “post”,
Rails
knows that if you’re posting to forums_path, you mean the create action.
And
now for the update action!
Here the form_for’s a little different (for Pre-Rails 2.0):
*Rails 2.0:
*form_for @forum do |f|
Again the same deal applies: Rails 2.0 knows that @forum is not a new
record, so it’ll link you to to the update action because it’s included
in a
form. This automatically specifies :html => { :method => :put } for
you.
Pre Rails 2.0
form_for :forum, @forum, :url => forum_path(@forum), :html => { :method
=>
“put” } do |f|
It knows to link you to the update action because of the method => “put”
we’ve specified.
Now lets escape from the confines of a single controller and bring the
topics controller into the mix. In the forum show action is where you
would
generally show all the topics for that forum, but for the purposes of
this
tutorial I will do it in the topics controller instead. This will have
something similar defined to the forums controller but personally I
would
define this for the controller:
class TopicsController < ApplicationController
before_filter :get_forum
def index
@topics = @forum.topics
end
#other actions
private
def get_forum
@forum = Forum.find(params[:forum_id]) if params[:forum_id]
end
The private call makes any method after it private, that means that if
you
were to try and access this method (without restful routing), it would
play
dumb. Personally I think something like this should be in-built to
Rails, if
you’re accessing a child object (topics) from a parent (forum) it should
automatically define @forum for you.
Because we’ve already defined the :has_many topics on map.resources
:forums,
the topic routes are already defined for us, so to view all the topics
for a
forum, before you would have to do define a route like this:
map.connect “/forums/:forum_id/topics/”, :controller => “topics”,
:action =>
“index”
and then calling it like this:
{ :controller => “topics”, :action => “index”, :forum_id => @ forum.id }
Instead you’ve already defined :has_many :topics, so instantly you’ll
gain
access to forum_topics_path. Again the wonderful Rails will realise that
you
want all topics for the @forum object and then direct you to
“/forums/1/topics/” through forum_topic_path. To edit a single topic,
you
could do edit_forum_topic_path as of Rails 2.0, or forum_edit_topic_path
prior to Rails 2.0. The first reads more like “edit this topic belonging
to
this forum” where the second reads like “in this forum, edit this
topic”.
Alternatively you could ditch the whole forum part out of the method
call
and just do edit_topic_path because we’ve defined map.resources :topics.
Throwing one more controller into the mix now, called posts_controller.
This
would be very similar to the topics controller but instead of get_forum
it
would have get_topic, modified correctly.
Now what if you wanted to add a custom action to posts_controller,
called
quote? This action would bring up a form with the post you were quoting
which would then send the information from the form into posts/new to
create
a new post:
In config/routes.rb:
map.resources :posts, :member => { :quote => :get }
The extra argument of :member indicates a hash of any further actions
and
their request methods you would like to be added onto singular posts.
You
can call these like all the other singular methods:
quote_post_path(@post),
for example. The request methods can be in string or symbol format, it
doesn’t matter.
def quote
@old_post = Post.find(params[:id])
@post = Post.new
@post.text = “[quote=‘#{@old_post.user}’]#{@old_post.text}[/quote]”
render :action => “new”
end
Here I’ve defined the old post only to get the text and the user’s name
from
it, and then we’re rendering the new view so we have the form.
Everything
from there on is taken care by Rails.
Now what if you want to define a new action to work with a group of
posts?
Well you define it like this:
routes.rb
map.resources :posts, :member => { :quote => :get }, :collection => {
:destroy_all => :delete }
Now if you wanted to destroy all posts for a topic, bar the first one,
with
an action like this (remembering @topic is defined in get_topic):
posts_controller.rb
def destroy_all
@posts = @topic.posts - @topic.posts.first
@posts.each { |post| post.destroy }
flash[:notice] = "All posts have been deleted.
end
You would link_to it like this:
link_to “Delete all posts”, destroy_all_topic_posts_path(@topic),
:method =>
“delete”, :confirm => “Are you sure you want to delete all posts from
this
topic?”
The :method => “delete” corresponds with the :delete_all => :delete we
specified in config/routes.rb.
That’s it for now. If it still isn’t clear how awesome restful routes
have,
then please state why, as I would love to know.
–
Ryan B.
Feel free to add me to MSN and/or GTalk as this email.