Our controllers are quite cleaned up now, but there's still a line that repeats in several actions:
@post = Post.find() ...
We can instead move this post lookup to filter method and call it before each action that needs it using before_action
:
### app/controllers/posts_controller.rb ###
class PostsController < ApplicationController
before_action :find_post, only: [:show, :edit, :update, :destroy]
# ...
def show
@comment = Comment.new
end
def edit
end
def update
if @post.set_attributes('title' => params['title'], 'author' => params['author'], 'body' => params['body'])
redirect_to posts_path
else
render 'edit'
end
end
def destroy
@post.destroy
redirect_to posts_path
end
private
def find_post
@post = Post.find(params['id'])
end
end
### app/controllers/comments_controller.rb ###
class CommentsController < ApplicationController
before_action :find_post, only: [:create, :destroy]
# ...
def create
@comment = @post.build_comment(
'body' => params['body'], 'author' => params['author']
)
if @comment.save
redirect_to post_path(@post.id)
else
render 'posts/show'
end
end
def destroy
@post.delete_comment(params['id'])
redirect_to post_path(@post.id)
end
private
def find_post
@post = Post.find(params['post_id'])
end
end
If we don't tell it otherwise, before_action
will call the specified method before every action. We don't want to do that, so we specify just the actions we want by passing in an extra argument which is a hash: {only: [:show, :edit, :update, :destroy]}
so it'll be applied only to those actions. If this filter only applies to one action then we can exclude the array and only pass in a single symbol (e.g. {only: :show}
). We can also pass in the hash with the except
key with an array of actions to exclude, and apply to all other actions.
You'll also notice that find_post
is private in both controllers. In Rails, a controller action that's a private method will never be routed to, which is exactly what we want.
Other than before_action
, Rails also allows you to define after_action
and around_action
. They are similar to before_action
filters. after_action
will execute code after the specified actions and around_action
will execute code before and after the specified actions -- a typical use case for it is logging, where you can write to the log when the code execution starts and when it finishes.
Notice, that as our app has progressed we have accessed values in our params
hash in two different ways: with symbols, and strings. Let's take a closer look at this rails feature:
# First we'll place a debugger in our index action in the comments controller
class CommentsController < ApplicationController
# ...
def index
binding.pry
@comments = Comment.all
end
# ...
end
Next, let's navigate to the comments page. From there we can look around using pry in the terminal.
4: def index
=> 5: binding.pry
6: @comments = Comment.all
7: end
[1] pry(#<CommentsController>)> params
=> {"controller"=>"comments", "action"=>"index"}
# Since we are accessing a collection, the only parameters by default are the controller name and action name.
# Let's try accessing the action name using our params hash.
[2] pry(#<CommentsController>)> params['action']
=> "index"
[3] pry(#<CommentsController>)> params[:action]
=> "index"
Rails gives us the option to access params using either a Symbol or a String. If we check the class of our hash, we'll see what type of data structure gives us this flexibility.
[4] pry(#<CommentsController>)> params.class
=> ActionController::Parameters
If we go a bit further and check the documentation, we'll see that this class inherits from ActiveSupport::HashWithIndifferentAccess
.
It's that parent class that allows us to access the values in our params hash with either a String
or a Symbol
.
Now would be a good time to explain why we've been slowly changing out code to use symbols. Symbols are unique identifiers, they also take up less memory, and are a bit easier to type.
Overall, using symbols to access our params is the preferred method. So, from now on that is what we'll be using. One last thing to do then, would be to go through our comments and posts controllers, and change the params
access from string to symbol.
### app/controllers/posts_controller.rb ###
# ...
private
def find_post
@post = Post.find(params[:id]) #used to be params['id']
end
### app/controllers/comments_controller.rb ###
def destroy
@post.delete_comment(params[:id])
redirect_to post_path(@post.id)
end
With that we're done investigating controller filters and parameters. In the next chapter, we'll cover more useful conventions that Rails provides for us. We'll also give a short review of what we've gone over thus far.