package Catalyst::Controller;
use Moose;
+use Class::MOP;
+use String::RewritePrefix;
use Moose::Util qw/find_meta/;
+use List::Util qw/first/;
use List::MoreUtils qw/uniq/;
use namespace::clean -except => 'meta';
with 'Catalyst::Component::ApplicationAttribute';
-has path_prefix =>
- (
- is => 'rw',
- isa => 'Str',
- init_arg => 'path',
- predicate => 'has_path_prefix',
- );
-
-has action_namespace =>
- (
- is => 'rw',
- isa => 'Str',
- init_arg => 'namespace',
- predicate => 'has_action_namespace',
- );
-
-has actions =>
- (
- accessor => '_controller_actions',
- isa => 'HashRef',
- init_arg => undef,
- );
+has path_prefix => (
+ is => 'rw',
+ isa => 'Str',
+ init_arg => 'path',
+ predicate => 'has_path_prefix',
+);
+
+has action_namespace => (
+ is => 'rw',
+ isa => 'Str',
+ init_arg => 'namespace',
+ predicate => 'has_action_namespace',
+);
+
+has actions => (
+ accessor => '_controller_actions',
+ isa => 'HashRef',
+ init_arg => undef,
+);
+
+has _action_role_args => (
+ traits => [qw(Array)],
+ isa => 'ArrayRef[Str]',
+ init_arg => 'action_roles',
+ default => sub { [] },
+ handles => {
+ _action_role_args => 'elements',
+ },
+);
+
+has _action_roles => (
+ traits => [qw(Array)],
+ isa => 'ArrayRef[RoleName]',
+ init_arg => undef,
+ lazy => 1,
+ builder => '_build__action_roles',
+ handles => {
+ _action_roles => 'elements',
+ },
+);
+
+has action_args => (is => 'ro');
+
+# ->config(actions => { '*' => ...
+has _all_actions_attributes => (
+ is => 'ro',
+ isa => 'HashRef',
+ init_arg => undef,
+ lazy => 1,
+ builder => '_build__all_actions_attributes',
+);
sub BUILD {
my ($self, $args) = @_;
my $actions = delete $args->{actions} || {};
my $attr_value = $self->merge_config_hashes($actions, $action);
$self->_controller_actions($attr_value);
+
+ # trigger lazy builder
+ $self->_all_actions_attributes;
+ $self->_action_roles;
}
+sub _build__action_roles {
+ my $self = shift;
+ my @roles = $self->_expand_role_shortname($self->_action_role_args);
+ Class::MOP::load_class($_) for @roles;
+ return \@roles;
+}
+sub _build__all_actions_attributes {
+ my ($self) = @_;
+ delete $self->_controller_actions->{'*'} || {};
+}
=head1 NAME
#I think both of these could be attributes. doesn't really seem like they need
#to ble class data. i think that attributes +default would work just fine
-__PACKAGE__->mk_classdata($_) for qw/_dispatch_steps _action_class/;
+__PACKAGE__->mk_classdata($_) for qw/_dispatch_steps _action_class _action_role_prefix/;
__PACKAGE__->_dispatch_steps( [qw/_BEGIN _AUTO _ACTION/] );
__PACKAGE__->_action_class('Catalyst::Action');
+__PACKAGE__->_action_role_prefix([ 'Catalyst::ActionRole::' ]);
sub _DISPATCH : Private {
sub get_action_methods {
my $self = shift;
my $meta = find_meta($self) || confess("No metaclass setup for $self");
- confess("Metaclass "
- . ref($meta) . " for "
- . $meta->name
- . " cannot support register_actions." )
- unless $meta->can('get_nearest_methods_with_attributes');
+ confess(
+ sprintf "Metaclass %s for %s cannot support register_actions.",
+ ref $meta, $meta->name,
+ ) unless $meta->can('get_nearest_methods_with_attributes');
my @methods = $meta->get_nearest_methods_with_attributes;
# actions specified via config are also action_methods
$meta->find_method_by_name($_)
|| confess( sprintf 'Action "%s" is not available from controller %s',
$_, ref $self )
- } keys %{ $self->_controller_actions }
+ } keys %{ $self->_controller_actions }
) if ( ref $self );
return uniq @methods;
}
}
}
+sub _apply_action_class_roles {
+ my ($self, $class, @roles) = @_;
+
+ Class::MOP::load_class($_) for @roles;
+ my $meta = Moose::Meta::Class->initialize($class)->create_anon_class(
+ superclasses => [$class],
+ roles => \@roles,
+ cache => 1,
+ );
+ $meta->add_method(meta => sub { $meta });
+
+ return $meta->name;
+}
+
sub action_class {
my $self = shift;
my %args = @_;
my %args = @_;
my $class = $self->action_class(%args);
- my $action_args = $self->config->{action_args};
+
+ load_class($class);
+ Moose->init_meta(for_class => $class)
+ unless Class::MOP::does_metaclass_exist($class);
+
+ unless ($args{name} =~ /^_(DISPATCH|BEGIN|AUTO|ACTION|END)$/) {
+ my @roles = $self->gather_action_roles(%args);
+ $class = $self->_apply_action_class_roles($class, @roles) if @roles;
+ }
+
+ my $action_args = (
+ ref($self)
+ ? $self->action_args
+ : $self->config->{action_args}
+ );
my %extra_args = (
%{ $action_args->{'*'} || {} },
return $class->new({ %extra_args, %args });
}
+sub gather_action_roles {
+ my ($self, %args) = @_;
+
+ return (
+ (blessed $self ? $self->_action_roles : ()),
+ @{ $args{attributes}->{Does} || [] },
+ );
+}
+
sub _parse_attrs {
my ( $self, $c, $name, @attrs ) = @_;
}
}
- my $actions;
+ my ($actions_config, $all_actions_config);
if( ref($self) ) {
- $actions = $self->_controller_actions;
+ $actions_config = $self->_controller_actions;
+ # No, you're not getting actions => { '*' => ... } with actions in MyApp.
+ $all_actions_config = $self->_all_actions_attributes;
} else {
my $cfg = $self->config;
- $actions = $self->merge_config_hashes($cfg->{actions}, $cfg->{action});
+ $actions_config = $self->merge_config_hashes($cfg->{actions}, $cfg->{action});
+ $all_actions_config = {};
}
- %raw_attributes = ((exists $actions->{'*'} ? %{$actions->{'*'}} : ()),
- %raw_attributes,
- (exists $actions->{$name} ? %{$actions->{$name}} : ()));
+ %raw_attributes = (
+ %raw_attributes,
+ # Note we deep copy array refs here to stop crapping on config
+ # when attributes are parsed. RT#65463
+ exists $actions_config->{$name} ? map { ref($_) eq 'ARRAY' ? [ @$_ ] : $_ } %{ $actions_config->{$name } } : (),
+ );
+ # Private actions with additional attributes will raise a warning and then
+ # be ignored. Adding '*' arguments to the default _DISPATCH / etc. methods,
+ # which are Private, will prevent those from being registered. They should
+ # probably be turned into :Actions instead, or we might want to otherwise
+ # disambiguate between those built-in internal actions and user-level
+ # Private ones.
+ %raw_attributes = (%{ $all_actions_config }, %raw_attributes)
+ unless $raw_attributes{Private};
my %final_attributes;
return ( 'ActionClass', $value );
}
+sub _parse_Does_attr {
+ my ($self, $app, $name, $value) = @_;
+ return Does => $self->_expand_role_shortname($value);
+}
+
+sub _expand_role_shortname {
+ my ($self, @shortnames) = @_;
+ my $app = $self->_application;
+
+ my $prefix = $self->can('_action_role_prefix') ? $self->_action_role_prefix : ['Catalyst::ActionRole::'];
+ my @prefixes = (qq{${app}::ActionRole::}, @$prefix);
+
+ return String::RewritePrefix->rewrite(
+ { '' => sub {
+ my $loaded = Class::MOP::load_first_existing_class(
+ map { "$_$_[0]" } @prefixes
+ );
+ return first { $loaded =~ /^$_/ }
+ sort { length $b <=> length $a } @prefixes;
+ },
+ '~' => $prefixes[0],
+ '+' => '' },
+ @shortnames,
+ );
+}
+
__PACKAGE__->meta->make_immutable;
1;
Allows you to set the attributes that the dispatcher creates actions out of.
This allows you to do 'rails style routes', or override some of the
-attribute defintions of actions composed from Roles.
+attribute definitions of actions composed from Roles.
You can set arguments globally (for all actions of the controller) and
specifically (for a single action).
Called with a hash of data to be use for construction of a new
Catalyst::Action (or appropriate sub/alternative class) object.
+=head2 $self->gather_action_roles(\%action_args)
+
+Gathers the list of roles to apply to an action with the given %action_args.
+
=head2 $self->_application
=head2 $self->_app