Skip to content

Commit 0889b4e

Browse files
committed
Named actions
Allows you to add an additional name to action in a global namespace
1 parent ff923f7 commit 0889b4e

File tree

5 files changed

+362
-19
lines changed

5 files changed

+362
-19
lines changed

Diff for: lib/Catalyst.pm

+10
Original file line numberDiff line numberDiff line change
@@ -2392,8 +2392,18 @@ sub get_action { my $c = shift; $c->dispatcher->get_action(@_) }
23922392
Gets all actions of a given name in a namespace and all parent
23932393
namespaces.
23942394
2395+
=head2 $c->action_for( $action_private_name )
2396+
2397+
Returns the action which matches the full private name or nothing if there's no
2398+
matching action
2399+
23952400
=cut
23962401

2402+
sub action_for {
2403+
my ($c, $action_private_name) = @_ ;
2404+
return $c->dispatcher->get_action_by_path($action_private_name);
2405+
}
2406+
23972407
sub get_actions { my $c = shift; $c->dispatcher->get_actions( $c, @_ ) }
23982408

23992409
=head2 $app->handle_request( @arguments )

Diff for: lib/Catalyst/Controller.pm

+121
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,8 @@ sub _parse_Chained_attr {
514514
my @levels = split '/', $rel;
515515

516516
$value = '/'.join('/', @parts[0 .. $#parts - @levels], $rest);
517+
} elsif ($value =~ /^\*/) {
518+
$value = "/$value";
517519
} elsif ($value !~ m/^\//) {
518520
my $action_ns = $self->action_namespace($c);
519521

@@ -1030,6 +1032,125 @@ like websockets.
10301032
10311033
See L<Catalyst::ActionRole::Scheme> for more.
10321034
1035+
=head2 Name
1036+
1037+
Allows you to give you action a globally addressable name, in addition to its private
1038+
name. Useful to decouple action referencing via Chaining and link creation from the
1039+
actions private name, which is tightly bound to the controller namespace as well as the
1040+
action subroutine name. Example:
1041+
1042+
package MyApp::Controller::Root;
1043+
1044+
use warnings;
1045+
use strict;
1046+
use base 'Catalyst::Controller';
1047+
1048+
sub root :Chained(/) PathPart('') CaptureArgs(0) Name(Root) {
1049+
my ($self, $c) = @_;
1050+
}
1051+
1052+
MyApp::Controller::Root->config(namespace=>'');
1053+
1054+
package MyApp::Controller::Home;
1055+
1056+
use warnings;
1057+
use strict;
1058+
use base 'Catalyst::Controller';
1059+
1060+
sub home :Chained(*Root) Args(0) {
1061+
my ($self, $c) = @_;
1062+
}
1063+
1064+
In this case the 'Home' controller's action '/home/home' is chained to the Root controllers action
1065+
'/root'. These declarations are the in practice the same:
1066+
1067+
package MyApp::Controller::Home;
1068+
1069+
use warnings;
1070+
use strict;
1071+
use base 'Catalyst::Controller';
1072+
1073+
# Reference the target prior chain link via its full private action name
1074+
sub home :Chained(/root) Args(0) {
1075+
my ($self, $c) = @_;
1076+
}
1077+
1078+
or:
1079+
1080+
# Reference the target prior chain link via a relative action path
1081+
sub home :Chained(../root) Args(0) {
1082+
my ($self, $c) = @_;
1083+
}
1084+
1085+
When using a named action's name in a :Chained attribute, when using forward/detach/go/visit or
1086+
when using $c->action_for and $controller->action_for you must prefix the name with a '*' so that
1087+
we can disambiguate a named action from an action relative path:
1088+
1089+
package MyApp::Controller::URI;
1090+
1091+
use warnings;
1092+
use strict;
1093+
use base 'Catalyst::Controller';
1094+
1095+
sub target :Path(/target) Args(0) Name (Target) {
1096+
my ($self, $c) = @_;
1097+
}
1098+
1099+
sub uri :Path(/uri) Args(0) {
1100+
my ($self, $c) = @_;
1101+
$c->response->body($c->uri_for( $c->action_for('*Target') ));
1102+
}
1103+
1104+
package MyApp::Controller::Flow;
1105+
1106+
use warnings;
1107+
use strict;
1108+
use base 'Catalyst::Controller';
1109+
1110+
sub test_forward :Path(/forward) Args(0) {
1111+
my ($self, $c) = @_;
1112+
$c->forward('*ForForward');
1113+
}
1114+
1115+
sub forward_target :Action Name(ForForward) {
1116+
my ($self, $c) = @_;
1117+
$c->response->body('forward');
1118+
}
1119+
1120+
sub test_detach :Path(/detach) Args(0) {
1121+
my ($self, $c) = @_;
1122+
$c->detach('*ForDetach');
1123+
}
1124+
1125+
sub detach_target :Action Name(ForDetach) {
1126+
my ($self, $c) = @_;
1127+
$c->response->body('detach');
1128+
}
1129+
1130+
sub test_go :Path(/go) Args(0) {
1131+
my ($self, $c) = @_;
1132+
$c->detach('*ForGo');
1133+
}
1134+
1135+
sub go_target :Action Name(ForGo) {
1136+
my ($self, $c) = @_;
1137+
$c->response->body('go');
1138+
}
1139+
1140+
sub test_visit :Path(/visit) Args(0) {
1141+
my ($self, $c) = @_;
1142+
$c->detach('*ForVisit');
1143+
}
1144+
1145+
sub visit_target :Action Name(ForVisit) {
1146+
my ($self, $c) = @_;
1147+
$c->response->body('visit');
1148+
}
1149+
1150+
B<NOTE>: Named actions are not a replacement for using an actions private name, but are offered
1151+
as an option for when additional clarity or action namespace decoupling improve code understanding
1152+
and maintainability.
1153+
10331154
=head1 OPTIONAL METHODS
10341155
10351156
=head2 _parse_[$name]_attr

Diff for: lib/Catalyst/DispatchType/Chained.pm

+30-19
Original file line numberDiff line numberDiff line change
@@ -278,25 +278,31 @@ sub recurse_match {
278278
next TRY_ACTION unless $action->match_captures($c, \@captures);
279279

280280
# try the remaining parts against children of this action
281-
my ($actions, $captures, $action_parts, $n_pathparts) = $self->recurse_match(
282-
$c, '/'.$action->reverse, \@parts
283-
);
284-
# No best action currently
285-
# OR The action has less parts
286-
# OR The action has equal parts but less captured data (ergo more defined)
287-
if ($actions &&
288-
(!$best_action ||
289-
$#$action_parts < $#{$best_action->{parts}} ||
290-
($#$action_parts == $#{$best_action->{parts}} &&
291-
$#$captures < $#{$best_action->{captures}} &&
292-
$n_pathparts > $best_action->{n_pathparts}))) {
293-
my @pathparts = split /\//, $action->attributes->{PathPart}->[0];
294-
$best_action = {
295-
actions => [ $action, @$actions ],
296-
captures=> [ @captures, @$captures ],
297-
parts => $action_parts,
298-
n_pathparts => scalar(@pathparts) + $n_pathparts,
299-
};
281+
my @action_names = ($action->reverse);
282+
# try Name first if that exists and then short circuit out
283+
unshift @action_names, map { "*${_}"} @{$action->attributes->{Name}} if exists $action->attributes->{Name};
284+
285+
foreach my $action_name (@action_names) {
286+
my ($actions, $captures, $action_parts, $n_pathparts) = $self->recurse_match(
287+
$c, '/'.$action_name, \@parts
288+
);
289+
# No best action currently
290+
# OR The action has less parts
291+
# OR The action has equal parts but less captured data (ergo more defined)
292+
if ($actions &&
293+
(!$best_action ||
294+
$#$action_parts < $#{$best_action->{parts}} ||
295+
($#$action_parts == $#{$best_action->{parts}} &&
296+
$#$captures < $#{$best_action->{captures}} &&
297+
$n_pathparts > $best_action->{n_pathparts}))) {
298+
my @pathparts = split /\//, $action->attributes->{PathPart}->[0];
299+
$best_action = {
300+
actions => [ $action, @$actions ],
301+
captures=> [ @captures, @$captures ],
302+
parts => $action_parts,
303+
n_pathparts => scalar(@pathparts) + $n_pathparts,
304+
};
305+
}
300306
}
301307
}
302308
else {
@@ -399,6 +405,11 @@ sub register {
399405

400406
$self->_actions->{'/'.$action->reverse} = $action;
401407

408+
if(my ($name) = @{$action->attributes->{Name}||[]}) {
409+
die "Named action '$name' is already defined" if exists $self->_actions->{"/*$name"};
410+
$self->_actions->{"/*$name"} = $action;
411+
}
412+
402413
if (exists $action->attributes->{Args} and exists $action->attributes->{CaptureArgs}) {
403414
Catalyst::Exception->throw(
404415
"Combining Args and CaptureArgs attributes not supported registering " .

Diff for: lib/Catalyst/Dispatcher.pm

+9
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ sub _action_rel2abs {
297297
sub _invoke_as_path {
298298
my ( $self, $c, $rel_path, $args ) = @_;
299299

300+
return $c->action_for($rel_path) if $rel_path =~ m/^\*/;
300301
my $path = $self->_action_rel2abs( $c, $rel_path );
301302

302303
my ( $tail, @extra_args );
@@ -585,6 +586,14 @@ sub register {
585586

586587
$self->_action_hash->{"$namespace/$name"} = $action;
587588
$self->_container_hash->{$namespace} = $container;
589+
590+
# Named Actions
591+
if(my (@names) = @{$action->attributes->{Name}||[]}) {
592+
foreach my $name (@names) {
593+
die "Named action '$name' is already defined" if $self->_action_hash->{"/*$name"};
594+
$self->_action_hash->{"/*$name"} = $action;
595+
}
596+
}
588597
}
589598

590599
sub _find_or_create_action_container {

0 commit comments

Comments
 (0)