O'Reilly logo

Embedding Perl in HTML with Mason by Ken Williams, Dave Rolsky

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Storage: Replacing the Resolver

Occasionally, people on the Mason users list wonder if they can store their component source in an RDBMS. The way to achieve this is to create your own HTML::Mason::Resolver subclass.

The resolver’s job is take a component path and figure out where the corresponding component is.

We will show an example that connects to a MySQL server containing the following table:

MasonComponent
----------------------------------------
path           VARCHAR(255)  PRIMARY KEY
component      TEXT          NOT NULL
last_modified  DATETIME      NOT NULL

Our code starts as follows:

package HTML::Mason::Resolver::MySQL;
$VERSION = '0.01';

use strict;

use DBI;
use Params::Validate qw(:all);

use HTML::Mason::ComponentSource;
use HTML::Mason::Resolver;
use base qw(HTML::Mason::Resolver);

_ _PACKAGE_ _->valid_params
    (
     db_name  => { parse => 'string', type => SCALAR },
     user     => { parse => 'string', type => SCALAR, optional => 1 },
     password => { parse => 'string', type => SCALAR, optional => 1 },
    );

These parameters will be used to connect to the MySQL server containing our components. Readers familiar with the Perl DBI will realize that there are a number of other parameters that we could take.

Our constructor method, new( ), needs to do a bit of initialization to set up the database connection, so we override our base class’s method:

sub new {
    my $class = shift;
    my $self = $class->SUPER::new(@_);

We invoke the new( ) method provided by our superclass, which validates the parameters in @_ and makes sure they get sent to the right contained objects. The latter concern doesn’t seem so important in this case since we don’t have any contained objects, but the point is that if somebody subclasses our HTML::Mason::Resolver::MySQL class and adds contained objects, our new( ) method will still do the right thing with its parameters.

Now we connect to the database in preparation for retrieving components later:

    $self->{dbh} =
        DBI->connect
            ( "dbi:mysql:$self->{db_name}",
              $self->{user}, $self->{password}, { RaiseError => 1 } );
    
    return $self;
  }

A resolver needs to implement two methods left unimplemented in the parent HTML::Mason::Resolver class. These are get_info() and glob_path(). The first is used to retrieve information about the component matching a particular component path. The second takes a glob pattern like /path/* or /path/*/foo/* and returns the component paths of all the components that match that wildcard path.

Additionally, if we want this resolver to be usable with the ApacheHandler module, we need to implement a method called apache_request_to_comp_path( ) , which takes an Apache object and translates it into a component path.

Given a path, we want to get the time when this component was last modified, in the form of a Unix timestamp, which is what Mason expects:

sub get_info {
    my ($self, $path) = @_;

    my ($last_mod) =
        $self->{dbh}->selectrow_array
            ( 'SELECT UNIX_TIMESTAMP(last_modified) 
               FROM MasonComponent WHERE path = ?',
              {}, $path );
return unless $last_mod;

If there was no entry in the database for the given path, we simply return, which lets Mason know that no matching component was found:

return
    HTML::Mason::ComponentSource->new
        ( comp_path => $path,
          friendly_name => $path,
          last_modified => $last_mod,
          comp_id => $path,
          source_callback => sub { $self->_get_source($path) },
        );
  }

The get_info() method returns its information in the form of a HTML::Mason::ComponentSource object. This is a very simple class that holds information about a component.

Its constructor accepts the following parameters:

comp_path

This is the component path as given to the resolver.

friendly_name

The string given for this parameter will be used to identify the component in error messages. For our resolver, the component path works for this parameter as well because it is the primary key for the MasonComponent table in the database, allowing us to uniquely identify a component.

For other resolvers, this might differ from the component path. For example, the filesystem resolver that comes with Mason uses the component’s absolute path on the filesystem.

last_modified

This is the last modification time for the component, as seconds since the epoch.

comp_id

This should be a completely unique identifier for the component. Again, since the component path is our primary key in the database, it works well here.

source_callback

This is a subroutine reference that, when called, returns the source text of the component.

Mason could have had you simply create an HTML::Mason::ComponentSource subclass that implemented a source() method for your resolver, but we thought that rather than requiring you to write such a do-nothing subclass, it would be easier to simply use a callback instead.

Our _get_source() method is trivially simple:

sub _get_source {
    my $self = shift;
    my $path = shift;

    return 
        $self->{dbh}->selectrow_array
            ( 'SELECT component FROM MasonComponent WHERE path = ?', {}, $path );
}
comp_class

This is the component class into which this particular component should be blessed when it is created. This must be a subclass of HTML::Mason::Component. The default is HTML::Mason::Component.

extra

This optional parameter should be a hash reference. It is used to pass information from the resolver to the component class.

This is needed since an HTML::Mason::Resolver subclass and an HTML::Mason::Component subclass can be rather tightly coupled, but they must communicate with each other through the interpreter (this may change in the future). Next is our glob_path() method:

sub glob_path {
    my $self = shift;
    my $pattern = shift;

    $pattern =~~ s/*/%/g;

The pattern given will be something that could be passed to Perl’s glob() function. We simply replace this with the SQL equivalent for a LIKE search:

return
    $self->{dbh}->selectcol_array
        ( 'SELECT path FROM MasonComponent WHERE path LIKE ?', {}, $pattern );
  }

Then we return all the matching paths in the database.

Since we may want to use this resolver with ApacheHandler, we will also implement the apache_request_to_comp_path( ) method:

sub apache_request_to_comp_path {
    my $self = shift;
    my $r = shift;

    my $path = $r->uri;

    return $path
        if $self->{dbh}->selectrow_array
            ( 'SELECT 1 FROM MasonComponent WHERE path = ?', {}, $path );

    return undef unless $r->path_info;

    $path .= $r->path_info;

    return $path
        if $self->{dbh}->selectrow_array
            ( 'SELECT 1 FROM MasonComponent WHERE path = ?', {}, $path );

    return undef;
}

We generate a component path by taking the requested URI and looking for that in the database. If it doesn’t exist, we will try appending the path info if possible or just give up. Finally, we try the altered path and, if that doesn’t exist either, we just give up and return undef, which will cause the ApacheHandler module to return a NOT FOUND status for this request.

That’s it, all done. And nothing left as an exercise for the reader this time.

As with the lexer, this can be used either via a httpd.conf directive:

PerlSetVar  MasonResolverClass  HTML::Mason::Resolver::MySQL

or by passing the resolver_class parameter to the new() method for HTML::Mason::Interp.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required