Thursday, 17 July 2008

The Controller Part 1 - Routing

Ruby on Rails uses the MVC architecture for web applications. This post is the first of three posts about the controller part, and is a quick look at routing.

The web log (blog) seems to be the archetypal Rails application, so in keeping with tradition, let us look at the controller for the posts model in a blog. This will be in a file called posts_controller, and a class called PostsController.

PostsController is a subclass of ApplicationController, a class generated in your application, in the file application.rb, by Rails when you created the project. This means you can make changes to ApplicationController and they will affect all the controllers in your application (and methods in application_helper.rb are available to all controllers, but not models).

ApplicationController is itself a subclass of the Base class in the ActionController module. You can find it in [ruby]\lib\ruby\gems\1.8\gems\actionpack-2.1.0\lib\action_controller\base.rb, however, this is not part of your application, and should never be modified. Base does a lot of the hard work so you do not have to.

Handling a request
Routing is configured through the routes.rb file. There is no class defined in the file (though in common with other classless Ruby files it automatically becomes part of some class to maintain the object-orientated paradigm), just Ruby code. The first line is:
ActionController::Routing::Routes.draw do map

What this does is invoke the draw method in the ActionController::Routing::Routes class, with each of the listed map entities. Each subsequent line in routes.rb (that has code on it) specifies a route to be mapped. When the web server receives a request, it checks the URL against the mapped routes, using the first one that matches, or returning a 404 "page not found" if none do (if public/index.html exists, this gets priority over any mapping for the root, usually http://localhost:3000/ during development).

The routes.rb file seems to be read for each request. Some changes made to the file will be acted on for the next request; however, some settings are not (eg map.resources), and require the web server to be restarted before they take effect.

The Rails generators handle default settings in routes.rb (you might want to set map.root though), and there are helpful comments in there too.

Rails will put in something like this (after a generate scaffold):
map.resouces :posts

This sets up some basic routing rules, allowing the usual RESTful operations (those created by scaffold, new, show, etc.).

Say your web server received a GET request like this:
http://localhost:3000/posts/show/5

The web server will take "posts" to indicate the controller. In this case it would look for the PostsController, and it will assume that that will be in the file posts_controller.rb. It will look through the methods in PostsController for a public method called either "show" or "5", and will invoke that method, using the other value as the value for the ID. Obviously it will find a show method, so it will take 5 as the ID.

In the absence of a method name in the URL, the web server will default to show, so this will do the same as the previous URL:
http://localhost:3000/posts/5

That presents a problem for pages that have no ID. For example, you might have a search page, and you would want to access it from this URL:
http://localhost:3000/posts/search

Unfortunately, the router will believe that it is the method missing, not the ID, and will complain that there is no post with £search" as the ID to show. We have to explicitly tell the router to expect pages that have no ID, which we can do like this:
map.resources :posts, :collection => {:search => :get, :options => :put}

In this example, two pages are added to the routes, search and options. The search page is set as an HTTP GET, and he options as an HTTP PUT.

Earlier versions of Rails (and of this page) have required that all pages outside the standard CRUD operations get declared in this way; pages that use an ID were assigned to :member, rather than :collection.

Data is sent from a web page form in the params hash, which includes the action, controller and id as above, as well as a hash of the data on your form, named as per your form (normally the name of the controller). In this example, a hash of the data from the form for a post would be accessed by params[:post]. To access a single value in the form hash, use something like params[:post][:title]

See also:
http://api.rubyonrails.org/classes/ActionController/Resources.html
http://api.rubyonrails.org/classes/ActionController/Routing.html

Struggling with Ruby: Contents Page

No comments: