Search code examples
perlmoose

Accessing a MooseX::ClassAttribute in a Moose::Role


Here's the conundrum. I'm using a Moose::Role as an interface, where the concrete classes have to implement the required attribute builders defined by the role. The role also defines some methods which do logic upon the attributes. Here's a slimmed down version of what I'm trying to do.

package Parent;
use Moose::Role;
requires '_build_permission_level';

has 'permission_level' => (
    is => 'ro',
    isa => Int,
    lazy_build => 1,
);

use constant {
    LEVEL1 = 1,
    LEVEL2 = 2,
    LEVEL3 = 3,
};

sub can_do_action {
    my $self = shift;
    return $self->permission_level() >= LEVEL2;
}

package Child;
use Moose;
with 'Parent';

sub _build_permission_level { return Parent->LEVEL3; }

Obviously I have many child classes, with differing permission levels. Now this works, except it's horribly inefficient. All Child instances will always have the same permission level, yet I have to instantiate it just to ask if it can perform the action. When running this in bulk 10,000 times, well, you get the picture.

So instead, I'd like to make permission_level a class attribute. Solves the efficiency issue in a Moose-y way. Note how permission_level no longer requires $self.

package Parent;
use Moose::Role;
use MooseX::ClassAttribute;
requires '_build_permission_level';

class_has 'permission_level' => (
    is => 'ro',
    isa => Int,
    builder => '_build_permission_level',
);

use constant {
    LEVEL1 = 1,
    LEVEL2 = 2,
    LEVEL3 = 3,
};

sub can_do_action {
    return permission_level() >= LEVEL2;
}

package Child;
use Moose;
with 'Parent';

sub _build_permission_level { return Parent->LEVEL3; }

This dies with an undefined subroutine error, can't find Parent::permission_level. So parent has no knowledge of permission_level. Really? I'm confused how it can't access its own class attribute. I must be missing something really simple. But even more fundamentally, how am I supposed to have Parent provide logic for a class attribute that Child provides?


Solution

  • What the error indicates is that at compile time there exists no sub Parent::permission_level. And this is true: has and class_has are run-time constructs; the sub is not created yet when the call in can_do_action is compiled.

    The real problem is that you're using the wrong syntax for calling a class method. Rewrite can_do_action thus:

    sub can_do_action {
        my ($class) = @_;
    
        return $class->permission_level >= LEVEL2;
    }
    

    and it should work fine. Unlike a plain function call, a method invocation is not resolved until run time, at which point Parent::permission_level will exist.