Creating reusable actions with Moose::Role - an example

overview

Have you ever implemented an action, which operates on one database entry, and
later realized
that it would be useful to apply this action to several entries at once?

Since Catalyst applications are, in general, very modular and easy to extend,
it is not a big problem to create some kind of wrapper-action, which prepares
the appropriate data and forwards to your action. There is nothing wrong about
solving the problem that way, but you have to repeat this for every
action (that shoud be applied to several database entries at once). You will
soon realize that most of your wrapper-actions are more or less identical, and
writing the same code again and again can be a big pain in the
CurseWord.

An other solution would be to adjust your action, but what if you still need
that old action (which only operates on one single database entry)? In that
case, you have to do a lot of parameter checking to figure out whether the
current request is a single-entry-request or a multi-entry-request. In the
worst case, this procedure does not work as you expected and you have to spend
a lot of time debugging your code, which is a even bigger pain in the ...
You know what I mean!

This Article describes how to create a generic multi-action which can easily
be used as a wrapper for any action, with minimal changes to your code.
The generic multi-action will be moved from the controller to an extra
Moose::Role. Since Roles can easily be applied to any Moose object, the
multi-action can be reused in any controller, with minimal effort.

Chapter 5 from the Catalyst Tutorial is used as example application (which is
some kind of book database, with the ability to delete single books by calling
the delete-action with the books id as parameter).
The code provided in this article will add the ability to select several books
from the list, and delete all of them at once.

Preparation

This Article is based on the example code provided by the Catalyst tutorial, chapter 5.
The code can be checked out by running:

This method searches the request parameters for possible Arguments to the
requested action. It returns a reference to a list, containting CatupreArgs
and Args for
each selected database-entry, or undef if no entries where selected.

Test the multiaction:

Start the test application, and point your browser to
localhost:3000/books/list

You should be able to select several books and delete them by clicking the
"delete selected" button

Increasing reusability

At this point, we created a generic multi-action-wrapper in our Books-controller.
The next part of this article shows how to increase reusability by moving
the code from the controller to an Moose::Role.

Creating the role

* Create the file lib/MyApp/MultiAction.pm and add the following Content:

Remove all new created code from lib/MyApp/Controller/Books.pm and paste it to
lib/MyApp/MultiAction.pm, after "use namespace::autoclean", but before "1;"

* Adapt the code

* Change the methodattribute from multiaction to ":Action"

After that the first line of your multiaction method should look like this:

sub multiaction :Action {

* Add requirements

Because our Role itself is not a Catalyst Controller, we have to make sure
that the required methods - list and action_for - are present. Add

requires qw/list action_for/;

anywhere between "use namespace::autoclean" and "1;". After that, our Role can
only be applied to Objects which provide this two methods.

Using the role

At this point, all new functionality has moved from our controller to a Moose::Role.
The only thing left is to apply the role to our controller:

Open lib/MyApp/Controller/Books.pm again and

* Make the controller to use the role

by adding

with 'MyApp::MultiAction';

after the BEGIN section at the top of the file.

Note: the with statement MUST NOT be included in the BEGIN section, because this
would make the perl interpreter to apply the role BEFORE the list method has
been compiled, which would result in a compile time error.

Test the Role

Start the test-application, and point your browser to
localhost:3000/books/list

You should be able to select several books and delete them by clicking the
"delete selected" button, just as before.

Adapt the behaviour of "multiaction" for a single controller

In addition to a better code structure, implenting a helper-action for every
identified task has another big benefit: You can change the behaviour of
every task, by just overriding the corresponding method in your controller.

EXAMPLE: The status-report created by our Role is very generic - and very ugly.
We can change the report for our deleted Books by overiding the "create_status_msg"
method in lib/MyApp/Controller/Books.pm.

Note: Methods implemented in Roles can not be overridden with the "override" and
"augment" pragmas provided by Moose. (This is, because these methods are not
inherited from a parent class. They are implemented in the calling class itself.)
If you don't need the original method at all, you can just redefine the complete
method in your controller. If you only want to change the behaviour under certain
conditions, and otherwise stick to the original behaviour, you can use Mooses
"before", "after" and "around" functionality.
In the above example, the status message is only changed for "delete" actions.
All other multiactions will produce the old, generic messages.
See Moose::Manual::MethodModifiers for details.

Reusing the multiaction

* using the role in a different controller

To use the role in another controller, just apply the role to it, and activate the
multiaction, as described above. Don't forget to update your list-template aswell.

* adding more multiactions

If you want to use the multiaction-feature with another action, implement you action
in your controller, and add a corresponding submit-button to your list
template.

Notes:

When implementing the multiaction as described above, most of your code
is generic and reusable, but the templates used in this example are very simple.
As long as your actions expect the entries ids as arguments or captureargs,
everything is fine. If your actions need more complex parameters, you have
to improve your templates. If different actions need very different sets of parameters,
you have to adapt your get_args-method. The name of the requested action is passed
as a parameter to get_args. Use it to find out what arguments you have to provide.

Conclusion

It is possible to apply an action to several datasets at once, by creating
a generic wrapper-method which calculates the correct parameters and
calls the requested action.

Moving actions from the Controller to a Moose::Role is one possibility to
make you code reuseable for any controller.
One benefit of using roles is, that they can easily be applied to any Moose
object, as long as this object fulfills all of the roles requirements.
The Objects do not even have to be Catalyst Controllers. If you want to
use your code in a Catalyst-independent application, you can do so. All you have
to care about is, that you code passes the correct parameters to the roles methods.

Splitting an action into several (more or less) atomic methods
makes your code more readable and adds the possibility to finetune
your actions behaviour on a per-controller basis.

The lack of some missing method modifiers in Roles can be bypassed with
standart Moose functionality, and without black magic.