#395 Action Controller Walkthrough pro
- Download:
- source codeProject Files in Zip (28.3 KB)
- mp4Full Size H.264 Video (33.8 MB)
- m4vSmaller H.264 Video (18.8 MB)
- webmFull Size VP8 Video (18.8 MB)
- ogvFull Size Theora Video (50.1 MB)
Whenever we use a Ruby gem in one of our applications it’s a good idea to take a look at its source code so that we can get an understanding of how it works. Doing this can really help when we’re debugging problems in our applications. Reading other people’s source code is also a good way of improving your own Ruby code and the chances are that you’ll pick up some interesting tricks along the way. In this episode we’ll be diving into the Rails source code to get a better understanding of how controllers work and how they handle requests. We’ll be using a blogging application to help us with this and focussing on its ArticlesController
which is a fairly standard RESTful controller with the standard seven actions.
Looking at ActionController
To start we’ll find the find the relevant source code. As with most controllers ours inherits from ApplicationController
which in turn inherits from ActionController::Base
and this base class is defined in the Rails source code. To get this we can clone its Git repository by running this command:
$ git clone https://github.com/rails/rails.git
Once this command has finished we can move into the rails
directory. This will be the master
branch which is currently the upcoming Rails 4 release. We want to browse the version of Rails that we’re using in our application so we’ll use git checkout
to get the right branch. We can then open the actionpack/lib
directory which is where the controller-related code is.
$ cd rails $ git checkout v3.2.9 $ cd actionpack/lib/
In the action_controller/base.rb
file we’ll find the source code for the class that our app’s ApplicationController
inherits from. This class inherits from another class called Metal
, defined in a metal.rb
file, and Metal
inherits from AbstractController::Base
. It’s these three classes that we’ll be focussing on in this episode.
These three files don’t contain all the methods that we have available to us in the ArticlesController
. If we call ancestors
on it we’ll see that there are a large number of modules that are included in to our controller and these have many methods defined in them. If we subtract the included modules we’ll see the inheritance chain.
>> puts ArticlesController.ancestors - ArticlesController.included_modules ArticlesController ApplicationController ActionController::Base ActionController::Metal AbstractController::Base Object BasicObject => nil
How ActionController Handles Requests
Now that we’ve located the relevant source code we can start to walk through what happens when our Rails application receives a request. Before it reaches a controller the request goes through a number of pieces of Rack Middleware. (We walked through how a request is handled by Middleware in episode 319.) After this it’s handed off to our application’s routes and this was covered in more detail in episodes 231 and 232. Rails’ routing behaviour is handled by ActionDispatch
. If we look at the RouteSet
class we’ll see its dispatch
method and it’s here that the request is handed off to the appropriate controller.
def dispatch(controller, action, env) controller.action(action).call(env) end
This is done by calling the controller’s action
method, passing in the action’s name and calling call
on that. Back in our example application’s console we can demonstrate this. If we call action
on our ArticlesController
and pass in the name of an action we’ll get a proc
object returned. This object acts as a Rack application and we can process the request through it.
>> ArticlesController.action(:index) => #<Proc:0x007fbf63bd5400@/Users/eifion/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/actionpack-3.2.9/lib/action_controller/metal.rb:245>
We can find our where this method is defined by running this command:
>> ArticlesController.method(:action).source_location => ["/Users/eifion/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/actionpack-3.2.9/lib/action_controller/metal.rb", 244]
The action method looks like this:
def self.action(name, klass = ActionDispatch::Request) middleware_stack.build(name.to_s) do |env| new.dispatch(name, klass.new(env)) end end
This method takes a second argument so that we can customize the class that’s used to represent the request. The first thing it does is call middleware_stack.build
, passing in the action’s name and a block. The build
method loops through all the middleware defined on the controller, checks if they apply to the action being requested then builds our Rack app through it.
def build(action, app=nil, &block) app ||= block action = action.to_s raise "MiddlewareStack#build requires an app" unless app middlewares.reverse.inject(app) do |a, middleware| middleware.valid?(action) ? middleware.build(a) : a end end
This means that each controller has its own mini middleware stack that we can apply to a given action and which is completely separate from the middleware stack that applies to our entire application. This stack is empty by default but we can easily add to it by calling use in a controller and passing in a middleware class. We can also restrict the actions that this is applied to.
class ArticlesController < ApplicationController use Rack::ShowExceptions, only: :show # actions omitted end
If we try to visit a record that doesn’t exist now the error that’s raised will be handled by Rack::ShowExceptions
middleware instead of the Rails one.
If we visit the edit
action for the same non-existent article we’ll see the standard Rails error page as the middleware only applies to show. This is really useful: we could use it with Rack::SSL
or some authentication or authorization middleware when we need to add that middleware to a specific set of actions.
After our request has passed through this middleware it makes a new instance of our controller and calls dispatch
on it, passing in the action’s name
and an instance of the request which is generated by passing in the Rack environment. The dispatch
method is defined in the same file and it stores some details in instance variables, processes the action then calls to_a
on it which returns the response.
def dispatch(name, request) #:nodoc: @_request = request @_env = request.env @_env['action_controller.instance'] = self process(name) to_a end def to_a #:nodoc: response ? response.to_a : [status, headers, response_body] end
If there’s a response
object this is turned into an array and returned; if not then a response is build up from the status
, headers
and response_body
variables.
The process method is defined in AbstractController::Base
. This remembers the name in an instance variable then checks to see if the action exists. If it doesn’t it raises an ActionNotFound
exception.
def process(action, *args) @_action_name = action_name = action.to_s unless action_name = method_for_action(action_name) raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" end @_response_body = nil process_action(action_name, *args) end
The method_for_action
method checks to see if an action exists or if a method called action_missing
exists. For the latter case a _handle_action_missing
method is called which calls action_missing
passing in the action name. This means that if we define action_missing
in our controller we can use it like method_missing
so that a controller can handle any action.
Back in the process
method if we have a valid action process_action
will be called. This in turn calls send_action
which is an alias to the send
method. This means that the action will be called on the controller.
The Included Modules
We now know how an action in our controller is called but this isn’t the full story. There’s a lot of additional behaviour that Rails is mixing in through modules. Let’s take another look at ActionController::Base
. This file includes a large number of modules which means that our app’s controllers will inherit all the modules’ behaviour. Some of these are in AbstractController
, but most of them are in action_controller/metal
. We’ll take a look at some of these modules, starting with RackDelegation
. This is a small module but it’s an important one. It overrides the dispatch method that we saw earlier in the ActionController::Metal
class and adds a third argument that accepts a response
object.
delegate :headers, :status=, :location=, :content_type=, :status, :location, :content_type, :to => "@_response" def dispatch(action, request, response = ActionDispatch::Response.new) @_response ||= response @_response.request ||= request super(action, request) end
The response is set to an instance variable so that it’s remembered and whenever we set the response body in our controller this response object is set. If we look back in the Metal
class at the to_a
method this checks for that response object. If it wasn’t for the RackDelegation
module this response would be nil
. RackDelegation
delegates several methods to @_response
. This ties in nicely to the Redirect
module which we’ll look at next.
The Redirect
module defines the redirect_to
method and when we call redirect_to
in a controller action this is the code that’s called.
def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(options) self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" end
This method sets the status
, location
and response_body
which will all be delegated to the response object thanks to RackDelegation
. One method that’s often used alongside redirect_to
in a controller action is render
. This is defined in the Rendering
module.
def render(*args) #:nodoc: raise ::AbstractController::DoubleRenderError if response_body super self.content_type ||= Mime[lookup_context.rendered_format].to_s response_body end
There isn’t much in this method. Most of the logic is done in the call to super
which calls the same method in the AbstractController
class. This has its own Rendering
module with a render method that look like this:
def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) end
This method sets the response_body
to the output of a render_to_body
method. In turn render_to_body
calls a _render_template
method and this delegates to a view renderer. This is defined in the same class an it instantiates a new instance of ActionView::Renderer
. This will delegate all the rendering behaviour to ActionView
which we won’t cover in this episode but which will be covered in a future one.
We now know that calling redirect_to
or render
simply sets some details on the response object that’s returned from a Rack application at the end of a controller action. What about actions that don’t call either of these methods, such as this new
action?
def new @article = Article.new end
Actions like these that don’t explicitly render a view are handled by the ImplicitRender
module. This overrides the send_action
method in the AbstractController::Base
class and this calls default_render
unless we already have a response_body
. This causes the view matching the action to be rendered. This module also handles the behaviour for rendering an action when no method is defined and only a template exists.
module ActionController module ImplicitRender def send_action(method, *args) ret = super default_render unless response_body ret end def default_render(*args) render(*args) end def method_for_action(action_name) super || if template_exists?(action_name.to_s, _prefixes) "default_render" end end end end
That’s it for the modules that we’ll cover in this episode but there are a lot more that you’re encouraged to explore on your own. Most of them are self-explanatory based on their names and they handle things such as cookies, flash messages, caching, HTTP authentication. There are other modules in the abstract_controller
directory. For example callbacks.rb
here is used to for before and after filters.