Wednesday, August 26, 2009

Moose: adding attributes to a base class

I'm using FormHandler for an example here, but the technique is general.

FormHandler forms are Perl classes with a number of attributes including an array of Field classes with another (probably too large) number of attributes. A few of the field attributes are validation and data related, but a lot of the other ones are related to producing HTML for display. Despite the too many attributes in the field classes, users still want yet more attributes. One of the FormHandler users was developing forms that used a javascript form package and wanted to interface the FormHandler forms to the javascript forms. To do this, he wanted an additional attribute in the fields to store information that would be used by extJs.

One possibility would be to subclass every last single field and add an additional attribute. This does not sound like either fun or a good idea. A much better alternative was to use Moose to add attributes to the base class by applying a role containing the attributes.

So I started out by creating a small role containing a single attribute:

   package MyApp::Field::Extra;
   use Moose::Role;
   has 'my_extra_attribute' => (is => 'rw', isa => 'Str' );
   1;

Now I needed someplace to apply the role. The BUILD method of the user form looks like a good place. Like good Moose classes, all of the fields have '__PACKAGE__->meta->make_immutable' in them. So in order to apply a role we have to temporarily make the class mutable and then make it mutable again. So I make the class mutable, apply the role using Moose::Util, then make it immutable again:

   my $class = 'HTML::FormHandler::Field';
   $class->meta->make_mutable;
   Moose::Util::apply_all_roles( $class->meta, ('MyApp::Field::Extra'));
   $class->meta->make_immutable;

Using a test file I make a form class using this code in the BUILD method, create an instance of the form, and find that the fields now have an additional attribute that I can retrieve and set.

This looks good until I try to set the new attribute in a 'has_field' declaration:

   has_field 'my_field' => ( my_extra_attribute => 'some_value' );

Ooops. This doesn't work. I'd forgotten that the fields are constructed in the base class BUILD method which fires before my form class's BUILD method. So now I need someplace else to move my role setting that will happen before the base class BUILD. Maybe after BUILDARGS...

   after 'BUILDARGS' => sub {
      my $class = 'HTML::FormHandler::Field';
      $class->meta->make_mutable;
      Moose::Util::apply_all_roles( $class->meta, ('MyApp::Field::Extra'));
      $class->meta->make_immutable;
   };

This works. Now I can treat the new attribute just like an original field attribute. And it's a lot easier than subclassing every field...

There are other ways of achieving the same thing. You could add an attribute instead of applying a role. But roles are more general purpose and flexible, so I'm satisfied with this solution for now.

And I definitely <3 the flexibility that comes with Moose.