Serving Templated Directories from a Catalyst Controller Role

One of my web projects has several directories which contain, essentially, static files with a little bit of dynamic content in the wrapper. This dynamic content is specific to each user—a change in navigation, some customization.

The prototype of this project split the responsibility for serving the site between the frontend web server and the backend Plack server. As you can imagine, it worked well enough for a prototype, but it clearly needed replacement once the project became serious.

I didn't want to invest a lot of effort into writing a bunch of unique controllers for each directory with templated files (/about, /contact, /resources, et cetera), but I want to share behavior between all of these routes.

Essentially I have several subdirectories in my templates/ directory which correspond to these routes. Each subdirectory contains one or more templates for each resource within that route, such as /about/jobs and /contact/press. Something in my Catalyst application needs to be able to resolve those requests to those templates.

That's easy enough.

I ended up factoring this code into a parametric role to apply to controllers because I want to reuse this code elsewhere, and because it needs a bit of customization. Here's all it is:

package Stockalyzer::Controller::Role::HasStaticPath;
# ABSTRACT: maps controller methods to static routes

use Modern::Perl;
use MooseX::Role::Parameterized;

parameter 'routes', required => 1;
parameter 'for',    required => 1;

role
{
    my $p     = shift;
    my $class = $p->for;

    for my $route (@{ $p->routes })
    {
        my $template = $route . '.tt';

        method $route => sub
        {
            my ($self, $c) = @_;
            my $args       = $c->req->args;
            my $page       = $args->[0] || 'index';

            return $c->res->redirect( "/$route/$page" ) if $page =~ s/\..+$//;

            $c->stash(
                template => $template,
                page     => $page,
            );
        };

        $class->config( action => { $route => { Local => '' } } );
    }
};

1;

A few things stand out. This role has two required parameters, an array reference of routes to add to the composing controller and the name of the controller. The latter will be obvious shortly.

Each route corresponds to a single template file which itself has a page attribute. I do this so that each subdirectory can have its own layout. (You can accomplish this in many ways with Template Toolkit, but this approach works best for me right now.)

This action doesn't use Catalyst's own path handling to handle any arguments, so it effectively takes as many path components as you can provide. If there's no path component beyond the name of the route, this request defaults to the index page. (The redirection is a temporary measure because the system has a few links to static pages such as /about/jobs.html we're changing to /about/jobs.)

The only remaining interesting part of the code is the call to the controller's config() method. This is the reason for the for parameter to this role. Because these controller methods come from the role, and because the actual body of the method is a closure, Catalyst can't easily process the normal function attributes you normally use to select special behaviors. I want to define these methods like:

sub about   :Local { ... }
sub contact :Local { ... }

... and so the alternate approach is to set configuration parameters for each method. That's all the final line of code in the role application block does.

Using this code is easy. My root controller contains a single line:

with 'Stockalyzer::Controller::Role::HasStaticPath'
    => { routes => [qw( about strategy )], for => __PACKAGE__ };

The best part is that if I want to add a new subdirectory, mapping it in Catalyst means adding a single entry to this list. Better yet, if I want to add features to these subdirectories (and I do, per How Would You Track User Behavior with Plack and Catalyst?, for the purpose of cohort logging), I can add it in one place.

All of this demonstrates one of my favorite features of Modern Perl: well-designed abstractions are available when they're necessary. (One of my other favorite features of Modern Perl is that someone's probably already done this, and I just have to find the right plugin on the CPAN to make it happen.)

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on August 15, 2012 8:49 AM.

Why I Use Perl: Testing was the previous entry in this blog.

Annotating User Events for Cohort Analysis is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Powered by the Perl programming language

what is programming?