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

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>