Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

In this chapter, we’ll see

sessions and session management

nondatabase models

error diagnosis and handling

the flash

logging

Chapter 8

Task C: Cart Creation

Now that we have the ability to display a catalog containing all our wonderful products, it would be nice to be able to sell them. Our customer agrees, so we’ve jointly decided to implement the shopping cart functionality next. This is going to involve a number of new concepts, including sessions, error handling, and the flash, so let’s get started.

8.1Sessions

Before we launch into our next wildly successful iteration, we need to spend just a little while looking at sessions, web applications, and Rails.

As a user browses our online catalog, he or she will (we hope) select products to buy. The convention is that each item selected will be added to a virtual shopping cart, held in our store. At some point, our buyers will have everything they need and will proceed to our site’s checkout, where they’ll pay for the stuff in the cart.

This means that our application will need to keep track of all the items added to the cart by the buyer. This sounds simple, except for one minor detail. The protocol used to talk between browsers and application programs is stateless— it has no memory built in. Each time your application receives a request from the browser is like the first time they’ve talked to each other. That’s cool for romantics but not so good when you’re trying to remember what products your user has already selected.

The most popular solution to this problem is to fake out the idea of stateful transactions on top of HTTP, which is stateless. A layer within the application tries to match an incoming request to a locally held piece of session data. If a particular piece of session data can be matched to all the requests that come from a particular browser, we can keep track of all the stuff done by the user of that browser using that session data.

SESSIONS 105

The underlying mechanisms for doing this session tracking are varied. Sometimes an application encodes the session information in the form data on each page. Sometimes the encoded session identifier is added to the end of each URL (the so-called URL Rewriting option). And sometimes the application uses cookies. Rails uses the cookie-based approach.

A cookie is simply a chunk of named data that a web application passes to a web browser. The browser remembers it. Subsequently, when the browser sends a request to the application, the cookie data tags along. The application uses information in the cookie to match the request with session information stored in the server. It’s an ugly solution to a messy problem. Fortunately, as a Rails programmer you don’t have to worry about all these low-level details. (In fact, the only reason to go into them at all is to explain why users of Rails applications must have cookies enabled in their browsers.)

Rather than have developers worry about protocols and cookies, Rails provides a simple abstraction. Within the controller, Rails maintains a special hash-like collection called session. Any key/value pairs you store in this hash during the processing of a request will be available during subsequent requests from the same browser.

In the Depot application we want to use the session facility to store the information about what’s in each buyer’s cart. But we have to be slightly careful here—the issue is deeper than it might appear. There are problems of resilience and scalability.

By default, Rails stores session information in a file on the server. If you have a single Rails server running, there’s no problem with this. But imagine that your store application gets so wildly popular that you run out of capacity on a single-server machine and need to run multiple boxes. The first request from a particular user might be routed to one back-end machine, but the second request might go to another. The session data stored on the first server isn’t available on the second; the user will get very confused as items appear and disappear in their cart across requests.

So, it’s a good idea to make sure that session information is stored somewhere external to the application where it can be shared between multiple application processes if needed. And if this external store is persistent, we can even bounce a server and not lose any session information. We talk all about setting up session information in Section 21.2, Rails Sessions, on page 437, and we’ll see that there are a number of different session storage options. For now, let’s arrange for our application to store session data in a table in our database.

hash

֒page 637

Report erratum

SESSIONS 106

Putting Sessions in the Database

Rails makes it easy to store session data in the database. We’ll need to run a couple of Rake tasks to create a database table with the correct layout. First, we’ll create a migration containing our session table definition. There’s a predefined Rake task that creates just the migration we need.

depot> rake db:sessions:create exists db/migrate

create db/migrate/004_add_sessions.rb

Then, we’ll apply the migration to add the table to our schema.

depot> rake db:migrate

If you now look at your database, you’ll find a new table called sessions.

Next, we have to tell Rails to use database storage for our application (because the default is to use the filesystem). This is a configuration option, so not surprisingly you’ll find it specified in a file in the config directory. Open the file environment.rb, and you’ll see a bunch of configuration options, all commented out. Scan down for the one that looks like

#Use the database for sessions instead of the file system

#(create the session table with 'rake db:sessions:create')

#config.action_controller.session_store = :active_record_store

Notice that the last line is commented out. Remove the leading # character on that line to activate database storage of sessions.

#Use the database for sessions instead of the file system

#(create the session table with 'rake db:sessions:create') config.action_controller.session_store = :active_record_store

The next time you restart your application (stopping and starting script/server), it will store its session data in the database. Why not do that now?

Carts and Sessions

So, having just plowed through all that theory, where does that leave us in practice? We need to be able to assign a new cart object to a session the first time it’s needed and find that cart object again every time it’s needed in the same session. We can achieve that by creating a method, find_cart, in the store controller. A simple (but verbose) implementation would be

def find_cart

 

unless session[:cart]

# if there's no cart in the session

session[:cart] = Cart.new

# add a new one

end

 

session[:cart]

# return existing or new cart

end

 

Remember that Rails makes the current session look like a hash to the controller, so we’ll store the cart in the session by indexing it with the symbol :cart.

Report erratum

ITERATION C1: CREATING A CAR T 107

We don’t currently know just what our cart will be—for now let’s assume that it’s a class, so we can create a new cart object using Cart.new. Armed with all this knowledge, we can now arrange to keep a cart in the user’s session.

It turns out there’s a more idiomatic way of doing the same thing in Ruby.

Download depot_f/app/controllers/store_controller.rb

private

def find_cart

session[:cart] ||= Cart.new end

This method is fairly tricky. It uses Ruby’s conditional assignment operator, ||=. If the session hash has a value corresponding to the key :cart, that value is returned immediately. Otherwise a new cart object is created and assigned to the session. This new cart is then returned.

Note that we make the find_cart method private. This prevents Rails from making it available as an action on the controller. Be careful as you add methods to this controller as we work further on the cart—if you add them after the private declaration, they’ll be invisible outside the class. New actions must go before the private line.

||=

֒page 642

8.2Iteration C1: Creating a Cart

We’re looking at sessions because we need somewhere to keep our shopping cart. We’ve got the session stuff sorted out, so let’s move on to implement the cart. For now, let’s keep it simple. It holds data and contains some business logic, so we know that it is logically a model. But, do we need a cart database table? Not necessarily. The cart is tied to the buyer’s session, and as long as that session data is available across all our servers (when we finally deploy in a multiserver environment), that’s probably good enough. So for now we’ll assume the cart is a regular class and see what happens. We’ll use our editor to create the file cart.rb in the app/models directory.1 The implementation is simple. The cart is basically a wrapper for an array of items. When a product is added (using the add_product method), it is appended to the item list.

Download depot_f/app/models/cart.rb

class Cart attr_reader :items

def initialize @items = []

end

1. Note that we don’t use the Rails model generator to create this file. The generator is used only to create database-backed models.

attr_reader

֒page 635

Report erratum

ITERATION C1: CREATING A CAR T

108

def add_product(product) @items << product

end end

Observant readers (yes, that’s all of you) will have noticed that our catalog listing view already includes an Add to Cart button for each product.

Download depot_f/app/views/store/index.rhtml

<%= button_to "Add to Cart", :action => :add_to_cart, :id => product %>

This button links back to an add_to_cart action in the store controller (and we haven’t written that action yet). It will pass in the product id as a form parameter.2 Here’s where we start to see how important the id field is in our models. Rails identifies model objects (and the corresponding database rows) by their id fields. If we pass an id to add_to_cart, we’re uniquely identifying the product to add.

Let’s implement the add_to_cart method now. It needs to find the shopping cart for the current session (creating one if there isn’t one there already), add the selected product to that cart, and display the cart contents. So, rather than worry too much about the details, let’s just write the code at this level of abstraction. Here’s the add_to_cart method in app/controllers/store_controller.rb.

Download depot_f/app/controllers/store_controller.rb

Line 1 def add_to_cart

-@cart = find_cart

-product = Product.find(params[:id])

-@cart.add_product(product)

5end

On line 2 we use the find_cart method we implemented on the preceding page to find (or create) a cart in the session. The next line uses the params object to get the id parameter from the request and then calls the Product model to find the product with that id. Line 4 then adds this product to the cart.

The params object is important inside Rails applications. It holds all of the parameters passed in a browser request. By convention, params[:id] holds the id, or the primary key, of the object to be used by an action. We set that id when we used :id => product in the button_to call in our view.

Be careful when you add the add_to_cart method to the controller. Because it is called as an action, it must be public and so must be added above the private directive we put in to hide the find_cart method.

What happens when we click one of the Add to Cart buttons in our browser?

2. Saying :id => product is idiomatic shorthand for :id => product.id. Both pass the product’s id back to the controller.

Report erratum

ITERATION C1: CREATING A CAR T 109

What does Rails do after it finishes executing the add_to_cart action? It goes and finds a template called add_to_cart in the app/views/store directory. We haven’t written one, so Rails complains. Let’s make it happy by writing a trivial template (we’ll tart it up in a minute).

Download depot_f/app/views/store/add_to_cart.rhtml

<h1>Your Pragmatic Cart</h1>

<ul>

<% for item in @cart.items %> <li><%= h(item.title) %></li>

<% end %>

</ul>

So, with everything plumbed together, let’s hit Refresh in our browser. Your browser will probably warn you that you’re about to submit form data again (because we added the product to our cart using button_to, and that uses a form). Click OK, and you should see our simple view displayed.

There are two products in the cart because we submitted the form twice (once when we did it initially and got the error about the missing view and the second time when we reloaded that page after implementing the view).

Go back to http://localhost:3000/store, the main catalog page, and add a different product to the cart. You’ll see the original two entries plus our new item in your cart. It looks like we’ve got sessions working. It’s time to show our customer, so we call her over and proudly display our handsome new cart. Somewhat to

Report erratum