Doing Rails-style routes with Catalyst

People often ask about using Rails-style routes, and why Catalyst doesn't "support" them. Well, actually, it does - through the magic of config.

Routes via the config file

The key is this: action attributes are actually just defaults - when you do

  sub foo :Local {

that makes the default attribute hash for foo { Local = [ '' ] } >. Well, it doesn't, because when Local is parsed - what you actually get for the following:

  package MyApp::Controller::Name;

  sub foo :Local { # ...

is { Path = [ 'name/foo' ] } >. Now, this can be overriden from the config file by doing something like:

  <Controller Name>
    <action foo>
      Path somewhere/else
    </action>
  </Controller>

Which is perhaps a little bit verbose, but then you only use config for per-deployment stuff, right? Right?

Routes via code, and controller reuse

So doing it from MyApp.pm is maybe a bit cleaner -

  __PACKAGE__->config(
    Controller => {
      Name => {
        actions => {
          foo => { Path => 'somewhere/else' }
        }
      }
    }
  );

although this is still quite verbose, of course. Generally, this approach is only ever used to allow URL localisations. For example, if you sell a white label app to a foreign client you can change the URL parts to their language. I'm aware of people having done this in production.

Of course, the other thing you can do is to use this to make reusable controllers -

  package MyApp::ControllerBase::List;

  sub list :Args(0) {
    my ($self, $c) = @_;
    my $rs = $c->stash->{rs}->page($c->req->query_params->{page}||1);
    $c->stash(
      pager => $rs->pager,
      results => [ $rs->all ]
    );
  }

  package MyApp::Controller::Thingies;

  use base qw(MyApp::ControllerBase::List);

  __PACKAGE__->config(
    actions => {
      list => { Chained => 'load_thingies' }
    }
  );

  sub load_thingies :Chained('/') :CaptureArgs(0) :PathPart('thingies') {
    my ($self, $c) = @_:
    $c->stash(
      rs => $c->model('DB::Thingy')
    );
  }

and now Thingies->list will be chained off Thingies->load_thingies.

Syntactic sugar

But we were talking about routes, weren't we? Though I would observe that controllers' self-contained-ness is what makes this sort of subclass-to-reuse stuff possible; the Rails guys say "re-use in the large is overrated", we say "well, actually, it's bloody hard, but if you're careful ...". Anyway.

Let's see if we can't make routes a bit prettier.

  {

    my %config;

    sub routes (&) {
      my $cr = $_[0];
      %config = ();
      $cr->();
      __PACKAGE__->config(\%config);
    }

    sub route {
      my ($path, $to) = @_;
      my ($controller, $action) = split(/->/, $to);
      $config{"Controller::$controller"}{actions}{$action}{Path} = [ $path ];
    }

    sub to { @_ }
  }
  use namespace::clean; # this will get rid of the subs on EOF

  routes {
    route 'some/path' to 'Name->foo';
  };

and the end result will be (provided we've marked foo as an action via sub foo :Action { ) that /some/path will dispatch to that method on MyApp::Controller::Name.

Of course, I still think that Catalyst's self-contained controller approach is better, but if you really want routes, please consider the code above as under the same license as Perl and send CatalystX::Routes to the CPAN :)

-- mst

AUTHOR

Matt S Trout <mst@shadowcat.co.uk> ( http://www.shadowcat.co.uk/ )