Snippets/CatalystX_Router

CatalystX::Router

This module was created to help with the task of breaking up large Catalyst Controllers.

Existing Methods

There are already a few common ways to accomplish this:

1. Break the Controller into multiple Controllers

This is the most straightforward method. There is clean separation of both code and templates since each Controller is fully in its own namespace, but navigation between them becomes harder because you can no longer safely rely on relative links in uri_for. Refactoring a method to a different Controller will also require significant changes.

2. Roles and Chained methods - see  http://www.catalystframework.org/calendar/2011/10

Chained handlers can be powerful, but incorporating them into an existing system can require a lot of changes. In addition, you need to worry about namespace collisions when you are creating your methods, and this extends to helper functions too. Using default view controllers, the templates will be in the calling Controller rather than the one they are implemented in. Finally, the Controller itself can look a bit like magic as there is no comprehensive list of implemented methods.

3. Roles alone

Using Chained methods is not required to take advantage of roles, and this method can be a simple way to simply break one Controller into multiple pieces. However, the limitations of the method above largely still apply.

Differences

The goal was to avoid magic and maintain isolation between controllers while minimizing the changes needed to an existing codebase.

Routes are defined in the PACKAGE->config of the controller whose namespace will be shared. This provides a clear way to see all of the paths that are being served from the Controller.

Dispatch to private methods is used so that everything appears to be under one controller, while still allowing each controller to freely use its namespace. The path does not need to match the method name, though it will by default if only a controller is specified.

If using the a standard View::TT handler, you may either keep templates in the calling controllers directory named after the path defined in the route, or you can use the routes_default_template sprintf pattern to set the default template to the action of the route target instead. Depending on the patterns of your re-use, either method can have advantages. I am not sure if there is a clean way to do this which would be portable across other View implementations, though the sprintf pattern should give enough flexibility for most. e.g. in the sample below, the default template for "logout" would be "root/mycontroller/authenticationhandler/logout.tt". If the default_template setting is removed, it would be "root/mycontroller/logout.tt".

If you are changing action names in the route, you will need to be careful that any redirects, forward, form submits, etc. do not assume the use of the internal names. You may wish to disable the default_template setting if you are rewriting the action names, as it will be easier to handle this in the controller where you know the actual action names.

Example

CatalystX::Router.pm

package CatalystX::Router;

use MooseX::MethodAttributes::Role;

sub BUILD
{
    my ( $self, $controller ) = @_;

    if ( ref( $controller->{routes} ) eq "HASH" ) {
        while ( my ( $path, $route ) = each %{ $controller->{routes} } ) {
            $controller->{catalyst_component_name}->meta->add_method(
                $path,
                sub  : Local {
                    my ( $self, $c ) = @_;
                    my $route = $self->{routes}{ $c->action->name };

                    my $target =
                      ref($route)
                      ? $c->controller( $route->[0] )->action_for( $route->[1] )
                      : $c->controller($route)->action_for( $c->action->name );

                    if ( $self->{routes_default_template} ) {
                        $c->stash->{template} = sprintf( $self->{routes_default_template}, $target );
                    }

                    $c->detach($target);
                }
            );
        }
    }
}

1;

MyApp/Controller/MyController?.pm

package MyApp::Controller::MyController;
use Moose;
use autoclean::namespace;
extends 'Catalyst::Controller';
with qw/CatalystX::Router/;

__PACKAGE__->config(
    routes => {
        'login'        => 'MyController::AuthenticationHandler',
        'logout'       => [ 'MyController::AuthenticationHandler', 'my_logout' ],
    },
    routes_default_template => "%s.tt",
);

MyApp/Controller/MyController/AuthenticationHandler?.pm

package MyApp::Controller::MyController::AuthenticationHandler;
use Moose;
use autoclean::namespace;
extends 'Catalyst::Controller';

sub login : Private
{
# ...
}

sub my_logout : Private
{
# ...
}