Reputation: 445
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
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
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