perltutorial
jeffa
<h2>The Motivation</h2>
My first real job assignment out of college was to
deliver a suped up front-end for an Access database
file implemented in Cold Fusion. That was when a very
important lesson that I had been taught reared its
ugly head at me: "seperate your interface from your
implementation". This mantra has more than one meaning
- in this particular scenario it meant don't mix the
business rules with the presentation documents.
<p>
What did I do wrong? I used ColdFusion to generate key
HTML elements - I painted myself in a corner. When the
person who wrote the HTML needed to change something,
I was the one who did the changing. I had just assigned
myself a new job on top of the one I had with no extra pay!
<p>
That's what [kobe://HTML::Template] is all about - the ability
to keep your Perl code decoupled from your HTML pages.
Instead of serving up HTML files or generating HTML
inside your Perl code, you create templates - text
files that contain HTML and special tags that will
be substituted with dynamic data by a Perl script.
<p>
If you find yourself lacking as a web designer or generally
couldn't care less, you can give your templates to a web
designer who can spice them up. As long as they do not
mangle the special tags or change form variable names,
your code will still work, providing your code worked
in the first place. :P
<p>
<hr align=center>
<h2>The Tags</h2>
HTML::Template provides 3 kinds of tags:
<ul>
<li>variables: <TMPL_VAR>
<li>loops: <TMPL_LOOP>
<li>conditionals: <TMPL_IF> <TMPL_UNLESS> <TMPL_ELSE>
</ul>
<b>Variables</b> and conditionals are very simple - a variable
tag such as <TMPL_VAR NAME=FOO> will be replaced
by whatever value is assigned to the HTML::Template
object's paramater FOO. Example Perl code follows, but
please note that this is not a CGI script - let's stick
to the command line for now:
<h4>Example 1</h4>
<code>
# secret.pl
use HTML::Template;
my $bar = 'World';
my $template = HTML::Template->new(filename => 'secret.tmpl');
$template->param(SECRET_MESSAGE => $bar);
print $template->output;
</code>
and it's corresponding template file:
<code>
<!-- secret.tmpl -->
<h1>Hello <TMPL_VAR NAME=SECRET_MESSAGE></h1>
</code>
(p.s. this tutorial discusses CGI below - if you
really want to try this out as a CGI script then don't
forget to print out a content header. I recommend the ubiquitous
[http://stein.cshl.org/WWW/software/CGI/#header|header]
method avaible from using CGI.pm)
<p>
<hr align=center width="50%"/>
</p>
A <b>conditional</b> is simply a boolean value - no predicates.
These tags require closing tags. For example, if you only
wanted to display a table that contained a secret message
to certain priviledged viewers, you could use something like:
<h4>Example 2</h4>
<code>
<!-- secret2.tmpl -->
<TMPL_IF NAME="ILLUMINATI">
<table><tr>
<td><TMPL_VAR NAME=SECRET_MESSAGE></td>
</tr></table>
</TMPL_IF>
# secret.pl
my $template = HTML::Template->new(filename => 'secret2.tmpl');
$template->param(
ILLUMINATI => is_member($id), # assume sub returns 0 or 1
SECRET_MESSAGE => 'There is no Perl Illuminati',
);
print $template->output;
</code>
Notice the quotes around the name attribute for a conditional,
these are the only tags that use quotes, and the quotes are
necessary.
<p>
Also, something very important - that last bit of perl code
was not very smart. In his documentation, the author mentions
that a maintenance problem can be created by thinking like
this. Don't write matching conditionals inside your Perl
code. My example is very simple, so it is hard to see how
this could get out of hand. Example 2 would be better as:
<h4>Example 2 (revised)</h4>
<code>
<!-- secret2.tmpl -->
<TMPL_IF NAME="SECRET">
<table><tr>
<td><TMPL_VAR NAME=SECRET></td>
</tr></table>
<TMPL_ELSE>
Move along, nothing to see . . .
</TMPL_IF>
# secret.pl
my $message = 'Yes there is' if is_member($id);
my $template = HTML::Template->new(filename => 'secret2.tmpl');
$template->param(SECRET => $message);
print $template->output;
</code>
Now only one parameter is needed instead of two. $message
will be undefined if <b>is_member()</b> returns false, and since an
undefined value is false, the TMPL_IF for 'SECRET' will be
false and the message will not be displayed.
<p>
By using the same attribute name in the TMPL_IF tag and the
TMPL_VAR tag, decoupling has been achieved. The conditional
in the code is for the message, not the conditional in the
template. The presence of the secret message triggers the
TMPL_IF. This becomes more apparent when using data from
a database - I find the best practice is to place template
conditionals on table column names, not a boolean value
that will be calculated in the Perl script. I will discuss
using a database shortly.
<p>
Now, you may be tempted to simply use one TMPL_VAR tag and
use a variable in your Perl script to hold the HTML code.
Now you don't need a TMPL_IF tag, right? Yes, but that is
wrong. The whole point of HTML::Template is to keep the
HTML out of your Perl code, and to have fun doing it!
<p>
<hr align=center width="50%">
<p>
<b>Loops</b> are more tricky than variables or conditionals,
even more so if you do not grok Perl's anonymous data structures.
HTML::Template's param method will only accept a reference
to an array that contains references to hashes. Here is an example:
<h4>Example 3</h4>
<codE>
<!-- students.tmpl -->
<TMPL_LOOP NAME=STUDENT>
<p>
Name: <TMPL_VAR NAME=NAME><br/>
GPA: <TMPL_VAR NAME=GPA>
</p>
</TMPL_LOOP>
# students.pl
my $template = HTML::Template->new(filename => 'students.tmpl');
$template->param(
STUDENT => [
{ NAME => 'Bluto Blutarsky', GPA => '0.0' },
{ NAME => 'Tracey Flick' , GPA => '4.0' },
]
);
print $template->output;
</code>
This might seem a bit cludgy at first, but it is actually
quite handy. As you will soon see, the complexity of the
data structure can actually make your code simpler.
<p>
<hr align=center>
<h2>A Concrete Example part 1</h2>
So far, my examples have not been very pratical - the real
power of HTML::Template does not kick in until you bring
DBI and CGI along for the ride.
<p>
To demonstrate, suppose you have information about your
mp3 files stored in a database table - no need for worrying
about normalization, keep it simple. All you need to do
is display the information to a web browser. The table
(named songs) has these 4 fields:
<ul>
<li>title</li>
<li>artist</li>
<li>album</li>
<li>year</li>
</ul>
Ok, confesion - I wrote a script that dumped my mp3 files'
ID3 tags into a database table for this tutorial. This
program has no usefulness other than to demonstrate the
features of HTML::Tempate in a relatively simple manner. Onward!
<p>
We will need to display similar information repeatly, sounds like a loop will be needed - one that displays 4
variables. And this time, just because it is possible,
the HTML::Template tags are in the form of HTML comments,
which is good for HTML syntax validation and editor
syntax highlighting.
<h4>Example 4</h4>
<code>
<!-- songs.tmpl -->
<html>
<head>
<title>Song Listing</title>
</head>
<body>
<h1>My Songs</h1>
<table>
<!-- TMPL_LOOP NAME=ROWS -->
<tr>
<td><!-- TMPL_VAR NAME=TITLE --></td>
<td><!-- TMPL_VAR NAME=ARTIST --></td>
<td><!-- TMPL_VAR NAME=ALBUM --></td>
<td><!-- TMPL_VAR NAME=YEAR --></td>
</tr>
<!-- /TMPL_LOOP -->
</table>
</body>
</html>
# songs.cgi
use DBI;
use CGI;
use HTML::Template;
use strict;
my $DBH = DBI->connect(
qw(DBI:vendor:database:host user pass),
{ RaiseError => 1}
);
my $CGI = CGI->new();
# grab the stuff from the database
my $sth = $DBH->prepare('
select title, artist, album, year
from songs
');
$sth->execute();
# prepare a data structure for HTML::Template
my $rows;
push @{$rows}, $_ while $_ = $sth->fetchrow_hashref();
# instantiate the template and substitute the values
my $template = HTML::Template->new(filename => 'songs.tmpl');
$template->param(ROWS => $rows);
print $CGI->header();
print $template->output();
$DBH->disconnect();
</code>
And that's it. Notice what I passed to the HTML::Template
object's param method: one variable that took care of that
entire loop. Now, how does it work? Everything should be
obvious except this little oddity:
<code>
push @{$rows}, $_ while $_ = $sth->fetchrow_hashref();
</code>
<i>fetchrow_hashref</i> returns a hash reference like so:
<code>
{
'artist' => 'Van Halen',
'title' => 'Spanish Fly',
'album' => 'Van Halen II',
'year' => '1979',
};
</code>
This hash reference describes one row. The line of code
takes each row-as-a-hash_ref and pushes it to an array
reference - which is exactly what param() wants for
a Template Loop: "a list (an array ref) of parameter assignments (hash refs)".
<font size="1">(ref: HTML::Template docs)</font>
<p>
Some of the older versions of DBI allowed you to utitize
an undocumented feature. [dkubb] presented it
[id://63435|here]. The result was being able to call
the DBI selectall_arrayref() method and be returned a
data structure that was somehow magically suited for
HTML::Template loops, but this feature did not survive
subsequent revisions.
</p>
<hr>
<h1>Bag of Tricks</h1>
The new() method has quite a few heplful attributes that you can
set. One of them is <i>die_on_bad_params</i>, which defaults to true.
By utilizing this, you can get real lazy:
<code>
# we don't need no stinkin' column names
my $rows = $DBH->selectall_arrayref('select * from songs');
# don't croak on template names that don't exist
my $template = HTML::Template->new(
filename => 'mp3.tmpl',
die_on_bad_params => 0,
);
$template->param(ROWS => $rows);
</code>
It is not good to blindly rely on
<i>die_on_bad_params</i>, but
[id://124526|sometimes] it is necessary.
Just be carefull to note that if someone changes the
name of a column, the script will not report an error, and
you might let the problem go unoticed for a longer period
of time than if you had used <i>die_on_bad_params</i>.
<p>
Another [extremely] useful attribute is <i>associate</i>. When I wrote
my first project with HTML::Template, I ran into a problem:
if the users of my application submitted bad form data, I needed
to show them the errors and allow them to correct them, without
having to fill in the ENTIRE form again.
<p>
In my templates I used variables like so:<br>
<code><input type=text name=ssn value="<TMLP_VAR NAME=SSN>"></code><br>
That way I could populate form elements with database information if the item already existing, or leave them blank when the user was creating a new item. I only needed one template for creating and updating items. (Notice that I named my text box the same as the template variable - also the same as the database field.)
<p>
But when the user had invalid data, they would loose what
they just typed in - either to the old data or to blank
form fields. Annoying!
<p>
That's where <i>associate</i> saves the day. It allows you to inherit
paramter values from other objects that have a <b>param()</b> method
that work like HTML::Template's <b>param()</b> - objects like CGI.pm!
<code>
my $CGI = CGI->new();
my $template = HTML::Template->new(
filename => 'foo.tmpl',
associate => $CGI,
);
</code>
Problem solved! The parameters are magically set, and you
can override them with your own values if need be. No need
for those nasty and cumbersome hidden tags. :)
<p>
<i>loop_context_vars</i> allows you to access 4 variables
that control loop output: first, last, inner, and odd.
They can be used in conditionals to vary your table output:
<code>
<!-- pill.tmpl -->
<table>
<TMPL_LOOP NAME=ROWS>
<tr>
<TMPL_IF NAME="__FIRST__">
<th>the first is usually a header</th>
</TMPL_IF>
<TMPL_IF NAME="__ODD__">
<td style="background: red">odd rows are red</td>
<TMPL_ELSE>
<td style="background: blue">even rows are blue</td>
</TMPL_IF>
<TMPL_IF NAME="__LAST__">
<TD>you have no chance to survive so choose</td>
</TMPL_IF>
</tr>
</TMPL_LOOP>
</table>
# pill.cgi
my $template = HTML::Template->new(
filename => 'pill.tmpl',
loop_context_vars => 1,
);
# etc.
</code>
No need to keep track of a counter in your Perl code,
the conditions take care of it for you. Remember, if you
use a conditional in a template, you should not have to
test for that condition in your code. Code smart.
<p>
<hr align=center>
<h2>A Concrete Example part 2</h2>
Let's supe up our previous song displayer to allow sorting
by the column names. And while we are at it, why bother
hard coding the names of the database fields in the
template. Let's set a goal to store the database field
names in one list and one list only.
<p>
Of course, this means that we will have to design a new
data structure, because the only way to accomplish our
lofty goal cleanly is to use two template loops: one for
each row of data, and one for the indidividual fields
themselves.
<p>
As for sorting - let's just use plain old anchor tags
instead of a full blown form. We can make the headers links
back to the script with a parameter set to sort by the name
of the header: <b><a href="mp3.cgi?sort=title">Title</a></b>.
Also, let's get rid of the hard coded script name, in case
we decide to change the extension from .cgi to .asp,
because we can. CGI.pm provides a method, <b>script_name</b> which returns the name of the script, relative to the web server's root.
<p>
Here is the final example. If you think the Perl code is a
bit convoluted, well you are right, it is. But it is also
flexible enough to allow you add or remove database fields
simply by changing the @COLS list. This makes it trivial to
allow the user to choose which fields she or he sees, an
exercise I leave to the reader, as well as adding the ability to sort fields in descending or ascending order.
<p>
Last note, notice the use of the built-in <DATA>
filehandle to store the template in this script. This
allows you to contain your code and template in one
text document, but still fully seperated. You can specify a
scalar reference in the constructor like so:
<code>
my $template = HTML::Template->new(scalarref => \$scalar);
</code>
And now...
<h4>The Last Example</h4>
<code>
#!/usr/bin/perl -Tw
use DBI;
use CGI;
use HTML::Template;
use strict;
my $DBH = DBI->connect(
qw(DBI:mysql:mp3:host user pass),
{ RaiseError => 1 },
);
my $CGI = CGI->new();
my @COLS = (qw(title artist album));
# verify the sort param - never trust user input
my %sort_lookup = map {$_ => $_} @COLS;
my $sort = $sort_lookup{$CGI->param('sort')||''} || 'title';
my $data = $DBH->selectall_arrayref("
select @{[join(',', @COLS)]}
from songs
order by ?
", undef, ($sort));
# prepare the DS for the headers
my $headers = [
map {{
URL => $CGI->script_name . "?sort=$_",
LINK => ucfirst($_),
}} @COLS
];
# prepare the DS for the rows
my $i;
my $rows = [
map {
my $row = $_;
(++$i % 2)
? { ODD => [ map { {VALUE => $_} } @{$row} ] }
: { EVEN => [ map { {VALUE => $_} } @{$row} ] }
} @{$data}
];
# remove excess blood from ears after that last expression
# read the template as a scalar from DATA
my $html = do { local $/; <DATA> };
# prepare the template and substitute the values
my $template = HTML::Template->new(
scalarref => \$html,
loop_context_vars => 1,
);
$template->param(
HEADERS => $headers,
ROWS => $rows,
SORT => $sort,
);
# print the goods
print $CGI->header();
print $template->output();
$DBH->disconnect();
__DATA__
<html>
<head>
<title>Songs sorted by <TMPL_VAR NAME=SORT></title>
</head>
<body>
<h1>Songs sorted by <TMPL_VAR NAME=SORT></h1>
<table>
<tr>
<TMPL_LOOP NAME=HEADERS>
<th><a href="<TMPL_VAR NAME=URL>"><TMPL_VAR NAME=LINK></a></th>
</TMPL_LOOP>
</tr>
<TMPL_LOOP NAME=ROWS>
<tr>
<TMPL_UNLESS NAME="__ODD__">
<TMPL_LOOP NAME=EVEN>
<td style="background: #B3B3B3"><TMPL_VAR NAME=VALUE></td>
</TMPL_LOOP>
<TMPL_ELSE>
<TMPL_LOOP NAME=ODD>
<td style="background: #CCCCCC"><TMPL_VAR NAME=VALUE></td>
</TMPL_LOOP>
</TMPL_UNLESS>
</tr>
</TMPL_LOOP>
</table>
</body>
</html>
</code>
<hr>
<p>
Thanks to [dkubb] and [deprecated] for corrections; Sam
Tregar for writing the module; [aijin], [orkysoft],
and [bladx] for pointing out typos; and [dws] for
bringing you the letter 'D'.
<p>
See also: [cpan://HTML::Template] and
'<b>perldoc HTML::Template</b>'
after you install it.
<p>