One of the features I enjoy the most about Perl's Catalyst MVC web framework is action chaining. In action chaining you can use the chain of command pattern to logically group and reuse actions that can span and share a URL. Here's an example for a controller that is a classic CRUD style setup for webpages to view a list of posts, edit, delete and view an individual post and create a post. For ease of explanation I'm leaving off a lot of stuff I'd normally do like validation on incoming POST parameters, etc.
First a root controller that everything chains off from:
package Example::Controller::Root;
use Moose;
use MooseX::MethodAttributes;
use feature 'signatures';
extends 'Catalyst::Controller;
# URL-PART: /...
sub root :Chained('/') PathPart('') CaptureArgs(0) ($self, $c) {
$c->stash->{user} = $c->user;
}
__PACKAGE__->config(namespace=>'');
__PACKAGE__->meta->make_immutable;
Next, the posts controller that continues the chain and defines endpoints:
package Example::Controller::Posts;
use Moose;
use MooseX::MethodAttributes;
use feature 'signatures';
extends 'Catalyst::Controller;
# URL-PART: /posts/...
sub root :Chained('../root') PathPart('posts') CaptureArgs(0) ($self, $c) {
$c->stash->{posts} = $c->stash->{user}->posts;
}
# GET /posts
sub list :GET Chained('root') PathPart('') Args(0) ($self, $c) {
}
# POST /posts
sub create :POST Chained('root') PathPart('') Args(0) ($self, $c) {
$c->stash->{posts}->create($c->req->body_parameters);
}
# GET /posts/new
sub build :GET Chained('root') PathPart('new') Args(0) ($self, $c) {
$c->stash->{new_post} = $c->stash->{posts}->build;
}
# URL-PART: /posts/{$id}/...
sub find :Chained('root') PathPart('') CaptureArgs(1) ($self, $c, $id) {
$c->stash->{post} = $c->stash->{posts}->find($id);
}
# GET /posts/{$id}
sub show :GET Chained('find') PathPart('') ($self, $c) {
}
# DELETE /posts/{$id}
sub delete :DELETE Chained('find') PathPart('') ($self, $c) {
$c->stash->{post}->delete;
}
# GET /posts/{$id}/edit
sub edit :GET Via('find') At('edit') ($self, $c) {
}
# PATCH /posts/{$id}
sub update :PATCH Via('find') PathPart('') ($self, $c) {
$c->stash->{post}->update($c->req->body_parameters);
}
__PACKAGE__->meta->make_immutable;
In this flow we use the stash a lot just to pass information from one action to another. Its a lot of extra typing that makes the code look messy and in a more real life example its easy to lose track of exactly whats in the stash at a given moment. Additionally you have a lot of namespace pollution in the stash (stash keys that are used by intermediate actions stick around to the end) and of course you have the classic issue with the stash of introducing hard to spot bugs with typos in the stash call:
$c->stash->{Posts}...
Instead, what if there was a way to directly pass arguments from a chain action directly to its children (and only its children?). Lets see what that would look like:
package Example::Controller::Root;
use Moose;
use MooseX::MethodAttributes;
use feature 'signatures';
extends 'Catalyst::Controller;
# URL-PART: /...
sub root :Chained('/') PathPart('') CaptureArgs(0) ($self, $c) {
$c->action->next(my $user = $c->user);
}
__PACKAGE__->config(namespace=>'');
__PACKAGE__->meta->make_immutable;`
Next, the posts controller that continues the chain and defines endpoints:
package Example::Controller::Posts;
use Moose;
use MooseX::MethodAttributes;
use feature 'signatures';
extends 'Catalyst::Controller;
# URL-PART: /posts/...
sub root :Chained('../root') PathPart('posts') CaptureArgs(0) ($self, $c, $user) {
$c->action->next(my $posts = $user->posts);
}
# GET /posts
sub list :GET Chained('root') PathPart('') Args(0) ($self, $c, $posts) {
}
# POST /posts
sub create :POST Chained('root') PathPart('') Args(0) ($self, $c, $posts) {
$posts->create($c->req->body_parameters);
}
# GET /posts/new
sub build :GET Chained('root') PathPart('new') Args(0) ($self, $c, $posts) {
my $new_post = $posts->build;
}
# URL-PART: /posts/{$id}/...
sub find :Chained('root') PathPart('') CaptureArgs(1) ($self, $c, $posts, $id) {
$c->action->next(my $post = $posts->find($id));
}
# GET /posts/{$id}
sub show :GET Chained('find') PathPart('') ($self, $c, $post) {
}
# DELETE /posts/{$id}
sub delete :DELETE Chained('find') PathPart('') ($self, $c, $post) {
$post->delete;
}
# GET /posts/{$id}/edit
sub edit :GET Chained('find') PathPart('edit') ($self, $c, $post) {
}
# PATCH /posts/{$id}
sub update :PATCH Chained('find') PathPart('') ($self, $c, $post) {
$post->update($c->req->body_parameters);
}
__PACKAGE__->meta->make_immutable;
In this version, we've cleaned up the code considerably as well as tightened the relationships between actions. There's no unneeded stash namespace pollution and of course we have named variables where a typo is going to give you a compile time error not a nasty hard to figure out error at runtime.
This is a very small tweak to chaining that doesn't monumentally change how your code works but I find that especially with very complicated applications it really aids in understanding and reduces overall boilerplate mess that you end up with in classic Catalyst chained actions. Adding this does not significantly change how chaining works, but I believe it is a small, iterative refinement that plays nice in existing code as well as in any greenfield project.
In this example we passed a single variable from one action to another, however the "next_action" method accepts lists as well. Just be careful when doing this and the receiving action has args or captures since those get tacked onto the end of the argument list so if you need to pass arbitrary length lists you should pass it as a reference or wrap it in an object.
The next action feature is a proposed update to Catalyst chaining that exists as a pull request right now (https://github.com/perl-catalyst/catalyst-runtime/pull/184) for your review and comments. You can view a larger example of code using it here (https://github.com/jjn1056/Valiant/tree/main/example) but just a warning that also contains some other new ideas that Catalyst developers might be unfamiliar with. Stay tuned for more on that later :)
One More Thing...
When you invoke "$c->action->next(@args)" in your code, it does more than send arguments to the connected children actions. It creates a call stack that reverses when you hit the last action in the chain, returning control back upward the action chain and allowing you to return arguments. This
can be a great way to return control to a parent action. Example:
sub a :Chained('/') PathPart('') CaptureArgs(0) ($self, $c) {
my $ret = $c->action->next(my $info1 = 'aaa'); # $ret = 'from b from c'
}
sub b :Chained('a') PathPart('') CaptureArgs(0) ($self, $c, $info1) {
my $ret = $c->action->next(my $info2 = 'bbb'); # $ret = 'from c';
return "from b $ret";
}
sub c :Chained('b') PathPart('') Args(0) ($self, $c, $info2) {
return 'from c';
}
Please note however that due to existing limitation in Catalyst you will need to return only a scalar so if you want to return an array or hash of stuff you will need to make it an arrayref or hashref.
This looks neat!
As a separate point, though, since we’re talking about messy stashing, I find the real problem with Catalyst chaining is that you can only say “this action captures two args”. The real solution would be if instead you could say “this action captures an arg called thread_id and an arg called post_id”.
This does mean you would be accessing the args by string key, so no strictures benefit there. But in every other respect it would be better. Most importantly it would remove all need to manually store or pass arg values – whether by stashing or as sub parameters or however – since all args would be available in the args hash at all times anyway. Only the code which actually deals with a particular arg value would ever look at it.
This would make it much easier to handle exceptions early in the setup part of a chain, like “let me fetch the record here, but if I’m ultimately going to the
edit
action then I need to include an optional parameter with the value of an arg from a later action”.It would also allow arg names chosen to have meaning shared among actions that process similar data, and then a root action could create objects based on which keys show up in the args hash. “If there are thread_id and post_id keys in the args, use them to initialize a Model::Post object on the stash.” Then if there are several actions that capture a post ID, you don’t have to repeat this step in each one of them. This would particularly make it much nicer to work with things than don’t have a DBIx::Class-style “build up a query step by step and only run it at the end” lazy evaluation-ish interface.
Basically you could do work at whichever point up or down the chain was convenient, instead of having to wait until dispatch gets far enough along the chain that you get access to some particular arg, while in the meantime only being able to pass along values collected along the chain – even if the passing-along happens more neatly than by stashing, such as with this “next action” feature.
Of course this would constitute a major disruption of the whole chained dispatch API… whereas “next action” can be tacked on quietly and fits into the existing interface seamlessly.