perlmoo

Is it possible to make an attribute configuration dependent on another attribute in Moo?


I have read various tutorials and the Moo documentation but I cannot find anything that describes what I want to do.

What I want to do is something like the following:

has 'status' => (
  is  => 'ro',
  isa => Enum[qw(pending waiting completed)],
);

has 'someother' => (
  is       => is_status() eq 'waiting'   ? 'ro' : 'rw',
  required => is_status() eq 'completed' ? 1    : 0,
  isa      => Str,
  lazy     => 1,
);

If I'm just way off base with this idea, how would I go about making an attribute 'ro' or 'rw' and required or not, depending on the value of another attribute?

Note, the Enum is from Type::Tiny.


Solution

  • Ask yourself why you want to do this. You are dealing with objects. Those are data that has a set of logic applied to them. That logic is described in the class, and the object is an instance of data that has the class's logic applied.

    If there is a property (which is data) that can have two different logics applied to it, is it still of the same class? After all, whether a property is changeable is a very distinct rule.

    So you really have two different classes. One where the someother property is read-only, and one where it is changeable.

    In Moo (and Moose) there are several ways to build that.

    Here is an example of the approach that uses roles.

    package Foo;
    use Moo;
    use Role::Tiny ();
    
    has 'status' => ( is => 'ro', );
    
    has 'someother' => (
        is      => 'ro',
        lazy    => 1,
    );
    
    
    sub BUILD {
      my ( $self) = @_;
    
      Role::Tiny->apply_roles_to_object($self, 'Foo::Role::Someother::Dynamic')
        if $self->status eq 'foo';
    }
    
    package Foo::Role::Someother::Dynamic;
    use Moo::Role;
    
    has '+someother' => ( is => 'rw', required => 1 );
    
    package main;
    use strict;
    use warnings;
    use Data::Printer;
    
    # ...
    

    First we'll create an object that has a dynamic someother.

    my $foo = Foo->new( status => 'foo', someother => 'foo' );
    p $foo;
    $foo->someother('asdf');
    print $foo->someother;
    
    __END__
    Foo__WITH__Foo::Role::Someother::Dynamic  {
        Parents       Role::Tiny::_COMPOSABLE::Foo::Role::Someother::Dynamic, Foo
        Linear @ISA   Foo__WITH__Foo::Role::Someother::Dynamic, Role::Tiny::_COMPOSABLE::Foo::Role::Someother::Dynamic, Role::Tiny::_COMPOSABLE::Foo::Role::Someother::Dynamic::_BASE, Foo, Moo::Object
        public methods (0)
        private methods (0)
        internals: {
            someother   "foo",
            status      "foo"
        }
    }
    asdf
    

    As you can see, that works. Now let's make a static one.

    my $bar = Foo->new( status => 'bar', someother => 'bar' );
    p $bar;
    $bar->someother('asdf');
    
    __END__
    Foo  {
        Parents       Moo::Object
        public methods (4) : BUILD, new, someother, status
        private methods (0)
        internals: {
            someother   "bar",
            status      "bar"
        }
    }
    Usage: Foo::someother(self) at /home/julien/code/scratch.pl line 327.
    

    Ooops. A warning. Not a nice 'read-only' exception like in Moose, but I guess this is as good as it gets.

    However, this will not help with the required attribute. You can create a Foo->new( status => 'foo' ) without someother and it will still come out ok.

    So you might want to settle for the subclass approach or use a role and build a factory class.