Using CatalystX::Syntax::Action for More Concise Controllers

Overview

This article discusses how to use CatalystX::Syntax::Action to make shorter controllers with less method block boilerplate. You may find it also helps cleanly separate your Catalyst::Controller actions from regular methods.

Classic Controllers

One of the things about Catalyst that is both powerful and yet somewhat perplexing to newcomers is how your actions are just regular methods on a class that inherits from Catalyst::Controller:

    package TestApp::Controller::User;

    use Moose;
    use MooseX::MethodAttributes;

    extends 'Catalyst::Controller';

    sub root : Chained('/') PathPrefix CaptureArgs(0) {
      my ($self, $c) = @_;
      $c->stash(user_rs=>$c->model('Schema::User'));
    }

    sub all_users : Chained('root') PathPart('') Args(0) {
      my ($self, $c) = @_;
      $c->stash(all_users => (delete $c->stash->{user_rs})->all_users);
    }

    sub one_user : Chained('root') PathPart('') Args(1) {
      my ($self, $c, $id) = @_;
      $c->stash(user => (delete $c->stash->{user_rs})->find($id));
    }

    sub regular_method {
      my ($self, @args) = @_;
      ## Some controller specific work
    }

    __PACKAGE__->meta->make_immutable;

If you are familiar with Catalyst the above probably doesn't seem so surprising to you. If we assume the example controller is lifted from a 'classic' style application (with a ::Root controller that has a base action from which everything chains off, and an end action which inherits from Catalyst::Action::RenderView) we can see that the following HTTP requests would be handled:

    http://localhost/user  => maps to ->all_users
    http://localhost/user/1 => maps to ->one_user and sets $id to 1

However for people new to the framework, there is often a lot of confusion regarding the difference between an action and a regular controller method, and when to use which. Also, it is not always immediately evident what is what when opening a Catalyst::Controller based class for the first time. And compared to some of the newer web frameworks floating around CPAN, it might seem like the syntax for Catalyst::Controllers is a bit, "over the hill".

More Modern Controllers?

Catalyst developers, in our quest to come with a more modern syntax that retains the overall power and flexibility, and hopefully solves some of the bigger problems with classic contollers, have experimented broadly. You can review CPAN and spot ideas like CatalystX::Declare or CatalystX::Controller::Sugar. In these cases, the authors have tried replacing the 'Plain old class' syntax with a type of domain specific language that is designed to better express the type of logic one needs to build good controllers.

Although the syntax is certainly shiny, the downside is that we no longer can leverage all our existing knowledge about how to properly model straight forward classes using standard practices (Like inheritance, roles and so forth.) We have to work within the DSL. Additionally those experiments are internally documented but are not part of the broader documentation ecosystem.

Mentioning these possible downsides, I am not intending to disparge the authors' efforts, but merely to point out upsides and downsides. Each project should seriously consider the value and demerits of all the possible approaches and choose something that makes sense for the team and business need.

In order to split the difference between trying to introduce shiny new syntax and yet keep things similar enough to the vast amounts of Catalyst documentation as to be immediately recognizable and understandable, I've introduced CatalystX::Syntax::Action on CPAN.

CatalystX::Syntax::Action sticks with the idea that a Controller is just a specialized class, so it doesn't really add a new domain specific language. What it does is add a bit of Devel::Declare based magic to create a new Perl keyword action which just encapsulates some of the basic boilerplate that goes into your Catalyst Controllers. It also plays nice with the syntax namespace and can be a good part of that ecosystem as well. Hopefully someone that did the Catalyst tutorial and read the book would be able to immediately understand this syntax, and would not require much additional study. Let's rewrite the above controller using CatalystX::Syntax::Action and some other members of the syntax ecosystem:

    package TestApp::Controller::User;

    use Moose;
    use MooseX::MethodAttributes;
    use syntax 'method', 'catalyst_action';

    extends 'Catalyst::Controller';

    action root : Chained('/') PathPrefix CaptureArgs(0) {
      $ctx->stash(user_rs=>$ctx->model('Schema::User'));
    }

    action all_users : Chained('root') PathPart('') Args(0) {
      $ctx->stash(all_users => (delete $ctx->stash->{user_rs})->all_users);
    }

    action one_user($id) : Chained('root') PathPart('') Args(1) {
      $ctx->stash(user => (delete $ctx->stash->{user_rs})->find($id));
    }

    method regular_method(@args) {
      ## Some controller specific work
    }

    __PACKAGE__->meta->make_immutable;

There's not a big difference here, hopefully if you understood the 'classic' controller, this change would not confuse you. Let's review the changes:

    use syntax 'method', 'catalyst_action';

When you installed CatalystX::Syntax::Action (via your Makefile.PL hopefully) we added support for the catalyst_action argument to the syntax pragma. This is a pragma for managing adding syntax features to Perl. In this case we also are using Syntax::Feature::Method, which adds a method argument as well. You can review the Syntax::Feature namespace on CPAN for more examples of pluggable syntax you can leverage in your code.

    action one_user($id) : Chained('root') PathPart('') Args(1) {
      $ctx->stash(user => (delete $ctx->stash->{user_rs})->find($id));
    }

    method regular_method(@args) {
      ## Some controller specific work
    }

The action and method keywords basically wrap the sub keyword, adding two features. First of all action will automatically expose two lexically scoped variables to your method block, $self and $ctx. These exactly map to my ($self, $c) = @_ in the classic controller. We use $ctx over $c in order to reduce confusion and to canonicalize evolving practices within the Catalyst community.

Secondly, it lets you specify arguments in the method prototype, which may seem nicer to programmers coming to Perl and Catalyst from other languages. It also leads to a bit less boilerplate code since you don't have to shift or map from @_ to get lexical variable into your method scope.

In all other cases methods that are declared with the action keyword function identically to those declare with a sub.

For how the method keyword works, you should look at Syntax::Feature::Method but the one sentence summary is that it is just like action but of course we don't create a lexically scoped $ctx since you are not expecting one.

So that is all there is, basically just like classic controllers, but a bit less boilerplate in the method blocks, and some support for method prototypes that hopefully will be attractive to programmers used to having those in other programming languges. I also find that the keywork action really jumps out at me in my source code, and makes it easier to immediate distinguish actions from regular methods.

Summary

Authors in the Catalyst ecosystem continue to experiment with possible approaches for advanced, new techniques for building concise Controllers that are both easy to understand and do what programmers need with a minimum of boilerplate code. Additionally, we want to consider how we can keep well synchronized with the broader ecosystem of code on CPAN and with evolving best practices for modeling objects. One of the great things about Catalyst is how well it lives in the larger Perl world. It tries hard to not introduce too much new syntax or practices, but instead wishes to say, "This is how one builds great Perl software, and we are going to be a good member of that community."

That is certainly something we'd wish to keep! Happy Holidays to all!

AUTHOR

John Napiorkowski <jjnapiork@cpan.org> or jnap on IRC.

Thanks to Shutterstock (http://www.shutterstock.com/jobs.mhtml) for giving me a bit of time to review and craft this advent article.