Dispatching Requests to Controllers

At its most basic, a web application accepts an incoming request from a browser, processes it, and sends a response.

The first question that springs to mind is, how does the application know what to do with the incoming request? A shopping cart application will receive requests to display a catalog, add items to a cart, create an order, and so on. How does it route these requests to the appropriate code?

It turns out that Rails provides two ways to define how to route a request: a comprehensive way that you will use when you need to and a convenient way that you will generally use whenever you can.

The comprehensive way lets you define a direct mapping of URLs to actions based on pattern matching, requirements, and conditions. The convenient way lets you define routes based on resources, such as the models that you define. And because the convenient way is built on the comprehensive way, you can freely mix and match the two approaches.

In both cases, Rails encodes information in the request URL and uses a subsystem called Action Dispatch to determine what should be done with that request. The actual process is very flexible, but at the end of it Rails has determined the name of the controller that handles this particular request, along with a list of any other request parameters. In the process, either one of these additional parameters or the HTTP method itself is used to identify the action to be invoked in the target controller.

Rails routes support the mapping between URLs and actions based on the contents of the URL and on the HTTP method used to invoke the request. We’ve seen how to do this on a URL-by-URL basis using anonymous or named routes. Rails also supports a higher-level way of creating groups of related routes. To understand the motivation for this, we need to take a little diversion into the world of Representational State Transfer.

REST: Representational State Transfer

The ideas behind REST were formalized in Chapter 5 of Roy Fielding’s 2000 PhD dissertation.[98] In a REST approach, servers communicate with clients using stateless connections. All the information about the state of the interaction between the two is encoded into the requests and responses between them. Long-term state is kept on the server as a set of identifiable resources. Clients access these resources using a well-defined (and severely constrained) set of resource identifiers (URLs in our context). REST distinguishes the content of resources from the presentation of that content. REST is designed to support highly scalable computing while constraining application architectures to be decoupled by nature.

There’s a lot of abstract stuff in this description. What does REST mean in practice?

First, the formalities of a RESTful approach mean that network designers know when and where they can cache responses to requests. This enables load to be pushed out through the network, increasing performance and resilience while reducing latency.

Second, the constraints imposed by REST can lead to easier-to-write (and maintain) applications. RESTful applications don’t worry about implementing remotely accessible services. Instead, they provide a regular (and straightforward) interface to a set of resources. Your application implements a way of listing, creating, editing, and deleting each resource, and your clients do the rest.

Let’s make this more concrete. In REST, we use a basic set of verbs to operate on a rich set of nouns. If we’re using HTTP, the verbs correspond to HTTP methods (GET, PUT, PATCH, POST, and DELETE, typically). The nouns are the resources in our application. We name those resources using URLs.

The Depot application that we produced contained a set of products. There are implicitly two resources here. First, there are the individual products. Each constitutes a resource. There’s also a second resource: the collection of products.

To fetch a list of all the products, we could issue an HTTP GET request against this collection, say on the path /products. To fetch the contents of an individual resource, we have to identify it. The Rails way would be to give its primary key value (that is, its ID). Again we’d issue a GET request, this time against the URL /products/1.

To create a new product in our collection, we use an HTTP POST request directed at the /products path, with the post data containing the product to add. Yes, that’s the same path we used to get a list of products. If you issue a GET to it, it responds with a list, and if you do a POST to it, it adds a new product to the collection.

Take this a step further. We’ve already seen you can retrieve the content of a product—you just issue a GET request against the path /products/1. To update that product, you’d issue an HTTP PUT request against the same URL. And, to delete it, you could issue an HTTP DELETE request, using the same URL.

Take this further. Maybe our system also tracks users. Again, we have a set of resources to deal with. REST tells us to use the same set of verbs (GET, POST, PATCH, PUT, and DELETE) against a similar-looking set of URLs (/users, /users/1, and so on).

Now we see some of the power of the constraints imposed by REST. We’re already familiar with the way Rails constrains us to structure our applications a certain way. Now the REST philosophy tells us to structure the interface to our applications too. Suddenly our world gets a lot simpler.

Rails has direct support for this type of interface; it adds a kind of macro route facility, called resources. Let’s take a look at how the config/routes.rb file might have looked back in Creating a Rails Application:

 Depot::Application.​routes​.​draw​ ​do
» resources ​:products
 end

The resources line caused seven new routes to be added to our application. Along the way, it assumed that the application will have a controller named ProductsController, containing seven actions with given names.

You can take a look at the routes that were generated for us. We do this by making use of the handy rails routes command.

  Prefix Verb URI Pattern
  Controller#​​Action
  products GET /products(.:format)
  {:action=>"index", :controller=>"products"}
  POST /products(.:format)
  {:action=>"create", :controller=>"products"}
  new_product GET /products/new(.:format)
  {:action=>"new", :controller=>"products"}
 edit_product GET /products/:id/edit(.:format)
  {:action=>"edit", :controller=>"products"}
  product GET /products/:id(.:format)
  {:action=>"show", :controller=>"products"}
  PATCH /products/:id(.:format)
  {:action=>"update", :controller=>"products"}
  DELETE /products/:id(.:format)
  {:action=>"destroy", :controller=>"products"}

All the routes defined are spelled out in a columnar format. The lines will generally wrap on your screen; in fact, they had to be broken into two lines per route to fit on this page. The columns are (optional) route name, HTTP method, route path, and (on a separate line on this page) route requirements.

Fields in parentheses are optional parts of the path. Field names preceded by a colon are for variables into which the matching part of the path is placed for later processing by the controller.

Now let’s look at the seven controller actions that these routes reference. Although we created our routes to manage the products in our application, let’s broaden this to talk about resources—after all, the same seven methods will be required for all resource-based routes:

index

Returns a list of the resources.

create

Creates a new resource from the data in the POST request, adding it to the collection.

new

Constructs a new resource and passes it to the client. This resource will not have been saved on the server. You can think of the new action as creating an empty form for the client to fill in.

show

Returns the contents of the resource identified by params[:id].

update

Updates the contents of the resource identified by params[:id] with the data associated with the request.

edit

Returns the contents of the resource identified by params[:id] in a form suitable for editing.

destroy

Destroys the resource identified by params[:id].

You can see that these seven actions contain the four basic CRUD operations (create, read, update, and delete). They also contain an action to list resources and two auxiliary actions that return new and existing resources in a form suitable for editing on the client.

If for some reason you don’t need or want all seven actions, you can limit the actions produced using :only or :except options on your resources:

 resources ​:comments​, ​except: ​[​:update​, ​:destroy​]

Several of the routes are named routes enabling you to use helper functions such as products_url and edit_product_url(id:1).

Note that each route is defined with an optional format specifier. We will cover formats in more detail in Selecting a Data Representation.

Let’s take a look at the controller code:

 class​ ProductsController < ApplicationController
  before_action ​:set_product​, ​only: ​[​:show​, ​:edit​, ​:update​, ​:destroy​]
 
 # GET /products
 # GET /products.json
 def​ ​index
  @products = Product.​all
 end
 
 # GET /products/1
 # GET /products/1.json
 def​ ​show
 end
 
 # GET /products/new
 def​ ​new
  @product = Product.​new
 end
 
 # GET /products/1/edit
 def​ ​edit
 end
 
 # POST /products
 # POST /products.json
 def​ ​create
  @product = Product.​new​(product_params)
 
  respond_to ​do​ |format|
 if​ @product.​save
  format.​html​ { redirect_to @product,
 notice: ​​'Product was successfully created.'​ }
  format.​json​ { render ​:show​, ​status: :created​,
 location: ​@product }
 else
  format.​html​ { render ​:new​ }
  format.​json​ { render ​json: ​@product.​errors​,
 status: :unprocessable_entity​ }
 end
 end
 end
 
 # PATCH/PUT /products/1
 # PATCH/PUT /products/1.json
 def​ ​update
  respond_to ​do​ |format|
 if​ @product.​update​(product_params)
  format.​html​ { redirect_to @product,
 notice: ​​'Product was successfully updated.'​ }
  format.​json​ { render ​:show​, ​status: :ok​, ​location: ​@product }
 else
  format.​html​ { render ​:edit​ }
  format.​json​ { render ​json: ​@product.​errors​,
 status: :unprocessable_entity​ }
 end
 end
 end
 
 # DELETE /products/1
 # DELETE /products/1.json
 def​ ​destroy
  @product.​destroy
  respond_to ​do​ |format|
  format.​html​ { redirect_to products_url,
 notice: ​​'Product was successfully destroyed.'​ }
  format.​json​ { head ​:no_content​ }
 end
 end
 
 private
 # Use callbacks to share common setup or constraints between actions.
 def​ ​set_product
  @product = Product.​find​(params[​:id​])
 end
 
 # Only allow a list of trusted parameters through.
 def​ ​product_params
  params.​require​(​:product​).​permit​(​:title​, ​:description​, ​:image_url​, ​:price​)
 end
 end

Notice how we have one action for each of the RESTful actions. The comment before each shows the format of the URL that invokes it.

Notice also that many of the actions contain a respond_to block. As we saw in Chapter 11, Task F: Add a Dash of Ajax, Rails uses this to determine the type of content to send in a response. The scaffold generator automatically creates code that will respond appropriately to requests for HTML or JSON content. We’ll play with that in a little while.

The views created by the generator are fairly straightforward. The only tricky thing is the need to use the correct HTTP method to send requests to the server. For example, the view for the index action looks like this:

 <%​ ​if​ notice ​%>
  <aside id=​"notice"​>​<%=​ notice ​%>​</aside>
 <%​ ​end​ ​%>
 
 <h1>Products</h1>
 
 <table>
  <tfoot>
  <tr>
  <td colspan=​"3"​>
 <%=​ link_to ​'New Product'​, new_product_path ​%>
  </td>
  </tr>
  </tfoot>
  <tbody>
 <%​ @products.​each​ ​do​ |product| ​%>
  <tr class=​"​​<%=​ cycle(​'list_line_odd'​, ​'list_line_even'​) ​%>​​"​>
 
  <td class=​"image"​>
 <%=​ image_tag(product.​image_url​, ​class: ​​'list_image'​) ​%>
  </td>
 
  <td class=​"description"​>
  <h1>​<%=​ product.​title​ ​%>​</h1>
  <p>
 <%=​ truncate(strip_tags(product.​description​),
 length: ​80) ​%>
  </p>
  </td>
 
  <td class=​"actions"​>
  <ul>
  <li>​<%=​ link_to ​'Show'​, product ​%>​</li>
  <li>​<%=​ link_to ​'Edit'​, edit_product_path(product) ​%>​</li>
  <li>
 <%=​ link_to ​'Destroy'​,
  product,
 method: :delete​,
 data: ​{ ​confirm: ​​'Are you sure?'​ } ​%>
  </li>
  </ul>
  </td>
  </tr>
 <%​ ​end​ ​%>
  </tbody>
 </table>

The links to the actions that edit a product and add a new product should both use regular GET methods, so a standard link_to works fine. However, the request to destroy a product must issue an HTTP DELETE, so the call includes the method: :delete option to link_to.

Adding Additional Actions

Rails resources provide you with an initial set of actions, but you don’t need to stop there. In Iteration G2: Atom Feeds, we added an interface to allow people to fetch a list of people who bought any given product. To do that with Rails, we use an extension to the resources call:

 Depot::Application.​routes​.​draw​ ​do
  resources ​:products​ ​do
  get ​:who_bought​, ​on: :member
 end
 end

That syntax is straightforward. It says “We want to add a new action named who_bought, invoked via an HTTP GET. It applies to each member of the collection of products.”

Instead of specifying :member, if we instead specified :collection, then the route would apply to the collection as a whole. This is often used for scoping; for example, you may have collections of products on clearance or products that have been discontinued.

Nested Resources

Often our resources themselves contain additional collections of resources. For example, we may want to allow folks to review our products. Each review would be a resource, and collections of review would be associated with each product resource. Rails provides a convenient and intuitive way of declaring the routes for this type of situation:

 resources ​:products​ ​do
  resources ​:reviews
 end

This defines the top-level set of product routes and additionally creates a set of subroutes for reviews. Because the review resources appear inside the products block, a review resource must be qualified by a product resource. This means that the path to a review must always be prefixed by the path to a particular product. To fetch the review with ID 4 for the product with an ID of 99, you’d use a path of /products/99/reviews/4.

The named route for /products/:product_id/reviews/:id is product_review, not simply review. This naming simply reflects the nesting of these resources.

As always, you can see the full set of routes generated by our configuration by using the rails routes command.

Routing Concerns

So far, we’ve been dealing with a fairly small set of resources. On a larger system there may be types of objects for which a review may be appropriate or to which a who_bought action might reasonably be applied. Instead of repeating these instructions for each resource, consider refactoring your routes using concerns to capture the common behavior.

 concern ​:reviewable​ ​do
  resources ​:reviews
 end
 
 resources ​:products​, ​concern: :reviewable
 resources ​:users​, ​concern: :reviewable

The preceding definition of the products resource is equivalent to the one in the previous section.

Shallow Route Nesting

At times, nested resources can produce cumbersome URLs. A solution to this is to use shallow route nesting:

 resources ​:products​, ​shallow: ​​true​ ​do
  resources ​:reviews
 end

This will enable the recognition of the following routes:

 /products/1 => product_path(1)
 /products/1/reviews => product_reviews_index_path(1)
 /reviews/2 => reviews_path(2)

Try the rails routes command to see the full mapping.

Selecting a Data Representation

One of the goals of a REST architecture is to decouple data from its representation. If a human uses the URL path /products to fetch products, they should see nicely formatted HTML. If an application asks for the same URL, it could elect to receive the results in a code-friendly format (YAML, JSON, or XML, perhaps).

We’ve already seen how Rails can use the HTTP Accept header in a respond_to block in the controller. However, it isn’t always easy (and sometimes it’s plain impossible) to set the Accept header. To deal with this, Rails allows you to pass the format of response you’d like as part of the URL. As you have seen, Rails accomplishes this by including a field called :format in your route definitions. To do this, set a :format parameter in your routes to the file extension of the MIME type you’d like returned:

 GET ​/products(.:format)
  {:action=>"index", :controller=>"products"}

Because a full stop (period) is a separator character in route definitions, :format is treated as just another field. Because we give it a nil default value, it’s an optional field.

Having done this, we can use a respond_to block in our controllers to select our response type depending on the requested format:

 def​ ​show
  respond_to ​do​ |format|
  format.​html
  format.​json​ { render ​json: ​@product.​to_json​ }
 end
 end

Given this, a request to /store/show/1 or /store/show/1.html will return HTML content, while /store/show/1.xml will return XML, and /store/show/1.json will return JSON. You can also pass the format in as an HTTP request parameter:

 GET HTTP://pragprog.com/store/show/123?format=xml

Although the idea of having a single controller that responds with different content types seems appealing, the reality is tricky. In particular, it turns out that error handling can be tough. Although it’s acceptable on error to redirect a user to a form, showing them a nice flash message, you have to adopt a different strategy when you serve XML. Consider your application architecture carefully before deciding to bundle all your processing into single controllers.

Rails makes it straightforward to develop an application that is based on resource-based routing. Many claim it greatly simplifies the coding of their applications. However, it isn’t always appropriate. Don’t feel compelled to use it if you can’t find a way of making it work. And you can always mix and match. Some controllers can be resource based, and others can be based on actions. Some controllers can even be resource based with a few extra actions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.170.65