The development of my latest project, Maypole, began over 4000 years ago, when, so the story goes, the goddess Ninkasi taught the Sumerian mortals a recipe for a magical drink. Her concoction of hops, barley, yeast, and water (and a few other special ingredients), was an instant hit, and has remained the cornerstone of a university education ever since.
As a student, I subdivided the entire world of beer into Guinness, fizzy lager, and proper English bitter. It was at the Open Source Conference 2000, when Adam Turoff presented me with a lovely bottle of Chimay Blue, that the scales fell from my eyes and I became introduced to the wide variety of different tastes and subtle flavors of the beers of the world.
The big problem with Ninkasi's recipe is that the nicer the end result, the more of it you consume, and, for some reason, the less likely you are to remember how good it was in the morning, and so you never know whether or not you want to buy that particular beer again. So you have to buy it anyway to try to work out whether or not you liked it. This is enjoyable, but not particularly economical. I found myself needing some kind of database to keep track of my tastings.
Not one to do things by halves (or should that be 284 ml?), what started as a simple database ended up as a complete framework for writing Web-based, database-backed applications in Perl.
The first bit was easy. I created an SQL schema that linked beers to their breweries and the pubs that served them:
Listing 1. Beer database schema
create table brewery (
id int not null auto_increment primary key,
name varchar(30),
url varchar(50),
notes text,
);
create table beer (
id int not null auto_increment primary key,
brewery integer,
style integer,
name varchar(30),
...
);
create table handpump (
id int not null auto_increment primary key,
beer integer, pub integer
);
create table pub (
id int not null auto_increment primary key,
name varchar(60),
url varchar(120),
notes text
);
create table style (
id int not null auto_increment primary key,
name varchar(60),
notes text
);
|
And then what? I didn't particularly want to be sitting in the
mysql shell typing precarious INSERT INTO statements, especially after the fourth
pint of Hook Norton Generation. Of course, I could write some command-line
Perl scripts to read little text files and put them into the database, or
something similar, but the ideal way of keeping the database up to date,
visible, and easy to edit would be to have a Web interface to it.
At this point I realized that the project had suddenly become vastly more generic than just a way of keeping track of my favorite tipples, and I could create a useful framework that put a Web front end onto any database. This was beginning to sound like work, so I left the idea alone for a couple of years.
Then there was a surge in interest in the Perl community in conglomeration
of ideas: the Template Toolkit, the Class::DBI
object database mapping layer, and the idea of an MVC
(Model-View-Controller)-style framework for Web applications. Andy
Wardley, the creator of the Template Toolkit, explains what this means:
What the MVC-for-the-Web crowd is really trying to achieve is a clear separation of concerns. Put your database code in one place, your application code in another, your presentation code in a third place. That way, you can chop and change different elements at will, hopefully without affecting the other parts (depending on how well your concerns are separated, of course). This is common sense and good practice. MVC achieves this separation of concerns as a by-product of clearly separating inputs (controls) and outputs (views).
But with so many people talking about MVC Web applications, I found it a little odd that nobody had really put together a really good generic framework for one in Perl and released it. Java™ programmers have Struts, which makes it nice and easy to deploy such applications, and that's quite the rage at the moment, but Perl programmers have been forced to write their own -- over, and over again. I imagine that each large Perl company has its own big application framework. Let's look at how they usually work.
Kake Pugh's article on writing Web applications without writing much code (see Resources for a link) introduced a common set of components used to develop MVC frameworks, and I've used many of her ideas in Maypole, including the same components.
We'll start at the model end -- this is a set of classes that define the behavior of the application: when someone clicks search, there's got to be some code that does the searching and returns the results so they can be handed off to the view.
Because most applications use a relational database as their data store,
and because objects are a handy way of representing, well, objects, the
model class is usually based on an object-database mapping system.
Amongst the many Perl libraries that do this, Class::DBI came out as my favorite due to its sheer
simplicity, and it has a bunch of extension classes that make it even
simpler. For instance, if I start with:
Listing 2a. Creating the classes with CDBI::Loader
use Class::DBI::Loader;
Class::DBI::Loader->new( namespace => "BeerDB", dsn => "dbi:mysql:beerdb" );
|
Then, magically, the BeerDB::Beer,
BeerDB::Brewery, BeerDB::Pub,
and BeerDB::Handpump classes come into existence, and I can say:
Listing 2b. Creating the classes with CDBI::Loader
my $hooky = BeerDB::Beer->retrieve(12); # Retrieve by ID
my $generation = BeerDB::Beer->search( name => "Generation" );
$generation->score(9); # Good beer!
|
If I then specify relationships between tables, I can say things like:
Listing 2c. Creating the classes with CDBI::Loader
BeerDB::Beer->has_a(brewery => "BeerDB::Brewery");
my $rch = BeerDB::Brewery->find_or_create( name => "RCH" );
my $pitchfork = BeerDB::Beer->create({
name => "Pitchfork",
brewery => $rch
});
print $pitchfork->brewery->name;
|
Now, this is a reasonable amount of what we want a model class to do, all
in two lines of code. I like things that don't take up very much code, so
Maypole currently uses a model class system based on Class::DBI::Loader. There's no reason why someone
couldn't come along and write his own Maypole::Model::Tangram, or a wrapper around any of
the many other database-object mapping libraries in Perl, since Maypole is
designed to allow you to do precisely that, but we have to start
somewhere, and I started with Maypole::Model::CDBI.
The Template Toolkit does a fantastic
job of abstracting out presentation logic from an application, and so
Maypole::View::TT is a thin wrapper around
that, presenting it with the objects returned from the model class.
Similarly, someone might want to write Maypole::View::HTMLTemplate, but Template Toolkit
does what I need.
On the controller side, Apache's mod_perl interface is a good way of
interacting with Perl-based Web applications. We could use CGI, and I
expect to write a CGI::Maypole sometime soon,
but Maypole is currently best front-ended by Apache::MVC. (In fact, Maypole was Apache::MVC before the CDBI and Apache components
were more properly factored out.)
Now we have the components. How do we put them all together to form our
Web application? To use Apache::MVC, we
need to write a driver package that Apache can talk to. For applications
that just want to do search, editing, adding, deleting, and viewing data,
this is very short indeed -- the whole point of Maypole is that you
shouldn't be writing any code for all the common operations.
So first, we start by inheriting from Apache::MVC, and telling it what database we plan to
use:
Listing 3. The simplest Maypole application
package BeerDB;
use base 'Apache::MVC';
BeerDB->setup("dbi:mysql:beerdb");
|
That single call to setup will connect to the
database, set up a single class for each table, and cause those classes to
inherit from Maypole::Model::CDBI.
Next we define some configuration parameters for our application: the base
URL of the site, so that we can construct links to other commands; the
maximum number of rows per page in a listing before we go to paged output;
and the tables we want to display -- in our example, we need to exclude
the handpump table, since that is only a link
table used to join the pubs and beers tables together. If we were okay displaying all
the tables in the database, we wouldn't need that last line.
BeerDB->config->{uri_base} = "http://neo.trinity-house.org.uk/beerdb/";
BeerDB->config->{rows_per_page} = 10;
BeerDB->config->{display_tables} = [qw[beer brewery pub style]];
We need to tell the various packages what kind of data they're expecting,
so that Class::DBI::FromCGI can automatically
update the database from CGI values:
Listing 4. Describing the acceptable data
BeerDB::Brewery->untaint_columns( printable => [qw/name notes url/] );
BeerDB::Style->untaint_columns( printable => [qw/name notes/] );
BeerDB::Beer->untaint_columns(
printable => [qw/abv name price notes/],
integer => [qw/style brewery score/],
date =>[ qw/date/],
);
|
Finally, we need to tell the classes how they relate to each other. For
instance, when we view a beer and look at its brewery, we don't want to
just get the numeric ID for the row in the brewery table; instead we want the name of the
brewery and a link to a page that views that brewery. Similarly, we need
to tell the BeerDB::Brewery class that it has
many beers attached to it, so it should display a list of all its beers.
We can do this with the has_a and has_many methods in Class::DBI, but I always forget how they work, so I
wrote a little module to do it for me:
Listing 5. Class::DBI::Loader::Relationship
use Class::DBI::Loader::Relationship;
BeerDB->config->{loader}->relationship($_) for (
"a brewery produces beers",
"a style defines beers",
"a pub has beers on handpumps");
1;
|
And that's all the programming that we need to do for our beer database -- roughly twenty lines of Perl code.
Of course, there's going to be a lot of templating required to present this mass of logic to the user, but fortunately, almost of all of what we need is provided by the Maypole factory default templates. So let's get the application up and running, and see what it looks like.
We have our driver class written. We now need to tell Apache about it, and put the templates in place. First, we put this into the Apache configuration:
Listing 6. Apache configuration for BeerDB
<Location /beerdb>
SetHandler perl-script
PerlHandler BeerDB
</Location>
|
And then we make two directories under the beerdb subdirectory of the Web root: custom and factory. factory should contain the templates shipped with Maypole, which we don't need to modify. We can override those templates specific to our application (there won't be that many) in custom. For instance, our custom/header will be called first by all of the main pages:
Listing 7. The custom header
<HTML>
<HEAD>
<TITLE> Beer Database </TITLE>
<META http-equiv=Content-Type content="text/html; charset=utf-8">
<LINK title=myStyle href="/beerdb.css" type=text/css rel=stylesheet>
</HEAD>
<BODY>
<DIV class="content">
|
We also provide a custom page called frontpage to serve as an entry point to the application. In a moment, we'll see how this comes into play.
Listing 8. The front page template
[% INCLUDE header %]
<h2> The beer database </h2>
<TABLE BORDER="0" ALIGN="center" WIDTH="70%">
[% FOR table = config.display_tables %]
<TR>
<TD>
<A HREF="[%table%]/list">List by [% table %]</A>
</TD>
</TR>
[% END %]
</TABLE>
<BR>
|
This simply provides links to the *class*/link page for every table in that list of displayable tables we configured earlier.
Now we are all set up, and we can point our browser at http://localhost/beerdb/beer/list and see something like this:
Figure 1. Listing of all beers
Our default templates provide a list page with a paged view of the rows; links to view individual entries; the ability to view related data, such as the links to the style and brewery pages; a search box; and a box for adding a new entry. If we select a brewery, we get a list of the beers produced by that brewery:
Figure 2. Hook Norton
We can also edit records, delete records, and so on. We have a fully featured interface to our database, with only twenty lines of code.
Of course, it's only short because this is a reasonably small simple application -- we're just putting a Web front end to a database, what is sometimes called a CRUD interface, for Creating, Reading, Updating, and Deleting database records.
Real-life applications require a bit more than just browsing a database. As a sample real-life application, I developed a social networking tool, similar to Friendster or Orkut, based on Maypole. It's called Flox, partially because it flocks people together and partially because it's localised for my home town of Oxford and its university.
We'll first look at how Maypole does its stuff, then what we're going to need it to do in order to make Flox work, before finally getting into the implementation details.
Maypole is based around the idea of a request object. This is a little
like the mod_perl Apache request object, but at
a much higher level -- it knows about tables, classes, templates, and
methods. So, for instance, the request process begins with handling the
URL. This is something that Maypole itself knows nothing about; getting
the URL is a function of how Maypole is being run, and so the abstract
parse_location method is actually implemented
in Apache::MVC as follows:
Listing 9. How the URL is parsed
sub parse_location {
my $self = shift;
$self->{path} = $self->{ar}->uri;
my $loc = $self->{ar}->location;
$self->{path} ||= "frontpage";
my @pi = split /\//, $self->{path};
shift @pi while @pi and !$pi[0];
$self->{table} = shift @pi;
$self->{action} = shift @pi;
$self->{args} = \@pi;
$self->{params} = { $self->{ar}->content };
$self->{query} = { $self->{ar}->args };
}
|
This retrieves the path from the Apache object
stashed in $self->{ar}, and tries to fill
the table, action,
and args slots. (@pi stands for path
info.)
So a URL of /brewery/view/1 would represent a call to the view method (the action)
on the class controlling the brewery table,
which Maypole knows is BeerDB::Brewery, and
pass in the argument 1 to the args array.
Additionally, any query parameters in the URL are stuck into $r->{query} and POSTed-in data is put into $r->{params}.
Now that we know a little about what the request is intended to do, we need
to make sure that it's allowed to do it. First, we check that the request
is "applicable" -- that is, that we're allowed to call that method over
the Web. Since the view method is defined with
the Perl attribute :Exported, this signifies
that we can call it via a Maypole URL. BeerDB::Brewery::view inherits from Maypole::Model::Base::View, and is defined like this:
sub view :Exported {}
That's right, it has no code. We'll come back to this in a moment.
So for any action we want to perform on our database, we can simply create
a method, add the :Exported attribute, and
it'll be accessible by Maypole.
Listing 10. Creating a new action
package BeerDB::Beer;
sub drink :Exported {
my ($self, $r) = @_;
# Implementation is left as an exercise
# for the interested reader.
}
|
With this in place, we can go to /beer/drink/1, Maypole will pick up drink as the action, and the drink method will be called. That is, of course, if
we're authenticated to do so.
The next phase in the request's journey from your browser back to your
browser involves checking that access to that particular request is
allowed -- you may not want everyone being able to drink all your beer.
The authenticate method in Maypole is
permissive by default, but can be overridden in your own class. We'll see
how to do that when we develop a full-sized application.
Now that the request has cleared all the checks and hurdles, it's ready to be
handed off to the model class for processing. The default process action on a model class, before it calls the
action, looks at the first argument in the args
slot. With /beer/view/1, we have a request object that looks a little like
this:
Listing 11. Filling the request object from the URL
$r = {
path => "/beer/view/1",
table => "beer",
model_class => "BeerDB::Beer",
action => "view",
args => [ 1 ],
...
}
|
This first argument is used as the row of the table to inspect. Maypole
will look for a row in the beer table with the
ID 1 and create a BeerDB::Beer object
representing that row. This will be placed in the objects slot. process also
sets the template slot to the name of the
action, so our object now looks like this:
Listing 12. Adding the data to the request
$r = {
path => "/beer/view/1",
table => "beer",
model_class => "BeerDB::Beer",
action => "view",
args => [ ],
objects => [ bless { id => 1, name => ... }, "BeerDB::Beer" ],
template => "view",
...
}
|
To simply view an object, we don't need to do any more work than have the
object accessible; the actual displaying will be handled by the template.
That's why there didn't need to be any code for our view method, although it did need to exist so that it
could be :Exported.
How does the templating work, then? Once the request has been processed and the action method view called, it is handed off to the template class;
by default, this is Maypole::View::TT, which
uses the Template Toolkit. This first finds the template: it looks first
in a directory specific to the class. If there's a file called beer/view,
this will be used first; next, a custom template that is generic to all
classes, custom/view; and finally, the factory-shipped do-the-right-thing
template, factory/view.
The objects are passed into the template along with the request and a load
of metadata about the class, so that you can create totally generic
templates. However, the objects are also passed in with an alias: in the
case of beer/view, the object is available as beer, so you can say:
Listing 13. Using an object by name
<h2> [% beer.name %] </h2>
Style: [% beer.style %]
|
But a brewery would be passed in as brewery:
<h2> [% brewery.name %] </h2> |
And if you went to brewery/list, where you have a whole set of breweries,
these are available, naturally, as breweries:
Listing 14. Plural objects in templates
<TABLE>
[% FOR brewery = breweries %]
<TR>
<TD>[% brewery.name %] </TD>
<TD>[% brewery.address %] </TD>
<TD>[% brewery.url %] </TD>
<TD>[% brewery.notes %] </TD>
</TR>
[% END %]
</TABLE>
|
Once the request has been through the templating system, the output is then returned to the browser. Here's a summary of the process:
Figure 3. Flowchart: A user request
Back to Flox. For the core of any social networking site, there are three concepts: the user, the "friendship" connection between two users, and an invitation between an existing user and one who's not yet on the system. Sure, there are communities and messages and lots of other things we could add in, but we'll start with the core of it, and we'll model these concepts with database tables.
A user will be identified by e-mail address, have a name, a profile, a password, and a status. We'll use the e-mail address as a login name. To make the invitation process easier, we'll have two groups of users: those with a status of real are real users who have accepted an invitation to the system, and those with a status of invited haven't yet taken up an invite.
Listing 15. The schema for Flox
CREATE TABLE user (
id int not null auto_increment primary key,
email varchar(255),
first_name varchar(50),
last_name varchar(50),
profile text,
password varchar(255),
status ENUM("real", "invitee")
);
|
Invites can now run from one entry in the user table to another, and have an additional column for when the invite is deemed to have been rejected by force of apathy:
Listing 16. The invitation table
CREATE TABLE invitation (
id char(32) not null primary key,
issuer int,
recipient int,
expires date
);
|
When an invite is accepted, the recipient's status changes to real, and the invite is removed from the table.
For the friendship links, we'll model them in a really lossy but simple way. A link happens between one entry in the user table and another, as before, but it is either "offered" or "confirmed." If it is accepted, a reciprocal "confirmed" link will be created. As with the invitations, if it's rejected, it's simply removed from the table.
The first thing we're going to need is authentication and
the concept of a current user. Maypole has a hook for authentication that
defaults to allowing everyone access to everything; we can change this by
overriding the authenticate method in our
subclass and examining the request to determine whether or not we should
let it through.
For instance, we're going to need to allow access for people to accept and
reject invitations if they're not logged in, since evidently they're
accepting an invitation because they don't already have an account. So we
can start our authenticate method like so:
Listing 17. A basic authentication method
sub authenticate {
my ($self, $r) = @_;
return OK if $r->table eq "invitation" and
($r->action eq "accept" or $r->action eq "reject");
return;
}
|
Next, we want to find a cookie or some authentication tokens to identify a
user. Let's assume we have a get_user method
that works out the current user, if there is one, and puts it in $r->{user}. If we had this, we would be able to
say:
Listing 18. Authentication with a user
sub authenticate {
my ($self, $r) = @_;
return OK if $r->table eq "invitation" and
($r->action eq "accept" or $r->action eq "reject");
$r->get_user;
return OK if $r->{user};
return;
}
|
This is almost what we want, except that it would rudely dump the user to a standard Apache 403 page, instead of giving the option to log in. We want to actually proceed with the request, but redirect the user to a login form. We do this like so:
Listing 19. Redirecting to a log-in form
sub authenticate {
my ($self, $r) = @_;
return OK if $r->table eq "invitation" and
($r->action eq "accept" or $r->action eq "reject");
$r->get_user;
return OK if $r->{user};
$r->template("login");
return OK;
}
|
Thankfully, Maypole is smart enough not to do the action if something else has set the template for the request: /user/delete/2 will not delete user 2 first and then later pop up a login box to ask questions about whether you are allowed to do that. The login template itself looks something like this:
Listing 20. The login form template
[% INCLUDE header %]
<H2> You need to log in </H2>
<DIV class="login">
[% IF login_error %]
<FONT COLOR="#FF0000"> [% login_error %] </FONT>
[% END %]
<FORM ACTION="/[% request.path %]" METHOD="post">
Email address: <INPUT TYPE="text" NAME="email"> <BR>
Password: <INPUT TYPE="password" NAME="password"> <BR>
<INPUT TYPE="submit">
</FORM>
</DIV>
|
Notice how we use request.path to redirect the
user back to where they were originally going; if the login is successful,
then the second time they make this request, the authentication method will
be successful and pass the request on unmolested.
The final piece of the puzzle is that get_user
method, and it is provided by the Maypole plug-in module
Maypole::Authentication::UserSessionCookie; this checks for a session
cookie and uses Apache::Session to associate a session cookie with a
logged-in user name. It also takes care of dispensing cookies to clients
that don't have them, if they also provide a valid username and password.
Most of the user viewing and editing is now relatively trivial manipulating of the templates; the hard work comes in setting up invitations and friend connections. We've already gone on a long time, so we'll only look at the invitations here; the connections are more or less a subset of the same behavior.
When a logged-in user clicks the Invite a friend button, they're
taken to /invitation/create; we want this to be a form that lets them put
in the name and e-mail address of a friend to whom they want to
issue an invitation.
This doesn't require any additional data, so we use another Maypole trick:
if we request a method like create, which is
not exported for the Flox::Invitation class, it
doesn't immediately give up. In fact, it checks to see if there's a valid
create template, and if so, just passes all
that it has so far to the templater. The flowchart above was a
slight simplification, and should really have looked like this:
Figure 4. Flowchart: Calling templates without action
If we need to display something on the request form that's specific to the
current user, we can make use of the fact that the authentication process
has already defined $r->{user} for us, and
this is accessible inside the template through the variable [% request.user %].
The "invite user" form submits to /invitation/do_edit/ -- this is the standard "back-end" record creation and editing method that applies the changes and then returns the user back to a sensible page.
Now we need to issue the invitation. The form has posted us in forename, surname, and
email parameters, so we use the wonderful
CGI::Untaint module to ensure that they're what we expect: forename and surname
should be plain strings, whereas email needs to
look like a valid e-mail address:
Listing 21. Untainting the data
my $h = CGI::Untaint->new(%{$r->{params}});
my (%errors, %ex);
my %map = (email => "email",
forename => "printable",
surname => "printable");
for (keys %map) {
$ex{$_} = $h->extract("-as_$map{$_}" => $_)
or $errors{$_} = $h->error;
}
|
If there were any errors, we send the form back to the user with the errors in a template variable so that they can be used to highlight the problem:
Listing 22. Returning errors to the user
if (keys %errors) {
$r->{template_args}{message} = "There was something wrong with that...";
$r->{template_args}{errors} = \%errors;
$r->{template} = "issue";
return;
}
|
We need to make sure that the invited user is not already in the system. If he does exist, then there are two possibilities: first, he could be a "real" user, in which case we redirect the browser to viewing his profile:
Listing 23. When an invited user has already accepted
my ($user) = Flox::User->search({ email => $ex{email} });
if ($user) {
if ($user->status eq "real") {
$r->objects([ $user ]);
$r->template("view");
$r->model_class("Flox::User");
$r->{template_args}{message} =
"That user already seems to exist on Flox.
Is this the one you meant?";
|
Notice that we're essentially changing the nature of the request. We could do this by redirecting the browser to /user/view/ and the ID of the user we found, but doing it this way allows us to add an additional message to the template.
The other possibility is if they're already an invited user, but haven't accepted the invitation from someone else yet.
Listing 24. When an invited user has not yet accepted
} else {
# Put it back to the form
$r->{template_args}{message} = "That user has already been invited;
please wait for them to accept";
$r->{template} = "issue";
}
return;
}
|
Now we know we have a new user, so we can create a record in the user table with the status of invitee:
Listing 25. Creating a new user
my $new_user = Flox::User->create({
email => $ex{email},
first_name => $ex{forename},
last_name => $ex{surname},
status => "invitee"
});
|
We then send off a mail asking them to visit /invitation/accept/$ID, where ID is a relatively secure 32-bit cookie for the ID of the invitation; we also create the row in the invitation table:
Listing 26. Adding the new invitation object
Flox::Invitation->create({
id => $cookie
issuer => $r->{user},
recipient => $new_user,
expires => Time::Piece->new(time + LIFETIME)->datetime
});
|
Finally, we set the template back to view and the class back to
Flox::User so that we can redirect the user to
viewing his own profile again:
Listing 27. Sending the user back to his profile
$r->objects([ $r->{user} ]);
$r->template("view");
$r->model_class("Flox::User");
$r->{template_args}{message} = "Invitation sent to $ex{email}.";
|
The last piece of fun comes when the user goes to the URL to accept the invitation. After some basic checks to make sure that the invitation is valid, we then say:
Listing 28. Deleting the invitation and redirecting the user
my $recipient = $invite->recipient;
my $issuer = $invite->issuer;
$invite->delete;
$recipient->status("real");
$r->{user} = $recipient;
$r->model_class("Flox::User");
$r->objects([ $recipient ]);
$r->template("firsttime_edit");
|
This deletes the invitation, since we don't need it any more. We now set the recipient's status to real, and set things up so that we show the firsttime_edit template on the new user. This allows him to customize his profile and password, and then start making connections!
This article has outlined some of the principles of MVC development with Maypole, and shown specifically how Maypole can help you rapidly develop CRUD applications in Perl along with more complicated Web applications using the MVC model. Maypole is designed to allow you to put together complex enterprise database Web applications with a minimal amount of code -- it aims to do all of the structural work for you, and allow you to write the code and the templates you need to encapsulate what you want to do. And that is, I believe, how Web programming ought to be.
-
The Beer Church has a page on Ninkasi, the Mesopotamian
goddess of brewing.
-
Simon maintains the Maypole home
page. Read more about Maypole, the Beer database, Flox, and more at
Simon's documentation page.
-
Simon's work on Maypole was funded in part by The Perl Foundation.
-
The article Rapid Web
Application Deployment with Maypole (Perl.com, April 2004),
also by Simon, gives more background on and examples of Maypole
development.
-
Many of the components used in Kake Pugh's How to Avoid
Writing Code (Perl.com, July 2003) are also used in Maypole.
-
Apache::MVCand the other Perl modules mentioned in this article all live at CPAN. -
Learn more about the
Model-View-Controller (MVC) framework from Wikipedia, the free
encyclopedia.
-
You can use Maypole to allow your applications to access DB2® for Linux.
-
The IBM WebSphere® software platform is the core Web services and
J2EE compatible application server with an
array of deployment configurations for
distributed multi-server and enterprise environments.
You can download a free trial.
-
Simon's
Flox
social networking application is similar to
Friendster and
Orkut.
-
Find more Perl tips and expertise in the IBM developerWorks column Cultured
Perl.
- Find more resources for Linux developers in the developerWorks Linux
zone.
- Browse for books on these and other technical topics.
- Develop and test your Linux applications using the latest IBM tools
and middleware with a developerWorks Subscription: you get IBM software from
WebSphere, DB2, Lotus, Rational, and Tivoli, and a license to use the
software for 12 months, all for less money than you might think.
- Download no-charge trial versions of selected developerWorks
Subscription products that run on Linux, including WebSphere Studio Site
Developer, WebSphere SDK for Web services, WebSphere Application Server,
DB2 Universal Database Personal Developers Edition, Tivoli Access Manager,
and Lotus Domino Server, from the Speed-start
your Linux app section of developerWorks. For an even speedier start,
help yourself to a product-by-product collection of how-to articles and
tech support.
Simon Cozens is a Perl programmer and author. He has released more than 90 Perl modules. His books include Beginning Perl, Extending and Embedding Perl, and the forthcoming second edition of Advanced Perl Programming. He also maintains the Perl.com site for O'Reilly & Associates. When not stuck in front of a computer, he enjoys making music, playing the Japanese game of Go, and teaching at his local church. You can reach Simon at simon@simon-cozens.org.





