Hannibal
Hannibal

Reputation: 445

Watch for changes of a Moose attribute

Is there any way in Moose of triggering a callback when the content of an attribute is changed via reference instead of setting its value via mutator?

Let's assume the following code:

has _changed  => ( is => 'rw' , isa=>'Bool' ) ;
has attribute => ( 
    is=>'rw', isa=>'Maybe[HashRef]', 
    default => sub { { a => 1 , b => 2 } },     
    trigger => sub { shift->_changed(1) } 
) ;

the trigger works as expected setting the attribute value through mutator:

$self->attribute({ a => 2 , b => 2 }) ; # OK

but setting directly a value through its key then the trigger doesn't fires (of course):

$self->attribute->{a} = 3 ; # KO

I discarded the idea of creating (and comparing) a digest of serialized attribute's content, because it can be a very huge hashref with several nesting levels, and making a digest at every attribute access can produce a performance issue.

A tied hashref (as attribute value) could be a possible solution? Any idea or suggestion is very appreciated.

NOTE: The structure of contained hashref is not known (I'm writing an ORM class, so the struct can vary depending on documents stored on NOSQL db side).

Upvotes: 2

Views: 398

Answers (2)

Hannibal
Hannibal

Reputation: 445

The following approach, based on Tie::Trace Perl module, demonstrates how to easily watch for a Moose attribute change, even if modified through a direct access to the contained hashref instead of using the appropriate setter method.

package Test::Document ;

use Mouse ;
use Tie::Trace qw<watch> ;

has _changed => ( is => 'rw', isa => 'Bool' ) ;
...
has value => (
    is      => 'rw', isa => 'HashRef',
    default => sub { { } },
    trigger => sub { shift->_changed( 1 )  }
) ;

sub BUILD {
    my ( $self ) = @_ ;
    $self->_changed( 0 ) ; # reset flag
    watch %{ $self->{ value } } , debug=> sub {
        $self->_changed(1)
    };
    return $self ;
}

package main ;

my $doc = Test::Document->new( value => { a => 1 , b => { c => 3 } } ) ;

my $x = $doc->value->{ b }->{ e } ; # not changed

$doc->value->{ b }->{ e } = 4 ; # changed

$doc->_changed(0);
delete $doc->value->{ b }->{ e } ; # changed

$doc->_changed(0);
$doc->value({ a => 1 }) ; # changed

PROS: It works :)

CONS: The recursive tied approach, on hashes with a lot of keys and nesting levels, may produce performance issues. I have to do some benchmark.

NOTE: I tried with the magic vars, but the scalar context propagation with a syntax like sub()->{a}->{b} forces the store event to fire even if there is no (explicit) assignment. Suggestions are welcome.

Upvotes: 2

amon
amon

Reputation: 57640

Once you change the hash ref directly rather than using accessor methods, Moose is no longer involved. Having your attributes return a reference to a tied hash would be the only strategy to make changes to the hash observable, yet this is not a particularly attractive solution. Tied variables are rare and likely to trigger bugs in some code. They are comparatively difficult to implement. And they imply a performance overhead for every hash access.

Strongly consider whether you can change your design to avoid exposing the internal hash. E.g. have a getter that only returns a (shallow) copy of the hash, and a setter for individual elements in the hash. You may be able to autogenerate some of these accessors using the handles and traits mechanisms, e.g. see Moose::Meta::Attribute::Native::Trait::Hash.

Upvotes: 3

Related Questions