The ControllerRole ChainAction Massacre (Part 1)

Overview

About this Article

The article will start with an introduction on how to implement
chained actions in Moose::Roles. The second part will deal with
increasing reusability of your ControllerRoles by following some simple
rules.

About the title

Did you see the movie "The Texas Chain Saw Massacre"?
I did. Did you like it? Well, I didn't. But I loved the title.
Thats all!

Chaining actions in Moose::Role

Motivation

Most Catalyst applications consist of several actions, distributed among
several controllers. Most of these actions
share some (more or less) identical code, for example if
they work with data provided by a model.
Connecting to a database, fetching the required data and storing it in a local
variable is not complicated, but it has to be done at the beginning of each
action.
I noticed that the first lines of code are almost identical for most of my actions:

Points of interest

* The dispatcher distinguishes between "Args" and "CaptureArgs".
CaptureArgs are "eaten" by their method. This means they are removed from the
argument list and are not visible to any action chained to the capturing one.
"Args" should only be configured for a last action in a chain.

* Since the id is now a CaptureArg for the "get_item" action, its
position in the path changes. Because the get_item action also has a PathPart,
the path of "foo" changes from

"/foo/$id"

to

"/get_item/$id/foo"

* Empty PathParts are allowed. By setting an empty PathPart for
the "get_item" action

sub get_item :Chained('/') :PathPart("") :CaptureArgs(1){
...

The resulting path will change from

"/get_item/$id/foo"

to

"/$id/foo"

which is more beautiful in my opinion. Take care that the paths stay unique!

* uri_for_action knows how to handle CaptureArgs. See the Catalyst documentation for details.

Role Baby Role!

At this point, we know that Catalyst makes it easy to reuse code by creating chained actions. But we still have to make our code available in our controller.

Using roles makes reusing your code easy. Roles allow you to specify subroutines and attributes. They will be
present in any class which has the role applied to it. Since Catalyst Controllers are Moose objects, applying a role to it is as easy as
adding

with "RoleName";

to your class.
The CPAN module MooseX::MethodAttributes::Role makes it possible to add method attributes to subroutines.
This allows you to implement complete Catalyst actions, including "Path", "Args", "Chains" and whatever you need.

To make the "get_item" action more reusable, move it to a role as shown in the following example:

If you feel like using the actions "foo" and "bar" in several controllers, you can move their code to roles aswell.
You can ensure that the actions you are chaining to are present in your controller by using Moose's require
keyword. Keep in mind that this ensures that the required subroutine is present, but it does not require it to be a Catalyst action.
The resulting roles for "foo" and "bar" will look like this:

Now it is possible to "plug" your actions to any controller by applying the corresponding roles to them.
You should consider changing some PathParts in your controllers, otherwise the actions will have the same
path in all controllers:

By creating chained actions and putting them into ControllerRoles, it is possible to
create some kind of "application bricks" which can easily be added to any controller.
If you have implemented some functionality once, and you need it somewhere else,
you can enable it by adding a single line of code to your controller.

Creating applications that way reminds me of playing with Lego. The only difference is that
I can create my own Lego-bricks, and modify existing bricks if they do not exactly fit my needs.
A childhood dream comes true. It's kind of cool, isn't it?

Increasing reusability

Code-reusability in the first chapter is very limited. This chapter will tell you why, and shows some simple tricks how to make your code more reusable:

Oh my tiny little actions!

One problem in the previous example is that the "get_item" action does more than getting one item. It can only be used by actions which
require exactly one item in the stash. By splitting "get_item" into three atomic parts, the code gets even more reusable:

If you realize that some controllers never need one item, but often need the model and resultset, you can distribute this actions
among two or three roles (named "MyGetModel", "MyGetRS" and "MyGetItem"). Remember to require the actions you are chaining to!

Distributing the code among several roles makes your code more flexible. If one of your controllers should get the model in
a different way, but needs the same resultset and item code, you can consume the "MyGetRS" and "MyGetItem" roles and implement
the "get_model" action in your controller. If you often need all actions in the same controller, and you don't want to write 3 three
"with"-lines, you can create a role which includes all three actions:

Don't force me! Don't force yourself! Don't force anybody!

The second problem in my example is that several things which should be flexible
are hardcoded in my roles. The most important examples are the name of the model and the
name of the table. This means that we can easily add these actions to any controller, but all
controllers would do EXACTLY the same thing, which is not what we want. Even if you plan to
use your role exactly once in each application, you force your application to use the same
model name and table name as your role.

Using attributes to store these information makes your roles configurable and much more
reusable:

Whats your name?

The next problem is related to the previous one. The stash-keys of model, resultset and item are hardcoded aswell.
This may result in conflicting names, overwritten values in the stash and a lot of trouble. Avoid this by making the
stash-keys configurable aswell. The default values can even be created dynamically:

In this example, the default stash keys are created dynamically from the controllers name.
If you apply the roles to a controller named "MyApp::Controller::Foo", the model will be
stashed as "foo_model". If you don't like this behaviour, you can override the default in
the __PACKAGE__->config(...);

Sorry, babe! I don't remember you...

Now we know how to write reusable, modular and configurable code with chained actions and Moose::Role.
My last tip is not new. In fact, its kind of old-fashioned:: Choose "good" names for your roles, and PLEASE write a POD for your modules!
If you use ControllerRoles as intensive as I do, most of your controllers will only consist of the package name, a few
"Moose" lines, the list of consumed roles and some configuration.
If you choose "good" names for your roles, the controllers code will be more or less self-explanatory.
If you don't choose your names wisely, it will be hard to understand what the consuming controller
does.

Here is an Example: Try to guess the purpose of the following controllers:

Hard to guess. Is this a CRUD controller? It might use chained actions...

When you write the documentation for your roles, remember to include all attributes and actions.
You should include some extra information about your chained actions:

* how many arguments does this action expect by default, and which

* what items does this action expect to be in the stash

* which items in the stash are modified

* which items are added to the stash

* which keys are used for each of the items, and where do the keys come from

When you add all these information, everybody (including yourself) will be able to
understand the purpose of your roles, and how to use them. Well, not everybody will
be able to understand your code. Maybe not even you. But the chance that yourself and others
understand and use your code increases.

Conclusion

* Chaining actions can make your code more reusable

* Making your actions as atomic as possible increases flexibility and reusability

* It is possible to write reuseable, chained actions in Moose::Role's

* Making your roles as configurable as possible dramatically increases the chance that you (and others) find it usefull in other projects

* Moose helps you making your modules configurabel in an easy and flexible way

* Roles can easily be applied to controllers. This makes it possible to create small
"Controller-Bricks" which can be plugged to almost every controller.

* Clever naming and documentation is mandatory

* The author does not like violent movies, but he sometimes likes violent titles