anders.com: words: advanced perl
how to build an shopping cart website with perl
[ home ]
[ anders ]
[ resume ]
[ choppers ]
[ projects ]
  [ netatalk ]
  [ route66 ]
  [ javascript ]
  [ webgallery ]
  [ mockMarket ]
  [ merits ]
  [ dailyBulletin ]
  [ panacea ]
[ words ]
[ pictures ]
[ movies ]
[ contact ]

Advanced Perl: Building an E-Commerce Shopping Cart
By Anders Brownworth

Intro:
Last issue, we got you started hacking around in perl. Our continuing course in Perl world domination has us studying some fairly advanced topics, but stick with us and you will end up with some fairly cool free software and the ability to make it do what you want!

Let's face it; the best way to learn anything is to have an interesting project to work on that uses it. We're going to launch right into advanced perl with a cookie based shopping cart system written in (you guessed it) perl. Besides being a fairly basic mainstay of any e-commerce website, a shopping cart system demonstrates many common obstacles that you will have to surmount as you become a Perl master.

We will move quickly and introduce new "Perl-isms" with the expectation that you will go to existing perl documentation to fill in the holes. This is in no means intended as a comprehensive primer, but rather a guide to an interesting project that will give you a nice excuse to hack some Perl and leave you with some fairly powerful software when all is said and done!

About the problem: Let's dive in!
There are a couple problems we will need to solve if we are to get a shopping cart system running in perl. By far the biggest is the problem of "state".

Web connections don't hang around. Essentially the web browser requests a page and the web server sends it. The browser doesn't stay continuously attached to the web server. CGI programs are executed in the same way. A request is made to a CGI URL and the web server fires off the named script. When the script finishes execution, the web server returns the results to the browser. The script doesn't hang around running forever.

What happens if you want to retain some information about the user (such as what products they have ordered) across these sessions? How does one instance of a CGI program remember things set in previous instances? One answer to this question might be to get an Oracle server going that the CGI program accesses, but another (more simple) way is to use what we will call "state". Simply put, state is a perl module that lets you create an object and stuff whatever variables you want into it for later retrieval. It's actually just a wrapper for the standard perl module called Storable, but I digress.

Another problem is that products and prices change all the time, so it would be nice if we could design something that can be updated easily. Obviously we don't want to hard-code the product and price information into the CGI programs, so the idea is to stick them in another file that all CGI programs access.

A solution:
We decided to build a simple shopping cart system in perl as an example. It took 3 days and we ended up with a fairly handy little demo. It isn't a perfect program, but it does demonstrate quite a few key things well. The important part is: it's simple! We purchased a domain name for it called myflowershop.com but ended up selling it, so that should explain why it says myflowershop.com everywhere, but the actual demo lives at http://myflowershop.maximumlinux.com/.

You will quickly realize that one of perl's strongest points is the fact that everybody and their cousin has created perl modules to do just about anything you can think of. We're going to use a package called Evantide::Web which gives you a set of functions useful for CGI programs. Simply put, Evantide::Web gives you the web argument parse routine presented in the last issue of MaximumLinux and a way to print out a text file replacing all occurrences of @@VARIABLE with a variable. (also covered last issue) We'll also look at a package called Evantide::State to help us out with saving variables from one execution of a CGI to the next.

Download this tar file with the sample code and follow along as you read this article.

ftp://ftp.maximumlinux.com/myflowershop.tgz

Look at index.cgi:

use Evantide::Web;
use Evantide::State;
use Products;

These lines evaluate the listed perl modules so you can call routines from them. The perl interpreter will look in the system perl module directory (usually something like: /usr/lib/perl5/site_perl/) and the local directory for files ending in .pm. In the case of the Evantide::* perl modules, the interpreter will look in a directory named Evantide for the files Web.pm and State.pm so you will want to install them there. (look at the README file in the distribution)

if ( $state = get_state_from_cookie () ) {  }

The first thing we will do at the start of all the cgi scripts is check to see if a cookie exists. If not, we are going to have to set one. (covered later) If this statement succeeds, then we have an object handle in the variable $state. We will need this handle to be able to access the variables we set in state. You can think of it as a serial number for a particular set of variables that have been stored on disk. As subsequent executions of CGI scripts happen, they will use this handle to manipulate those variables.

What's a cookie?
You can think of a cookie as a serial number. When a web browser makes a request from a web server, the web server may decide to pick a random number and hand it to the web browser saying, "remind me of this number every time you ask for something so I will know who you are." We will use the cookie as the state serial number, or "evoid". (Evantide object identifier)

You can take a look at Evantide/Web.pm to see how get_state_from_cookie actually works. $state is set to a pointer to the state referenced by the id from the cookie. It's a two step process that pulls the serial number from the cookie environment variable (HTTP_COOKIE) and looks up the state with that serial number returning a pointer to the specific state object.

One way or another, it's going to return to us a state object for this session. If the user has never hit the site before, or for some other reason isn't returning us a new cookie each time, then Evantide::State returns a new state object ready to be populated with variables. If we're getting a cookie, this must be a returning user, so we are returned a state object with possibly some variables stored such as a shopping cart full of products.

So now that we have a pointer to the proper state, or our saved variables, how does one manipulate a variable?

$state = new Evantide::State;
$random_id = $state->get_oid;
$state->put ( my_name, "Anders" );
$state->sync ();

And then later

$state = new Evantide::State ( $random_id );
$name = $state->get ( my_name );
print "$name";

The output of the above lines is the name "Anders". This only becomes useful if you use the state object in two different scripts and hold the $random_id that was assigned in the first script somehow. In the case of the web, this is an excellent use for a cookie.

As you become a seasoned programmer, you will hear more and more about objects. Basically an object is another way to think about programming. Everything is an object and you can call things on those objects. Consider the following code:

use Cat;

$sosha = new Cat;
$sosha->eat ($food);
$sosha->sleep;

Here we "create a new cat" that we can use through the variable $sosha. We could have created 2 or 3 different ones and interacted with them separately. This is a concise example because that's basically all my cat does: eat and sleep! You will get used to object oriented thinking as we start using it in more practical examples.

As we move on through the code, we come across the following snip:

print inject_vars ( "header.inc",
        "TITLE",        $section{$cgi{'section'}}->[0],
        "BUYNOW",       $section{$cgi{'section'}}->[2] );

Inject_vars is a simple routine that opens the named file (header.inc) and spits out it's contents replacing every instance of the named variables with their replacements. In the above example, @@TITLE is going to be replaced with the variable $section{$cgi{'section'}}->[0]. $section is a "hash of arrays". A hash is similar to an array but is keyed on names rather than numbers starting from 0. For instance, $section{'spring'} is a variable. In that variable we have stored an array of elements, so $section{'spring'}->[0] is the 0'th, or first element of the array stored in the hash refrenced by the key 'spring'. Just to add in a little more spice, instead of explicitly identifying the section as 'spring', we are going to let the web browser dictate that and use the variable $cgi{'section'} instead. Therefore $section{$cgi{'section'}}->[0] refers to the first element of the $section hash who's key is the $cgi hash's key named 'section'. But wait, it gets even more fun!

So where do this hashes get set? Well, as we covered last issue, cgi_parse () is a routine that builds the $cgi{} hash. $section{} gets set in the file Products.pm that was included in the beginning of the program. Check it out and see if you can follow the syntax. [0] is the section name, [1] is the map used for the section that the user is interested in and [2] is the jpeg file containing the picture for a section.

The concept for index.cgi and the rest of the CGI programs in this site is to display a navigator so that the user can look around and hopefully buy something. Therefore, for every time index.cgi is run, we need to build this little navigator and attach the user's shopping cart if they have something in it. We are storing the variable "orders" in state, so each time the CGI is run, we need to check to see if there are any orders, and if so, spit out the appropriate HTML. We also store a variable called "items" which is just the number of different items that have been ordered. We need this because we need to know how many different line items the shopping cart is so that we can specify table size and stretch the border images appropriately.

We then extract "items" from state and test weather it is more than 0. In other words, "does the user have anything in their shopping cart?" If so, we extract "orders", which is a pointer to a hash of arrays containing the description of each item ordered and the quantity, and we pass off to the show_basket subroutine. We decided to key items on description rather than an order number because we didn't want to deal with yet another mapping.

Show_basket does some table height calculations and spits out a little bit of HTML with inject_vars replaceing @@HEIGHT with the calculated table height. Next we iterate through the orders array and print a line for each item. If $orders were actually just an array, this would be simpler, but it is not. State has returned us a pointer to a hash, so we must deference it and iterate through the keys.

foreach $order (keys %$orders) {  }

The notation %$orders means "the hash pointed to by the pointer $orders" and to iterate through a hash, you have to make hash look like an array by saying "keys". So the notation "foreach $order (keys %$orders) { }" iterates through each key in the hash that is pointed to by $orders. It may seem convoluted, but it is necessary because when saving things in state, complex structures like hashes of arrays can only be saved by pointers.

Just to make things a little simpler, we extract the horribly nested variables to more human representations.

$quantity = $orders->{$order}->[1];

The second element of the array pointed to by the key $order in the hash $orders is the quantity. We rip out description and price in a similar way and do some simple math. The sprintf line cuts or elongates the string $price to two decimal places. Sprintf comes from C, and is quite a powerful routine. Check out the man page to see everything you can do with sprintf.

The last thing we do in this subroutine is to launch off into another subroutine called "show" passing it the $quantity, $description and $price. Show prints out the line with the specified variables filled out. Lastly, back in the show_basket subroutine, we do the sprintf decimal places trick and print out the shopping basket footer injecting the $total variable among others.

Processing returns to the main program, which spits out the final HTML and exits. But what happens if we didn't have a cookie to begin with? The else statement deals with this situation.

$state = new Evantide::State;

Calling a "new" on State returns us a new state object with it's own randomly picked object id. We don't have to get at this object id because the get_state_from_cookie does this for us. Take a look at get_state_from_cookie in Evantide/Web.pm to see how this works.

In the case of an empty basket, we only need to initialize our state object and spit out the HTML without a shopping basket. We set $items to 0 and do the following two puts:

$state->put ( 'orders', {} );
$state->put ( 'items', $items );

This enters a blank for "orders" and a 0 for "items" effectively creating an empty shopping basket. Lastly we do a $state->sync ( ); to write these new values that we just entered. (state isn't always writing everything down, you have to tell it to do that!)

Now that state is initialized, we'll also have to send the cookie with the state id in it to the browser. Luckily for us, we have a handy little routine that returns us a text string that can be printed that sends the appropriate cookie to the browser.

print get_state_cookie_header ($state, '/', $domain), "\n";

You might want to print this in a test script to see what it actually looks like. Lastly we print out the header and the footer with a few simple replaces for @@TITLE and whatnot.

So how does someone add something to their cart?

Additions are handled through the add.cgi script which is almost exactly the same as the index.cgi script. We start out by checking the cookie and pulling up the proper state object. From there we will fill out the HTML until we get to the shopping cart. Since the user is executing add.cgi, we are adding something to the shopping cart, so we have to figure out if the product we are adding is a new one, or is it just an additional order for something already ordered.

$items++ if ($orders->{$cgi{'item'}}->[1] < 1 );

If the user is ordering "roses" and he hasn't ordered roses before, $orders->{'roses'}->[1] (or the quantity of roses ordered) will be less than 1 (undefined) and we want to increment $items. If he has, then we don't want to $items++ because this isn't a new item.

Now we will incrament the quantity of the ordered item. (this is distinct from incrementing $items because $items is the total number of different products in the cart, not the total number of products)

$orders->{$cgi{'item'}}->[1]++;

With our quantities set correctly, we now need to actually add the item to the basket.

$orders->{$cgi{'item'}} = [ $items, $orders->{$cgi{'item'}}->[1] ];

Next we write the newly formed variables to state and sync the state so it gets written to disk.

$state->put ( 'orders', $orders );
$state->put ( 'items', $items );
$state->sync;

The rest of the script is just the same as index.cgi. The argument can be made to roll the contents of add.cgi into index.cgi, (as well as all the other scripts on this site) but for simplicity's sake, they are left separate.)

If a user presses the "empty cart" option invoking empty.cgi, we want to zero out all the data in the state object. The heart of empty.cgi is the same as the process for initializing a new state object. You just put a 0 for items and a { } for orders.

Dealing with shopping cart checkouts has largely been ignored with this demo. The forms will be shown and accept any data sent. In an ideal world you would perform various validation tasks with JavaScript and in the perl on the backend. But if you were able to follow this far in the discussion, dealing with form validation in perl is not going to be very difficult for you.

Instead we will turn our eyes to the task of using perl to build images for the website. Admittedly, the task of setting up a shopping cart system doesn't exactly depend on auto image generation, but it makes for an interesting object oriented perl example as well.

Each day, a cron job runs to create a gif file called "todaysdate.gif" to be used to display the date in the upper right hand corner of the website. It is built using a package called Tess which you can download from:

http://www.evantide.com/~jeremyw/software.new.html#tess 

To render text, Tess uses FreeType:

http://www.freetype.org/

After installing these packages, follow along with source/todaysdate.pl in the myflowershop source directory.

Tess works like Photoshop. Essentially you create layers of images and then you composite them together and write the output to a PPM file. (ppm is a Unix-ish image file format. From there I use convert (an ImageMagick utility that is included in most distributions) to convert the ppm file to a gif file.

$background = new TessSurface ($width, $height);

We have preset $height and $width to 18 and 200 respectively. In this line, we create a pointer to a TessSurface object of 200 x 18 size.

$background->set_draw_color (194, 188, 214);

To this background we set our draw color to the light purple background that the site uses. (RGB values came from Photoshop)

$background->draw_filled_rect (0, 0, $width, $height);

Then we fill the background with the purple draw color by createing a filled rectangle the size of the background surface.

$font = new TessType ("souvenirm.ttf", 10, 12, 12);

Next we create a font object setting the font face and some metrics.

$text_layer = new TessSurface ($width, $height);
$text_layer->set_type ($font);
$text_layer->set_draw_color (0, 16, 88);
$text_layer->set_type_alignment ($TessSurface::Right);

Here we have created a another layer called $text_layer and set the font, draw color and alignment.

$text_layer->draw_string ($width, $height - 5, $date);

Starting from 5 pixels from the bottom of the image, we print $date which was previously built up to include a nice text version of today's date.

$stack = new TessStack ();
$stack->add_surface ($background, 0, 0);
$stack->add_surface ($text_layer, 0, 0);
$final = $stack->make_composite ();
$final->write (new TessPPMWriter ("temp.ppm"));

Lastly we composite all the layers and write out the temporary ppm file to be converted to a gif by the last line in the script.

Looking through the source/ directory you will see that many of the images for the site were created with Tess. With a fair amount of variable information in an e-commerce website, it makes sense to have a program generate the image files rather than doing it by hand with an application such as Photoshop or Fireworks. It avoids quite a bit of user error.

Improving the system

No matter what you do, you can always improve. Perhaps one of the best ways to improve this shopping cart system is to merge everything into the index.cgi file. That doesn't make for a good example, but as far as repetition of code and the URL are concerned, it looks much better.

Another enhancement would be to include a configuration file, or possibly a web interface to insert, delete and edit product information. The next level for this sort of thing would be to use a database such as MySQL to hold the product data. You could also use MySQL to keep track of the state information instead of using Evantide::State.

Summary

In a few simple perl scripts and a couple handy modules we have been able to generate a fully functional e-commerce shopping cart system. Advanced perl is all about attacking object oriented programming and the vast number of perl modules out there or perhaps writing some perl modules on your own. At this point, you should familiarize yourself with some of the hard-core perl resources online and in print form. O'Reilly and Associates has a book called "Advanced Perl Programming" which is an excellent tutorial for such pursuits.

Without a doubt, the most handy perl resource is "perldoc". It's like man pages for perl and it has a great object oriented tutorial: "perldoc perltoot" To see a list of the other perl documentation, run a "perldoc perl". Literally, you could spend a year going through all the information and examples in the perldoc archives. Check it out.