Writing Your Application Declaratively
Some History
There have been many shiny new developments in the Perl community of late. With Catalyst's switch to Moose, a whole new world of possibilities was introduced and suddenly available to all Catalyst developers. Another exciting new idea came along with Devel::Declare: the ability to provide a declarative syntax for things that used to be more verbose or repetitious, or just simply to provide an API that goes beyond what the Perl parser itself provides.
One of the neater examples that came with the availability of those modules is MooseX::Declare, which allows you to write your classes like this:
class SomeClass extends SomeParentClass { has foo => (is => 'rw'); method bar (Int $baz) { $self->foo + $baz } }
While it might look like it, this is not a source filter in the sense
that we all (hopefully) learned to avoid. The module is still parsed by
perl. When for example the class
keyword is hit, MooseX::Declare
takes over and parses the special syntax. When the {
opening brace is
found, it gives control back to perl. This has the big advantage that
extensions can be safely combined and also be written in Perl itself. In
fact, the class
and method
keywords are handled by separate parts
of MooseX::Declare.
What's this got to do with Catalyst?
A very good question! The module CatalystX::Declare is an extension of MooseX::Declare that allows you to write your Catalyst applications declaratively. An example CatalystX::Declare controller would look like this:
controller MyApp::Controller::Foo { action base as '' under '/'; final action bar (Int $id) under base { $ctx->stash(object => $ctx->model('Baz')->find($id)); } }
The controller
keyword is an extension of the class
handler from
MooseX::Declare. While class
(like Moose) will always
automatically inherit from Moose::Object when no extends
option is
provided, controller
s will have a default superclass of
Catalyst::Controller.
The first thing you might notice about the actions is that there are no attributes. Everything is specified with a simple declarative syntax. The first action you see is this:
action base as '' under '/';
This is roughly equivalent to the following:
sub base: Chained('/') PathPart('') CaptureArgs(0) { }
Yes, all actions in CatalystX::Declare are chained actions, since Catalyst::DispatchType::Chained is the most flexible way of designing Catalyst applications (and one that lends itself especially well to declarative syntax).
The next action is where it becomes really interesting:
final action bar (Int $id) under base { $ctx->stash(object => $ctx->model('Baz')->find($id)); }
The final action
creates an endpoint chained to the base
we
declared above. The actions all automatically receive a $ctx
context
variable in addition to the $self
that MooseX::Declare already
provides for methods.
The interesting part is the (Int $id)
signature for the action. The
number of positional arguments is used to determine the number of
arguments in the public URL. The above chain would run if the resource
/bar/23
is hit, and it will only match if its argument is an
Int.
To expand a bit on the last comment, let me show you an example:
controller MyApp::Controller::Foo { # see MooseX::Types use MyApp::Types qw( PageName SequentialID Language ); action base (Language $lang) as '' under '/' { $ctx->stash(language => $lang); } final action show_page (PageName $name) as show under base { $ctx->stash(page => $ctx->model('DB::Page')->find($name)); } final action show_item (SequentialID $id) as show under base { $ctx->stash(item => $ctx->model('DB::Item')->find($id)); } }
We assume that Language
must be a valid language string (such as
en
); PageName
must be an identifier in the Perl sense (starts with
a letter or underscore, not a digit); and SequentialID
is a positive
integer. Then, you can hit the above controller with the following:
- * /en/show/foo
-
This will load
base
with a$lang
ofen
and then dispatch toshow_page
with a$name
offoo
. - * /de/show/23
-
This will first run
base
like before, but this time withde
as$lang
argument and then executeshow_item
with an$id
of23
.
Of course you can use any number of arguments, and for endpoints even slurpy arguments are possible:
final action show_page (Str @path) as page under base { ... }
Grouping actions by their base
You often have a single action as a base and many actions that chain off
of it. If that is the case for you, you can use under
as a keyword to
group the actions together and save yourself the repetition of the
under
option:
action base as '' under '/'; under base { final action foo { ... } final action bar { ... } }
Both foo
and bar
in the above example will implicitly chain off
base
.
Declaring other parts of the application
Of course controllers and actions are the most important components for which you'd want to have a declarative syntax. But they are not all that CatalystX::Declare provides you with. You can also declare your application in a shinier syntax:
application MyApp with ConfigLoader with Static::Simple { $CLASS->config(name => 'My App'); }
There you go; no need to inherit from Catalyst, no need to call
setup
. And the plugins are nicely specified as roles (which is what
plugins are likely to become in the future).
So now we have the application, and we already had the C
in MVC
,
but CatalystX::Declare also gives you keywords for the two other
letters:
model MyApp::Model::DBIC extends Catalyst::Model::DBIC::Schema { method foo (Int $x) { ... } }
for models, and
view MyApp::View::HTML extends Catalyst::View::TT { after process { ... } }
for views. Additionally you can also define controller roles like this:
controller_role MyApp::ControllerRole::DefaultBase { action base as '' under '/'; }
and you can consume them on the other side like usual in MooseX::Declare based modules:
controller MyApp::Controller::Qux with MyApp::ControllerRole::DefaultBase { action view under base { ... } }
Methods and attributes
Since CatalystX::Declare is just an extension of MooseX::Declare you can use its full power to declare methods and attributes as well. Here is an example of a complete root controller with methods and attributes:
controller MyApp::Controller::Root { use MooseX::Types::Moose qw( Str ); has home_action => ( is => 'ro', isa => Str, required => 1, default => '/somewhere/else', ); method home_uri (Object $ctx) { return $ctx->uri_for_action($self->home_action); } action app_base as '' under '/'; action end (@) isa RenderView; under base { # matches / final action app_root as '' { $ctx->response->redirect($self->home_uri($ctx)); } # matches /... final action fallback (@) as '' { $ctx->response->status(404); $ctx->response->body('File not found'); } } }
Conclusion
The real hard work for all this shinyness is done by MooseX::Declare. Take a look at CatalystX::Declare on CPAN for a full description of the available syntax, since the above is only a quick tour. I also casually skipped over the use of method modifiers that are applied to actions, which adds even more possibilities.
If you want more samples, there is an example application shipped with the distribution at http://cpansearch.perl.org/src/PHAYLON/CatalystX-Declare-0.011/examples/MyApp-Web/.
Author and Copyright
(c) 2009, Robert 'phaylon' Sedlacek (r.sedlacek at shadowcat.co.uk
).