The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

CatalystX::RouteMaster - role for components providing Catalyst actions

VERSION

version 1.08

SYNOPSIS

  package MyApp::Base::Stuff;
  use Moose;
  extends 'Catalyst::Component';
  with 'CatalystX::RouteMaster';

  sub _kind_name {'Stuff'}
  sub _wrap_code {
    my ($self,$c,$url_prefix,$action_name,$route) = @_;
    my $code = $route->{code};
    my $extra_config = $route->{extra_config};
    return sub {
      my ($controller,$ctx) = @_;
      $self->$code($ctx->req,$extra_config);
    }
  }

Then:

  package MyApp::Stuff::One;
  use Moose;
  extends 'MyApp::Base::Stuff';

  sub routes {
    return {
      '/some/url' => {
        action_name => {
          code => \&my_action_method,
          extra_config => $whatever,
        },
        ...
      },
      ...
    }
  }

  sub my_action_method {
    my ($self,$request) = @_;

    # do something
  }

Also, remember to tell Catalyst to load your Stuff components:

  <setup_components>
   search_extra [ ::Stuff ]
  </setup_components>

DESCRIPTION

First of all, this role does not give you anything that you could not do with Catalyst directly. It does not add any functionality. What it does is allow you to write some simple controllers in a different style.

This was originally part of CatalystX::ConsumesJMS, then I realised it could easily be generalised, so here is the "general" part.

Rationale

So, since this module is quite complicated, and does not actually provide anything that Catalyst doesn't already do, why does it exist? When I wrote CatalystX::ConsumesJMS and Plack::Handler::Stomp, I was (ab)using Catalyst as a combination dependency injection container + dispatcher, to write an application to consume ActiveMQ messages. One of the design goals was to hide Catalyst from the actual application code, since in the end there was no need to expose the framework. If you don't need to hide Catalyst from your code, or you need non-trivial dispatching, you're probably better off without this module.

Usage

This role is to be used to define base classes for components in your Catalyst applications. It's not to be consumed directly by application components. Classes inheriting from those base classes will, when loaded, create controllers and register their actions inside them.

Routing

Subclasses of your component base specify which URLs they are interested in, by writing a routes method, see the synopsis for an example.

They can specify as many URLs and action names as they want / need, and they can re-use the code values as many times as needed.

The main limitation is that you can't have two components using the exact same URL / action name pair (even if they derive from different base classes!). If you do, the results are undefined. This is the same "limitation" as not having two actions for the same request in regular Catalyst.

It is possible to alter the URL via configuration, like:

  <Stuff::One>
   <routes_map>
    logical_url  /the/actual/url
   </routes_map>
  </Stuff::One>

You can also do this:

  <Stuff::One>
   <routes_map>
    logical_url  /the/actual/url
    logical_url  /another/url
   </routes_map>
  </Stuff::One>

to get your class to respond to two different URLs without altering the code.

You can even alter the action name via the configuration:

  <Stuff::One>
   <routes_map>
    <logical_url /the/actual/url>
      action_name local_action
    </local_url>
    <logical_url /another/url>
      action_name local_action1
      action_name local_action2
    </local_url>
   </routes_map>
  </Stuff::One>

That would (with the default "_action_extra_params" method) install 3 identical actions for the following URLs:

/the/actual/url/local_action
/another/url/local_action1
/another/url/local_action2

(Again, nothing that you couldn't do with normally-written controllers, it's just a different way to do it).

The "code"

The hashref specified by each URL / action name pair will be passed to the "_wrap_code" function (that the consuming class has to provide), and the coderef returned will be installed as the action to invoke for that name under that URL.

Required methods

_kind_name

As in the synopsis, this should return a string that, in the names of the classes deriving from the consuming class, separates the "application name" from the "component name".

These names are mostly used to access the configuration.

_wrap_code

This method is called with:

  • the Catalyst application as passed to register_actions

  • the URL (mapped via "routing")

  • the action name (mapped via "routing")

  • the value from the routes corresponding to the URL and action name slot (see "Routing" above)

You can do whatever you need in this method, see the synopsis for an idea.

The coderef returned will be invoked as a Catalyst action for each matching request, which means it will get:

  • the controller instance (you should rarely need this)

  • the Catalyst application context

Implementation Details

HERE BE DRAGONS.

This role should be consumed by sub-classes of Catalyst::Component.

The consuming class is supposed to be used as a base class for application components (see the "SYNOPSIS", and make sure to tell Catalyst to load them!).

Since these components won't be in the normal Model/View/Controller namespaces, we need to modify the COMPONENT method to pick up the correct configuration options. This is were "_kind_name" is used.

We hijack the expand_modules method to generate various bits of Catalyst code on the fly.

If the component has a configuration entry enabled with a false value, it is ignored, thus disabling it completely.

We generate a controller package for each (mapped) URL, by calling "_generate_controller_package", and we add an after method modifier to its register_actions method inherited from Catalyst::Controller, to create all the (mapped) actions. The modifier is generated by calling "_generate_register_action_modifier".

_generate_controller_package

  my $pkg = $self->_generate_controller_package(
                $appname,$url,
                $config,$route);

Generates a controller package, inheriting from whatever "_controller_base_classes" returns, called ${appname}::Controller::${url} (invalid characters are replaced with _). Any roles returned by "_controller_roles" are applied to the controller.

Inside the controller, we set the namespace config slot to the $url.

_controller_base_classes

List (not arrayref!) of class names that the controllers generated by "_generate_controller_package" should inherit from. Defaults to 'Catalyst::Controller'.

_controller_roles

List (not arrayref!) of role names that should be applied to the controllers created by "_generate_controller_package". Defaults to the empty list.

_generate_register_action_modifier

  my $modifier = $self->_generate_register_action_modifier(
                   $appname,$url,
                   $controller_pkg,
                   $config,$route);

Returns a coderef to be installed as an after method modifier to the register_actions method. The coderef will register each (mapped) action with the Catalyst dispatcher. You can pass additional parameters to the controller's create_action method by overriding "_action_extra_params".

Each action's code is obtained by calling "_wrap_code".

_action_extra_params

  my %extra_params = $self->_action_extra_params(
                      $c,$url,
                      $action_name,$route->{$action_name},
                     );

You can override this method to provide additional arguments for the create_action call inside "_generate_register_action_modifier". For example you could return:

  attributes => { MySpecialAttr => [ 'foo' ] }

to set that attribute for all generated actions. Defaults to:

  attributes => { 'Path' => ["$url/$action_name"] }

to make all the action "local" to the generated controller (i.e. they will be invoked for requests to $url/$action_name).

AUTHOR

Gianni Ceccarelli <gianni.ceccarelli@net-a-porter.com>

CONTRIBUTORS

Thanks to Peter Sergeant (SARGIE) for the name.

COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Net-a-porter.com.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.