CGI::Application: A Simple, Extensible Web Framework

Last time, we introduced the concept of placing our HTML, instead of our Perl code, in separate templates. As well as providing an easy way to maintain the look and feel of a site, this approach also removes a lot of unwanted noise from our programs. As developers, our task is to consider the business need at hand; having to worry about the user interface can be a distraction.

The next step is to consider how we can actually add structure and maintainability to our code. When we write simple apps, we may have a script that runs from the first line to the last, but ultimately, we want a structured way of building our application that encourages re-usability and maintainability. Basically, we want a well-defined structure.

Many Web applications have the concept of a "run mode", although they may not use this terminology. Example run modes might be:

  • create user
  • edit user
  • list usrs

The run modes are typically encoded in the page parameters and it’s not uncommon to see convoluted logic like that found in the following pseudo-code:

if (run_mode  eq 'create_user') { 
   perform create user code
} elsif (run mode eq 'edit_user') {
   perform edit user code  
} elsif (run mode eq 'list_users') {
   perform list code
} else {
   error - invalid run mode

CGI::Application aims to provide a framework for re-usable code that is based on the run mode concept, but in a way that avoids a lot of spaghetti code. Because it’s Object Oriented (OO), it can easily be extended and enhanced, and later we’ll be looking at some plugins that will make your job a whole lot easier. But if you’re not an OO expert, have no fear: you aren’t required to make heavy use of OO techniques if you don’t want to.

CGI::Application encourages you to adopt a Model-View-Controller (MVC) framework. In the Web development world, MVC has emerged as a popular concept for engineering applications, although its roots predate the Web. The model is the actual business domain that you are working in. It includes the logic and rules that the application understands and enforces. The view is simply the user interface, and by default CGI::Application supports HTML::Template, although any templating solution can be used. Finally, the controller mediates between the model and the view. It takes input, passes it to the model components and invokes the view components as appropriate.

A Sample Application

Now that we’ve got the buzzwords out of the way, it’s time to discover that implementing a simple application is really quite easy. Imagine, for example, that we need to maintain users in our application, and we need to record the following details:

  • username — the user’s login name
  • first_name
  • last_name
  • email
  • password
  • receive_newsletter — a flag denoting whether the user wishes to receive the newsletter

We know in advance that we’ll need the following run-modes:

  • display_user_form — This is required regardless of whether we’re creating a new user, or editing an existing one
  • maintain_user — Takes the input from the form and either creates or updates the user information
  • list_users — When we want to edit a user, we’ll need to list the current ones from which the selection can be made

The skeleton of our application will look like this:

  1  package MyApp::User; 
 2  use strict;
 3  use base 'CGI::Application';
 4  use CGI::Application::Plugin::DBH        (qw/dbh_config dbh/);
 5  use CGI::Application::Plugin::ConfigAuto (qw/cfg cfg_file/);
 6  use Data::Dumper;
 8  # Define our run modes
 9  sub setup {
10      my $self = shift;
11      $self->start_mode('display_user_form');
12      $self->error_mode('error');
13      $self->mode_param('rm');
14      $self->run_modes(
15          'display_user_form' => 'display_user_form',
16          'maintain_user'     => 'maintain_user',
17          'list_users'        => 'list_users',
18      );
19  }
21  # Execute the following before we execute the requested run mode
22  sub cgiapp_prerun {
23  }
25  # Process any fatal errors
26  sub error {
27      my $self  = shift;
28      my $error = shift;
29      return "There has been an error: $error";
30  }
32  # Display the form for creating/maintaining users
33  sub display_user_form {
34  }
36  # Process the submitted form
37  sub maintain_user {
38  }
40  # List users for editing
41  sub list_users {
42  }
44  # Rules for validating the submitted form parameters
45  sub _user_profile {
46  }
48  # We don't want to store plain text passwords....
49  sub _encrypt_password {
50  }
52  # Create a new user record in the database
53  sub _create_user {
54  }
56  # Update an existing user record in the database
57  sub _update_user {
58  }
60  # Retrieve the list of users from the database
61  sub _get_users {
62  }
64  # Retrieve the record for a given user_id
65  sub _retrieve_user_details {
66  }
67  1;

Line 1: Our application is implemented in a package, which we’ve named MyApp::User. Perl implements its classes in packages.

Line 2: This pragma turns on strict checking. It catches mistakes that are often made in scripting languages, such as making a typo in a variable name by forcing the developer to declare a variable before using it ($banana is not the same as $bananana). There are other advantages that we don’t have time to discuss here. You should always use this pragma.

Line 3: This is where we inherit all of CGI::Application’s features. For many applications, this is almost as much OO as you need to know.

Line 4: This plugin processes configuration files.

Line 5: This plugin provides an API to the standard Perl database interface.

Lines 8-18: This is where we set up our run modes.

Line 10: If no run mode is supplied, use display_user_form as the default.

Line 11: We can specify the method that is called when a fatal error occurs.

Line 12: The query parameter rm will contain the run mode name.

Lines 13-17: We map each run mode name to a method that will be invoked when the run mode is requested. Note that the run mode name doesn’t have to be the same as the method name, but my preference is to keep them the same to avoid confusion.

Lines 21-22: CGI::Application will invoke the cgiapp_prerun method before it calls any run mode. This useful if you need to set up any application configuration or database connection.

Lines 25-29: Handle any fatal errors. In this case, the error handler doesn’t do much — the trapped error is simply displayed in the browser. However, in your production code, you’d probably log the message to a file or a database, and possibly send an email to the system administrator. The user would receive a simple message saying, "We apologise for technical difficulties." You don’t want to show the SQL statement that failed, or anything that would reveal the inner workings of the application.

Lines 32-41: Stubs for the run mode methods that will be invoked (essentially the controller component of our MVC). We’ll flesh some of these out later on.

Lines 44-45: This is where we’ll place our validation code. By convention, we prefix an underscore to any method that should be considered private.

Lines 48-65: These lines represent the model component of our MVC. We could place them in a separate package, but it makes sense in this application to keep everything together.

We’ll need an instance script, user.cgi, to call the package:

  1  #!c:/perl/bin/perl -T 
 2  use strict;
 3  use CGI::Carp qw(fatalsToBrowser);
 4  use MyApp::User;
 5  my $webapp = MyApp::User->new(tmpl_path => '/path/to/templates/');
 6  $webapp->run();

And that’s it: our CGI script is only six lines long! A quick explanation:

Line 1: Lets the Web server know where our Perl interpreter is located.

Line 3: This is useful for debugging, as it displays any CGI script error in the browser (other errors will be trapped by our package’s error run mode). You should remove this from production code.

Line 4: Use our new package.

Line 5: Create a new object based on our package. We pass the name of the directory where our templates are located.

Line 6: Run the application.

Perl looks for its packages in the directories defined in the @INC array. In other words, the interpreter will be search for a file called MyApp/ in any of the @INC directories. Alternatively, if none of these directories suit, you can programmatically add to @INC at run time with use lib:

  1  #!c:/perl/bin/perl -T 
 2  use lib 'path/to/my/libs';
 3  use strict;
 4  use CGI::Carp qw(fatalsToBrowser);
 5  use MyApp::User;
 6  my $webapp = MyApp::User->new(tmpl_path => '/path/to/templates/');
 7  $webapp->run();

A side note: one of the many advantages of CGI::Application is that the main application is implemented in a package, not a CGI script. This means that the bootstrap CGI instance script could be replaced with a mod_perl instance script. For those who don’t know, mod_perl is an Apache module that compiles the Perl interpreter into the Apache kernel. There are many benefits to this, one of which is that it makes your code blindingly fast. You can find out more about mod_perl at

Displaying the User Form

Creating the Template

As mentioned earlier, HTML::Template is supported by default, so our form will be quite simple:

  1  <!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN">  
 2  <html>  
 3  <head>  
 4  <title>Maintain Users</title>  
 5  </head>  
 6  <body>  
 7  <tmpl_include menu.tmpl><br><br>  
 8  <tmpl_var name=message>  
 9  <tmpl_if some_errors>There are errors in your form</tmpl_if>  
10  <form action="" method="post">  
11    <table>  
12      <tr>  
13        <td>Username<tmpl_unless user_id>*</tmpl_unless></td>  
14        <td><tmpl_unless user_id>  
15                        <input type="Text" name="username" size="15"  
16                            value="<tmpl_var name=username>">  
17                    <tmpl_var name=err_username>  
18                    <tmpl_else>  
19                        <tmpl_var name=username>  
20                    </tmpl_unless>  
21            </td>  
22      </tr>  
23      <tr>  
24        <td>First Name*</td>  
25        <td><input type="Text" name="first_name" size="15"  
26                value="<tmpl_var name=first_name>">  
27                <tmpl_var name=err_first_name></td>  
28      </tr>  
29      <tr>  
30        <td>Last Name*</td>  
31        <td><input type="Text" name="last_name" size="15"  
32                value="<tmpl_var name=last_name>">  
33                <tmpl_var name=err_last_name></td>  
34      </tr>  
35      <tr>  
36        <td>Email</td>  
37        <td><input type="Text" name="email" size="15"  
38                value="<tmpl_var name=email>">  
39                <tmpl_var name=err_email></td>  
40      </tr>  
41      <tr>  
42        <td>Password<tmpl_unless user_id>*</tmpl_unless></td>  
43        <td><input type="Password" name="password" size="15"  
44                value="<tmpl_var name=password>">  
45                <tmpl_var name=err_password> <tmpl_var name=err_user_id></td>  
46      </tr>  
47      <tr>  
48        <td>Repeat Password<tmpl_unless user_id>*</tmpl_unless></td>  
49        <td><input type="Password" name="password2" size="15"  
50                value="<tmpl_var name=password2>">  
51                <tmpl_var name=err_password2></td>  
52      </tr>  
53      <tr>  
54        <td>Receive Newsletter</td>  
55        <td><input type="Checkbox" name="receive_newsletter" value="y"  
56                <tmpl_var name=checked>></td>  
57      </tr>  
58      <tr>  
59        <td colspan="2"><input type="Submit" value="Save"></td>  
60      </tr>  
61    </table>  
62    <input type="Hidden" name="user_id" value="<tmpl_var name=user_id>">  
63    <input type="Hidden" name="rm" value="maintain_user">  
64  </form>  
65  </body>  
66  </html>

If you haven’t encountered HTML::Template before, have a look at the previous article. The only differences between standard HTML and our template are the placeholders for the value attribute of the input tags (e.g. username) and the field error messages (e.g. err_username). The placeholders will be empty when we create new users, and populated with the appropriate details at runtime.

Points of interest include:

  • Line 7: We include a simple menu which has "create user" and "edit user" options.
  • Lines 14-21: If we’re creating a new user, the user_id will be 0 (false) and we should display an input tag for the username. If we modify a user, we simple display the username. We also indicate mandatory fields with an asterisk. Some of these fields aren’t mandatory when we edit a user, so we hide the asterisk if the user_id is not 0.
  • Line 63: When we submit the form, CGI::Application will invoke the run mode identified by the "rm" parameter.

Creating our First Run-mode Method

The display_user_form() method is pretty sparse:

  1  sub display_user_form {  
 2      my $self = shift;  
 3      my $errs = shift;  
 4      my $q    = $self->query;  
 6      my $user_id  = $q->param('user_id') || 0;  
 7      # If we have a user id, then get the user's details  
 8      $self->_retrieve_user_details($user_id) if ($user_id);  
10      # Process the template  
11      my $template = $self->load_tmpl('user_form.tmpl');  
13      # Populate the template  
14      foreach my $form_val qw(username first_name last_name email message) {  
15          $template->param($form_val, $self->param($form_val) || '');  
16      }  
18      $template->param('checked', 'checked')  
19          if ($self->param('receive_newsletter') eq 'y');  
21      $template->param('user_id', $user_id);  
22      $template->param($errs) if $errs;  
24      #Display the form  
25      return $template->output();  
26  }

Line 3: We can ignore this for now. It is used when we wish to display our validation errors, which I’ll discuss later on.

Line 4: By default, CGI::Application uses the CGI module behind the scenes, and all the query parameters are available to the query method. We assign them to $q purely as a matter of convenience, since we only need to type $q->param to retrieve any of the parameters.

Line 6: Get the user_id from the submitted form. If there isn’t one (i.e. we’re creating a new user), set the id to zero.

Line 8: If we’re updating a user (the user_id is not zero), then retrieve the user record.

Line 11: Load the template.

Lines 14-16: CGI::Application allows you to save application-wide parameters using the param method. We saved the user’s details in the method called at line 8, and here we transfer these values to the template. Because HTTP is a stateless protocol, application parameters will not be accessible to subsequent requests. If you need persistent data, you should save the values in session variables.

Lines 18-22: Set the remaining template parameters. Line 22 sets any error messages that may be returned when validating the form. Validation is discussed later on.

Line 25: CGI::Application handles all output for us. We never issue the "print" command, nor do we output the HTTP headers directly. If you wish to modify the headers, you can use the header_type and header_prop methods.

Processing the Form Data

Let’s consider a trivial version of the maintain_user method.

  1  sub maintain_user {  
 2      my $self = shift;  
 3      my $q    = $self->query;  
 5      if ($q->param('user_id') == 0) {  
 6          $self->_create_user();  
 7      } else {  
 8          $self->_update_user();  
 9      }  
11      $self->param('message', 'User saved');  
12      $self->display_user_form();  
13  }

In this run mode, if a user_id of zero is submitted, it means that we will create a new user, otherwise we will update an existing user. We have two other methods, _create_user and _update_user, that will do the actual database-related work. But ultimately, we’ll want to validate the data against a set of rules:

  • The username, first name and last name are compulsory
  • The passwords must match
  • The email isn’t compulsory unless the user has elected to receive the newsletter
  • The email address must be of an email format
  • The username must be at least 6 alpha characters long, and at most 10 alpha characters

Validating forms can be a tedious task. Fortunately, we can use a plugin to do the heavy lifting for us.

Introducing CGI::Application::Plugin::ValidateRM

CGI::Application::Plugin::ValidateRM is an aggregation of a number of other modules: Data::FormValidator validates user input using a profile expressed as a Perl data structure; HTML::FillInForm populates HTML forms with data by parsing the HTML itself.

The easiest way to validate the data is to create a method that returns the validation profile:

  1  sub _user_profile {   
 2      my $self = shift;  
 3      return {  
 4          required          => [qw(username first_name last_name user_id)],  
 5          optional          => [qw(email receive_newsletter)],  
 6          dependencies      => {receive_newsletter => ['email']},  
 7          dependency_groups => {password_group => [qw/password password2/]},  
 8          filters           => ['trim'],  
 9          constraints       => {  
10              email    => 'email',  
11              username => qr/^w{6,10}$/,  
12              password => [  
13              {  
14                  constraint => qr/^w{6,10}$/  
15              },  
16              {  
17                  name       => 'password_mismatch',  
18                  constraint => sub {  
19                      my ($password, $password2) = @_;  
20                      return ($password eq $password2);  
21                  },  
22                  params => [qw(password password2)]  
23              }  
24              ]  
25          },  
26          msgs => {  
27              any_errors  => 'some_errors',  
28              prefix      => 'err_',  
29              constraints => {'password_mismatch' =>    
                                    "Passwords don't match"}  
30          }  
31      };  
32  }

Line 2: This may not be required if you’re not using any object attributes. In this example we aren’t, but if we needed to extend validation routine to include database access, then we would require access to the database handle. We’ll discuss database access later on.

Line 3: We return the data validation data structure. Note that we could define this in a hash reference in the calling method instead, but we’re separating our controller methods from our model methods. If you were very strict, you could place them in separate packages, but I haven’t found this to be particularly useful in practice.

Line 3: A list of mandatory fields.

Line 4: A list of optional fields.

Line 5: The email field is optional. However, if the user clicks on the "Receive Newsletter" option, then the email address is required.

Line 6: With a dependency group, if one value is entered, all other values are required.

Lines 7: I typically trim leading and trailing space, as it’s very rarely required for my applications. Leave this out if whitespace is important to you.

Lines 9-25: This is a list of more complex constraints.

Line 9: The ValidateRM plugin comes with a set of common format constraints, one of which checks email addresses. To quote the documentation: "[The constraint] checks if the email LOOKS LIKE an email address. This checks if the input contains one @, and a two level domain name. The address portion is checked quite liberally."

Line10: We can use our own regular expressions. We say that a username must contain a minimum of six alphanumeric characters and a maximum of ten. Because I’m trying to illustrate the usefulness of the validation plugin, I’ve avoided using a more complex expression that would distract from the point I’m trying to get across. You may decide that "123456" should not be a valid username, in which case you may wish to change your regex to "qr/^[a-z]w{2,5}$/i" so that the first character must be an alpha.

Lines 12-25: You can have multiple constraints per field. The first simply applies the same validation rule to the password that we used for the username.

Lines 16-23: This section illustrates three concepts:

  1. You can name your constraints. We make use of this later on when we specify custom error messages.
  2. A constraint can be enforced by an anonymous subroutine. You’ll need to do this if your rule is more complex that a simple pattern.
  3. The params array supplies the fields that will be passed to the subroutine.

Lines 26-30: Define the messages we will return.

Line 27: If any errors are encountered then the some_errors template variable will be set.

Lines 28: Whenever we encounter a field error, we will pass the error message to the template variable called err_<original_variable_name>.

Lines 29: We supply a custom error message for the "password_mismatch" constraint.

Any form that fails validation is redisplayed with the appropriate messages. By default, an error message of "missing" will be displayed next to a mandatory field where no value has been supplied, and "invalid" will be displayed when the supplied data does not meet our rules. You can change these defaults or use custom error messages. The documentation for Data::FormValidator provides many examples; check out the email lists if you have any questions.

Once we’ve set up the profile, we’ll need to modify our maintain_user method to use it:

  1  sub maintain_user {   
 2      my $self = shift;  
 3      my $q    = $self->query;  
 5      use CGI::Application::Plugin::ValidateRM (qw/check_rm/);  
 6      my ($results, $err_page) =  
 7          $self->check_rm('display_user_form', '_user_profile');  
 8      return $err_page if $err_page;
10      if ($q->param('user_id') == 0) {  
11          $self->_create_user();  
12      } else {  
13          $self->_update_user();  
14      }  
16      $self->param('message', 'User saved');  
17      $self->display_user_form();  
18  }

The new code is found on lines 5-8. We provide the check_rm method the name of our display form method and the name of our profile method.

Now, whenever there’s an input error, the user will be presented with a message next to each field that was incorrect, and the form values will be repopulated. And all we had to do was define the rules in a hash reference.

User Input Security

One of Perl’s largely unsung features is the taint mode: it assumes that all user input is potentially malicious, and the tainted input may not be used in code that accesses the system environment. While taint mode is obviously useful, we can provide an extra layer of data checking in a validation routine. As illustrated in our validation profile, it’s easy to add a regular expression that ensures that the input isn’t attempting to inject HTML or JavaScript that might be used in a scripting attack.

Such checks can be quite laborious when coding from scratch, but the burden is greatly reduced with this ValidateRM plugin.

Additional Checks

For the purposes of the discussion, I kept the validation profile simple, but you may want to include some additional rules:

  • A username is compulsory when creating a record. It’s not required when updating an existing user.
  • When updating a user, the password field is optional. If left blank, the original password remains.
  • We shouldn’t be allowed to create a username that already exists.

If you don’t wish to write these checks yourself, you can find them in the code archive that accompanies this tutorial.

Application Configuration and Database Connections

For the purposes of our application, two other plugins bear further investigation. Firstly, most applications require some kind of configuration. For instance, our application will need to know the database login details so that we can maintain the user information. Secondly, we will need to connect to the database.

CGI::Application::Plugin::ConfigAuto uses the Config::Auto framework. We can place our configuration information in a separate file, and our instance script passes the details to our application package. CGI::Application::Plugin::DBH provides easy DBI access. One of its advantages is that it’s lazy-loading, which means that our application doesn’t actually connect to the database until we need it to. This is particularly useful because some of our run modes don’t require database access.

Firstly, create the configuration file, myapp.cfg:

$cfg{user}            = 'myusername';         
$cfg{password}        = 'mypassword';    
$cfg{dsn}             = 'dbi:driver:mydb';    
$cfg{template_path}   = 'templates/';    

Next, we need to make some minor changes to the instance script:

  1  #!c:/perl/bin/perl -T    
 2  use lib 'path/to/my/libs';    
 3  use strict;    
 4  use CGI::Carp qw(fatalsToBrowser);    
 5  use MyApp::User;    
 6  my $webapp = MyApp::User->new();    
 7  $webapp->cfg_file('myapp.cfg');
 8  $webapp->run();

We have removed the template path from line 6, and have added line 7. Next, we flesh out cgiapp_prerun:

  1  sub cgiapp_prerun {    
 2      my $self = shift;    
 3      $self->dbh_config($self->cfg('dsn'), $self->cfg('user'),    
 4          $self->cfg('password'));    
 5      $self->tmpl_path($self->cfg('template_path'));    
 6  }

The database handle can now be accessed via the object attribute $self->dbh. So, where we would write the following DBI code in a standard application:

$sth = $dbh->prepare("select column from table");

Instead, we write:

$sth = $self->dbh->prepare("select column from table");

Now we can write the code to create our user:

  1  sub _encrypt_password {    
 2      my $password = shift;    
 3      my @chars    = ('.', '/', 0 .. 9, 'A' .. 'Z', 'a' .. 'z');    
 4      my $salt     = $chars[rand @chars] . $chars[rand @chars];    
 5      return crypt($password, $salt);    
 6  }    
 8  sub _create_user {    
 9      my $self = shift;    
10      my $q    = $self->query;    
12      my $encrypted_password = _encrypt_password($q->param('password'));    
14      my $sql = "insert into app_user (username, password, first_name, last_name,    
15          email, receive_newsletter) values (?, ?, ?, ?, ?, ?)";    
17      $self->dbh->{PrintError} = 0;    
18      $self->dbh->{RaiseError} = 1;    
19      $self->dbh->do($sql, undef, ($q->param('username'), $encrypted_password,    
20          $q->param('first_name'), $q->param('last_name'), $q->param('email'),    
21          $q->param('receive_newsletter')));    
22  }

Lines 1-6: Encrypt the password — we don’t store it as plain text. This method is also called from _update_user().

Lines 8-22: Create the user in the database.

Line 17: We turn off the printing of errors to standard out, as this would cause CGI::Application to fail. Remember, CGI::Application handles all the output for us, including generating the HTTP headers, so we don’t want to interfere with that. We could have set this attribute in the cgiapp_prerun method, but that would require us to connect to the database first, and we don’t want to do so unless we specifically need to access data.

Line 18: Whenever a SQL error is encountered we want the application to die, in which case CGI::Application’s signal handler will trap the error and send it to the error method defined in the setup.

Lines 19-21: Insert the data.

The code for the remaining run modes is in the zip file that accompanies this tutorial.

Extending CGI::Application

Say, for example, that all of our packages require users to be logged in before they can do anything. We don’t want to implement these functions in each package we write. So instead of inheriting from CGI::Application directly, we could write a base class, MyApp::Base, which would sub-class CGI::Application. All of the other packages would inherit from MyApp::Base instead. That is, we would replace:

use base 'CGI::Application';


use base 'MyApp::Base';

Then, in MyApp::Base’s cgiapp_prerun method, we could check to see if the user is logged in and, if not, direct them to the log-in page. Because this pre-run method is automatically called prior to executing the requested run mode, it’s the ideal place to centralise this kind of logic.

Hint: CGI::Application::Plugin:Session should help you out here. It provides a nice interface to CGI::Session, and makes creating and reading session variables as simple as:

$self->session->param('logged-in', 'y');


return if ($self->session->param('logged-in') eq 'y');

I hope the benefits of CGI::Application are apparent, but let’s summarise some of them:

  1. CGI::Application calls our run modes for us. This gets rid of a lot of the spaghetti code that developers often write when implementing run mode logic themselves.
  2. Our code is more maintainable. We can hand over the templates to our HTML designers, while we just worry about the application logic. Remember MVC: the database methods and validation profiles act as our models, the run modes are the controllers, and the templates are our views. Some developers might shuffle or redistribute the responsibility for these components slightly differently — possibly over more than one package. But in essence we’ve got a powerful but simple model for developing our applications.
  3. The framework is extensible. We can extend or over-ride any method we like to suit our needs.
  4. There is a growing number of plugins, including everything from logging to streaming files. These plugins are often used as wrappers around other modules, but they do much of the heavy lifting for us.
  5. We’re not restricted to CGI scripts. Because almost all of our code is implemented in packages, we can use mod_perl instance scripts instead of the CGI equivalents. In this way, we gain all the benefits of direct access to the Apache innards, as well as super-fast code and persistent database connections.
  6. There is an active and helpful user community. You can sign up to the email list, or search the archive . There is also a rather fine wiki.

Finally, I’d like to point out that some intrepid developers have ported CGI::Application to other languages. A PHP version is available, and the author provides a simple first-hand case study of its usefulness in his blog. Likewise, any Python programmers should visit

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.