Those of you who are not regulars on irc.perl.org or don’t hang out in the
#catalyst channel might not be aware of this, but the
discussion, planning and chanting was going on for a while. With the recent
rise of Devel::Declare this discussion has flamed up again.
There are many people thinking about how to design this, and even some implementations are already around. I’d like to use this article to discuss my own take on how a declarative Catalyst might look like.
First: The Competition
I currently only know of Florian Ragwitz (from here on just called “rafl”)
and John Napiorkowski, but I’m sure others are experimenting with this too.
++ to both of them, especially since I borrowed some of their ideas in my
own draft.
Is this for real?
Yes. While all of this is of course still highly experimental and not available on the CPAN (which is a good indicator of its unfinished nature), you can get it from github, or if you just want to take a closer look at some examples, there is a prototype test controller in the test suite implementing the current functionality.
How it looks like
First, let’s take a look at a traditional Catalyst controller.
CatalystX::Declarative is planned to allow declaration of components,
views, etc. as well, but for the moment we only have controllers. Why? Because that’s
the most code that we have to write. This is along of what you usually have:
package MyApp::Controller::Foo;
use strict;
use warnings;
use parent 'Catalyst::Controller';
sub base: Chained PathPart('') CaptureArgs(0) { }
sub foo: Chained('base') Args(1) {
my ($self, $ctx, $id) = @_;
$ctx->stash(foo_id => $id);
}
1;
Wouldn’t it be nice if we could instead say this?
use CatalystX::Declarative;
controller MyApp::Controller::Foo {
action base under '/' as '';
action foo (Int $id) under base {
$ctx->stash(foo_id => $id);
}
}
While it might not look like it, this is still Perl 5. The controller
construct is an extension of MooseX::Declare’s class
keyword handler. IT automatically sets up Catalyst::Controller as
base class if none was specified via extends.
The action keywords seem to work like methods, but are a bit more
complicated. First of all: They don’t need blocks behind them. The base
action above is just simply empty and provides nothing more than an anchor point.
The foo action additionally has a signature. This will be
introspected when the actions are populated. That means the above will correctly
only match on a single argument. Types are currently validated like normal
methods. I’m not yet sure whether to keep it validating and provide a
multi action keyword or to extend it so the action will only match
when the arguments match the signature. This would mean that you would have to say
action foo_int (Int $id) under base as foo { ... }
action foo_str (Str $st) under base as foo { ... }
Which isn’t that nice, really. I’ll probably go with an adaption of
multi method and do this instead:
multi action foo (Int $id) under base { ... }
multi action foo (Str $st) under base { ... }
This seems much nicer.
You might have already noticed, this appraoch to declarative controllers is
completely based on :Chained actions. Everything that is not
denoted as is final will have an implicit CaptureArgs.
You can also specify a slurpy parameter like this:
action foo (@bar) under '/' is final {
$ctx->stash(bar => join ', ', @bar);
}
And Catalyst will know to expect a dynamic amount of arguments, the same as if you’d have specified it like this:
sub foo: Chained Args {
my ($self, $ctx, @bar) = @_;
$ctx->stash(bar => join ', ', @bar);
}
There are already some implemented syntax elements to declare:
action $name $signature?under $private_pathas $path_partis finalis privatewith $action_roleisa $action_classfinal
These were the rather basic ones. Paths can always be specified as string or as bareword if they follow the rules of Perl identifiers:
action foo as bar;
is the same as
action foo as 'bar';
This is more than TIMTOWTDI to me. It allows me to make an easy distinction
between actions chaining to a local or an external action (including /):
action base under '/';
action foo under base;
action bar under '/base';
Since one of the main differences between rafl’s and my approach seem be the
ordering of specific parts of the syntax (namely the under keyword),
I made action and under equals. This means you can
write both
action foo under base as 'myfoo';
and
under base as 'myfoo' action foo;
and they will mean the same. The order of the other syntax elements are not important. They are extendable, so being enforced to provide them in a specific order would probably become a huge pain in the future.
Another issue rafl had was that he intended to use under in
block format to bundle a bunch of actions under a common chain point. Turns
out this wasn’t that hard to implement:
action base as '';
under base {
action list { ... };
action create { ... };
action object ($id) {
$ctx->stash(object => $ctx->model('Foo')->get($id);
}
under object {
action update { ... };
action delete { ... };
}
}
In the above example, both list and create will
chain off base, just like object which provides
the chain base for update and delete.
At this point you, as well as I, might ave noticed that these declarations can get pretty long, and full of barewords. Here is an example:
action foo (Int $x, Str $y) under some_base as two_foo is final { }
That’s declarative, but a bit hard to parse. Fortunately, you can throw in commas between the syntax elements:
action foo (Int $x, Str $y), under some_base, as two_foo, is final { }
reads a lot nicer.
You can also customize your action by using with and isa. To
add an ActionRole you’d simply say
action foo under bar with OnlyMatchOnSundays { }
This will try to find an ActionRole fitting that name. In sequence, it would look for
- A subroutine (like an alias or constant) named
OnlyMatchOnSundays. This will be executed if found and its return vaule used as ActionRole name. MyApp::ActionRole::OnlyMatchOnSundays, which would be the logical place if this ActionRole was provided by your application itself.Catalyst::ActionRole::OnlyMatchOnSundaysfor roles you installed via CPAN or those you intend to go there.- A package fully named
OnlyMatchOnSundays.
Besides ActionRoles, you can also specify ActionClasses:
action end is private isa RenderView;
This will look for an ActionClass with the same naming rules as ActionRoles above, except
that it will use the Action namespace instead of ActionRole.
(The above usage of is private is the only exception to the “All actions are
chained” rule.)
Besides all of the features required to declare actions, there is a big emphasis on readability
and flexibility on writing, just like we know and love it from Perl. Since I often have rather
large controllers with complex action chains setting up specific environments for the final
actions, I wanted a way to have the final actions in the chains stand out. This is why I
introduced the final keyword that can be used as normal syntax element
or even as declarator (meaning at the beginning of the syntax) just like action
and under:
final action foo ($x) under bar { $ctx->response->body($x) }
under bar, final action foo ($x) { $ctx->response->body($x) }
action foo ($x) final under bar { $ctx->response->body($x) }
This should help those nasty endpoints to stand out visually if they need to.
Reaction
For those who were hoping for another Reaction post, give this a quick thought:
use ReactionX::Declarative; # made-up name, doesn't exist yet
controller MyApp::Controller::Foo {
action bar, under '/base', is final, pushing SiteLayout {
$vp->title('Foo Title');
$vp->static_base_uri( $ctx->uri_for('/action')->as_string );
}
}
Questions? Feedback?
There are many opinions flowing around at the moment, and many people seem to have their own impressions and ideas on how a declarative approach would make their lives easier. If you have any questions or comments, they would be greatly appreciated.




Very nice.
Can't wait until this is CPAN-ready. :)