tags:

views:

226

answers:

1

I'm using Moose roles to apply some wrapper behaviour around some accessor methods in a class. I want to apply this role to a number of modules, each of which have a different set of attributes whose accessors I want to wrap. Is there a way to access the meta class of the module being applied to, from within the role? i.e. something like this:

package My::Foo;
use Moose;
with 'My::Role::X';

has [ qw(attr1 attr2) ] => (
    is => 'rw', # ...
);

has 'fields' => (
    is => 'bare', isa => 'ArrayRef[Str]',
    default => sub { [qw(attr1 attr2) ] },
);
1;

package My::Role::X;
use Moose::Role;

# this should be a Moose::Meta::Class object
my $target_meta = '????';

# get Class::MOP::Attribute object out of the metaclass
my $fields_attr = $target_meta->find_attribute_by_name('fields');

# extract the value of this attribute - should be a coderef
my $fields_to_modify = $fields_attr->default;

# evaluate the coderef to get the arrayref
$fields_to_modify = &$fields_to_modify if ref $fields_to_modify eq 'CODE';

around $_ => sub {
    # ...
} for @$fields_to_modify;
1;
+4  A: 

It looks like MooseX::Role::Parameterized will do the trick:

Ordinary roles can require that its consumers have a particular list of method names. Since parameterized roles have direct access to its consumer, you can inspect it and throw errors if the consumer does not meet your needs. (link)

The details of the role specialization is kept from the class being augmented; it doesn't even need to pass any parameters all it needs to know is what parameters (the list of fields to wrap) to pass to the role. The only key is that the role must be used after the relevant attributes have been defined on the class.

Therefore, the consumed class and the role become defined like so:

package My::Foo;
use Moose;

my @fields = qw(attr1 attr2);

has \@fields => (
    is => 'rw', # ...
);

has 'fields' => (
    is => 'bare', isa => 'ArrayRef[Str]',
    default => sub { \@fields },
);

with 'My::Role::X' => {};

1;

package My::Role::X;
use MooseX::Role::Parameterized;

role {
    my $p = shift;

    my %args = @_;

    # this should be a Moose::Meta::Class object
    my $target_meta = $args{consumer};

    # get Class::MOP::Attribute object out of the metaclass
    my $fields_attr = $target_meta->find_attribute_by_name('fields');

    # extract the value of this attribute - should be a coderef
    my $fields_to_modify = $fields_attr->default;

    # evaluate the coderef to get the arrayref
    $fields_to_modify = &$fields_to_modify if ref $fields_to_modify eq 'CODE';

    around $_ => sub {
        # ...
    } for @$fields_to_modify;
};

1;

Addendum: I have discovered that if a parameterized role consumes another parameterized role, then $target_meta in the nested role will actually be the meta-class of the parent role (isa MooseX::Role::Parameterized::Meta::Role::Parameterized), rather than the meta-class of the consuming class (isa Moose::Meta::Class). In order for the proper meta-class to be derived, you need to explicitly pass it as a parameter. I have added this to all my parameterized roles as a "best practice" template:

package MyApp::Role::SomeRole;

use MooseX::Role::Parameterized;

# because we are used by an earlier role, meta is not actually the meta of the
# consumer, but of the higher-level parameterized role.
parameter metaclass => (
    is => 'ro', isa => 'Moose::Meta::Class',
    required => 1,
);

# ... other parameters here...

role {
    my $params = shift;
    my %args = @_;

    # isa a Moose::Meta::Class
    my $meta = $params->metaclass;

    # class name of what is consuming us, om nom nom
    my $consumer = $meta->name;

    # ... code here...

}; # end role
no Moose::Role;
1;

Addendum 2: I have further discovered that if the role is being applied to an object instance, as opposed to a class, then $target_meta in the role will actually be the class of the object doing the consuming:

package main;
use My::Foo;
use Moose::Util;

my $foo = My::Foo->new;
Moose::Util::apply_all_roles($foo, MyApp::Role::SomeRole, { parameter => 'value' });

package MyApp::Role::SomeRole;
use MooseX::Role::Parameterized;
# ... use same code as above (in addendum 1):

role {
    my $meta = $args{consumer};
    my $consumer = $meta->name;     # fail! My::Foo does not implement the 'name' method

Therefore, this code is necessary when extracting the meta-class at the start of the parameterized role:

role {
    my $params = shift;
    my %args = @_;

    # could be a Moose::Meta::Class, or the object consuming us
    my $meta = $args{consumer};
    $meta = $meta->meta if not $meta->isa('Moose::Meta::Class');   # <-- important!
Ether
This is one of the things the module was written for.
perigrin
` `` `` `` `` `:D
Ether
Note: I no longer consider the above a "best practice", and indeed have refactored out all of this (ab)use of MXRP. IMHO if you need to access `$meta` from within a role, you have something stinky in your design.
Ether