Day 10 - The Chained Dispatch Type
A complete example of how to use Catalyst::DispatchType::Chained.
A basic introduction to Chained actions with Catalyst.
Frequently when building web applications there is a chain of dependency along the url path. That is, the controller dispatch logic has a close relationship to the data you want to draw from the model. Chained dispatch types provide an easy way to encapsulate this logic within your Catalyst controllers.
This Advent calendar entry is associated with a complete example application which shows how to use chained actions to provide this logic. The example application is available from the Catalyst subversion repository by issuing the following command:
$ svn co http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/ChainedEg
Please get a copy of this code before you proceed. The application is
a very simple proof-of-concept application which will work with a
basic Catalyst. There is no model, and the view
is handled in the Root controller's end action
(i.e. Controller::Root->end
).
As an interesting aside, with appropriate introspection, by using Class::Inspector for example, it would be relatively straightforward to develop a custom view based on this end action to provide skeleton output during application development. For instance, you might want to use this in a situation where you have a pre-existing model but are not yet able to provide data for it.
The LoadModel
base controller
The package MyAdvApp::ControllerBase::LoadModel
Getting started.
Before we look at our application in any detail, first, let's run the example application. Check out the code as shown above, and then issue the following command from the application root:
$ perl script/myadvapp_server.pl [ snipped the early part of the output ] [debug] Loaded Chained actions: .-------------------------------------+--------------------------------------. | Path Spec | Private | +-------------------------------------+--------------------------------------+ | | /app_root | | /*/bar | /language (1) | | | -> /bar/base (0) | | | -> /bar/resultset (0) | | | => /bar/list | | /*/bar/*/something | /language (1) | | | -> /bar/base (0) | | | -> /bar/load_model (1) | | | => /bar/something | | /*/foo/*/edit | /language (1) | | | -> /foo/base (0) | | | -> /foo/load_model (1) | | | => /foo/edit_foo | | /*/foo | /language (1) | | | -> /foo/base (0) | | | -> /foo/resultset (0) | | | => /foo/list | | /*/foo/* | /language (1) | | | -> /foo/base (0) | | | -> /foo/load_model (1) | | | => /foo/show_foo | '-------------------------------------+--------------------------------------' [info] MyAdvApp powered by Catalyst 5.7005 You can connect to your server at http://localhost:3000
Looking at the debug output from the application above, we see that it is a little different from a standard Catalyst application. The Chained dispatch type alters the usual behavior of the arguments and dispatch chain.
Visiting the application root
http://localhost:3000 displays a message and a collection of URLs
which the application accesses. This page is in itself a Chained
action. By the end of this article, hopefully you will understand the
equivalent Chained code that provides the eqivalent of a index :
Private
action for the root controller action. Let's visit the
first link on that page.
http://localhost:3000/en/foo
This produces the following output:
$VAR1 = { 'language' => 'en', 'list' => 'Showing a list from the Resultset', 'resultset' => 'Foo called with no trailing arguments' };
If we change the url slightly to http://localhost:3000/de/foo the first line of output changes subtly:
'language' => 'de'
The line of code responsible for doing this in the Foo
Controller
is this:
sub base : Chained('/language') PathPart('foo') CaptureArgs(0) { }
which is an empty subroutine. What's happening here?
Well, the attribute Chained('/language')
is telling the Foo
controller to use the root (/
) controller's private method
language
first. Following this, if the next part of the url path
is foo
(PathPart('foo')
) then pass all remaining arguments to
the current controller class (Controller::Foo
) for further
processing. In this case, two further subroutines are called from
this controller. The first is within the file lib/Controller/Foo
:
resultset
, which populates $c->stash->resultset
with the text
'Foo called with no trailing arguments'.
The second subroutine is one inherited from
MyAdvApp::ControllerBase::LoadModel
which provides the list
method under the same circumstances as the resultset
sub in
Controller::Foo
. So both actions are called, and both actions
populate $c->stash
. Look at the subroutine declaration for the
resultset
and list
subroutines:
sub resultset : Chained('base') PathPart('') CaptureArgs(0) { sub list : Chained('resultset') PathPart('') Args (0) {
Note the difference in the declaration. list
uses the Args
attribute, whereas resultset
uses the CaptureArgs
attribute.
What's the difference?
CaptureArgs
indicates that this is not an endpoint, and the
dispatcher should look for more subroutines to run before
proceeding to the appropriate end
action (which usually renders the
view). So this means when we visit http://localhost:3000/en/foo
the resultset
action is called, and does not capture any
arguments into $c->request->captures
(this will make more sense
in the next section).
On the other hand, the list
declaration has Args
as an attribute.
This means that it is an endpoint, and after the subroutine is
executed, the appropriate end
action is called. Note that the
expected number of arguments (in this case none) are put into
$c->request->args
by the Catalyst dispatcher.
The result of this code is that when we call our application from its
base path with the additional path /en/foo/
we can easily provide
localisation via the language
subroutine in Controller::Root
and when nothing is in the path after the name of the controller, we
execute the list
method.
We've done "list", what about showing a single item?
Visiting http://localhost:3000/en/foo/23 takes us to the following display:
$VAR1 = { 'language' => 'en', 'loaded_model' => 'DBIC::Foo', 'loaded_item_id' => '23', 'message' => 'Showing a Foo' };
Replace "23" with "something%20else" and we get:
[other output snipped] 'loaded_item_id' => 'something else',
The ControllerBase package, which is a base class for
Controller::Foo (use base 'MyAdvApp::ControllerBase::LoadModel'
)
provides the following method to handle this:
sub load_model : Chained('base') PathPart('') CaptureArgs(1) { my ($self, $c, $item_id) = @_; $c->stash->{loaded_item_id} = $item_id; $c->stash->{loaded_model} = $self->config->{model}; }
It takes one argument for internal use by the controller (placed into
the variable $item_id
in the first line of the sub) and uses this to
populate the stash. No additional PathPart is added before the argument
(if we added one, say 'bar', it would match /en/foo/bar/23 instead of
/en/foo/23).
The code responsible for the 'message' portion of the data structure is in the Controller:Foo package:
sub show_foo : Chained('load_model') PathPart('') Args(0) { my ($self, $c) = @_; $c->stash->{message} = 'Showing a Foo'; }
which is the end point, as we can see by the use of Args
rather than
CaptureArgs
.
The edit action is somewhat similar. By this point you can see that
there is a pattern developing. The edit_foo
endpoint is called from
Controller::Foo
after the load_model
subroutine from the base
controller class (code above).
sub edit_foo : Chained('load_model') PathPart('edit') Args(0) { my ($self, $c) = @_; $c->stash->{message} = 'Editing a Foo'; }
The Bar
Controller and Going Further.
Working this out is left as an exercise to the reader. Examine the example code and ensure that you understand how the same base controller is being used to provide results for a different package.
As we mentioned in the beginning of this discussion, the end
action
used in this module is a "proof of concept" view, and is mainly for
illustrative purposes. A useful exercise to understand the Chained
dispatch type would be to use the database model presented in
DBIx::Class::Manual::Example and the Template Toolkit to flesh
out the application presented here into a working, database-driven
example. You would put the MyDatabase
directory in your
application's lib
directory (or anywhere else in @INC
).
Assuming that you put the SQLite database example.db
in the
application root you could create the Catalyst model via calling a
helper in the following way:
$ script/myadvapp_create.pl model MyCatalystModel DBIC::Schema MyDatabase dbi:SQLite:example.db
From there it's up to you.
Wrap Up
That's it. Simple use of the Chained dispatch type. See the
Catalyst::DispatchType::Chained
documentation for more details. As
someone said on the mailing list (I'm paraphrasing here), "Chained is
a bit challenging to get your head around initially, but it's well
worth doing so. It's like built-in information architecture."
AUTHORS
Words: Kieren Diment <diment@gmail.com>
Code: Robert 'phaylon' Sedlacek <rs@474.at>
COPYRIGHT
Copyright 2006 Robert Sedlacek. This document can be modified and re-distributed under the same conditions as Perl itself.