Question

I have a role that provides a method modifier like so:

package MyApp::Role::MenuExtensionRed;

use Moose::Role;
requires 'BuildMenu';

after 'BuildMenu' => sub {...};

Due to requirements elsewhere, I need to apply a number of roles at runtime, like so:

package MyApp::MainMenu

before 'BuildMenu' => sub {
    my ( $self, $args ) = @_;

    my $roles  = $args->{menu_extensions};
    apply_all_roles($self,@$roles);
};

sub BuildMenu {...}

However, the 'after' method modifier is never called. Clearly I'm breaking some rule, but I'd really like to understand why this doesn't work!

It works if instead of applying the roles before 'BuildMenu', I apply them in the BUILD method. But unfortunately, my list of menu_extension roles isn't available at that point, so I have to wait.

Any alternate solutions would be appreciated!

EDIT Interestingly, after 'BuildMenu' IS called, but only on subsequent calls to BuildMenu. So a method's modifiers cannot be altered from within another of it's own modifiers. Is this expected behavior? Is there a way to add to the "list" of modifiers at runtime?

Était-ce utile?

La solution

Thats a side effect of how its implemented.

When you use a method modifier, they basically replace the existing method with a new one, and the new one contains a reference to the old.

So when you see

before foo => sub {
}

What happens at role composition time is:

my $orig = *package::foo;
*package::foo = sub {
     $beforetrigger->( @args );
     return $orig->( @args );
}

More or less.

So imagine, you have 2 subs, "A", the version of BuildMenu that gets invoked before applying the role, and "B", the version of BuildMenu that gets invoked after applying the role.

So what happens, is your calling order pans out like this:

First Call ->
   A ->  
    before ->
         apply roles ->
           replace A with B
    <-- return from before
    A body   ->
    <-- return from A Body
Subsequent Call 
   B -> something

Etc, so, what I think you need to do, is have your wrapping code acknowledge this, and pass control over after application.

 around foo => sub { 
    my ( $orig, $self , @args ) = @_;
    # apply role to $self here
    # this gets the sub that will be resolved *after* the role is applied
    my $wrapped_sub = $self->can('foo');
    my $rval = $wrapped_sub->( $self, @args );      
    return $wrapped_sub->( $self, @args );
 }

Note there's the potential for that code to have a fun thing where $wrapped_sub is the sub I just wrote to apply the roles, ... I haven't worked out exactly what will happen there yet, but you may need to put in some conditions to prevent a self-calling-sub death loop from occurring.

But the gist of the problem essentially boils down to the fact you can't replace a sub that is executing, with a new sub, from the sub itself, and expect it to daisy chain automatically, Because the sub being replaced is not inherently aware of it being replaced, and because "method modifiers" amount to replacing subs with new ones that chain to old ones =)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top