Cataly...atching(3pm)

๐Ÿ“ฆ
version 5.90132 ยท 6 versions
, in debian trixie, ubuntu resolute, ubuntu stonking

๐Ÿ’– Catalyst::RouteMatching(3pm) ๐Ÿ’– โ€” NAME ๐ŸŒˆ

okay bestie, welcome to Cataly...atching(3pm) โ€” User Contributed Perl Documentation โ€” aka Catalyst::RouteMatching, and it's here to spill the whole tea on How Catalyst maps an incoming URL to actions in controllers. ๐Ÿซถ


โœจ DESCRIPTION โœจ

So real talk: this is a WIP document โ€” yes, work in progress, it's giving "we're getting there" โ€” and its entire job is to help you understand the logic Catalyst uses to determine how to match an incoming request to an action (or an action chain) in a controller. ๐Ÿ’…


๐Ÿฆ‹ Request to Controller/Action Matching ๐Ÿฆ‹

Catalyst maps requests to action using a 'longest path wins' approach. So if the request is /foo/bar/baz, that means the action baz matches:

package MyApp::Controller::Foo;

use Moose;
use MooseX::MethodAttributes

extends 'Catalyst::Controller';

sub bar :Path('bar') Args(1) { ...}
sub baz :Path('bar/baz') Args(0) { ... }

Path length matches take precedence over all other types of matches โ€” and yes, that includes HTTP Method, Scheme, etc. The same holds true for Chained actions. Generally the chain that matches the most PathParts wins. ๐Ÿ‘‘


๐Ÿ“ Args(N) versus Args ๐Ÿ“

Args matches any number of args. And because this functions as a sort of catchall, we treat Args as the lowest precedence of any Args(N) when N is 0 to infinity. An action with Args always gets the last chance to match. periodt. ๐Ÿ’…


๐Ÿ’€ When two or more actions match a given Path ๐Ÿ’€

Sometimes two or more actions match the same path and they all have the same PathPart length. (rude, honestly.) For example:

package MyApp::Controller::Root;

use Moose;
use MooseX::MethodAttributes

extends 'Catalyst::Controller';

sub root :Chained(/) CaptureArgs(0) { }

  sub one :Chained(root) PathPart('') Args(0) { }
  sub two :Chained(root) PathPart('') Args(0) { }
  sub three :Chained(root) PathPart('') Args(0) { }

__PACKAGE__->meta->make_immutable;

In this case the last defined action wins โ€” for the example, that's action three. ๐Ÿ‘‘

This is most common to happen when you're using action matching beyond paths, such as when using method matching:

package MyApp::Controller::Root;

use Moose;
use MooseX::MethodAttributes

extends 'Catalyst::Controller';

sub root :Chained(/) CaptureArgs(0) { }

  sub any :Chained(root) PathPart('') Args(0) { }
  sub get :GET Chained(root) PathPart('') Args(0) { }

__PACKAGE__->meta->make_immutable;

In the above example GET /root could match both actions. In this case you should define your 'catchall' actions higher in the controller. ๐Ÿ“Œ


๐ŸŒธ Type Constraints in Args and Capture Args ๐ŸŒธ

Beginning in Version 5.90090+ you may use Moose, MooseX::Types or Type::Tiny type constraints to further declare allowed matching for Args or CaptureArgs. Here's a simple little example for you:

package MyApp::Controller::User;

use Moose;
use MooseX::MethodAttributes;
use MooseX::Types::Moose qw(Int);

extends 'Catalyst::Controller';

sub find :Path('') Args(Int) {
  my ($self, $c, $int) = @_;
}

__PACKAGE__->meta->make_immutable;

In this case the incoming request http://localhost:/user/100 would match the action find, but http://localhost:/user/not_a_number would not. You may find declaring constraints in this manner aids with debugging, automatic generation of documentation and reducing the amount of manual checking you might need to do in your actions. Like, for example, if the argument in the given action was going to be used to lookup a row in a database, and the matching field expected an integer, a string might cause a database exception โ€” which would then prompt you to add additional checking of the argument prior to using it. In general it is hoped this feature can lead to reduced validation boilerplate and more easily understood and declarative actions. โœจ

More than one argument may be added by comma separating your type constraint names, for example:

use Types::Standard qw/Int Str/;

sub find :Path('') Args(Int,Int,Str) {
  my ($self, $c, $int1, $int2, $str) = @_;
}

Would require three arguments: an integer, integer and a string. Note in this example we constrained the args using imported types via Types::Standard. Although you may use stringy Moose types, we recommend imported types since this is less ambiguous to your readers. If you want to use Moose stringy types. you must quote them (either "Int" or 'Int' is fine).

Conversely, you should not quote types that are imported! ๐Ÿšจ

๐ŸŽ€ Using type constraints in a controller ๐ŸŽ€

By default Catalyst allows all the standard, built-in, named type constraints that come bundled with Moose. However it is trivial to create your own Type constraint libraries and export them to a controller that wishes to use them. We recommend using Type::Tiny or MooseX::Types for this. Here is an example using some extended type constraints via the Types::Standard library that is packaged with Type::Tiny:

package MyApp::Controller::User;

use Moose;
use MooseX::MethodAttributes;
use Types::Standard qw/StrMatch Int/;

extends 'Catalyst::Controller';

sub looks_like_a_date :Path('') Args(StrMatch[qr{\d\d-\d\d-\d\d}]) {
  my ($self, $c, $int) = @_;
}

__PACKAGE__->meta->make_immutable;

This would match URLs like http://localhost/user/11-11-2015 for example. If you've been missing the old RegExp matching, this can emulate a good chunk of that ability, and more. ๐Ÿช„

A tutorial on how to make custom type libraries is outside the scope of this document. I'd recommend looking at the copious documentation in Type::Tiny or in MooseX::Types if you prefer that system. The author recommends Type::Tiny if you are unsure which to use. ๐Ÿ‘‘

๐Ÿ’œ Type constraint namespace. ๐Ÿ’œ

By default we assume the namespace which defines the type constraint is in the package which contains the action declaring the arg or capture arg. However if you do not wish to import type constraints into you package, you may use a fully qualified namespace for your type constraint. If you do this you must install Type::Tiny, which defines the code used to lookup and normalize the various types of Type constraint libraries.

Example:

package MyApp::Example;

use Moose;
use MooseX::MethodAttributes;

extends 'Catalyst::Controller';

sub an_int_ns :Local Args(MyApp::Types::Int) {
  my ($self, $c, $int) = @_;
  $c->res->body('an_int (withrole)');
}

Would basically work the same as:

package MyApp::Example;

use Moose;
use MooseX::MethodAttributes;
use MyApp::Types 'Int';

extends 'Catalyst::Controller';

sub an_int_ns :Local Args(Int) {
  my ($self, $c, $int) = @_;
  $c->res->body('an_int (withrole)');
}

๐Ÿงš namespace::autoclean ๐Ÿงš

If you want to use namespace::autoclean in your controllers you must 'except' imported type constraints, since the code that resolves type constraints in args / capture args run after the cleaning. For example:

package MyApp::Controller::Autoclean;

use Moose;
use MooseX::MethodAttributes;
use namespace::autoclean -except => 'Int';
use MyApp::Types qw/Int/;

extends 'Catalyst::Controller';

sub an_int :Local Args(Int) {
  my ($self, $c, $int) = @_;
  $c->res->body('an_int (autoclean)');
}

๐Ÿ”ฎ Using roles and base controller with type constraints ๐Ÿ”ฎ

If your controller is using a base class or a role that has an action with a type constraint, you should declare your use of the type constraint in that role or base controller in the same way as you do in main controllers. Catalyst will try to find the package with declares the type constraint first by looking in any roles and then in superclasses. It will use the first package that defines the type constraint. For example:

package MyApp::Role;

use Moose::Role;
use MooseX::MethodAttributes::Role;
use MyApp::Types qw/Int/;

sub an_int :Local Args(Int) {
  my ($self, $c, $int) = @_;
  $c->res->body('an_int (withrole)');
}

sub an_int_ns :Local Args(MyApp::Types::Int) {
  my ($self, $c, $int) = @_;
  $c->res->body('an_int (withrole)');
}

package MyApp::BaseController;

use Moose;
use MooseX::MethodAttributes;
use MyApp::Types qw/Int/;

extends 'Catalyst::Controller';

sub from_parent :Local Args(Int) {
  my ($self, $c, $id) = @_;
  $c->res->body('from_parent $id');
}

package MyApp::Controller::WithRole;

use Moose;
use MooseX::MethodAttributes;

extends 'MyApp::BaseController';

with 'MyApp::Role';

If you have a complex controller hierarchy, we do not at this time attempt to look for all packages with a match type constraint โ€” instead we take the first one found. In the future we may add code that attempts to insure a sane use of subclasses with type constraints, but right now there are no clear use cases so report issues and interests. ๐Ÿ™ˆ

๐Ÿ”ฅ Match order when more than one Action matches a path. ๐Ÿ”ฅ

As previously described, Catalyst will match 'the longest path', which generally means that named path / path_parts will take precedence over Args or CaptureArgs. However, what will happen if two actions match the same path with equal args? For example:

sub an_int :Path(user) Args(Int) {
}

sub an_any :Path(user) Args(1) {
}

In this case Catalyst will check actions starting from the LAST one defined. Generally this means you should put your most specific action rules LAST and your 'catch-alls' first. In the above example, since Args(1) will match any argument, you will find that the an_int action NEVER gets hit. ๐Ÿ˜ญ You would need to reverse the order:

sub an_any :Path(user) Args(1) {
}

sub an_int :Path(user) Args(Int) {
}

Now requests that match this path would first hit the an_int action and will check to see if the argument is an integer. If it is, then the action will execute โ€” otherwise it will pass and the dispatcher will check the next matching action (in this case we fall through to the an_any action). ๐Ÿ’…

๐Ÿ‘‘ Type Constraints and Chained Actions ๐Ÿ‘‘

Using type constraints in Chained actions works the same as it does for Path and Local or Global actions. The only difference is that you may declare type constraints on CaptureArgs as well as Args. For Example:

use Types::Standard qw/Int Tuple/;

sub chain_base :Chained(/) CaptureArgs(1) { }

  sub any_priority_chain :GET Chained(chain_base) PathPart('') Args(1) {  }

  sub int_priority_chain :Chained(chain_base) PathPart('') Args(Int) {  }

  sub link_any :Chained(chain_base) PathPart('') CaptureArgs(1) { }

okay so these chained action sub definitions are literally the blueprint, bestie:

             sub any_priority_link_any :Chained(link_any) PathPart('') Args(1) {  }

             sub int_priority_link_any :Chained(link_any) PathPart('') Args(Int) {  }

           sub link_int :Chained(chain_base) PathPart('') CaptureArgs(Int) { }

             sub any_priority_link :Chained(link_int) PathPart('') Args(1) {  }

             sub int_priority_link :Chained(link_int) PathPart('') Args(Int) {  }

           sub link_int_int :Chained(chain_base) PathPart('') CaptureArgs(Int,Int) { }

             sub any_priority_link2 :Chained(link_int_int) PathPart('') Args(1) {  }

             sub int_priority_link2 :Chained(link_int_int) PathPart('') Args(Int) {  }

           sub link_tuple :Chained(chain_base) PathPart('') CaptureArgs(Tuple[Int,Int,Int]) { }

             sub any_priority_link3 :Chained(link_tuple) PathPart('') Args(1) {  }

             sub int_priority_link3 :Chained(link_tuple) PathPart('') Args(Int) {  }

And these chained actions might create match tables like the following:

       [debug] Loaded Chained actions:
       .-------------------------------------+--------------------------------------.
       | Path Spec                           | Private                              |
       +-------------------------------------+--------------------------------------+
       | /chain_base/*/*                     | /chain_base (1)                      |
       |                                     | => GET /any_priority_chain (1)       |
       | /chain_base/*/*/*                   | /chain_base (1)                      |
       |                                     | -> /link_int (Int)                   |
       |                                     | => /any_priority_link (1)            |
       | /chain_base/*/*/*/*                 | /chain_base (1)                      |
       |                                     | -> /link_int_int (Int,Int)           |
       |                                     | => /any_priority_link2 (1)           |
       | /chain_base/*/*/*/*/*               | /chain_base (1)                      |
       |                                     | -> /link_tuple (Tuple[Int,Int,Int])  |
       |                                     | => /any_priority_link3 (1)           |
       | /chain_base/*/*/*                   | /chain_base (1)                      |
       |                                     | -> /link_any (1)                     |
       |                                     | => /any_priority_link_any (1)        |
       | /chain_base/*/*/*/*/*/*             | /chain_base (1)                      |
       |                                     | -> /link_tuple (Tuple[Int,Int,Int])  |
       |                                     | -> /link2_int (UserId)               |
       |                                     | => GET /finally (Int)                |
       | /chain_base/*/*/*/*/*/...           | /chain_base (1)                      |
       |                                     | -> /link_tuple (Tuple[Int,Int,Int])  |
       |                                     | -> /link2_int (UserId)               |
       |                                     | => GET /finally2 (...)               |
       | /chain_base/*/*                     | /chain_base (1)                      |
       |                                     | => /int_priority_chain (Int)         |
       | /chain_base/*/*/*                   | /chain_base (1)                      |
       |                                     | -> /link_int (Int)                   |
       |                                     | => /int_priority_link (Int)          |
       | /chain_base/*/*/*/*                 | /chain_base (1)                      |
       |                                     | -> /link_int_int (Int,Int)           |
       |                                     | => /int_priority_link2 (Int)         |
       | /chain_base/*/*/*/*/*               | /chain_base (1)                      |
       |                                     | -> /link_tuple (Tuple[Int,Int,Int])  |
       |                                     | => /int_priority_link3 (Int)         |
       | /chain_base/*/*/*                   | /chain_base (1)                      |
       |                                     | -> /link_any (1)                     |
       |                                     | => /int_priority_link_any (Int)      |
       '-------------------------------------+--------------------------------------'

So like, notice how the same general path can be matched by a whole bunch of different action chains? When that happens, you gotta follow the rule from the previous section: Catalyst will start with the last defined action and work upward. For example, the action int_priority_chain would be checked before any_priority_chain. Same energy for actions that are midway links in a longer chain โ€” in that case, link_int would be checked before link_any. So as always, we recommend that you place your priority or most constrained actions last and your least or catch-all actions first. Periodt. ๐Ÿ’…

Even though this reverse-order checking may seem counterintuitive, it's actually lowkey iconic because it has an added benefit: when you inherit controllers, any new actions you add would take check precedence over those in your parent controller or consumed role. Slay. ๐Ÿ‘‘

Also pls note: your declared type constraint names will now appear in the debug console. Keep that in mind, bestie. ๐Ÿซถ


โœจ Author โœจ

This absolute legend is John Napiorkowski โ€” you can reach him at jjnapiork@cpan.org or <email:jjnapiork@cpan.org>. ๐Ÿ’–


That's the whole vibe, bestie. This page is from perl v5.40.0, dated 2024-11-15, and it's the Cataly...atching(3pm) man page. Stay iconic ๐Ÿ’…โœจ

๐Ÿ“ฆ where this page comes from