Perl CookbookPerl CookbookSearch this book

20.9. Using Templates to Generate HTML

20.9.1. Problem

You want to store a parameterized template in an external file, read that template from your CGI script, and substitute your own variables for escapes embedded in the text. This separates your program from the static parts of the document.

20.9.2. Solution

To expand only variable references, use this template function:

sub template {
    my ($filename, $fillings) = @_;
    my $text;
    local $/;                    # slurp mode (undef)
    open(my $fh, "<", $filename) or return;
    $text = <$fh>;               # read whole file
    close($fh);                  # ignore retval
    # replace quoted words with value in %$fillings hash
    $text =~ s{ %% ( .*? ) %% }
              { exists( $fillings->{$1} )
                      ? $fillings->{$1}
                      : ""
              }gsex;
    return $text;
}

on a data file like this:

<!-- simple.template for internal template( ) function -->
<HTML><HEAD><TITLE>Report for %%username%%</TITLE></HEAD>
<BODY><H1>Report for %%username%%</H1>
%%username%% logged in %%count%% times, for a total of %%total%% minutes.

If you can guarantee the data file is secure from tampering, use the CPAN module Text::Template to expand full expressions. A data file for Text::Template looks like this:

<!-- fancy.template for Text::Template -->
<HTML><HEAD><TITLE>Report for {$user}</TITLE></HEAD>
<BODY><H1>Report for {$user}</H1>
{ lcfirst($user) } logged in {$count} times, for a total of 
{ int($total / 60) } minutes.

For a complete templating solution, see the Template Toolkit's Template module This offers a scripting language and mod_perl integration, and is covered in Recipe 21.17.

20.9.3. Discussion

Parameterized output for your CGI scripts is a good idea for many reasons. Separating your program from its data lets other people (art directors, for instance) change the HTML but not the program. Even better, two programs can share the same template, so style changes in the template are immediately reflected in both programs' output.

For example, suppose you have the first template from the Solution stored in a file. Then your CGI program contains the definition of the template subroutine shown earlier and makes appropriate settings for variables $username, $count, and $total. You can fill in the template by simply using:

%fields = (
            username => $whats_his_name,
            count    => $login_count,
            total    => $minute_used,
);

print template("/home/httpd/templates/simple.template", \%fields);

The template file contains keywords surrounded by double percent symbols (%%KEYWORD%%). These keywords are looked up in the %$fillings hash whose reference was passed as the second argument to template. Example 20-7 is a more elaborate example using an SQL database.

Example 20-7. userrep1

  #!/usr/bin/perl -w
  # userrep1 - report duration of user logins using SQL database
  
  use DBI;
  use CGI qw(:standard);
  
  # template( ) defined as in the Solution section above
  
  $user = param("username")                   or die "No username";
  
  $dbh = DBI->connect("dbi:mysql:connections:mysql.domain.com",
      "connections", "seekritpassword")       or die "Couldn't connect\n";
  $sth = $dbh->prepare(<<"END_OF_SELECT")     or die "Couldn't prepare SQL";
      SELECT COUNT(duration),SUM(duration) 
      FROM logins WHERE username='$user'
  END_OF_SELECT
  
  # this time the duration is assumed to be in seconds
  if (@row = $sth->fetchrow_array( )) {
      ($count, $seconds) = @row;
  } else {
      ($count, $seconds) = (0,0);
  } 
  
  $sth->finish( );
  $dbh->disconnect;
  
  print header( );
  print template("report.tpl", {   
      'username' => $user,
      'count'    => $count,
      'total'    => $total 
  });

For a fancier, more flexible solution, look at the second template in the Solution section, which relies upon the CPAN module Text::Template. Contents of braces found within the template file are evaluated as Perl code. Ordinarily, these substitutions are just simple variables:

You owe: {$total}

but they can also include full expressions:

The average was {$count ?  ($total/$count) : 0}.

Example 20-8 is an example of using that template.

Example 20-8. userrep2

  #!/usr/bin/perl -w
  # userrep2 - report duration of user logins using SQL database
  
  use Text::Template;
  use DBI;
  use CGI qw(:standard);
  
  $tmpl = "/home/httpd/templates/fancy.template";
  $template = Text::Template->new(-type => "file", -source => $tmpl);
  $user = param("username")                   or die "No username";
  
  $dbh = DBI->connect("dbi:mysql:connections:mysql.domain.com",
      "connections", "secret passwd")         or die "Couldn't db connect\n";
  $sth = $dbh->prepare(<<"END_OF_SELECT")     or die "Couldn't prepare SQL";
      SELECT COUNT(duration),SUM(duration) 
      FROM logins WHERE username='$user'
  END_OF_SELECT
  
  $sth->execute( )                             or die "Couldn't execute SQL";
  
  if (@row = $sth->fetchrow_array( )) {
      ($count, $total) = @row;
  } else {
      $count = $total = 0;
  }
  
  $sth->finish( );
  $dbh->disconnect;
  
  print header( );
  print $template->fill_in( );

But this approach raises security concerns. Anyone who can write to the template file can insert code that your program will run. See Recipe 8.17 for ways to lessen this danger.

20.9.4. See Also

The documentation for the CPAN modules Text::Template and Template; Recipe 8.16; Recipe 14.9



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.