Why isn't Moose Role exclude excluding particular role attributes?

171 views Asked by At

I have a Moose::Role that has (among other things):

package My::Role;

use strict;
use warnings;

use Moose::Role;
use MooseX::ClassAttribute;

class_has table => (
    is => 'ro'
    isa => 'Str',
    lazy => 1,
);

has id => (
    is => 'ro',
    isa => 'Int',
    predicate => 'has_id',
    writer => '_id',
    required => 0,
);

has other => (
    is => 'rw',
    isa => 'Int',
);

...

1;

Then, in a module that consumes that Role,

package Some::Module;

with 'My::Role' => {
    -excludes => [qw( id table )]
};

has module_id => (
    is => 'ro',
    isa => 'Int',
);
...

1;

Then, in a script I'm instantiating an instance of Some::Module:

my $some_module = Some::Module->new({ other => 3 });

and I'm able to call

$some_module->id;  # I'd expect this to die but returns undef.

However, I'm unable to call

$some_module->table;  # this dies as I'd expect

As I'd expect calling $some_module->table causes the script to cease. Calling $some_module->id doesn't.

When I use Data::Dumper to dump out the attribute list of the $some_module meta class it show that the id attribute is defined but the table attribute is not.

Does anyone know why the 'id' attribute defined in the Role would not be excluded from the meta class but the 'table' class_attribute would? The problem being, as described above, is that users of Some::Module can call id() when they should be required to call module_id().

Furthermore, when dumping $some_module object, the 'id' doesn't show up in the dump.

Edit:

Here's a sample that illustrates the problem. I've defined a role that implements an id then I'm consuming the role in the package My::Product. I'm excluding the id when consuming it however. When I print the attribute from the meta object it shows that it is in fact there. I was under the impression that excluding the id from a role when consuming it wouldn't allow it to be called. I'd expect that it would not only be NOT in the meta object but also to die on an attempt to call it.

#!/usr/bin/perl

package My::Model;

use Moose::Role;
use MooseX::ClassAttribute;

class_has first_name => (
    is  => 'rw',
    isa => 'Str',
);

class_has last_name => (
    is  => 'rw',
    isa => 'Str',
);

has id => (
    is        => 'rw',
    isa       => 'Int',
    predicate => 'has_id',
    writer    => '_id',
    required  => 0,
);

1;

package My::Product;

use Moose;
use Class::MOP::Class;
use Data::Dumper;

with 'My::Model' => { -excludes => [ qw( first_name id ) ], };

has count => (
    is => 'rw',
    isa => 'Int',
);

has product_id => (
    is        => 'ro',
    isa       => 'Int',
    required  => 0,
    predicate => 'has_product_id'
);

sub create_classes {
    my @list = ();
    foreach my $subclass (qw( one two three )) {
          Class::MOP::Class->create(
            "My::Product::"
              . $subclass => (
                superclasses => ["My::Product"],
              )
          );
        push @list, "My::Product::$subclass";
    }

    return \@list;
}

__PACKAGE__->meta()->make_immutable;

1;

package main;

use strict;
use warnings;
use Data::Dumper;

my $product = My::Product->new();
my $classes = $product->create_classes();

my @class_list;
foreach my $class ( @{ $classes } ) {
    my $temp = $class->new( { count => time } );
    $temp->first_name('Don');
    $temp->last_name('MouseCop');
    push @class_list, $temp;
}

warn "what is the id for the first obj => " . $class_list[0]->id ;
warn "what is the first_name for the first obj => " . $class_list[0]->first_name ;
warn "what is the last_name for the first obj => " . $class_list[0]->last_name ;

warn "\nAttribute list:\n";
foreach my $attr ( $class_list[2]->meta->get_all_attributes ) {
    warn "name => " . $attr->name;
#    warn Dumper( $attr );
}

Edit 2: Upon dumping the $attr I am seeing that first_name and id are in the method_exclusions.

 'role_applications' => [
                        bless( {
                                 'class' => $VAR1->{'associated_class'},
                                 'role' => $VAR1->{'associated_class'}{'roles'}[0],
                                 'method_aliases' => {},
                                 'method_exclusions' => [
                                                          'first_name',
                                                          'id'
                                                        ]
                               }, 'Moose::Meta::Class::__ANON__::SERIAL::8' )
                      ]
1

There are 1 answers

0
tcn On

I have no idea how the innards of this works but I believe this is to do with the fact that the two methods you are excluding are attribute methods. The only relevant article I can find is here, where it says:

A roles attributes are similar to those of a class, except that they are not actually applied. This means that methods that are generated by an attributes accessor will not be generated in the role, but only created once the role is applied to a class.

Therefore I'm guessing the problem is that when your classes are being constructed, the role is applied (and the methods are excluded), but after that the role's attributes are applied and the accessor methods (including id and first_name) are constructed.

To demonstrate, change the id attribute to _id, give it a different writer and create an id sub to access it:

# This replaces id
has _id => (
    is        => 'rw',
    isa       => 'Int',
    writer => 'set_id',
    required  => 0,
);

sub id {
    my $self = shift;
    return $self->_id();
}

The script will now die with an exception:

Can't locate object method "id" via package "My::Product::one" at ./module.pm line 89.