Copyright Notice
This text is copyright by InfoStrada Communications, Inc., and is used with their permission. Further distribution or use is not permitted.This text has appeared in an edited form in Linux Magazine magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.
Please read all the information in the table of contents before using this article.
Linux Magazine Column 71 (Jun 2005)
[Suggested title: ``Introduction to CGI::Prototype (part 2)'']
Last time, I talked about why I had created my CGI::Prototype
framework,
and the basics of creating an application. I'll continue the discussion
from where we left off, from where I had just described the basic
class as:
sub activate { eval { my $self = shift; my $this_page = $self->dispatch; my $next_page = $this_page->respond; $next_page->render; }; $self->error($@) if $@; }
In this case, the application provides a dispatch
method to determine
which page object will get to look at the incoming parameters. That
respond page object then has the responsibility to return a render
page object, which returns the results to the user.
Let's look at this framework to implement the classic ``two-pass'' application. A CGI script responds first with a form to fill out, and then handles the submission of that form to generate a result.
First, we'll create MyApp.pm
:
package MyApp; use base CGI::Prototype;
(I'm omitting the 1;
from these example modules, just for brevity.)
And then use it:
#!/usr/bin/perl use MyApp; MyApp->activate;
As stated last time, the default behavior simply prints ``This page intentionally left blank''. We'll need to create page objects for the first and second passes. For the first pass, we'll just override the template:
package MyApp::One; use base MyApp; sub template { 'the_form.tt' }
We'll dispatch to the second pass if there are any incoming parameters. Because Template Toolkit code can access the CGI object (more on this later), we still have no code to write! So the second pass looks similar:
package MyApp::Two; use base MyApp; sub template { 'the_response.tt' }
We'll now need a dispatcher to select which one of these two pages
should be in charge. So, back in to MyApp.pm
:
sub dispatch { my $self = shift; return $self->param ? 'MyApp::Two' : 'MyApp::One'; }
This says ``if there are any parameters, use page object MyApp::Two
,
otherwise use MyApp::One
.''
Where is param
? This method is provided by the base class. Similarly,
a CGI
method returns the CGI.pm object to enable form generation and
miscellaneous incoming parameter access.
When a template is called, the page object is passed as the self
variable. Thus, a template can call the param
method to get the
first_name
incoming parameter simply with
self.param('first_name')
. So, in the_form.tt
, we will find:
[% self.CGI.header %]<html><head></head><body> <h1>Welcome!</h1> Please enter your first and last name [% self.CGI.start_form; self.CGI.textfield('first'); self.CGI.textfield('last'); self.CGI.submit; self.CGI.end_form %] </body></html>
Note that we're using CGI.pm's header and form methods, but writing the rest of the HTML ourselves. Therefore, this file is mostly editable by someone without a lot of Perl knowledge, except for nudging to ``put this magic code here and there'' to get to the parameters or creating the fields.
We'll also use callbacks to fetch the response in the_response.tt
:
[% self.CGI.header %] <html><head></head><body> <h1>Welcome!</h1> Greetings, [% self.param('first') | html %] [% self.param('second') | html %]! </body></html>
Here, I'm using Template Toolkit's html
filter to ensure that less-thans
and greater-than's don't ruin my day with a cross-site scripting attack.
And now, in one master CGI script of a few lines, three controller classes (application, page one, page two) in separate modules, and two templates, I now have a working application again. The view code is in Template Toolkit format, which I can hand off to a web designer, and the controller code is in Perl, as it belongs.
But now let's deal with the problems. If there are parameters,
but they are incomplete, we should render the form again instead
of the response. We could make this the responsibility of
the current dispatcher, but let's show an alternative by making
the MyApp::Two
page decide when it doesn't have enough data.
We'll do this by adding a respond
method from its prior return $self
to something like:
sub respond { my $self = shift; unless ($self->param('first') =~ /\S/ and $self->param('last') =~ /\S/) { return 'MyApp::One'; # back to the form } return $self; }
Now, when dispatch
hands control to MyApp::Two
for the response,
the response can decide that this is the wrong state, and send the
control back to the MyApp::One
page object for the rendering.
Although this solves the problem one way, it tightly couples the error
checking for page two with the form elements of page one. A better
model is ``stay here until you get it right'', which we can implement by
a responder on page one instead of two. First, we'll tell the
dispatcher to always send to state one (in MyApp.pm
):
sub dispatch { return 'MyApp::One' }
And now in MyApp/One.pm
:
sub respond { my $self = shift; if ($self->param('first') =~ /\S/ and $self->param('last') =~ /\S/) { return 'MyApp::Two'; # good params, move on } return $self; # bad params? stay here }
We'd also remove the respond
introduced earlier into MyApp/Two.pm
.
Thus, when someone has provided good values (containing at least one
non-whitespace character) for both the first
and last
parameters, we then render from page two instead of page one. Until
this is successful, we'll keep redisplaying page one. (In practice,
we'd also set a variable that the form would consult to indicate why
the user is seeing the same form again.) And CGI.pm's
sticky-form-field mechanism also ensures that the default value for
the form parameters retain their value from one invocation to the
next.
For multi-page applications, this gets a bit trickier. We need to figure out what state we are in, and dispatch to the proper corresponding page object. We don't want the dispatcher to always dispatch to a hardwired page object, as that would merely shove the problem down one level. But where will the dispatcher get the state information?
We could do it with a hidden field in a form. (Hint: this is what
CGI::Prototype::Hidden
does.) This requires that each page
use a form that has a hidden field somewhere.
We could determine the current state with a mangled URL. This means
that some part of the URL includes the state, usually in the
``pathinfo'' portion after the name of the CGI script. For example, if
foo.cgi
is our CGI script, both:
/my/path/to/foo.cgi/one
and
/my/path/to/foo.cgi/two
invoke foo.cgi
. But within the script, we can examine the value of
CGI.pm's path_info
, which will be /one
or /two
, respectively.
And this can give us the right state information to dispatch back to
the proper page object.
We could also determine the current state with a cookie. On each page display, we could send out a cookie that has that state, and then read the cookie on the reply. However, this method prevents the user from having two browser windows open at once that may be in different states in the application.
We might even mix together some combination of those, or combine them with some server-side database. The important thing is that we have to know how to associate this incoming hit with some prior rendering from a page object.
The activate
method shown earlier is actually a bit more
customizable. Each action is bracketed in a pair of MUMBLE_enter and
MUMBLE_leave calls. For example, app_enter
is called before
anything else happens, and app_leave
is called after everything
else is done. By default, these hooks do nothing, but they provide a
nice place for setup and teardown steps.
For the remaining hooks, let's define control for a page object as
the time for which it is acting as either a respond or render page.
The control_enter
and control_leave
hooks bracket this period of
time. Because these are called only once per hit against a given
page, they're a great place to put database connect and disconnect
calls, for example.
A page also gets respond_enter
and respond_leave
calls when it
is acting as a responder, and corresponding render_enter
and
render_leave
calls when acting as a renderer. For example, a good
use for render_enter
is to preload data needed for sticky fields.
This complicates the activate
method a bit, so here's the full
activate
method defined in CGI::Prototype
:
sub activate { my $self = shift; eval { $self->app_enter; my $this_page = $self->dispatch; ### DISPATCH ### $this_page->control_enter; $this_page->respond_enter; my $next_page = $this_page->respond; ### RESPOND ### $this_page->respond_leave; if ($this_page ne $next_page) { $this_page->control_leave; $next_page->control_enter; } $next_page->render_enter; $next_page->render; ### RENDER ### $next_page->render_leave; $next_page->control_leave; $self->app_leave; }; $self->error($@) if $@; }
The default render
engine takes the result of calling the
template
method, running it through the Template Toolkit processor,
created by calling the engine
method. The result is captured, and
sent to the output
method. (I override output
for testing,
sending the output into a variable, for example.)
The default engine
creates a Template Toolkit object, configured
according to the hashref returned by the engine_config
method
(default empty). This parameter is autoloaded (thanks to
Class::Prototyped
), so we don't keep recreating the engine in a
persistent environment (such as mod_perl
).
There's nothing stopping you from easily overriding render
to call
your favorite templating engine instead, even using template
if it
makes sense to you. I tried to keep it easy to use Template Toolkit
(the best templating engine for Perl today), but I realize this is
a religious issue for some folks.
And that's the basics of CGI::Prototype
. Nothing revolutionary,
but a lot of the right stuff in the right place.
Next time, I'll take a look at a longer application, created
with CGI::Prototype::Hidden
. Until then, enjoy!